@cmo-so/cli 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.mjs +1437 -219
- package/package.json +1 -1
package/dist/cmo.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;
|
|
@@ -7226,13 +7226,13 @@ var ApiError = class extends Error {
|
|
|
7226
7226
|
};
|
|
7227
7227
|
|
|
7228
7228
|
// ../../packages/api-client/src/api.ts
|
|
7229
|
-
function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.fetch }) {
|
|
7229
|
+
function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.fetch, streamFetch }) {
|
|
7230
7230
|
const base = baseUrl.replace(/\/+$/, "");
|
|
7231
|
-
async function send(path2, init, token) {
|
|
7231
|
+
async function send(path2, init, token, via = fetchImpl) {
|
|
7232
7232
|
const headers = new Headers(init.headers);
|
|
7233
7233
|
headers.set("Authorization", `Bearer ${token}`);
|
|
7234
7234
|
headers.set("Accept", "application/json");
|
|
7235
|
-
return
|
|
7235
|
+
return via(`${base}${path2}`, { ...init, headers });
|
|
7236
7236
|
}
|
|
7237
7237
|
async function request(path2, init = {}) {
|
|
7238
7238
|
let res = await send(path2, init, await session2.getAccessToken());
|
|
@@ -7249,15 +7249,32 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
|
|
|
7249
7249
|
}
|
|
7250
7250
|
return data;
|
|
7251
7251
|
}
|
|
7252
|
-
async function
|
|
7252
|
+
async function requestBytes(path2) {
|
|
7253
|
+
let res = await send(path2, {}, await session2.getAccessToken());
|
|
7254
|
+
if (res.status === 401) {
|
|
7255
|
+
res = await send(path2, {}, await session2.getAccessToken({ forceRefresh: true }));
|
|
7256
|
+
if (res.status === 401) {
|
|
7257
|
+
await session2.end();
|
|
7258
|
+
throw new SignInRequiredError();
|
|
7259
|
+
}
|
|
7260
|
+
}
|
|
7261
|
+
if (!res.ok) {
|
|
7262
|
+
const data = await res.json().catch(() => null);
|
|
7263
|
+
throw new ApiError(res.status, errorMessage(data, res.status), res.headers.get("X-Request-ID"));
|
|
7264
|
+
}
|
|
7265
|
+
return { type: res.headers.get("content-type") ?? "image/jpeg", bytes: await res.arrayBuffer() };
|
|
7266
|
+
}
|
|
7267
|
+
async function stream(path2, body, onEvent, signal) {
|
|
7253
7268
|
const init = {
|
|
7254
7269
|
method: "POST",
|
|
7255
7270
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
7256
|
-
body: JSON.stringify(body)
|
|
7271
|
+
body: JSON.stringify(body),
|
|
7272
|
+
...signal ? { signal } : {}
|
|
7257
7273
|
};
|
|
7258
|
-
|
|
7274
|
+
const via = streamFetch ?? fetchImpl;
|
|
7275
|
+
let res = await send(path2, init, await session2.getAccessToken(), via);
|
|
7259
7276
|
if (res.status === 401) {
|
|
7260
|
-
res = await send(path2, init, await session2.getAccessToken({ forceRefresh: true }));
|
|
7277
|
+
res = await send(path2, init, await session2.getAccessToken({ forceRefresh: true }), via);
|
|
7261
7278
|
if (res.status === 401) {
|
|
7262
7279
|
await session2.end();
|
|
7263
7280
|
throw new SignInRequiredError();
|
|
@@ -7327,8 +7344,87 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
|
|
|
7327
7344
|
},
|
|
7328
7345
|
// Ask Maggie (ADR-015): `history` is opaque; send back what `done` returned.
|
|
7329
7346
|
maggie: {
|
|
7330
|
-
chat: (body, onEvent) => stream("/v1/maggie/chat", body, onEvent),
|
|
7331
|
-
usage: (domain) => request(`/v1/maggie/usage${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`)
|
|
7347
|
+
chat: (body, onEvent, signal) => stream("/v1/maggie/chat", body, onEvent, signal),
|
|
7348
|
+
usage: (domain) => request(`/v1/maggie/usage${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`),
|
|
7349
|
+
// The user's own Maggie Bridge (docs/20 route D): Maggie on their ChatGPT plan.
|
|
7350
|
+
bridge: {
|
|
7351
|
+
/** `live: false`: from the pairing alone, without waking the bridge. */
|
|
7352
|
+
status: (opts = {}) => request(`/v1/maggie/bridge${opts.live === false ? "?live=0" : ""}`),
|
|
7353
|
+
/** Pair in one step: CMO.so issues the bridge its own key with this access (read by default). */
|
|
7354
|
+
pair: (body) => request("/v1/maggie/bridge", {
|
|
7355
|
+
method: "POST",
|
|
7356
|
+
headers: { "Content-Type": "application/json" },
|
|
7357
|
+
body: JSON.stringify(body)
|
|
7358
|
+
}),
|
|
7359
|
+
/** Pair with CMO.so's own bridge in one step (accounts on its list); changes you approve by default. */
|
|
7360
|
+
connectHosted: (access) => request("/v1/maggie/bridge/hosted", {
|
|
7361
|
+
method: "POST",
|
|
7362
|
+
headers: { "Content-Type": "application/json" },
|
|
7363
|
+
body: JSON.stringify(access ? { access } : {})
|
|
7364
|
+
}),
|
|
7365
|
+
setEnabled: (enabled) => request("/v1/maggie/bridge/settings", {
|
|
7366
|
+
method: "POST",
|
|
7367
|
+
headers: { "Content-Type": "application/json" },
|
|
7368
|
+
body: JSON.stringify({ enabled })
|
|
7369
|
+
}),
|
|
7370
|
+
/** Pictures on ChatGPT (on) or Gemini only (off). */
|
|
7371
|
+
setImages: (images) => request("/v1/maggie/bridge/settings", {
|
|
7372
|
+
method: "POST",
|
|
7373
|
+
headers: { "Content-Type": "application/json" },
|
|
7374
|
+
body: JSON.stringify({ images })
|
|
7375
|
+
}),
|
|
7376
|
+
remove: () => request("/v1/maggie/bridge", { method: "DELETE" }),
|
|
7377
|
+
/** Start "Sign in with ChatGPT" for one of the bridge's accounts (the active one by default). */
|
|
7378
|
+
connectChatgpt: (account3) => request("/v1/maggie/bridge/chatgpt", {
|
|
7379
|
+
method: "POST",
|
|
7380
|
+
headers: { "Content-Type": "application/json" },
|
|
7381
|
+
body: JSON.stringify(account3 ? { account: account3 } : {})
|
|
7382
|
+
}),
|
|
7383
|
+
disconnectChatgpt: (account3) => request("/v1/maggie/bridge/chatgpt/logout", {
|
|
7384
|
+
method: "POST",
|
|
7385
|
+
headers: { "Content-Type": "application/json" },
|
|
7386
|
+
body: JSON.stringify(account3 ? { account: account3 } : {})
|
|
7387
|
+
}),
|
|
7388
|
+
/** What Maggie may do in CMO.so; rotates the bridge's key when read / read_write changes. */
|
|
7389
|
+
setAccess: (access) => request("/v1/maggie/bridge/access", {
|
|
7390
|
+
method: "POST",
|
|
7391
|
+
headers: { "Content-Type": "application/json" },
|
|
7392
|
+
body: JSON.stringify({ access })
|
|
7393
|
+
}),
|
|
7394
|
+
/** Which model answers and how hard it thinks; null for the default. */
|
|
7395
|
+
setModel: (body) => request("/v1/maggie/bridge/model", {
|
|
7396
|
+
method: "POST",
|
|
7397
|
+
headers: { "Content-Type": "application/json" },
|
|
7398
|
+
body: JSON.stringify(body)
|
|
7399
|
+
}),
|
|
7400
|
+
models: () => request("/v1/maggie/bridge/models"),
|
|
7401
|
+
/** Add a ChatGPT account to the bridge (then connect it). */
|
|
7402
|
+
addAccount: (label) => request("/v1/maggie/bridge/accounts", {
|
|
7403
|
+
method: "POST",
|
|
7404
|
+
headers: { "Content-Type": "application/json" },
|
|
7405
|
+
body: JSON.stringify({ label: label ?? null })
|
|
7406
|
+
}),
|
|
7407
|
+
/** Make one of the bridge's ChatGPT accounts the one that answers. */
|
|
7408
|
+
chooseAccount: (id) => request(`/v1/maggie/bridge/accounts/${encodeURIComponent(id)}/use`, { method: "POST" }),
|
|
7409
|
+
removeAccount: (id) => request(`/v1/maggie/bridge/accounts/${encodeURIComponent(id)}`, { method: "DELETE" })
|
|
7410
|
+
}
|
|
7411
|
+
},
|
|
7412
|
+
// Help Maggie grow (docs/22): api.cmo.so forwards these to feedback.cmo.so.
|
|
7413
|
+
feedback: {
|
|
7414
|
+
send: (body) => request("/v1/feedback", {
|
|
7415
|
+
method: "POST",
|
|
7416
|
+
headers: { "Content-Type": "application/json" },
|
|
7417
|
+
body: JSON.stringify(body)
|
|
7418
|
+
}),
|
|
7419
|
+
/** The user's own feedback, newest first, with the team's replies. */
|
|
7420
|
+
mine: () => request("/v1/feedback/mine"),
|
|
7421
|
+
/** The user has read the team's latest reply or status change. */
|
|
7422
|
+
seen: (id) => request(`/v1/feedback/${encodeURIComponent(id)}/seen`, { method: "POST" }),
|
|
7423
|
+
/** One of the user's feedback photos as a data: URL (an <Image> source on web and native). */
|
|
7424
|
+
photo: async (id, n) => {
|
|
7425
|
+
const { type, bytes } = await requestBytes(`/v1/feedback/${encodeURIComponent(id)}/photos/${n}`);
|
|
7426
|
+
return dataUrl(type, bytes);
|
|
7427
|
+
}
|
|
7332
7428
|
},
|
|
7333
7429
|
// Web Push for this browser (APP-9). `domains` are the workspaces it shows.
|
|
7334
7430
|
push: {
|
|
@@ -7359,8 +7455,25 @@ function errorMessage(data, status) {
|
|
|
7359
7455
|
}
|
|
7360
7456
|
return `Request failed (${status})`;
|
|
7361
7457
|
}
|
|
7458
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7459
|
+
function dataUrl(type, bytes) {
|
|
7460
|
+
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
7461
|
+
const parts = [];
|
|
7462
|
+
let chunk = "";
|
|
7463
|
+
for (let i = 0; i < b.length; i += 3) {
|
|
7464
|
+
const n = (b[i] ?? 0) << 16 | (b[i + 1] ?? 0) << 8 | (b[i + 2] ?? 0);
|
|
7465
|
+
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) : "=");
|
|
7466
|
+
if (chunk.length >= 8192) {
|
|
7467
|
+
parts.push(chunk);
|
|
7468
|
+
chunk = "";
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
parts.push(chunk);
|
|
7472
|
+
return `data:${type.split(";")[0]?.trim() || "image/jpeg"};base64,${parts.join("")}`;
|
|
7473
|
+
}
|
|
7362
7474
|
|
|
7363
7475
|
// ../../packages/api-client/src/domains.ts
|
|
7476
|
+
var MEMORY_CHANNELS = ["general", "blog", "linkedin", "facebook", "instagram", "google_business"];
|
|
7364
7477
|
function addWebsite(api2, url) {
|
|
7365
7478
|
return api2.request("/v1/domains", {
|
|
7366
7479
|
method: "POST",
|
|
@@ -7444,6 +7557,20 @@ function domainApi(api2, domain) {
|
|
|
7444
7557
|
email: email2
|
|
7445
7558
|
}),
|
|
7446
7559
|
verifyEmailConfirm: (code) => post(`${base}/verify/email/confirm`, { code }),
|
|
7560
|
+
/**
|
|
7561
|
+
* The key to paste into the AI CMO WordPress plugin: once the plugin is connected on the website, it is
|
|
7562
|
+
* verified. Viewable until then (or until it expires); create:false only reads it, new:true replaces it.
|
|
7563
|
+
*/
|
|
7564
|
+
verifyWordpressKey: (opts = {}) => post(
|
|
7565
|
+
`${base}/verify/wordpress/key`,
|
|
7566
|
+
opts
|
|
7567
|
+
),
|
|
7568
|
+
/** The personalised WordPress setup guide: the website's own name / icon / colour, links into its wp-admin, plugin status. */
|
|
7569
|
+
setupGuide: () => api2.request(`${base}/setup-guide`),
|
|
7570
|
+
/** "Ask us": email the CMO.so team (with the account and website) to verify it by hand. */
|
|
7571
|
+
verifyRequest: (note = "") => post(`${base}/verify/request`, { note }),
|
|
7572
|
+
/** Check now: is the plugin connected on the website with that key? */
|
|
7573
|
+
verifyWordpressCheck: () => post(`${base}/verify/wordpress/check`),
|
|
7447
7574
|
/** The TXT record of the user's own claim (never makes a new one). */
|
|
7448
7575
|
dnsRecord: () => api2.request(`${base}/verify/dns`),
|
|
7449
7576
|
/** Look for the TXT record now; verifies the claim when it is there. */
|
|
@@ -7453,16 +7580,30 @@ function domainApi(api2, domain) {
|
|
|
7453
7580
|
// Writes (need autopilot write on the domain).
|
|
7454
7581
|
addIdea: (input) => post(`${base}/ideas`, input),
|
|
7455
7582
|
/** Ideas built on up to 10 photos (base64 JPEG). The server keeps no copy. */
|
|
7456
|
-
ideasFromPhotos: (photos, count,
|
|
7457
|
-
generateIdeas: (count,
|
|
7583
|
+
ideasFromPhotos: (photos, count, brief2 = "") => post(`${base}/ideas/from-photos`, { photos, count, brief: brief2 }),
|
|
7584
|
+
generateIdeas: (count, brief2 = "") => post(`${base}/ideas/generate`, { count, brief: brief2 }),
|
|
7458
7585
|
/** This website's sitemap, or `source` (a sitemap, site or page) on any website. */
|
|
7459
7586
|
importSitemap: (limit = 10, source) => post(`${base}/ideas/import-sitemap`, {
|
|
7460
7587
|
limit,
|
|
7461
7588
|
...source ? { sitemap_url: source } : {}
|
|
7462
7589
|
}),
|
|
7463
7590
|
discardIdea: (id, restore = false) => post(`${base}/ideas/${encodeURIComponent(id)}/discard${restore ? "?restore=true" : ""}`),
|
|
7591
|
+
/** OK a draft (or retry a failed post): it goes to the front of the queue; the post comes back with its eta. */
|
|
7464
7592
|
approveSocial: (id) => post(`${base}/social/${id}/approve`),
|
|
7465
7593
|
skipSocial: (id) => post(`${base}/social/${id}/skip`),
|
|
7594
|
+
/** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
|
|
7595
|
+
/** Post it now. `background`: the reply comes at once (status "publishing") and it goes out behind the
|
|
7596
|
+
* scenes (a notice when it's out); 409 `picture_busy` while its picture is being made. */
|
|
7597
|
+
publishSocialNow: (id, opts = {}) => post(
|
|
7598
|
+
`${base}/social/${id}/publish`,
|
|
7599
|
+
opts.background ? { background: true } : void 0
|
|
7600
|
+
),
|
|
7601
|
+
/** Take a posted LinkedIn post down (its likes and comments go with it). */
|
|
7602
|
+
/** How a posted Instagram post is doing (read from Instagram, kept 15 minutes; `refresh` reads again). */
|
|
7603
|
+
socialInsights: (id, refresh = false) => api2.request(`${base}/social/${id}/insights${refresh ? "?refresh=true" : ""}`),
|
|
7604
|
+
deletePostedSocial: (id) => api2.request(`${base}/social/${id}/published`, { method: "DELETE" }),
|
|
7605
|
+
/** Change a posted LinkedIn post and put it up again: the old one comes down, the new text goes out now. */
|
|
7606
|
+
republishSocial: (id, socialCopy) => post(`${base}/social/${id}/republish`, socialCopy ? { social_copy: socialCopy } : {}),
|
|
7466
7607
|
restoreSocial: (id) => post(`${base}/social/${id}/restore`),
|
|
7467
7608
|
pauseSocial: () => post(`${base}/social/pause`),
|
|
7468
7609
|
/**
|
|
@@ -7495,10 +7636,10 @@ function domainApi(api2, domain) {
|
|
|
7495
7636
|
/** The template library, searchable. */
|
|
7496
7637
|
socialTemplates: (search = "") => api2.request(`${base}/social/templates?search=${encodeURIComponent(search)}`),
|
|
7497
7638
|
socialTemplatePreview: (key) => api2.request(`${base}/social/templates/${encodeURIComponent(key)}/preview`),
|
|
7498
|
-
socialSetupTemplates: (keys3,
|
|
7639
|
+
socialSetupTemplates: (keys3, writing2) => api2.request(`${base}/social/setup/templates`, {
|
|
7499
7640
|
method: "PUT",
|
|
7500
7641
|
headers: { "Content-Type": "application/json" },
|
|
7501
|
-
body: JSON.stringify({ keys: keys3, writing })
|
|
7642
|
+
body: JSON.stringify({ keys: keys3, writing: writing2 })
|
|
7502
7643
|
}),
|
|
7503
7644
|
socialSetupSchedule: (schedule) => api2.request(`${base}/social/setup/schedule`, {
|
|
7504
7645
|
method: "PUT",
|
|
@@ -7508,16 +7649,174 @@ function domainApi(api2, domain) {
|
|
|
7508
7649
|
/** Maggie drafts the first LinkedIn post (1 credit): from a few lines, or one of your services. */
|
|
7509
7650
|
socialFirstPost: (input) => post(`${base}/social/setup/first-post`, input),
|
|
7510
7651
|
socialSetupDone: () => post(`${base}/social/setup/done`),
|
|
7652
|
+
// LinkedIn v2 (docs/prd/linkedin-journey-v2.md): Maggie's plan instead of the step-by-step setup.
|
|
7653
|
+
/** What is set up now, as a plan (no AI). */
|
|
7654
|
+
socialSetupPlan: () => api2.request(`${base}/social/setup/plan`),
|
|
7655
|
+
/** Maggie prepares what's missing (logo, look, voice, writing, frequency); `force` redoes it. Free. */
|
|
7656
|
+
socialSetupAuto: (timezone, force = false) => post(`${base}/social/setup/auto`, {
|
|
7657
|
+
...timezone ? { timezone } : {},
|
|
7658
|
+
...force ? { force: true } : {}
|
|
7659
|
+
}),
|
|
7660
|
+
socialSetupFrequency: (preset, timezone) => api2.request(`${base}/social/setup/frequency`, {
|
|
7661
|
+
method: "PUT",
|
|
7662
|
+
headers: { "Content-Type": "application/json" },
|
|
7663
|
+
body: JSON.stringify(timezone ? { preset, timezone } : { preset })
|
|
7664
|
+
}),
|
|
7665
|
+
/** "Looks good, start": setup done, and Maggie writes the first week's posts. */
|
|
7666
|
+
socialSetupConfirm: () => post(`${base}/social/setup/confirm`),
|
|
7667
|
+
/** OK every post waiting for you (not a series that hasn't been approved), on one channel or all. */
|
|
7668
|
+
approveAllSocial: (channel) => post(
|
|
7669
|
+
`${base}/social/approve-all${channel ? `?channel=${channel}` : ""}`
|
|
7670
|
+
),
|
|
7671
|
+
/** A Facebook sign-in link: after it the user lands on returnTo with ?facebook=connected (or =error). */
|
|
7672
|
+
connectFacebook: (returnTo = "close") => post(`${base}/social/facebook/connect`, { return_to: returnTo }),
|
|
7673
|
+
/** The Facebook Pages you connected, and the one this website posts to. */
|
|
7674
|
+
facebookPages: () => api2.request(`${base}/social/facebook/pages`),
|
|
7675
|
+
chooseFacebookPage: (connectionId) => api2.request(
|
|
7676
|
+
`${base}/social/facebook/page`,
|
|
7677
|
+
{
|
|
7678
|
+
method: "PUT",
|
|
7679
|
+
headers: { "Content-Type": "application/json" },
|
|
7680
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7681
|
+
}
|
|
7682
|
+
),
|
|
7683
|
+
disconnectFacebook: (connectionId) => api2.request(`${base}/social/facebook/${connectionId}`, { method: "DELETE" }),
|
|
7684
|
+
/** Instagram connects through the Facebook sign-in (the accounts linked to your Pages): the same link;
|
|
7685
|
+
* back with ?facebook=connected, then `channels` says whether an Instagram account was found. */
|
|
7686
|
+
connectInstagram: (returnTo = "close") => post(`${base}/social/instagram/connect`, { return_to: returnTo }),
|
|
7687
|
+
/** Your Instagram accounts here (each with the Facebook Page it posts through), and the chosen one. */
|
|
7688
|
+
instagramAccounts: () => api2.request(`${base}/social/instagram/accounts`),
|
|
7689
|
+
chooseInstagramAccount: (connectionId) => api2.request(
|
|
7690
|
+
`${base}/social/instagram/account`,
|
|
7691
|
+
{
|
|
7692
|
+
method: "PUT",
|
|
7693
|
+
headers: { "Content-Type": "application/json" },
|
|
7694
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7695
|
+
}
|
|
7696
|
+
),
|
|
7697
|
+
disconnectInstagram: (connectionId) => api2.request(`${base}/social/instagram/${connectionId}`, { method: "DELETE" }),
|
|
7698
|
+
/** A Google sign-in link for this website's Google Business Profile; back with ?google_business=connected
|
|
7699
|
+
* (or none / error&message=…). Every profile the account can post to is saved; this website's is picked. */
|
|
7700
|
+
connectGoogleBusiness: (returnTo = "close") => post(`${base}/social/google_business/connect`, {
|
|
7701
|
+
return_to: returnTo
|
|
7702
|
+
}),
|
|
7703
|
+
/** Your Google Business profiles here (name, address, website), and the one this website posts to. */
|
|
7704
|
+
googleBusinessLocations: () => api2.request(`${base}/social/google_business/locations`),
|
|
7705
|
+
chooseGoogleBusinessLocation: (connectionId) => put(
|
|
7706
|
+
`${base}/social/google_business/location`,
|
|
7707
|
+
{ connection_id: connectionId }
|
|
7708
|
+
),
|
|
7709
|
+
disconnectGoogleBusiness: (connectionId) => api2.request(`${base}/social/google_business/${connectionId}`, { method: "DELETE" }),
|
|
7710
|
+
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7711
|
+
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7712
|
+
/** How often one channel posts. */
|
|
7713
|
+
setChannelFrequency: (channel, preset) => api2.request(
|
|
7714
|
+
`${base}/social/channels/${channel}/frequency`,
|
|
7715
|
+
{ method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ preset }) }
|
|
7716
|
+
),
|
|
7717
|
+
/** Start a new picture for the post in the background (one AI generation); `replace` stops the one
|
|
7718
|
+
* already running; `instruction` (up to 300 characters) steers it: "gold border, our logo". */
|
|
7719
|
+
// `mode: "edit"` changes the post's current picture as `instruction` says. Another picture
|
|
7720
|
+
// being made here: this one waits its turn (status queued).
|
|
7721
|
+
// `count: 4` makes a set of 4 pictures from one storyboard (one AI generation), with words on each in
|
|
7722
|
+
// `textEffect` (an id from socialTextEffects; omit = Maggie picks).
|
|
7723
|
+
regenerateSocialImage: (id, opts = {}) => post(`${base}/social/${id}/image/regenerate`, {
|
|
7724
|
+
...opts.style ? { style: opts.style } : {},
|
|
7725
|
+
...opts.replace ? { replace: true } : {},
|
|
7726
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7727
|
+
...opts.mode === "edit" ? { mode: "edit" } : {},
|
|
7728
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7729
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7730
|
+
}),
|
|
7731
|
+
/** Change a picture Maggie made without a new one (free): the words on each picture, its style, its text
|
|
7732
|
+
* style, the order of a set, or drop pictures from it. `words` follow the new order when `order` is given. */
|
|
7733
|
+
editSocialPicture: (id, changes) => api2.request(`${base}/social/${id}/picture`, {
|
|
7734
|
+
method: "PATCH",
|
|
7735
|
+
headers: { "Content-Type": "application/json" },
|
|
7736
|
+
body: JSON.stringify(changes)
|
|
7737
|
+
}),
|
|
7738
|
+
/** A title and a line on your own photo(s) in your look (Maggie plans the words; no AI picture, free). */
|
|
7739
|
+
wordsOnPhoto: (id, style = "real_photo") => post(`${base}/social/${id}/picture/words`, { style }),
|
|
7740
|
+
/** The brand's picture look (made the first time it's asked for). */
|
|
7741
|
+
socialLook: () => api2.request(`${base}/social/look`),
|
|
7742
|
+
updateSocialLook: (changes) => put(`${base}/social/look`, changes),
|
|
7743
|
+
/** Maggie looks again and suggests a new look (one AI generation). */
|
|
7744
|
+
suggestSocialLook: () => post(`${base}/social/look/suggest`, {}),
|
|
7745
|
+
/** A sample picture of a style in the brand's look: a data: URL. */
|
|
7746
|
+
socialLookSample: (style) => api2.request(`${base}/social/look/sample?style=${encodeURIComponent(style)}`),
|
|
7747
|
+
/** The text effects for the words on a set of pictures, each with a sample picture. */
|
|
7748
|
+
socialTextEffects: () => api2.request(`${base}/social/text-effects`),
|
|
7749
|
+
/** Your own photo (base64 JPEG) as the post's picture: no AI, no charge; only before it goes out.
|
|
7750
|
+
* With `instruction`, Maggie changes the photo as it says, in the background (one AI generation). */
|
|
7751
|
+
uploadSocialPhoto: (id, photo, opts = {}) => post(`${base}/social/${id}/image`, {
|
|
7752
|
+
photo,
|
|
7753
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
|
|
7754
|
+
}),
|
|
7755
|
+
/** A new picture for every post here without one (or these), one after another in the background. */
|
|
7756
|
+
fillSocialImages: (opts = {}) => post(
|
|
7757
|
+
`${base}/social/images/fill`,
|
|
7758
|
+
{
|
|
7759
|
+
...opts.item_ids ? { item_ids: opts.item_ids } : {},
|
|
7760
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7761
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7762
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7763
|
+
}
|
|
7764
|
+
),
|
|
7765
|
+
/** Stop the picture being made (it's thrown away; the next in line starts). `all` empties the line too. */
|
|
7766
|
+
stopSocialImage: (all = false) => api2.request(`${base}/social/image-job${all ? "?all=true" : ""}`, {
|
|
7767
|
+
method: "DELETE"
|
|
7768
|
+
}),
|
|
7769
|
+
/** Maggie changes the post as asked, in 3-255 characters (one AI generation). */
|
|
7770
|
+
rewriteSocial: (id, instruction) => post(`${base}/social/${id}/rewrite`, { instruction }),
|
|
7771
|
+
/** Maggie plans and writes a series; it waits for one OK. */
|
|
7772
|
+
/** A series of `count` (1-7) posts on one theme; they are ordinary posts (Review), labelled with the series. */
|
|
7773
|
+
createSocialSeries: (input) => post(`${base}/social/series`, input),
|
|
7774
|
+
approveSocialSeries: (id) => post(`${base}/social/series/${id}/approve`),
|
|
7775
|
+
/** A post from photos (and a line about them): it waits for your OK like any other. */
|
|
7776
|
+
socialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7777
|
+
photos,
|
|
7778
|
+
brief: brief2,
|
|
7779
|
+
...channels ? { channels } : {}
|
|
7780
|
+
}),
|
|
7781
|
+
// In the background: the answer is the job at once; watch `write_jobs` in GET /social.
|
|
7782
|
+
startSocialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7783
|
+
photos,
|
|
7784
|
+
brief: brief2,
|
|
7785
|
+
background: true,
|
|
7786
|
+
...channels ? { channels } : {}
|
|
7787
|
+
}),
|
|
7788
|
+
startSocialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7789
|
+
source_post_id: sourcePostId,
|
|
7790
|
+
background: true,
|
|
7791
|
+
...channels ? { channels } : {}
|
|
7792
|
+
}),
|
|
7793
|
+
startSocialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7794
|
+
days,
|
|
7795
|
+
timezone,
|
|
7796
|
+
background: true,
|
|
7797
|
+
...channels ? { channels } : {}
|
|
7798
|
+
}),
|
|
7799
|
+
startWriteSocialIdea: (id, channels) => post(`${base}/social/ideas/${id}/write`, {
|
|
7800
|
+
background: true,
|
|
7801
|
+
...channels ? { channels } : {}
|
|
7802
|
+
}),
|
|
7511
7803
|
/** Disconnect the website's LinkedIn account. */
|
|
7512
7804
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
7513
7805
|
/** Published articles a LinkedIn post can be made from. */
|
|
7514
7806
|
socialSources: (search = "") => api2.request(
|
|
7515
7807
|
`${base}/social/sources?search=${encodeURIComponent(search)}`
|
|
7516
7808
|
),
|
|
7517
|
-
/** Maggie writes a
|
|
7518
|
-
socialFromPost: (sourcePostId) => post(`${base}/social/from-post`, {
|
|
7809
|
+
/** Maggie writes a post about one article for each channel; `failed` lists a channel that could not be written while the others were. */
|
|
7810
|
+
socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7811
|
+
source_post_id: sourcePostId,
|
|
7812
|
+
...channels ? { channels } : {}
|
|
7813
|
+
}),
|
|
7519
7814
|
/** Drafts for the next 1-7 days from the best recent articles. */
|
|
7520
|
-
socialPlan: (days, timezone) => post(`${base}/social/plan`, {
|
|
7815
|
+
socialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7816
|
+
days,
|
|
7817
|
+
timezone,
|
|
7818
|
+
...channels ? { channels } : {}
|
|
7819
|
+
}),
|
|
7521
7820
|
/** Campaign ideas waiting to be written (this person's, newest campaign first). */
|
|
7522
7821
|
socialIdeas: (page = 1, perPage = 20) => api2.request(
|
|
7523
7822
|
`${base}/social/ideas?page=${page}&per_page=${perPage}`
|
|
@@ -7537,14 +7836,41 @@ function domainApi(api2, domain) {
|
|
|
7537
7836
|
`${base}/social/campaigns/${id}/start`,
|
|
7538
7837
|
options
|
|
7539
7838
|
),
|
|
7540
|
-
/**
|
|
7541
|
-
|
|
7839
|
+
/**
|
|
7840
|
+
* Maggie writes the post for this idea (1 credit); it waits in Upcoming.
|
|
7841
|
+
* With `written` ({ linkedin: { body } }, from socialIdeaBrief), the user's own
|
|
7842
|
+
* assistant wrote it: saved through the same checks, for the same credit.
|
|
7843
|
+
*/
|
|
7844
|
+
writeSocialIdea: (id, written, channels) => post(
|
|
7845
|
+
`${base}/social/ideas/${id}/write`,
|
|
7846
|
+
written || channels ? { ...written ? { written } : {}, ...channels ? { channels } : {} } : void 0
|
|
7847
|
+
),
|
|
7848
|
+
/** What Maggie would write this idea's post from: the prompt, JSON shape and length limits (free). */
|
|
7849
|
+
socialIdeaBrief: (id) => api2.request(`${base}/social/ideas/${id}/brief`),
|
|
7542
7850
|
/** Change a LinkedIn post's text before it goes out. */
|
|
7543
7851
|
updateSocial: (id, socialCopy) => api2.request(`${base}/social/${id}`, {
|
|
7544
7852
|
method: "PATCH",
|
|
7545
7853
|
headers: { "Content-Type": "application/json" },
|
|
7546
7854
|
body: JSON.stringify({ social_copy: socialCopy })
|
|
7547
7855
|
}),
|
|
7856
|
+
/** The post's link: it replaces the old one in the text, and the preview card shows the new page. */
|
|
7857
|
+
setSocialLink: (id, link) => api2.request(`${base}/social/${id}`, {
|
|
7858
|
+
method: "PATCH",
|
|
7859
|
+
headers: { "Content-Type": "application/json" },
|
|
7860
|
+
body: JSON.stringify({ link })
|
|
7861
|
+
}),
|
|
7862
|
+
/** Go out as the post's image, or as a link preview. */
|
|
7863
|
+
setSocialPublishMode: (id, mode) => api2.request(`${base}/social/${id}`, {
|
|
7864
|
+
method: "PATCH",
|
|
7865
|
+
headers: { "Content-Type": "application/json" },
|
|
7866
|
+
body: JSON.stringify({ publish_mode: mode })
|
|
7867
|
+
}),
|
|
7868
|
+
/** A Google Business post's button (LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL); "" = Learn more. */
|
|
7869
|
+
setSocialButton: (id, ctaType) => api2.request(`${base}/social/${id}`, {
|
|
7870
|
+
method: "PATCH",
|
|
7871
|
+
headers: { "Content-Type": "application/json" },
|
|
7872
|
+
body: JSON.stringify({ cta_type: ctaType })
|
|
7873
|
+
}),
|
|
7548
7874
|
resumeSocial: () => post(`${base}/social/resume`),
|
|
7549
7875
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7550
7876
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
@@ -7552,6 +7878,9 @@ function domainApi(api2, domain) {
|
|
|
7552
7878
|
visibility: () => api2.request(`${base}/visibility`),
|
|
7553
7879
|
searchConsole: (period2 = "30d") => api2.request(`${base}/search-console?period=${period2}`),
|
|
7554
7880
|
searchConsoleQueries: (period2 = "30d", filter = "top", limit = 20) => api2.request(`${base}/search-console/queries?period=${period2}&filter=${filter}&limit=${limit}`),
|
|
7881
|
+
/** A Google sign-in link that connects Search Console for this website (open it in a
|
|
7882
|
+
* browser; back at returnTo with ?gsc=connected or ?gsc=error&message=…). 10 minutes. */
|
|
7883
|
+
connectSearchConsole: (returnTo = "close") => post(`${base}/search-console/connect`, { return_to: returnTo }),
|
|
7555
7884
|
/** Fresh numbers from Google now; at most once an hour per website. */
|
|
7556
7885
|
syncSearchConsole: () => post(`${base}/search-console/sync`),
|
|
7557
7886
|
contentPerformance: (sort = "clicks", band, limit = 20) => api2.request(
|
|
@@ -7574,6 +7903,8 @@ function domainApi(api2, domain) {
|
|
|
7574
7903
|
planCampaign: (id) => post(`${base}/social/campaigns/${id}/plan`),
|
|
7575
7904
|
pauseCampaign: (id) => post(`${base}/social/campaigns/${id}/pause`),
|
|
7576
7905
|
resumeCampaign: (id) => post(`${base}/social/campaigns/${id}/resume`),
|
|
7906
|
+
/** End a series: its posts that haven't gone out are dropped; published ones stay on LinkedIn. */
|
|
7907
|
+
endCampaign: (id) => post(`${base}/social/campaigns/${id}/end`),
|
|
7577
7908
|
setApprovalMode: (mode) => api2.request(`${base}/social/approval-mode`, {
|
|
7578
7909
|
method: "PUT",
|
|
7579
7910
|
headers: { "Content-Type": "application/json" },
|
|
@@ -7590,6 +7921,22 @@ function domainApi(api2, domain) {
|
|
|
7590
7921
|
}),
|
|
7591
7922
|
/** What Maggie changed for you on this website, newest first (docs/18 MT-SEC-6). */
|
|
7592
7923
|
maggieActions: (limit = 30) => api2.request(`${base}/maggie/actions?limit=${limit}`),
|
|
7924
|
+
// Maggie Memory (docs/21): the user's own memory on this website.
|
|
7925
|
+
maggieMemory: () => api2.request(`${base}/maggie/memory`),
|
|
7926
|
+
maggieMemorySettings: (s) => put(`${base}/maggie/memory/settings`, s),
|
|
7927
|
+
addMaggieMemory: (item2) => post(`${base}/maggie/memory/items`, item2),
|
|
7928
|
+
updateMaggieMemory: (id, patch) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, {
|
|
7929
|
+
method: "PATCH",
|
|
7930
|
+
headers: { "Content-Type": "application/json" },
|
|
7931
|
+
body: JSON.stringify(patch)
|
|
7932
|
+
}),
|
|
7933
|
+
deleteMaggieMemory: (id) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, { method: "DELETE" }),
|
|
7934
|
+
/** Learn from recent activity now; `full` rebuilds the learned items from scratch. */
|
|
7935
|
+
learnMaggieMemory: (o = {}) => post(`${base}/maggie/memory/learn`, o),
|
|
7936
|
+
/** Forget what was learned (one channel, or all); the user's own items stay. */
|
|
7937
|
+
clearMaggieMemory: (channel) => api2.request(`${base}/maggie/memory${channel ? `?channel=${encodeURIComponent(channel)}` : ""}`, {
|
|
7938
|
+
method: "DELETE"
|
|
7939
|
+
}),
|
|
7593
7940
|
// docs/19 P3: autopilot cadence and publish-now.
|
|
7594
7941
|
autopilot: () => api2.request(`${base}/autopilot`),
|
|
7595
7942
|
/** Posts per day for pushed blogs (one of `choices`, up to the plan's limit). */
|
|
@@ -7753,6 +8100,11 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
|
|
|
7753
8100
|
|
|
7754
8101
|
// ../../packages/domain/src/copy/maggie.ts
|
|
7755
8102
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
8103
|
+
var named = (title, id) => {
|
|
8104
|
+
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
8105
|
+
const s = String(id ?? "").trim();
|
|
8106
|
+
return s && s.length <= 8 ? ` #${s}` : "";
|
|
8107
|
+
};
|
|
7756
8108
|
var list = (xs, n = 5) => {
|
|
7757
8109
|
const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
|
|
7758
8110
|
return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
|
|
@@ -7835,41 +8187,45 @@ var confirmCopy = {
|
|
|
7835
8187
|
"zh-Hant": (a) => `\u70BA ${list(a.target_audience)} \u5EFA\u7ACB\u54C1\u724C\u8A9E\u6C23 ${q(a.name ?? "\u65B0\u8A9E\u6C23")}\u3002`
|
|
7836
8188
|
},
|
|
7837
8189
|
brand_voice_update_style: {
|
|
7838
|
-
en: (a) => `Change how the brand voice
|
|
7839
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u54C1\u724C\u8A9E\u6C23
|
|
8190
|
+
en: (a) => `Change how the brand voice${named(a.name, a.id)} writes: ${keys(a).filter((k) => k !== "id" && k !== "name").join(", ")}.`,
|
|
8191
|
+
"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`
|
|
7840
8192
|
},
|
|
7841
8193
|
brand_voice_set_default: {
|
|
7842
|
-
en: (a) => `Make
|
|
7843
|
-
"zh-Hant": (a) => `\u628A
|
|
8194
|
+
en: (a) => `Make${named(a.name, a.id)} your default brand voice.`,
|
|
8195
|
+
"zh-Hant": (a) => `\u628A${named(a.name, a.id)} \u8A2D\u70BA\u9810\u8A2D\u54C1\u724C\u8A9E\u6C23\u3002`
|
|
7844
8196
|
},
|
|
7845
8197
|
brand_voice_delete: {
|
|
7846
|
-
en: (a) => `Delete the brand voice
|
|
7847
|
-
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23
|
|
8198
|
+
en: (a) => `Delete the brand voice${named(a.name, a.id)}. This can't be undone.`,
|
|
8199
|
+
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23${named(a.name, a.id)}\uFF0C\u522A\u9664\u5F8C\u7121\u6CD5\u6062\u5FA9\u3002`
|
|
7848
8200
|
},
|
|
7849
8201
|
// ------------------------------------------------------------------- blog
|
|
7850
8202
|
draft_update: {
|
|
7851
|
-
en: (a) => `Change the draft
|
|
7852
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8349\u7A3F
|
|
8203
|
+
en: (a) => `Change the draft${named(a.title_for_card, a.id)}: ${keys(a).filter((k) => !["id", "title_for_card"].includes(k)).join(", ")}.`,
|
|
8204
|
+
"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`
|
|
7853
8205
|
},
|
|
7854
8206
|
draft_rewrite: {
|
|
7855
|
-
en: (a) => a.mode === "fix" ? `Let Maggie fix the draft
|
|
7856
|
-
"zh-Hant": (a) => a.mode === "fix" ? `\u8B93 Maggie \u4FEE\u6B63\u8349\u7A3F
|
|
8207
|
+
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.`,
|
|
8208
|
+
"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`
|
|
8209
|
+
},
|
|
8210
|
+
draft_rewrite_save: {
|
|
8211
|
+
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.`,
|
|
8212
|
+
"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`
|
|
7857
8213
|
},
|
|
7858
8214
|
draft_delete: {
|
|
7859
|
-
en: (a) => `Delete the draft
|
|
7860
|
-
"zh-Hant": (a) => `\u522A\u9664\u8349\u7A3F
|
|
8215
|
+
en: (a) => `Delete the draft${named(a.title, a.id)}. Its idea is dropped too and the credit is not refunded.`,
|
|
8216
|
+
"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`
|
|
7861
8217
|
},
|
|
7862
8218
|
plan_move: {
|
|
7863
|
-
en: (a) => `Move
|
|
7864
|
-
"zh-Hant": (a) => `\u628A
|
|
8219
|
+
en: (a) => `Move${named(a.title, a.id)} ${a.to === "top" ? "to the top of" : a.to} the plan.`,
|
|
8220
|
+
"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`
|
|
7865
8221
|
},
|
|
7866
8222
|
plan_update: {
|
|
7867
|
-
en: (a) => `Change the planned post
|
|
7868
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0
|
|
8223
|
+
en: (a) => `Change the planned post${named(a.title_for_card, a.id)}.`,
|
|
8224
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0${named(a.title_for_card, a.id)}\u3002`
|
|
7869
8225
|
},
|
|
7870
8226
|
plan_remove: {
|
|
7871
|
-
en: (a) => `Take
|
|
7872
|
-
"zh-Hant": (a) => `\u628A
|
|
8227
|
+
en: (a) => `Take${named(a.title, a.id)} out of the plan (its idea can be restored).`,
|
|
8228
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u79FB\u51FA\u8A08\u5283\uFF08idea \u53EF\u4EE5\u6062\u5FA9\uFF09\u3002`
|
|
7873
8229
|
},
|
|
7874
8230
|
idea_add: { en: (a) => `Add the idea ${q(a.url || a.text)}.`, "zh-Hant": (a) => `\u52A0\u5165 idea ${q(a.url || a.text)}\u3002` },
|
|
7875
8231
|
ideas_generate: {
|
|
@@ -7881,24 +8237,24 @@ var confirmCopy = {
|
|
|
7881
8237
|
"zh-Hant": (a) => `\u5F9E ${a.sitemap_url || "\u4F60\u7684 sitemap"} \u5C0E\u5165\u6700\u591A ${a.limit || 10} \u500B idea\u3002`
|
|
7882
8238
|
},
|
|
7883
8239
|
idea_update: {
|
|
7884
|
-
en: (a) => `Change the idea
|
|
7885
|
-
"zh-Hant": (a) => `\u4FEE\u6539 idea
|
|
8240
|
+
en: (a) => `Change the idea${named(a.title_for_card, a.id)}.`,
|
|
8241
|
+
"zh-Hant": (a) => `\u4FEE\u6539 idea${named(a.title_for_card, a.id)}\u3002`
|
|
7886
8242
|
},
|
|
7887
8243
|
idea_priority: {
|
|
7888
|
-
en: (a) => `${Number(a.delta) > 0 ? "Raise" : "Lower"} the priority of
|
|
7889
|
-
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}
|
|
8244
|
+
en: (a) => `${Number(a.delta) > 0 ? "Raise" : "Lower"} the priority of${named(a.title, a.id)}.`,
|
|
8245
|
+
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
|
|
7890
8246
|
},
|
|
7891
8247
|
idea_plan: {
|
|
7892
|
-
en: (a) => `Add
|
|
7893
|
-
"zh-Hant": (a) => `\u628A
|
|
8248
|
+
en: (a) => `Add${named(a.title, a.id)} to the end of the plan.`,
|
|
8249
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u52A0\u5230\u8A08\u5283\u6700\u5F8C\u3002`
|
|
7894
8250
|
},
|
|
7895
8251
|
idea_discard: {
|
|
7896
|
-
en: (a) => `Remove the idea
|
|
7897
|
-
"zh-Hant": (a) => `\u522A\u9664 idea
|
|
8252
|
+
en: (a) => `Remove the idea${named(a.title, a.id)}.`,
|
|
8253
|
+
"zh-Hant": (a) => `\u522A\u9664 idea${named(a.title, a.id)}\u3002`
|
|
7898
8254
|
},
|
|
7899
8255
|
idea_restore: {
|
|
7900
|
-
en: (a) => `Bring back the idea
|
|
7901
|
-
"zh-Hant": (a) => `\u6062\u5FA9 idea
|
|
8256
|
+
en: (a) => `Bring back the idea${named(a.title, a.id)}.`,
|
|
8257
|
+
"zh-Hant": (a) => `\u6062\u5FA9 idea${named(a.title, a.id)}\u3002`
|
|
7902
8258
|
},
|
|
7903
8259
|
autopilot_set_paused: {
|
|
7904
8260
|
en: (a) => a.paused ? "Pause publishing blog posts." : "Resume publishing blog posts.",
|
|
@@ -7909,8 +8265,8 @@ var confirmCopy = {
|
|
|
7909
8265
|
"zh-Hant": (a) => `\u6BCF\u5929\u767C\u4F48 ${a.posts_per_day} \u7BC7\u90E8\u843D\u683C\u6587\u7AE0\u3002`
|
|
7910
8266
|
},
|
|
7911
8267
|
post_publish_now: {
|
|
7912
|
-
en: (a) => `Publish
|
|
7913
|
-
"zh-Hant": (a) => `\u7ACB\u5373\u628A
|
|
8268
|
+
en: (a) => `Publish${named(a.title, a.id)} to your website now. Everyone can see it once it's out.`,
|
|
8269
|
+
"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`
|
|
7914
8270
|
},
|
|
7915
8271
|
rewrite_request: {
|
|
7916
8272
|
en: (a) => `Rewrite the live article ${a.url}${a.instructions ? ` (${String(a.instructions).slice(0, 80)})` : ""}. The published page will change.`,
|
|
@@ -7924,11 +8280,11 @@ var confirmCopy = {
|
|
|
7924
8280
|
social_approve: {
|
|
7925
8281
|
en: (a) => {
|
|
7926
8282
|
const n = ids(a);
|
|
7927
|
-
return n.length > 1 ? `Approve ${n.length} LinkedIn posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes
|
|
8283
|
+
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.`;
|
|
7928
8284
|
},
|
|
7929
8285
|
"zh-Hant": (a) => {
|
|
7930
8286
|
const n = ids(a);
|
|
7931
|
-
return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7 LinkedIn \u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\
|
|
8287
|
+
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`;
|
|
7932
8288
|
}
|
|
7933
8289
|
},
|
|
7934
8290
|
social_skip: { en: (a) => `Skip LinkedIn post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E LinkedIn \u8CBC\u6587 #${a.id}\u3002` },
|
|
@@ -7936,21 +8292,29 @@ var confirmCopy = {
|
|
|
7936
8292
|
en: (a) => `Bring back LinkedIn post #${a.id}.`,
|
|
7937
8293
|
"zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
|
|
7938
8294
|
},
|
|
8295
|
+
social_image_new: {
|
|
8296
|
+
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" : ""}.`,
|
|
8297
|
+
"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`
|
|
8298
|
+
},
|
|
8299
|
+
social_image_stop: {
|
|
8300
|
+
en: () => "Stop the LinkedIn picture being made; the post keeps the one it had.",
|
|
8301
|
+
"zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684 LinkedIn \u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
|
|
8302
|
+
},
|
|
7939
8303
|
social_move: {
|
|
7940
|
-
en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the
|
|
7941
|
-
"zh-Hant": (a) => `\u628A LinkedIn \u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\
|
|
8304
|
+
en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the top of" : a.to} Upcoming.`,
|
|
8305
|
+
"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`
|
|
7942
8306
|
},
|
|
7943
8307
|
social_set_paused: {
|
|
7944
8308
|
en: (a) => a.paused ? "Pause all LinkedIn posting." : "Resume LinkedIn posting.",
|
|
7945
8309
|
"zh-Hant": (a) => a.paused ? "\u66AB\u505C\u6240\u6709 LinkedIn \u767C\u6587\u3002" : "\u6062\u5FA9 LinkedIn \u767C\u6587\u3002"
|
|
7946
8310
|
},
|
|
7947
8311
|
social_from_article: {
|
|
7948
|
-
en: (a) => `Write a LinkedIn post about ${q(a.title || a.source_id)}; it
|
|
7949
|
-
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} \u5BEB\u4E00\u7BC7 LinkedIn \u8CBC\u6587\uFF0C\
|
|
8312
|
+
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.`,
|
|
8313
|
+
"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`
|
|
7950
8314
|
},
|
|
7951
8315
|
social_plan_days: {
|
|
7952
|
-
en: (a) => `Write LinkedIn posts for the next ${a.days} day${Number(a.days) === 1 ? "" : "s"} from your best articles.`,
|
|
7953
|
-
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684 LinkedIn \u8CBC\u6587\u3002`
|
|
8316
|
+
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.`,
|
|
8317
|
+
"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`
|
|
7954
8318
|
},
|
|
7955
8319
|
social_idea_add: {
|
|
7956
8320
|
en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
|
|
@@ -7965,12 +8329,30 @@ var confirmCopy = {
|
|
|
7965
8329
|
"zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
|
|
7966
8330
|
},
|
|
7967
8331
|
social_idea_write: {
|
|
7968
|
-
en: (a) => `Write the LinkedIn post for idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}.`,
|
|
7969
|
-
"zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB LinkedIn \u8CBC\u6587\u3002`
|
|
8332
|
+
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.`,
|
|
8333
|
+
"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`
|
|
8334
|
+
},
|
|
8335
|
+
social_idea_save: {
|
|
8336
|
+
en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
|
|
8337
|
+
"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)}`
|
|
7970
8338
|
},
|
|
7971
8339
|
campaign_start: {
|
|
7972
|
-
en: (a) => `Start the
|
|
7973
|
-
"zh-Hant": (a) => `${a.start_date ? `\u5F9E ${a.start_date}` : "\u5F9E\u4ECA\u5929"}\u958B\u59CB\
|
|
8340
|
+
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.`,
|
|
8341
|
+
"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`
|
|
8342
|
+
},
|
|
8343
|
+
// ---------------------------------------------------------- Maggie Memory
|
|
8344
|
+
memory_remember: {
|
|
8345
|
+
en: (a) => `Remember${a.channel === "blog" ? " for blog posts" : a.channel === "linkedin" ? " for LinkedIn" : ""}: ${String(a.text ?? "").slice(0, 160)}`,
|
|
8346
|
+
"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)}`
|
|
8347
|
+
},
|
|
8348
|
+
memory_forget: {
|
|
8349
|
+
en: (a) => `Forget${a.text_for_card ? ` ${q(a.text_for_card)}` : " this"} from your Maggie Memory.`,
|
|
8350
|
+
"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`
|
|
8351
|
+
},
|
|
8352
|
+
// ---------------------------------------------------------- Help Maggie grow
|
|
8353
|
+
feedback_send: {
|
|
8354
|
+
en: (a) => `Send this to the cmo.so team: ${q(String(a.summary ?? "").slice(0, 200))}`,
|
|
8355
|
+
"zh-Hant": (a) => `\u628A\u9019\u5247\u56DE\u994B\u9001\u7D66 cmo.so \u5718\u968A\uFF1A${q(String(a.summary ?? "").slice(0, 200))}`
|
|
7974
8356
|
},
|
|
7975
8357
|
social_voice_learn: {
|
|
7976
8358
|
en: () => "Learn your LinkedIn voice from this post and save it as your default.",
|
|
@@ -8000,17 +8382,29 @@ var confirmCopy = {
|
|
|
8000
8382
|
en: () => "Make a link for you to connect LinkedIn.",
|
|
8001
8383
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8002
8384
|
},
|
|
8385
|
+
social_approve_all: {
|
|
8386
|
+
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.",
|
|
8387
|
+
"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"
|
|
8388
|
+
},
|
|
8389
|
+
series_create: {
|
|
8390
|
+
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.`,
|
|
8391
|
+
"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`
|
|
8392
|
+
},
|
|
8393
|
+
series_approve: {
|
|
8394
|
+
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.`,
|
|
8395
|
+
"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`
|
|
8396
|
+
},
|
|
8003
8397
|
campaign_create: {
|
|
8004
|
-
en: (a) => `
|
|
8005
|
-
"zh-Hant": (a) => `\
|
|
8398
|
+
en: (a) => `Plan the LinkedIn series ${q(a.name)}.`,
|
|
8399
|
+
"zh-Hant": (a) => `\u898F\u5283 LinkedIn \u7CFB\u5217 ${q(a.name)}\u3002`
|
|
8006
8400
|
},
|
|
8007
8401
|
campaign_pause: {
|
|
8008
|
-
en: (a) => `${a.paused ? "Pause" : "Resume"} the
|
|
8009
|
-
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\
|
|
8402
|
+
en: (a) => `${a.paused ? "Pause" : "Resume"} the series${named(a.name, a.id)}.`,
|
|
8403
|
+
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\u7CFB\u5217${named(a.name, a.id)}\u3002`
|
|
8010
8404
|
},
|
|
8011
8405
|
social_approval_mode_set: {
|
|
8012
|
-
en: (a) => a.mode === "auto" ? "Post LinkedIn
|
|
8013
|
-
"zh-Hant": (a) => a.mode === "auto" ? "LinkedIn \
|
|
8406
|
+
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.",
|
|
8407
|
+
"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"
|
|
8014
8408
|
},
|
|
8015
8409
|
// ---------------------------------------------------------------- reports
|
|
8016
8410
|
gsc_sync: {
|
|
@@ -8115,7 +8509,7 @@ function ws(ctx) {
|
|
|
8115
8509
|
if (!ctx.domain) throw new Error("No website is selected yet. Add one first.");
|
|
8116
8510
|
return domainApi(ctx.api, ctx.domain);
|
|
8117
8511
|
}
|
|
8118
|
-
var clip = (
|
|
8512
|
+
var clip = (text3, n = 280) => typeof text3 === "string" ? text3.length > n ? `${text3.slice(0, n)}\u2026` : text3 : text3 ?? null;
|
|
8119
8513
|
var plain = (html) => String(html ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
8120
8514
|
var untrusted = (value) => ({ untrusted: value });
|
|
8121
8515
|
function upsert(list2, item2, key) {
|
|
@@ -8463,9 +8857,9 @@ var blogTools = [
|
|
|
8463
8857
|
id: { type: "string" },
|
|
8464
8858
|
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8465
8859
|
instruction: { type: "string", description: "What to change, for mode edit" },
|
|
8466
|
-
title: { type: "string", description: "
|
|
8860
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8467
8861
|
},
|
|
8468
|
-
required: ["id", "mode"]
|
|
8862
|
+
required: ["id", "mode", "title"]
|
|
8469
8863
|
},
|
|
8470
8864
|
group: "blog",
|
|
8471
8865
|
need: write,
|
|
@@ -8481,13 +8875,73 @@ var blogTools = [
|
|
|
8481
8875
|
return { saved: true, lost_links: out.lost_links, ...untrusted({ preview: clip(out.markdown, 600) }) };
|
|
8482
8876
|
}
|
|
8483
8877
|
}),
|
|
8878
|
+
tool({
|
|
8879
|
+
name: "draft_rewrite_brief",
|
|
8880
|
+
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.",
|
|
8881
|
+
parameters: {
|
|
8882
|
+
type: "object",
|
|
8883
|
+
properties: {
|
|
8884
|
+
id: { type: "string" },
|
|
8885
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8886
|
+
instruction: { type: "string", description: "What to change, for mode edit" }
|
|
8887
|
+
},
|
|
8888
|
+
required: ["id", "mode"]
|
|
8889
|
+
},
|
|
8890
|
+
group: "blog",
|
|
8891
|
+
need: read,
|
|
8892
|
+
risk: "R0",
|
|
8893
|
+
assistantWrites: true,
|
|
8894
|
+
run: async (ctx, { id, mode, instruction }) => {
|
|
8895
|
+
const p = await ws(ctx).post(id);
|
|
8896
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8897
|
+
if (mode === "edit" && !instruction?.trim()) throw new Error("Say what to change.");
|
|
8898
|
+
return {
|
|
8899
|
+
id: p.id,
|
|
8900
|
+
title: p.title,
|
|
8901
|
+
mode,
|
|
8902
|
+
task: rewriteTask(mode, instruction),
|
|
8903
|
+
links_to_keep: links(p.markdown).length,
|
|
8904
|
+
...untrusted({ markdown: p.markdown })
|
|
8905
|
+
};
|
|
8906
|
+
}
|
|
8907
|
+
}),
|
|
8908
|
+
tool({
|
|
8909
|
+
name: "draft_rewrite_save",
|
|
8910
|
+
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).",
|
|
8911
|
+
parameters: {
|
|
8912
|
+
type: "object",
|
|
8913
|
+
properties: {
|
|
8914
|
+
id: { type: "string" },
|
|
8915
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8916
|
+
markdown: { type: "string", description: "The whole rewritten post" },
|
|
8917
|
+
title: { type: "string", description: "The draft's title, for the approval card" }
|
|
8918
|
+
},
|
|
8919
|
+
required: ["id", "mode", "markdown"]
|
|
8920
|
+
},
|
|
8921
|
+
group: "blog",
|
|
8922
|
+
need: write,
|
|
8923
|
+
risk: "W1",
|
|
8924
|
+
assistantWrites: true,
|
|
8925
|
+
run: async (ctx, { id, mode, markdown }) => {
|
|
8926
|
+
const api2 = ws(ctx);
|
|
8927
|
+
const p = await api2.post(id);
|
|
8928
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8929
|
+
const check2 = checkRewrite(p.markdown, String(markdown ?? ""), mode === "fix" ? "fix" : "edit");
|
|
8930
|
+
if (check2.error) throw new Error(check2.error);
|
|
8931
|
+
await api2.updatePost(id, { markdown });
|
|
8932
|
+
return { saved: true, lost_links: check2.lost };
|
|
8933
|
+
}
|
|
8934
|
+
}),
|
|
8484
8935
|
tool({
|
|
8485
8936
|
name: "draft_delete",
|
|
8486
8937
|
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.",
|
|
8487
8938
|
parameters: {
|
|
8488
8939
|
type: "object",
|
|
8489
|
-
properties: {
|
|
8490
|
-
|
|
8940
|
+
properties: {
|
|
8941
|
+
id: { type: "string" },
|
|
8942
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8943
|
+
},
|
|
8944
|
+
required: ["id", "title"]
|
|
8491
8945
|
},
|
|
8492
8946
|
group: "blog",
|
|
8493
8947
|
need: write,
|
|
@@ -8502,9 +8956,9 @@ var blogTools = [
|
|
|
8502
8956
|
properties: {
|
|
8503
8957
|
id: { type: "string", description: "The planned post's id" },
|
|
8504
8958
|
to: { type: "string", enum: moves },
|
|
8505
|
-
title: { type: "string", description: "
|
|
8959
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8506
8960
|
},
|
|
8507
|
-
required: ["id", "to"]
|
|
8961
|
+
required: ["id", "to", "title"]
|
|
8508
8962
|
},
|
|
8509
8963
|
group: "blog",
|
|
8510
8964
|
need: write,
|
|
@@ -8535,8 +8989,11 @@ var blogTools = [
|
|
|
8535
8989
|
description: "Take a planned post out of the plan; its idea can be restored later.",
|
|
8536
8990
|
parameters: {
|
|
8537
8991
|
type: "object",
|
|
8538
|
-
properties: {
|
|
8539
|
-
|
|
8992
|
+
properties: {
|
|
8993
|
+
id: { type: "string" },
|
|
8994
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8995
|
+
},
|
|
8996
|
+
required: ["id", "title"]
|
|
8540
8997
|
},
|
|
8541
8998
|
group: "blog",
|
|
8542
8999
|
need: write,
|
|
@@ -8593,9 +9050,9 @@ var blogTools = [
|
|
|
8593
9050
|
need: write,
|
|
8594
9051
|
risk: "W2",
|
|
8595
9052
|
gates: ["LLM"],
|
|
8596
|
-
run: async (ctx, { url, text:
|
|
8597
|
-
if (!url && !
|
|
8598
|
-
return ws(ctx).addIdea(url ? { url } : { text:
|
|
9053
|
+
run: async (ctx, { url, text: text3 }) => {
|
|
9054
|
+
if (!url && !text3) throw new Error("Need a link or a sentence.");
|
|
9055
|
+
return ws(ctx).addIdea(url ? { url } : { text: text3 });
|
|
8599
9056
|
}
|
|
8600
9057
|
}),
|
|
8601
9058
|
tool({
|
|
@@ -8613,7 +9070,7 @@ var blogTools = [
|
|
|
8613
9070
|
need: write,
|
|
8614
9071
|
risk: "W2",
|
|
8615
9072
|
gates: ["LLM"],
|
|
8616
|
-
run: async (ctx, { count, brief }) => ws(ctx).generateIdeas(int(count, 1, 10, 3),
|
|
9073
|
+
run: async (ctx, { count, brief: brief2 }) => ws(ctx).generateIdeas(int(count, 1, 10, 3), brief2)
|
|
8617
9074
|
}),
|
|
8618
9075
|
tool({
|
|
8619
9076
|
name: "ideas_import_sitemap",
|
|
@@ -8658,9 +9115,9 @@ var blogTools = [
|
|
|
8658
9115
|
properties: {
|
|
8659
9116
|
id: { type: "string" },
|
|
8660
9117
|
delta: { type: "integer", minimum: -1, maximum: 1 },
|
|
8661
|
-
title: { type: "string", description: "
|
|
9118
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8662
9119
|
},
|
|
8663
|
-
required: ["id", "delta"]
|
|
9120
|
+
required: ["id", "delta", "title"]
|
|
8664
9121
|
},
|
|
8665
9122
|
group: "blog",
|
|
8666
9123
|
need: write,
|
|
@@ -8672,8 +9129,11 @@ var blogTools = [
|
|
|
8672
9129
|
description: "Add a waiting idea to the end of the plan (the plan holds at most 10).",
|
|
8673
9130
|
parameters: {
|
|
8674
9131
|
type: "object",
|
|
8675
|
-
properties: {
|
|
8676
|
-
|
|
9132
|
+
properties: {
|
|
9133
|
+
id: { type: "string" },
|
|
9134
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9135
|
+
},
|
|
9136
|
+
required: ["id", "title"]
|
|
8677
9137
|
},
|
|
8678
9138
|
group: "blog",
|
|
8679
9139
|
need: write,
|
|
@@ -8685,8 +9145,11 @@ var blogTools = [
|
|
|
8685
9145
|
description: "Remove one idea so it won't be written (it can be restored).",
|
|
8686
9146
|
parameters: {
|
|
8687
9147
|
type: "object",
|
|
8688
|
-
properties: {
|
|
8689
|
-
|
|
9148
|
+
properties: {
|
|
9149
|
+
id: { type: "string" },
|
|
9150
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9151
|
+
},
|
|
9152
|
+
required: ["id", "title"]
|
|
8690
9153
|
},
|
|
8691
9154
|
group: "blog",
|
|
8692
9155
|
need: write,
|
|
@@ -8698,8 +9161,11 @@ var blogTools = [
|
|
|
8698
9161
|
description: "Bring back a discarded idea (ideas_list status discarded).",
|
|
8699
9162
|
parameters: {
|
|
8700
9163
|
type: "object",
|
|
8701
|
-
properties: {
|
|
8702
|
-
|
|
9164
|
+
properties: {
|
|
9165
|
+
id: { type: "string" },
|
|
9166
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9167
|
+
},
|
|
9168
|
+
required: ["id", "title"]
|
|
8703
9169
|
},
|
|
8704
9170
|
group: "blog",
|
|
8705
9171
|
need: write,
|
|
@@ -8716,6 +9182,24 @@ var blogTools = [
|
|
|
8716
9182
|
run: async (ctx, { paused }) => paused ? ws(ctx).pauseAutopilot() : ws(ctx).resumeAutopilot()
|
|
8717
9183
|
})
|
|
8718
9184
|
];
|
|
9185
|
+
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.";
|
|
9186
|
+
var LINK = /\]\((\S+?)(?:\s+"[^"]*")?\)/g;
|
|
9187
|
+
var links = (markdown) => [...markdown.matchAll(LINK)].map((m) => m[1]);
|
|
9188
|
+
function rewriteTask(mode, instruction) {
|
|
9189
|
+
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 ?? ""}
|
|
9190
|
+
Change only what that needs; keep the rest as it is. ${KEEP}`;
|
|
9191
|
+
}
|
|
9192
|
+
function checkRewrite(before, after, mode) {
|
|
9193
|
+
const lost = links(before).filter((u) => !after.includes(u)).length;
|
|
9194
|
+
if (after.length < before.length * (mode === "fix" ? 0.8 : 0.3))
|
|
9195
|
+
return {
|
|
9196
|
+
error: "The new version is much shorter than the draft, so nothing was saved. Send the whole post.",
|
|
9197
|
+
lost
|
|
9198
|
+
};
|
|
9199
|
+
if (mode === "fix" && lost)
|
|
9200
|
+
return { error: `The fixed version dropped ${lost} link or image, so nothing was saved. Keep every one.`, lost };
|
|
9201
|
+
return { lost };
|
|
9202
|
+
}
|
|
8719
9203
|
|
|
8720
9204
|
// ../../packages/tools/src/brand.ts
|
|
8721
9205
|
var write2 = { resource: "overview", level: "write" };
|
|
@@ -9038,8 +9522,11 @@ var brandTools = [
|
|
|
9038
9522
|
description: "Delete one of the user's brand voices. Cannot be undone; put its name on the card.",
|
|
9039
9523
|
parameters: {
|
|
9040
9524
|
type: "object",
|
|
9041
|
-
properties: {
|
|
9042
|
-
|
|
9525
|
+
properties: {
|
|
9526
|
+
id: { type: "string" },
|
|
9527
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9528
|
+
},
|
|
9529
|
+
required: ["id", "name"]
|
|
9043
9530
|
},
|
|
9044
9531
|
group: "brand",
|
|
9045
9532
|
need: write2,
|
|
@@ -9066,6 +9553,65 @@ function brandGaps(b) {
|
|
|
9066
9553
|
return { complete: gaps.length === 0, gaps };
|
|
9067
9554
|
}
|
|
9068
9555
|
|
|
9556
|
+
// ../../packages/tools/src/feedback.ts
|
|
9557
|
+
var account = { resource: "account", level: "read" };
|
|
9558
|
+
var TYPES = ["bug", "quality", "idea", "praise", "other"];
|
|
9559
|
+
var text = (v, n) => String(v ?? "").trim().slice(0, n);
|
|
9560
|
+
var feedbackTools = [
|
|
9561
|
+
tool({
|
|
9562
|
+
name: "feedback_send",
|
|
9563
|
+
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.",
|
|
9564
|
+
parameters: {
|
|
9565
|
+
type: "object",
|
|
9566
|
+
properties: {
|
|
9567
|
+
type: {
|
|
9568
|
+
type: "string",
|
|
9569
|
+
enum: TYPES,
|
|
9570
|
+
description: "bug: did something wrong \xB7 quality: works but not good enough \xB7 idea: a new feature \xB7 praise \xB7 other"
|
|
9571
|
+
},
|
|
9572
|
+
summary: { type: "string", description: "One sentence, \u2264200 characters" },
|
|
9573
|
+
details: { type: "string", description: "What happened, \u22644000 characters" },
|
|
9574
|
+
expected: { type: "string", description: "For a bug: what the user expected" },
|
|
9575
|
+
actual: { type: "string", description: "For a bug: what happened instead" }
|
|
9576
|
+
},
|
|
9577
|
+
required: ["type", "summary"]
|
|
9578
|
+
},
|
|
9579
|
+
group: "account",
|
|
9580
|
+
need: account,
|
|
9581
|
+
risk: "W1",
|
|
9582
|
+
run: async (ctx, a) => {
|
|
9583
|
+
const f = await ctx.api.feedback.send({
|
|
9584
|
+
type: TYPES.find((t) => t === a.type) ?? "other",
|
|
9585
|
+
summary: text(a.summary, 200),
|
|
9586
|
+
...a.details ? { details: text(a.details, 4e3) } : {},
|
|
9587
|
+
...a.expected ? { expected: text(a.expected, 1e3) } : {},
|
|
9588
|
+
...a.actual ? { actual: text(a.actual, 1e3) } : {},
|
|
9589
|
+
domain: ctx.domain,
|
|
9590
|
+
source: ctx.client ?? "chat"
|
|
9591
|
+
});
|
|
9592
|
+
return { sent: true, id: f.id, status: f.status };
|
|
9593
|
+
}
|
|
9594
|
+
}),
|
|
9595
|
+
tool({
|
|
9596
|
+
name: "feedback_list",
|
|
9597
|
+
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.",
|
|
9598
|
+
parameters: none,
|
|
9599
|
+
group: "account",
|
|
9600
|
+
need: account,
|
|
9601
|
+
risk: "R0",
|
|
9602
|
+
run: async (ctx) => ({
|
|
9603
|
+
items: (await ctx.api.feedback.mine()).items.slice(0, 20).map((f) => ({
|
|
9604
|
+
type: f.type,
|
|
9605
|
+
summary: clip(f.summary, 200),
|
|
9606
|
+
status: f.status,
|
|
9607
|
+
reply: clip(f.reply, 600),
|
|
9608
|
+
new_reply: f.unread,
|
|
9609
|
+
sent: f.created_at
|
|
9610
|
+
}))
|
|
9611
|
+
})
|
|
9612
|
+
})
|
|
9613
|
+
];
|
|
9614
|
+
|
|
9069
9615
|
// ../../packages/tools/src/insights.ts
|
|
9070
9616
|
var read2 = { resource: "report", level: "read" };
|
|
9071
9617
|
var write3 = { resource: "report", level: "write" };
|
|
@@ -9157,14 +9703,14 @@ var insightTools = [
|
|
|
9157
9703
|
];
|
|
9158
9704
|
|
|
9159
9705
|
// ../../packages/tools/src/me.ts
|
|
9160
|
-
var
|
|
9706
|
+
var account2 = { resource: "account", level: "read" };
|
|
9161
9707
|
var meTools = [
|
|
9162
9708
|
tool({
|
|
9163
9709
|
name: "api_keys_list",
|
|
9164
9710
|
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).",
|
|
9165
9711
|
parameters: none,
|
|
9166
9712
|
group: "account",
|
|
9167
|
-
need:
|
|
9713
|
+
need: account2,
|
|
9168
9714
|
risk: "R0",
|
|
9169
9715
|
run: async ({ api: api2 }) => (await api2.account.keys()).items
|
|
9170
9716
|
}),
|
|
@@ -9173,7 +9719,7 @@ var meTools = [
|
|
|
9173
9719
|
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.",
|
|
9174
9720
|
parameters: none,
|
|
9175
9721
|
group: "account",
|
|
9176
|
-
need:
|
|
9722
|
+
need: account2,
|
|
9177
9723
|
risk: "R0",
|
|
9178
9724
|
run: async ({ api: api2 }) => (await api2.account.apps()).items.map((a) => ({
|
|
9179
9725
|
grant_id: a.grant_id,
|
|
@@ -9196,7 +9742,7 @@ var meTools = [
|
|
|
9196
9742
|
required: ["grant_id", "app"]
|
|
9197
9743
|
},
|
|
9198
9744
|
group: "account",
|
|
9199
|
-
need:
|
|
9745
|
+
need: account2,
|
|
9200
9746
|
risk: "W3",
|
|
9201
9747
|
run: async ({ api: api2 }, { grant_id }) => api2.account.revokeApp(String(grant_id))
|
|
9202
9748
|
}),
|
|
@@ -9205,7 +9751,7 @@ var meTools = [
|
|
|
9205
9751
|
description: "The user's latest invoices across the subscriptions they pay for: date, amount, status and a link to view or download each.",
|
|
9206
9752
|
parameters: none,
|
|
9207
9753
|
group: "billing",
|
|
9208
|
-
need:
|
|
9754
|
+
need: account2,
|
|
9209
9755
|
risk: "R0",
|
|
9210
9756
|
run: async ({ api: api2 }) => (await api2.account.invoices()).items.slice(0, 15).map((i) => ({
|
|
9211
9757
|
website: i.domain,
|
|
@@ -9221,7 +9767,7 @@ var meTools = [
|
|
|
9221
9767
|
description: "Change the user's display name (the only profile field Maggie changes; email and password are on ai.cmo.so).",
|
|
9222
9768
|
parameters: { type: "object", properties: { display_name: { type: "string" } }, required: ["display_name"] },
|
|
9223
9769
|
group: "account",
|
|
9224
|
-
need:
|
|
9770
|
+
need: account2,
|
|
9225
9771
|
risk: "W1",
|
|
9226
9772
|
idempotent: true,
|
|
9227
9773
|
run: async ({ api: api2 }, { display_name }) => api2.account.rename(String(display_name).slice(0, 80))
|
|
@@ -9231,7 +9777,7 @@ var meTools = [
|
|
|
9231
9777
|
description: "Which app notifications the user gets: LinkedIn posts to review (social_review), LinkedIn posts that failed (social_failed), new blog posts published (published).",
|
|
9232
9778
|
parameters: none,
|
|
9233
9779
|
group: "account",
|
|
9234
|
-
need:
|
|
9780
|
+
need: account2,
|
|
9235
9781
|
risk: "R0",
|
|
9236
9782
|
run: async ({ api: api2 }) => api2.account.notifications()
|
|
9237
9783
|
}),
|
|
@@ -9247,13 +9793,99 @@ var meTools = [
|
|
|
9247
9793
|
}
|
|
9248
9794
|
},
|
|
9249
9795
|
group: "account",
|
|
9250
|
-
need:
|
|
9796
|
+
need: account2,
|
|
9251
9797
|
risk: "W1",
|
|
9252
9798
|
idempotent: true,
|
|
9253
9799
|
run: async ({ api: api2 }, prefs) => api2.account.setNotifications(prefs)
|
|
9254
9800
|
})
|
|
9255
9801
|
];
|
|
9256
9802
|
|
|
9803
|
+
// ../../packages/tools/src/memory.ts
|
|
9804
|
+
var own = { resource: "overview", level: "read" };
|
|
9805
|
+
var SECTIONS = ["prefer", "always", "never"];
|
|
9806
|
+
var KINDS = ["tone", "length", "structure", "format", "wording", "topic", "image", "cta", "approval", "other"];
|
|
9807
|
+
var brief = (m) => ({
|
|
9808
|
+
enabled: m.enabled,
|
|
9809
|
+
channels: Object.fromEntries(
|
|
9810
|
+
MEMORY_CHANNELS.map((ch) => [
|
|
9811
|
+
ch,
|
|
9812
|
+
{
|
|
9813
|
+
summary: m.channels[ch]?.summary ?? "",
|
|
9814
|
+
items: (m.channels[ch]?.items ?? []).map((i) => ({
|
|
9815
|
+
id: i.id,
|
|
9816
|
+
section: i.section,
|
|
9817
|
+
kind: i.kind,
|
|
9818
|
+
text: clip(i.text, 200),
|
|
9819
|
+
source: i.source
|
|
9820
|
+
}))
|
|
9821
|
+
}
|
|
9822
|
+
])
|
|
9823
|
+
)
|
|
9824
|
+
});
|
|
9825
|
+
var memoryTools = [
|
|
9826
|
+
tool({
|
|
9827
|
+
name: "memory_get",
|
|
9828
|
+
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.",
|
|
9829
|
+
parameters: none,
|
|
9830
|
+
group: "memory",
|
|
9831
|
+
need: own,
|
|
9832
|
+
risk: "R0",
|
|
9833
|
+
run: async (ctx) => brief(await ws(ctx).maggieMemory())
|
|
9834
|
+
}),
|
|
9835
|
+
tool({
|
|
9836
|
+
name: "memory_remember",
|
|
9837
|
+
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.',
|
|
9838
|
+
parameters: {
|
|
9839
|
+
type: "object",
|
|
9840
|
+
properties: {
|
|
9841
|
+
channel: { type: "string", enum: [...MEMORY_CHANNELS] },
|
|
9842
|
+
section: {
|
|
9843
|
+
type: "string",
|
|
9844
|
+
enum: SECTIONS,
|
|
9845
|
+
description: "prefer: a leaning \xB7 always: do it every time \xB7 never: don't do or use it"
|
|
9846
|
+
},
|
|
9847
|
+
text: { type: "string", description: "The whole rule, \u2264160 characters, e.g. \u201CNever use emojis.\u201D" },
|
|
9848
|
+
kind: { type: "string", enum: KINDS }
|
|
9849
|
+
},
|
|
9850
|
+
required: ["channel", "section", "text"]
|
|
9851
|
+
},
|
|
9852
|
+
group: "memory",
|
|
9853
|
+
need: own,
|
|
9854
|
+
risk: "W1",
|
|
9855
|
+
run: async (ctx, { channel, section, text: text3, kind }) => {
|
|
9856
|
+
const ch = MEMORY_CHANNELS.find((c) => c === channel) ?? "general";
|
|
9857
|
+
const sec = SECTIONS.includes(String(section)) ? section : "prefer";
|
|
9858
|
+
const m = await ws(ctx).addMaggieMemory({
|
|
9859
|
+
channel: ch,
|
|
9860
|
+
section: sec,
|
|
9861
|
+
text: String(text3 ?? "").trim().slice(0, 160),
|
|
9862
|
+
...kind && KINDS.includes(kind) ? { kind } : {}
|
|
9863
|
+
});
|
|
9864
|
+
return { saved: true, enabled: m.enabled };
|
|
9865
|
+
}
|
|
9866
|
+
}),
|
|
9867
|
+
tool({
|
|
9868
|
+
name: "memory_forget",
|
|
9869
|
+
description: "Remove one item from the user's Maggie Memory (ids from memory_get).",
|
|
9870
|
+
parameters: {
|
|
9871
|
+
type: "object",
|
|
9872
|
+
properties: {
|
|
9873
|
+
id: { type: "string" },
|
|
9874
|
+
text_for_card: { type: "string", description: "The item's text, for the card" }
|
|
9875
|
+
},
|
|
9876
|
+
required: ["id"]
|
|
9877
|
+
},
|
|
9878
|
+
group: "memory",
|
|
9879
|
+
need: own,
|
|
9880
|
+
risk: "W1",
|
|
9881
|
+
run: async (ctx, { id }) => {
|
|
9882
|
+
await ws(ctx).deleteMaggieMemory(String(id));
|
|
9883
|
+
return { forgotten: true };
|
|
9884
|
+
}
|
|
9885
|
+
})
|
|
9886
|
+
];
|
|
9887
|
+
var MEMORY_TOOLS = new Set(memoryTools.map((t) => t.name));
|
|
9888
|
+
|
|
9257
9889
|
// ../../packages/tools/src/publishing.ts
|
|
9258
9890
|
var read3 = { resource: "autopilot", level: "read" };
|
|
9259
9891
|
var write4 = { resource: "autopilot", level: "write" };
|
|
@@ -9329,7 +9961,7 @@ var publishingTools = [
|
|
|
9329
9961
|
}),
|
|
9330
9962
|
tool({
|
|
9331
9963
|
name: "rewrite_request",
|
|
9332
|
-
description: "Rewrite one
|
|
9964
|
+
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.",
|
|
9333
9965
|
parameters: {
|
|
9334
9966
|
type: "object",
|
|
9335
9967
|
properties: {
|
|
@@ -9491,9 +10123,21 @@ var reportTools = [
|
|
|
9491
10123
|
};
|
|
9492
10124
|
}
|
|
9493
10125
|
}),
|
|
10126
|
+
tool({
|
|
10127
|
+
name: "gsc_connect_link",
|
|
10128
|
+
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.",
|
|
10129
|
+
parameters: none,
|
|
10130
|
+
group: "reports",
|
|
10131
|
+
need: { resource: "autopilot", level: "write" },
|
|
10132
|
+
risk: "W1",
|
|
10133
|
+
run: async (ctx) => {
|
|
10134
|
+
const out = await ws(ctx).connectSearchConsole("close");
|
|
10135
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10136
|
+
}
|
|
10137
|
+
}),
|
|
9494
10138
|
tool({
|
|
9495
10139
|
name: "gsc_performance_get",
|
|
9496
|
-
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.",
|
|
10140
|
+
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.",
|
|
9497
10141
|
parameters: { type: "object", properties: { period: { type: "string", enum: periods } } },
|
|
9498
10142
|
group: "reports",
|
|
9499
10143
|
need: read4,
|
|
@@ -9660,8 +10304,26 @@ var reportTools = [
|
|
|
9660
10304
|
var read5 = { resource: "autopilot", level: "read" };
|
|
9661
10305
|
var write5 = { resource: "autopilot", level: "write" };
|
|
9662
10306
|
var moves2 = ["up", "down", "top"];
|
|
10307
|
+
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10308
|
+
var channelsParam = {
|
|
10309
|
+
type: "array",
|
|
10310
|
+
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10311
|
+
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."
|
|
10312
|
+
};
|
|
10313
|
+
var channelsOf = (value) => {
|
|
10314
|
+
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
10315
|
+
return list2.length ? list2 : void 0;
|
|
10316
|
+
};
|
|
10317
|
+
var writing = (job) => ({
|
|
10318
|
+
job_id: job.id,
|
|
10319
|
+
status: "writing",
|
|
10320
|
+
posts_expected: job.expected,
|
|
10321
|
+
channels: job.channels,
|
|
10322
|
+
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."
|
|
10323
|
+
});
|
|
9663
10324
|
var item = (i) => ({
|
|
9664
10325
|
id: i.id,
|
|
10326
|
+
channel: i.provider || "linkedin",
|
|
9665
10327
|
status: i.status,
|
|
9666
10328
|
eta: i.eta ?? null,
|
|
9667
10329
|
scheduled_at: i.scheduled_at,
|
|
@@ -9674,7 +10336,7 @@ var item = (i) => ({
|
|
|
9674
10336
|
var socialTools = [
|
|
9675
10337
|
tool({
|
|
9676
10338
|
name: "social_list",
|
|
9677
|
-
description: "The user's LinkedIn:
|
|
10339
|
+
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.",
|
|
9678
10340
|
parameters: none,
|
|
9679
10341
|
group: "social",
|
|
9680
10342
|
need: read5,
|
|
@@ -9683,6 +10345,14 @@ var socialTools = [
|
|
|
9683
10345
|
const s = await ws(ctx).social();
|
|
9684
10346
|
return {
|
|
9685
10347
|
connected: s.connected,
|
|
10348
|
+
channels: (s.channels ?? []).map((c) => ({
|
|
10349
|
+
channel: c.key,
|
|
10350
|
+
connected: c.connected,
|
|
10351
|
+
account: c.account?.display_name ?? null,
|
|
10352
|
+
paused: c.paused,
|
|
10353
|
+
needs_page: c.needs_page,
|
|
10354
|
+
per_week: c.frequency?.per_week ?? null
|
|
10355
|
+
})),
|
|
9686
10356
|
account: s.linkedin ? { name: s.linkedin.display_name, status: s.linkedin.status } : null,
|
|
9687
10357
|
posting_paused: s.posting_paused,
|
|
9688
10358
|
approval_mode: s.approval_mode,
|
|
@@ -9703,69 +10373,353 @@ var socialTools = [
|
|
|
9703
10373
|
}
|
|
9704
10374
|
}),
|
|
9705
10375
|
tool({
|
|
9706
|
-
name: "social_update_copy",
|
|
9707
|
-
description: "Change the text of a
|
|
10376
|
+
name: "social_update_copy",
|
|
10377
|
+
description: "Change the text of a social post that hasn't gone out yet (a draft or queued post).",
|
|
10378
|
+
parameters: {
|
|
10379
|
+
type: "object",
|
|
10380
|
+
properties: { id: { type: "integer" }, text: { type: "string", description: "The whole new post text" } },
|
|
10381
|
+
required: ["id", "text"]
|
|
10382
|
+
},
|
|
10383
|
+
group: "social",
|
|
10384
|
+
need: write5,
|
|
10385
|
+
risk: "W1",
|
|
10386
|
+
gates: ["VER"],
|
|
10387
|
+
run: async (ctx, { id, text: text3 }) => {
|
|
10388
|
+
const out = await ws(ctx).updateSocial(Number(id), String(text3 ?? ""));
|
|
10389
|
+
return { id: out.id, status: out.status };
|
|
10390
|
+
}
|
|
10391
|
+
}),
|
|
10392
|
+
tool({
|
|
10393
|
+
name: "social_approve",
|
|
10394
|
+
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.",
|
|
10395
|
+
parameters: {
|
|
10396
|
+
type: "object",
|
|
10397
|
+
properties: { ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" } },
|
|
10398
|
+
required: ["ids"]
|
|
10399
|
+
},
|
|
10400
|
+
group: "social",
|
|
10401
|
+
need: write5,
|
|
10402
|
+
risk: "W3",
|
|
10403
|
+
gates: ["VER", "LI"],
|
|
10404
|
+
run: async (ctx, { ids: ids3, id }) => {
|
|
10405
|
+
const list2 = (Array.isArray(ids3) && ids3.length ? ids3 : id != null ? [id] : []).map(Number).slice(0, 20);
|
|
10406
|
+
if (!list2.length) throw new Error("Which post?");
|
|
10407
|
+
const api2 = ws(ctx);
|
|
10408
|
+
const results = [];
|
|
10409
|
+
for (const n of list2) {
|
|
10410
|
+
try {
|
|
10411
|
+
await api2.approveSocial(n);
|
|
10412
|
+
results.push({ id: n, ok: true });
|
|
10413
|
+
} catch (e) {
|
|
10414
|
+
results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
10415
|
+
}
|
|
10416
|
+
}
|
|
10417
|
+
return { approved: results.filter((r) => r.ok).length, results };
|
|
10418
|
+
}
|
|
10419
|
+
}),
|
|
10420
|
+
tool({
|
|
10421
|
+
name: "social_skip",
|
|
10422
|
+
description: "Skip a LinkedIn post (it won't be posted; it can be restored).",
|
|
10423
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10424
|
+
group: "social",
|
|
10425
|
+
need: write5,
|
|
10426
|
+
risk: "W1",
|
|
10427
|
+
gates: ["VER"],
|
|
10428
|
+
run: async (ctx, { id }) => ws(ctx).skipSocial(Number(id))
|
|
10429
|
+
}),
|
|
10430
|
+
tool({
|
|
10431
|
+
name: "social_restore",
|
|
10432
|
+
description: "Bring back a skipped LinkedIn post.",
|
|
10433
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10434
|
+
group: "social",
|
|
10435
|
+
need: write5,
|
|
10436
|
+
risk: "W1",
|
|
10437
|
+
gates: ["VER"],
|
|
10438
|
+
run: async (ctx, { id }) => ws(ctx).restoreSocial(Number(id))
|
|
10439
|
+
}),
|
|
10440
|
+
tool({
|
|
10441
|
+
name: "social_image_new",
|
|
10442
|
+
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.",
|
|
10443
|
+
parameters: {
|
|
10444
|
+
type: "object",
|
|
10445
|
+
properties: {
|
|
10446
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10447
|
+
replace: { type: "boolean", description: "Stop the picture being made for another post first" },
|
|
10448
|
+
instruction: {
|
|
10449
|
+
type: "string",
|
|
10450
|
+
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."
|
|
10451
|
+
},
|
|
10452
|
+
mode: {
|
|
10453
|
+
type: "string",
|
|
10454
|
+
enum: ["new", "edit"],
|
|
10455
|
+
description: "edit: change the post's current picture as the instruction says (it must have one)"
|
|
10456
|
+
},
|
|
10457
|
+
count: {
|
|
10458
|
+
type: "integer",
|
|
10459
|
+
description: "1 (default) or 4: a set of 4 pictures telling one story, a line of words on each"
|
|
10460
|
+
},
|
|
10461
|
+
text_effect: {
|
|
10462
|
+
type: "string",
|
|
10463
|
+
description: "For a set: the words' style id from social_text_effects (omit = Maggie picks)"
|
|
10464
|
+
},
|
|
10465
|
+
style: {
|
|
10466
|
+
type: "string",
|
|
10467
|
+
description: "Picture style id from social_look (omit = Maggie picks from the brand's 3)"
|
|
10468
|
+
}
|
|
10469
|
+
},
|
|
10470
|
+
required: ["id"]
|
|
10471
|
+
},
|
|
10472
|
+
group: "social",
|
|
10473
|
+
need: write5,
|
|
10474
|
+
risk: "W2",
|
|
10475
|
+
gates: ["VER", "CR"],
|
|
10476
|
+
run: async (ctx, { id, replace, instruction, mode, count, text_effect, style }) => {
|
|
10477
|
+
const out = await ws(ctx).regenerateSocialImage(Number(id), {
|
|
10478
|
+
replace: replace === true,
|
|
10479
|
+
...typeof style === "string" && style.trim() && mode !== "edit" ? { style: style.trim() } : {},
|
|
10480
|
+
...mode === "edit" ? { mode: "edit" } : {},
|
|
10481
|
+
...Number(count) === 4 && mode !== "edit" ? { count: 4 } : {},
|
|
10482
|
+
...typeof text_effect === "string" && text_effect.trim() ? { textEffect: text_effect.trim() } : {},
|
|
10483
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10484
|
+
});
|
|
10485
|
+
return {
|
|
10486
|
+
id: out.id,
|
|
10487
|
+
image_job: out.image_job,
|
|
10488
|
+
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."
|
|
10489
|
+
};
|
|
10490
|
+
}
|
|
10491
|
+
}),
|
|
10492
|
+
tool({
|
|
10493
|
+
name: "social_images_fill",
|
|
10494
|
+
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.",
|
|
10495
|
+
parameters: {
|
|
10496
|
+
type: "object",
|
|
10497
|
+
properties: {
|
|
10498
|
+
ids: { type: "array", items: { type: "integer" }, description: "Only these posts (from social_list)" },
|
|
10499
|
+
instruction: { type: "string", description: "What the user wants in every picture, up to 300 characters" }
|
|
10500
|
+
}
|
|
10501
|
+
},
|
|
10502
|
+
group: "social",
|
|
10503
|
+
need: write5,
|
|
10504
|
+
risk: "W2",
|
|
10505
|
+
gates: ["VER", "CR"],
|
|
10506
|
+
run: async (ctx, { ids: ids3, instruction }) => {
|
|
10507
|
+
const out = await ws(ctx).fillSocialImages({
|
|
10508
|
+
...Array.isArray(ids3) && ids3.length ? { item_ids: ids3.map(Number) } : {},
|
|
10509
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10510
|
+
});
|
|
10511
|
+
return { queued: out.queued, already: out.skipped, stopped_because: out.message, line: out.image_jobs.length };
|
|
10512
|
+
}
|
|
10513
|
+
}),
|
|
10514
|
+
tool({
|
|
10515
|
+
name: "social_text_effects",
|
|
10516
|
+
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').",
|
|
10517
|
+
parameters: none,
|
|
10518
|
+
group: "social",
|
|
10519
|
+
need: read5,
|
|
10520
|
+
risk: "R0",
|
|
10521
|
+
run: async (ctx) => {
|
|
10522
|
+
const out = await ws(ctx).socialTextEffects();
|
|
10523
|
+
return { effects: out.effects.map(({ id, name, family }) => ({ id, name, family })), auto: out.auto };
|
|
10524
|
+
}
|
|
10525
|
+
}),
|
|
10526
|
+
tool({
|
|
10527
|
+
name: "social_look",
|
|
10528
|
+
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.",
|
|
10529
|
+
parameters: none,
|
|
10530
|
+
group: "social",
|
|
10531
|
+
need: read5,
|
|
10532
|
+
risk: "R0",
|
|
10533
|
+
run: async (ctx) => {
|
|
10534
|
+
const l = await ws(ctx).socialLook();
|
|
10535
|
+
return {
|
|
10536
|
+
styles: l.styles.map((s) => ({ id: s.id, name: s.name, about: s.about })),
|
|
10537
|
+
text_effects: l.text_effects.map((e) => ({ id: e.id, name: e.name, family: e.family })),
|
|
10538
|
+
fonts: l.fonts,
|
|
10539
|
+
colours: l.colours,
|
|
10540
|
+
description: l.brand_suffix,
|
|
10541
|
+
never: l.never,
|
|
10542
|
+
why: l.suggested.reason ?? null,
|
|
10543
|
+
can_choose: {
|
|
10544
|
+
styles: l.catalogue.styles.map((s) => ({ id: s.id, name: s.name, led: s.led, needs_photo: s.needs_photo })),
|
|
10545
|
+
fonts: l.catalogue.fonts,
|
|
10546
|
+
text_effect_families: [...new Set(l.catalogue.text_effects.map((e) => e.family))]
|
|
10547
|
+
}
|
|
10548
|
+
};
|
|
10549
|
+
}
|
|
10550
|
+
}),
|
|
10551
|
+
tool({
|
|
10552
|
+
name: "social_look_update",
|
|
10553
|
+
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.",
|
|
9708
10554
|
parameters: {
|
|
9709
10555
|
type: "object",
|
|
9710
|
-
properties: {
|
|
9711
|
-
|
|
10556
|
+
properties: {
|
|
10557
|
+
style_ids: { type: "array", items: { type: "string" }, description: "1-3 picture style ids" },
|
|
10558
|
+
text_effect_ids: { type: "array", items: { type: "string" }, description: "1-5 text style ids" },
|
|
10559
|
+
font_display: { type: "string", description: "Font for big words (from social_look can_choose.fonts)" },
|
|
10560
|
+
font_body: { type: "string", description: "Font for lines and lists" },
|
|
10561
|
+
colours: {
|
|
10562
|
+
type: "object",
|
|
10563
|
+
description: "Any of these as #RRGGBB",
|
|
10564
|
+
properties: {
|
|
10565
|
+
ink: { type: "string", description: "Text on paper" },
|
|
10566
|
+
paper: { type: "string", description: "The light ground" },
|
|
10567
|
+
primary: { type: "string", description: "The brand colour" },
|
|
10568
|
+
accent: { type: "string", description: "A second colour" },
|
|
10569
|
+
muted: { type: "string", description: "Small text" }
|
|
10570
|
+
}
|
|
10571
|
+
},
|
|
10572
|
+
description: { type: "string", description: "How the brand's photos look, up to 600 characters" },
|
|
10573
|
+
never: { type: "array", items: { type: "string" }, description: "Things never to show (the whole list)" },
|
|
10574
|
+
reset: { type: "boolean", description: "Back to Maggie's suggested look" }
|
|
10575
|
+
}
|
|
9712
10576
|
},
|
|
9713
10577
|
group: "social",
|
|
9714
10578
|
need: write5,
|
|
9715
10579
|
risk: "W1",
|
|
9716
10580
|
gates: ["VER"],
|
|
9717
|
-
run: async (ctx,
|
|
9718
|
-
const
|
|
9719
|
-
|
|
10581
|
+
run: async (ctx, a) => {
|
|
10582
|
+
const fonts = {
|
|
10583
|
+
...a.font_display ? { display: a.font_display } : {},
|
|
10584
|
+
...a.font_body ? { body: a.font_body } : {}
|
|
10585
|
+
};
|
|
10586
|
+
const l = await ws(ctx).updateSocialLook({
|
|
10587
|
+
...a.style_ids ? { style_ids: a.style_ids } : {},
|
|
10588
|
+
...a.text_effect_ids ? { text_effect_ids: a.text_effect_ids } : {},
|
|
10589
|
+
...Object.keys(fonts).length ? { fonts } : {},
|
|
10590
|
+
...a.colours ? { colours: a.colours } : {},
|
|
10591
|
+
...typeof a.description === "string" ? { brand_suffix: a.description } : {},
|
|
10592
|
+
...a.never ? { never: a.never } : {},
|
|
10593
|
+
...a.reset ? { reset: true } : {}
|
|
10594
|
+
});
|
|
10595
|
+
return {
|
|
10596
|
+
styles: l.styles.map((s) => s.id),
|
|
10597
|
+
text_effects: l.text_effects.map((e) => e.id),
|
|
10598
|
+
fonts: l.fonts,
|
|
10599
|
+
colours: l.colours
|
|
10600
|
+
};
|
|
9720
10601
|
}
|
|
9721
10602
|
}),
|
|
9722
10603
|
tool({
|
|
9723
|
-
name: "
|
|
9724
|
-
description: "
|
|
10604
|
+
name: "social_picture_edit",
|
|
10605
|
+
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.",
|
|
9725
10606
|
parameters: {
|
|
9726
10607
|
type: "object",
|
|
9727
|
-
properties: {
|
|
9728
|
-
|
|
10608
|
+
properties: {
|
|
10609
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10610
|
+
words: {
|
|
10611
|
+
type: "array",
|
|
10612
|
+
items: {
|
|
10613
|
+
type: "object",
|
|
10614
|
+
properties: { headline: { type: "string" }, sub: { type: "string" } },
|
|
10615
|
+
required: ["headline"]
|
|
10616
|
+
},
|
|
10617
|
+
description: "The words on each picture, in order (in the new order when order is given)"
|
|
10618
|
+
},
|
|
10619
|
+
style: { type: "string", description: "A picture style id from social_look" },
|
|
10620
|
+
text_effect: { type: "string", description: "A text style id from social_text_effects" },
|
|
10621
|
+
order: { type: "array", items: { type: "integer" }, description: "The pictures in their new order" },
|
|
10622
|
+
remove: { type: "array", items: { type: "integer" }, description: "Pictures to drop from a set" }
|
|
10623
|
+
},
|
|
10624
|
+
required: ["id"]
|
|
9729
10625
|
},
|
|
9730
10626
|
group: "social",
|
|
9731
10627
|
need: write5,
|
|
9732
|
-
risk: "
|
|
9733
|
-
gates: ["VER"
|
|
9734
|
-
run: async (ctx, {
|
|
9735
|
-
const
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
} catch (e) {
|
|
9744
|
-
results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
9745
|
-
}
|
|
9746
|
-
}
|
|
9747
|
-
return { approved: results.filter((r) => r.ok).length, results };
|
|
10628
|
+
risk: "W1",
|
|
10629
|
+
gates: ["VER"],
|
|
10630
|
+
run: async (ctx, { id, words, style, text_effect, order, remove: remove2 }) => {
|
|
10631
|
+
const out = await ws(ctx).editSocialPicture(Number(id), {
|
|
10632
|
+
...Array.isArray(words) ? { words: words.map((w) => ({ headline: String(w.headline ?? ""), sub: String(w.sub ?? "") })) } : {},
|
|
10633
|
+
...style ? { style } : {},
|
|
10634
|
+
...text_effect ? { text_effect } : {},
|
|
10635
|
+
...Array.isArray(order) ? { order: order.map(Number) } : {},
|
|
10636
|
+
...Array.isArray(remove2) ? { remove: remove2.map(Number) } : {}
|
|
10637
|
+
});
|
|
10638
|
+
return { id: out.id, pictures: out.image_urls?.length ? out.image_urls : [out.image_url], picture: out.picture };
|
|
9748
10639
|
}
|
|
9749
10640
|
}),
|
|
9750
10641
|
tool({
|
|
9751
|
-
name: "
|
|
9752
|
-
description: "
|
|
9753
|
-
parameters: {
|
|
10642
|
+
name: "social_photo_words",
|
|
10643
|
+
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.",
|
|
10644
|
+
parameters: {
|
|
10645
|
+
type: "object",
|
|
10646
|
+
properties: {
|
|
10647
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10648
|
+
style: {
|
|
10649
|
+
type: "string",
|
|
10650
|
+
description: "real_photo (default), product_hero, photo_headline, editorial, duotone"
|
|
10651
|
+
}
|
|
10652
|
+
},
|
|
10653
|
+
required: ["id"]
|
|
10654
|
+
},
|
|
9754
10655
|
group: "social",
|
|
9755
10656
|
need: write5,
|
|
9756
10657
|
risk: "W1",
|
|
9757
10658
|
gates: ["VER"],
|
|
9758
|
-
run: async (ctx, { id }) =>
|
|
10659
|
+
run: async (ctx, { id, style }) => {
|
|
10660
|
+
const out = await ws(ctx).wordsOnPhoto(Number(id), style || "real_photo");
|
|
10661
|
+
return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
|
|
10662
|
+
}
|
|
9759
10663
|
}),
|
|
9760
10664
|
tool({
|
|
9761
|
-
name: "
|
|
9762
|
-
description: "
|
|
9763
|
-
parameters: {
|
|
10665
|
+
name: "social_post_insights",
|
|
10666
|
+
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).",
|
|
10667
|
+
parameters: {
|
|
10668
|
+
type: "object",
|
|
10669
|
+
properties: {
|
|
10670
|
+
id: { type: "integer", description: "A posted Instagram post (from social_list posted)" },
|
|
10671
|
+
refresh: { type: "boolean", description: "Read the numbers again now" }
|
|
10672
|
+
},
|
|
10673
|
+
required: ["id"]
|
|
10674
|
+
},
|
|
10675
|
+
group: "social",
|
|
10676
|
+
need: read5,
|
|
10677
|
+
risk: "R0",
|
|
10678
|
+
run: async (ctx, { id, refresh }) => ws(ctx).socialInsights(Number(id), refresh === true)
|
|
10679
|
+
}),
|
|
10680
|
+
tool({
|
|
10681
|
+
name: "social_write_status",
|
|
10682
|
+
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.",
|
|
10683
|
+
parameters: none,
|
|
10684
|
+
group: "social",
|
|
10685
|
+
need: read5,
|
|
10686
|
+
risk: "R0",
|
|
10687
|
+
run: async (ctx) => ({
|
|
10688
|
+
jobs: ((await ws(ctx).social()).write_jobs ?? []).map((j) => ({
|
|
10689
|
+
id: j.id,
|
|
10690
|
+
kind: j.kind,
|
|
10691
|
+
status: j.status,
|
|
10692
|
+
channels: j.channels,
|
|
10693
|
+
expected: j.expected,
|
|
10694
|
+
written: j.item_ids,
|
|
10695
|
+
queued: j.queued,
|
|
10696
|
+
...j.failed?.length ? { failed: j.failed } : {},
|
|
10697
|
+
...j.error ? { error: j.error } : {},
|
|
10698
|
+
started_at: j.started_at
|
|
10699
|
+
}))
|
|
10700
|
+
})
|
|
10701
|
+
}),
|
|
10702
|
+
tool({
|
|
10703
|
+
name: "social_image_status",
|
|
10704
|
+
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).",
|
|
10705
|
+
parameters: none,
|
|
10706
|
+
group: "social",
|
|
10707
|
+
need: read5,
|
|
10708
|
+
risk: "R0",
|
|
10709
|
+
run: async (ctx) => {
|
|
10710
|
+
const s = await ws(ctx).social();
|
|
10711
|
+
return { image_job: s.image_job ?? null, line: s.image_jobs ?? [] };
|
|
10712
|
+
}
|
|
10713
|
+
}),
|
|
10714
|
+
tool({
|
|
10715
|
+
name: "social_image_stop",
|
|
10716
|
+
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.",
|
|
10717
|
+
parameters: { type: "object", properties: { all: { type: "boolean" } } },
|
|
9764
10718
|
group: "social",
|
|
9765
10719
|
need: write5,
|
|
9766
10720
|
risk: "W1",
|
|
9767
10721
|
gates: ["VER"],
|
|
9768
|
-
run: async (ctx, {
|
|
10722
|
+
run: async (ctx, { all }) => ws(ctx).stopSocialImage(all === true)
|
|
9769
10723
|
}),
|
|
9770
10724
|
tool({
|
|
9771
10725
|
name: "social_move",
|
|
@@ -9783,16 +10737,24 @@ var socialTools = [
|
|
|
9783
10737
|
}),
|
|
9784
10738
|
tool({
|
|
9785
10739
|
name: "social_set_paused",
|
|
9786
|
-
description: "Pause or resume
|
|
9787
|
-
parameters: {
|
|
10740
|
+
description: "Pause or resume the user's social posting: one channel (linkedin or facebook), or every channel when no channel is given.",
|
|
10741
|
+
parameters: {
|
|
10742
|
+
type: "object",
|
|
10743
|
+
properties: { paused: { type: "boolean" }, channel: { type: "string", enum: CHANNEL_KEYS } },
|
|
10744
|
+
required: ["paused"]
|
|
10745
|
+
},
|
|
9788
10746
|
group: "social",
|
|
9789
10747
|
need: write5,
|
|
9790
10748
|
risk: "W1",
|
|
9791
|
-
run: async (ctx, { paused }) =>
|
|
10749
|
+
run: async (ctx, { paused, channel }) => {
|
|
10750
|
+
const key = channelsOf(channel ? [channel] : [])?.[0];
|
|
10751
|
+
if (key) return ws(ctx).setChannelPaused(key, Boolean(paused));
|
|
10752
|
+
return paused ? ws(ctx).pauseSocial() : ws(ctx).resumeSocial();
|
|
10753
|
+
}
|
|
9792
10754
|
}),
|
|
9793
10755
|
tool({
|
|
9794
10756
|
name: "social_sources_list",
|
|
9795
|
-
description: "Published articles a
|
|
10757
|
+
description: "Published articles a social post can be made from (optionally matching a search).",
|
|
9796
10758
|
parameters: { type: "object", properties: { search: { type: "string" } } },
|
|
9797
10759
|
group: "social",
|
|
9798
10760
|
need: read5,
|
|
@@ -9801,29 +10763,31 @@ var socialTools = [
|
|
|
9801
10763
|
}),
|
|
9802
10764
|
tool({
|
|
9803
10765
|
name: "social_from_article",
|
|
9804
|
-
description: "Maggie writes a
|
|
10766
|
+
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).",
|
|
9805
10767
|
parameters: {
|
|
9806
10768
|
type: "object",
|
|
9807
|
-
properties: {
|
|
9808
|
-
|
|
10769
|
+
properties: {
|
|
10770
|
+
source_id: { type: "string" },
|
|
10771
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10772
|
+
channels: channelsParam
|
|
10773
|
+
},
|
|
10774
|
+
required: ["source_id", "title"]
|
|
9809
10775
|
},
|
|
9810
10776
|
group: "social",
|
|
9811
10777
|
need: write5,
|
|
9812
10778
|
risk: "W2",
|
|
9813
10779
|
gates: ["VER", "LI", "LLM"],
|
|
9814
|
-
run: async (ctx, { source_id }) =>
|
|
9815
|
-
const out = await ws(ctx).socialFromPost(String(source_id));
|
|
9816
|
-
return { id: out.id, status: out.status, text: clip(out.social_copy, 300) };
|
|
9817
|
-
}
|
|
10780
|
+
run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
|
|
9818
10781
|
}),
|
|
9819
10782
|
tool({
|
|
9820
10783
|
name: "social_plan_days",
|
|
9821
|
-
description: "Maggie writes
|
|
10784
|
+
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).",
|
|
9822
10785
|
parameters: {
|
|
9823
10786
|
type: "object",
|
|
9824
10787
|
properties: {
|
|
9825
10788
|
days: { type: "integer", minimum: 1, maximum: 7 },
|
|
9826
|
-
timezone: { type: "string", description: "IANA timezone, if known" }
|
|
10789
|
+
timezone: { type: "string", description: "IANA timezone, if known" },
|
|
10790
|
+
channels: channelsParam
|
|
9827
10791
|
},
|
|
9828
10792
|
required: ["days"]
|
|
9829
10793
|
},
|
|
@@ -9831,10 +10795,7 @@ var socialTools = [
|
|
|
9831
10795
|
need: write5,
|
|
9832
10796
|
risk: "W2",
|
|
9833
10797
|
gates: ["VER", "LI", "LLM"],
|
|
9834
|
-
run: async (ctx, { days, timezone }) =>
|
|
9835
|
-
const out = await ws(ctx).socialPlan(int(days, 1, 7, 3), timezone || "UTC");
|
|
9836
|
-
return { written: out.items.length, items: out.items.map(item) };
|
|
9837
|
-
}
|
|
10798
|
+
run: async (ctx, { days, timezone, channels }) => writing((await ws(ctx).startSocialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels))).job)
|
|
9838
10799
|
}),
|
|
9839
10800
|
tool({
|
|
9840
10801
|
name: "social_ideas_list",
|
|
@@ -9862,21 +10823,21 @@ var socialTools = [
|
|
|
9862
10823
|
}),
|
|
9863
10824
|
tool({
|
|
9864
10825
|
name: "social_idea_add",
|
|
9865
|
-
description: "Add an idea for a
|
|
10826
|
+
description: "Add an idea for a social post (not written yet; writing it later uses a credit per channel).",
|
|
9866
10827
|
parameters: {
|
|
9867
10828
|
type: "object",
|
|
9868
|
-
properties: { title: { type: "string" }, summary: { type: "string" } },
|
|
10829
|
+
properties: { title: { type: "string" }, summary: { type: "string" }, channels: channelsParam },
|
|
9869
10830
|
required: ["title"]
|
|
9870
10831
|
},
|
|
9871
10832
|
group: "social",
|
|
9872
10833
|
need: write5,
|
|
9873
10834
|
risk: "W1",
|
|
9874
10835
|
gates: ["VER", "LI"],
|
|
9875
|
-
run: async (ctx, idea) => ws(ctx).addSocialIdea(idea)
|
|
10836
|
+
run: async (ctx, { channels, ...idea }) => ws(ctx).addSocialIdea({ ...idea, channels: channelsOf(channels) })
|
|
9876
10837
|
}),
|
|
9877
10838
|
tool({
|
|
9878
10839
|
name: "social_idea_update",
|
|
9879
|
-
description: "Change a
|
|
10840
|
+
description: "Change a social idea's title or summary.",
|
|
9880
10841
|
parameters: {
|
|
9881
10842
|
type: "object",
|
|
9882
10843
|
properties: { id: { type: "integer" }, title: { type: "string" }, summary: { type: "string" } },
|
|
@@ -9890,11 +10851,14 @@ var socialTools = [
|
|
|
9890
10851
|
}),
|
|
9891
10852
|
tool({
|
|
9892
10853
|
name: "social_idea_delete",
|
|
9893
|
-
description: "Delete a
|
|
10854
|
+
description: "Delete a social idea that hasn't been written.",
|
|
9894
10855
|
parameters: {
|
|
9895
10856
|
type: "object",
|
|
9896
|
-
properties: {
|
|
9897
|
-
|
|
10857
|
+
properties: {
|
|
10858
|
+
id: { type: "integer" },
|
|
10859
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10860
|
+
},
|
|
10861
|
+
required: ["id", "title"]
|
|
9898
10862
|
},
|
|
9899
10863
|
group: "social",
|
|
9900
10864
|
need: write5,
|
|
@@ -9904,21 +10868,79 @@ var socialTools = [
|
|
|
9904
10868
|
}),
|
|
9905
10869
|
tool({
|
|
9906
10870
|
name: "social_idea_write",
|
|
9907
|
-
description: "Maggie writes
|
|
10871
|
+
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).",
|
|
9908
10872
|
parameters: {
|
|
9909
10873
|
type: "object",
|
|
9910
|
-
properties: {
|
|
9911
|
-
|
|
10874
|
+
properties: {
|
|
10875
|
+
id: { type: "integer" },
|
|
10876
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10877
|
+
channels: channelsParam
|
|
10878
|
+
},
|
|
10879
|
+
required: ["id", "title"]
|
|
9912
10880
|
},
|
|
9913
10881
|
group: "social",
|
|
9914
10882
|
need: write5,
|
|
9915
10883
|
risk: "W2",
|
|
9916
10884
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
9917
|
-
run: async (ctx, { id }) => ws(ctx).
|
|
10885
|
+
run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
|
|
10886
|
+
}),
|
|
10887
|
+
tool({
|
|
10888
|
+
name: "social_idea_brief",
|
|
10889
|
+
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.",
|
|
10890
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10891
|
+
group: "social",
|
|
10892
|
+
need: read5,
|
|
10893
|
+
risk: "R0",
|
|
10894
|
+
assistantWrites: true,
|
|
10895
|
+
run: async (ctx, { id }) => {
|
|
10896
|
+
const b = await ws(ctx).socialIdeaBrief(Number(id));
|
|
10897
|
+
return {
|
|
10898
|
+
id: Number(id),
|
|
10899
|
+
channels: b.platforms,
|
|
10900
|
+
limits: b.limits,
|
|
10901
|
+
limit: b.limits.linkedin ?? null,
|
|
10902
|
+
json_schema: b.json_schema,
|
|
10903
|
+
// Built by CMO.so from the user's idea, brand voice and template.
|
|
10904
|
+
brief: b.prompt
|
|
10905
|
+
};
|
|
10906
|
+
}
|
|
10907
|
+
}),
|
|
10908
|
+
tool({
|
|
10909
|
+
name: "social_idea_save",
|
|
10910
|
+
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.",
|
|
10911
|
+
parameters: {
|
|
10912
|
+
type: "object",
|
|
10913
|
+
properties: {
|
|
10914
|
+
id: { type: "integer" },
|
|
10915
|
+
posts: {
|
|
10916
|
+
type: "object",
|
|
10917
|
+
description: "One body per channel from the brief (only the channels you wrote)",
|
|
10918
|
+
properties: { linkedin: { type: "string" }, facebook: { type: "string" } }
|
|
10919
|
+
},
|
|
10920
|
+
text: { type: "string", description: "Only a LinkedIn post: its body (older use; prefer posts)" },
|
|
10921
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10922
|
+
},
|
|
10923
|
+
required: ["id", "title"]
|
|
10924
|
+
},
|
|
10925
|
+
group: "social",
|
|
10926
|
+
need: write5,
|
|
10927
|
+
risk: "W2",
|
|
10928
|
+
gates: ["VER", "LI", "CR"],
|
|
10929
|
+
assistantWrites: true,
|
|
10930
|
+
run: async (ctx, { id, text: text3, posts: posts2 }) => {
|
|
10931
|
+
const written = {};
|
|
10932
|
+
for (const [channel, body] of Object.entries(posts2 ?? {})) {
|
|
10933
|
+
if (CHANNEL_KEYS.includes(channel) && String(body ?? "").trim())
|
|
10934
|
+
written[channel] = { body: String(body).trim() };
|
|
10935
|
+
}
|
|
10936
|
+
if (!Object.keys(written).length && String(text3 ?? "").trim()) written.linkedin = { body: String(text3).trim() };
|
|
10937
|
+
if (!Object.keys(written).length) throw new Error("Write the posts first.");
|
|
10938
|
+
return ws(ctx).writeSocialIdea(Number(id), written);
|
|
10939
|
+
}
|
|
9918
10940
|
}),
|
|
9919
10941
|
tool({
|
|
9920
10942
|
name: "campaign_list",
|
|
9921
|
-
description: "The user's
|
|
10943
|
+
description: "The user's social campaigns: ideas written so far, whether ready or started, start day and posts a day.",
|
|
9922
10944
|
parameters: none,
|
|
9923
10945
|
group: "social",
|
|
9924
10946
|
need: read5,
|
|
@@ -9927,16 +10949,16 @@ var socialTools = [
|
|
|
9927
10949
|
}),
|
|
9928
10950
|
tool({
|
|
9929
10951
|
name: "campaign_start",
|
|
9930
|
-
description: "Start a ready campaign (every idea written): its posts go out on
|
|
10952
|
+
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.",
|
|
9931
10953
|
parameters: {
|
|
9932
10954
|
type: "object",
|
|
9933
10955
|
properties: {
|
|
9934
10956
|
id: { type: "integer" },
|
|
9935
10957
|
start_date: { type: "string", description: "YYYY-MM-DD" },
|
|
9936
10958
|
daily_limit: { type: "integer", minimum: 1, maximum: 5 },
|
|
9937
|
-
name: { type: "string", description: "
|
|
10959
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9938
10960
|
},
|
|
9939
|
-
required: ["id"]
|
|
10961
|
+
required: ["id", "name"]
|
|
9940
10962
|
},
|
|
9941
10963
|
group: "social",
|
|
9942
10964
|
need: write5,
|
|
@@ -9984,13 +11006,65 @@ var socialTools = [
|
|
|
9984
11006
|
gates: ["VER"],
|
|
9985
11007
|
run: async (ctx, c) => ws(ctx).createCampaign(c)
|
|
9986
11008
|
}),
|
|
11009
|
+
tool({
|
|
11010
|
+
name: "social_approve_all",
|
|
11011
|
+
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.",
|
|
11012
|
+
parameters: { type: "object", properties: { channel: { type: "string", enum: CHANNEL_KEYS } } },
|
|
11013
|
+
group: "social",
|
|
11014
|
+
need: write5,
|
|
11015
|
+
risk: "W3",
|
|
11016
|
+
gates: ["VER", "LI"],
|
|
11017
|
+
run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
|
|
11018
|
+
}),
|
|
11019
|
+
tool({
|
|
11020
|
+
name: "series_create",
|
|
11021
|
+
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.",
|
|
11022
|
+
parameters: {
|
|
11023
|
+
type: "object",
|
|
11024
|
+
properties: {
|
|
11025
|
+
brief: { type: "string", description: "What the series is about, in the user's words" },
|
|
11026
|
+
count: { type: "integer", minimum: 1, maximum: 7, description: "How many posts (3 if not said)" },
|
|
11027
|
+
channels: channelsParam
|
|
11028
|
+
},
|
|
11029
|
+
required: ["brief"]
|
|
11030
|
+
},
|
|
11031
|
+
group: "social",
|
|
11032
|
+
need: write5,
|
|
11033
|
+
risk: "W2",
|
|
11034
|
+
gates: ["VER", "LI", "LLM"],
|
|
11035
|
+
run: async (ctx, { brief: brief2, count, channels }) => ws(ctx).createSocialSeries({
|
|
11036
|
+
brief: String(brief2 ?? ""),
|
|
11037
|
+
count: Math.max(1, Math.min(7, Number(count) || 3)),
|
|
11038
|
+
channels: channelsOf(channels)
|
|
11039
|
+
})
|
|
11040
|
+
}),
|
|
11041
|
+
tool({
|
|
11042
|
+
name: "series_approve",
|
|
11043
|
+
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).",
|
|
11044
|
+
parameters: {
|
|
11045
|
+
type: "object",
|
|
11046
|
+
properties: {
|
|
11047
|
+
id: { type: "integer" },
|
|
11048
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
11049
|
+
},
|
|
11050
|
+
required: ["id"]
|
|
11051
|
+
},
|
|
11052
|
+
group: "social",
|
|
11053
|
+
need: write5,
|
|
11054
|
+
risk: "W3",
|
|
11055
|
+
gates: ["VER", "LI"],
|
|
11056
|
+
run: async (ctx, { id }) => ws(ctx).approveSocialSeries(Number(id))
|
|
11057
|
+
}),
|
|
9987
11058
|
tool({
|
|
9988
11059
|
name: "campaign_plan",
|
|
9989
11060
|
description: "Maggie plans a campaign's LinkedIn post ideas with AI (uses 1 credit).",
|
|
9990
11061
|
parameters: {
|
|
9991
11062
|
type: "object",
|
|
9992
|
-
properties: {
|
|
9993
|
-
|
|
11063
|
+
properties: {
|
|
11064
|
+
id: { type: "integer" },
|
|
11065
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
11066
|
+
},
|
|
11067
|
+
required: ["id", "name"]
|
|
9994
11068
|
},
|
|
9995
11069
|
group: "social",
|
|
9996
11070
|
need: write5,
|
|
@@ -10009,9 +11083,9 @@ var socialTools = [
|
|
|
10009
11083
|
properties: {
|
|
10010
11084
|
id: { type: "integer" },
|
|
10011
11085
|
paused: { type: "boolean" },
|
|
10012
|
-
name: { type: "string", description: "
|
|
11086
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10013
11087
|
},
|
|
10014
|
-
required: ["id", "paused"]
|
|
11088
|
+
required: ["id", "paused", "name"]
|
|
10015
11089
|
},
|
|
10016
11090
|
group: "social",
|
|
10017
11091
|
need: write5,
|
|
@@ -10105,9 +11179,9 @@ var socialTools = [
|
|
|
10105
11179
|
run: async (ctx, { keys: keys3, ...options }) => {
|
|
10106
11180
|
const api2 = ws(ctx);
|
|
10107
11181
|
const current = (await api2.socialSetup()).writing;
|
|
10108
|
-
const
|
|
10109
|
-
for (const [k, v] of Object.entries(options)) if (v !== void 0)
|
|
10110
|
-
return api2.socialSetupTemplates((keys3 ?? []).slice(0, 10),
|
|
11182
|
+
const writing2 = { ...current };
|
|
11183
|
+
for (const [k, v] of Object.entries(options)) if (v !== void 0) writing2[k] = v;
|
|
11184
|
+
return api2.socialSetupTemplates((keys3 ?? []).slice(0, 10), writing2);
|
|
10111
11185
|
}
|
|
10112
11186
|
}),
|
|
10113
11187
|
tool({
|
|
@@ -10175,6 +11249,65 @@ var socialTools = [
|
|
|
10175
11249
|
const out = await ws(ctx).connectLinkedIn("close");
|
|
10176
11250
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10177
11251
|
}
|
|
11252
|
+
}),
|
|
11253
|
+
tool({
|
|
11254
|
+
name: "facebook_connect_link",
|
|
11255
|
+
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.",
|
|
11256
|
+
parameters: none,
|
|
11257
|
+
group: "social",
|
|
11258
|
+
need: write5,
|
|
11259
|
+
risk: "W1",
|
|
11260
|
+
gates: ["VER"],
|
|
11261
|
+
run: async (ctx) => {
|
|
11262
|
+
const out = await ws(ctx).connectFacebook("close");
|
|
11263
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11264
|
+
}
|
|
11265
|
+
}),
|
|
11266
|
+
tool({
|
|
11267
|
+
name: "google_business_connect_link",
|
|
11268
|
+
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.",
|
|
11269
|
+
parameters: none,
|
|
11270
|
+
group: "social",
|
|
11271
|
+
need: write5,
|
|
11272
|
+
risk: "W1",
|
|
11273
|
+
gates: ["VER"],
|
|
11274
|
+
run: async (ctx) => {
|
|
11275
|
+
const out = await ws(ctx).connectGoogleBusiness("close");
|
|
11276
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11277
|
+
}
|
|
11278
|
+
}),
|
|
11279
|
+
tool({
|
|
11280
|
+
name: "social_google_button",
|
|
11281
|
+
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.",
|
|
11282
|
+
parameters: {
|
|
11283
|
+
type: "object",
|
|
11284
|
+
properties: {
|
|
11285
|
+
id: { type: "integer", description: "A Google Business post (from social_list)" },
|
|
11286
|
+
button: { type: "string", enum: ["LEARN_MORE", "BOOK", "ORDER", "SHOP", "SIGN_UP", "CALL"] }
|
|
11287
|
+
},
|
|
11288
|
+
required: ["id", "button"]
|
|
11289
|
+
},
|
|
11290
|
+
group: "social",
|
|
11291
|
+
need: write5,
|
|
11292
|
+
risk: "W1",
|
|
11293
|
+
gates: ["VER"],
|
|
11294
|
+
run: async (ctx, { id, button }) => {
|
|
11295
|
+
const out = await ws(ctx).setSocialButton(Number(id), button);
|
|
11296
|
+
return { id: out.id, button: out.cta_type ?? "LEARN_MORE", link: out.cta_url ?? null };
|
|
11297
|
+
}
|
|
11298
|
+
}),
|
|
11299
|
+
tool({
|
|
11300
|
+
name: "instagram_connect_link",
|
|
11301
|
+
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).",
|
|
11302
|
+
parameters: none,
|
|
11303
|
+
group: "social",
|
|
11304
|
+
need: write5,
|
|
11305
|
+
risk: "W1",
|
|
11306
|
+
gates: ["VER"],
|
|
11307
|
+
run: async (ctx) => {
|
|
11308
|
+
const out = await ws(ctx).connectInstagram("close");
|
|
11309
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11310
|
+
}
|
|
10178
11311
|
})
|
|
10179
11312
|
];
|
|
10180
11313
|
|
|
@@ -10325,7 +11458,9 @@ var tools = [
|
|
|
10325
11458
|
...blogTools,
|
|
10326
11459
|
...publishingTools,
|
|
10327
11460
|
...socialTools,
|
|
10328
|
-
...teamTools
|
|
11461
|
+
...teamTools,
|
|
11462
|
+
...memoryTools,
|
|
11463
|
+
...feedbackTools
|
|
10329
11464
|
];
|
|
10330
11465
|
var toolByName = new Map(tools.map((t) => [t.name, t]));
|
|
10331
11466
|
function allowed(t, a) {
|
|
@@ -13477,9 +14612,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
13477
14612
|
helpWidth: context.helpWidth,
|
|
13478
14613
|
outputHasColors: context.hasColors
|
|
13479
14614
|
});
|
|
13480
|
-
const
|
|
13481
|
-
if (context.hasColors) return
|
|
13482
|
-
return this._outputConfiguration.stripColor(
|
|
14615
|
+
const text3 = helper.formatHelp(this, helper);
|
|
14616
|
+
if (context.hasColors) return text3;
|
|
14617
|
+
return this._outputConfiguration.stripColor(text3);
|
|
13483
14618
|
}
|
|
13484
14619
|
/**
|
|
13485
14620
|
* @typedef HelpContext
|
|
@@ -13639,7 +14774,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
13639
14774
|
* @param {(string | Function)} text - string to add, or a function returning a string
|
|
13640
14775
|
* @return {Command} `this` command for chaining
|
|
13641
14776
|
*/
|
|
13642
|
-
addHelpText(position,
|
|
14777
|
+
addHelpText(position, text3) {
|
|
13643
14778
|
const allowedValues = ["beforeAll", "before", "after", "afterAll"];
|
|
13644
14779
|
if (!allowedValues.includes(position)) {
|
|
13645
14780
|
throw new Error(`Unexpected value for position to addHelpText.
|
|
@@ -13648,10 +14783,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
13648
14783
|
const helpEvent = `${position}Help`;
|
|
13649
14784
|
this.on(helpEvent, (context) => {
|
|
13650
14785
|
let helpStr;
|
|
13651
|
-
if (typeof
|
|
13652
|
-
helpStr =
|
|
14786
|
+
if (typeof text3 === "function") {
|
|
14787
|
+
helpStr = text3({ error: context.error, command: context.command });
|
|
13653
14788
|
} else {
|
|
13654
|
-
helpStr =
|
|
14789
|
+
helpStr = text3;
|
|
13655
14790
|
}
|
|
13656
14791
|
if (helpStr) {
|
|
13657
14792
|
context.write(`${helpStr}
|
|
@@ -13718,7 +14853,7 @@ var program = new Command();
|
|
|
13718
14853
|
// package.json
|
|
13719
14854
|
var package_default = {
|
|
13720
14855
|
name: "@cmo-so/cli",
|
|
13721
|
-
version: "0.
|
|
14856
|
+
version: "0.3.0",
|
|
13722
14857
|
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.",
|
|
13723
14858
|
type: "module",
|
|
13724
14859
|
license: "MIT",
|
|
@@ -20962,6 +22097,23 @@ function hmacSigner(secret) {
|
|
|
20962
22097
|
const body = b64(canonical(approval));
|
|
20963
22098
|
return `${body}.${mac(body)}`;
|
|
20964
22099
|
},
|
|
22100
|
+
open(token, principal, now = Date.now()) {
|
|
22101
|
+
if (!token) return { ok: false, reason: "missing" };
|
|
22102
|
+
const [body, sig] = token.split(".");
|
|
22103
|
+
if (!body || !sig) return { ok: false, reason: "bad" };
|
|
22104
|
+
const want = Buffer.from(mac(body));
|
|
22105
|
+
const got = Buffer.from(sig);
|
|
22106
|
+
if (want.length !== got.length || !timingSafeEqual(want, got)) return { ok: false, reason: "bad" };
|
|
22107
|
+
let approval;
|
|
22108
|
+
try {
|
|
22109
|
+
approval = JSON.parse(unb64(body));
|
|
22110
|
+
} catch {
|
|
22111
|
+
return { ok: false, reason: "bad" };
|
|
22112
|
+
}
|
|
22113
|
+
if (!(approval.expires > now)) return { ok: false, reason: "expired" };
|
|
22114
|
+
if (approval.principal !== principal) return { ok: false, reason: "changed" };
|
|
22115
|
+
return { ok: true, approval };
|
|
22116
|
+
},
|
|
20965
22117
|
verify(token, expect, now = Date.now()) {
|
|
20966
22118
|
if (!token) return { ok: false, reason: "missing" };
|
|
20967
22119
|
const [body, sig] = token.split(".");
|
|
@@ -20986,7 +22138,7 @@ var idempotencyKey = (approvalId, call, request) => createHash("sha256").update(
|
|
|
20986
22138
|
|
|
20987
22139
|
// ../../packages/mcp/src/widgets.ts
|
|
20988
22140
|
var WIDGET_MIME = "text/html;profile=mcp-app";
|
|
20989
|
-
var VERSION = "
|
|
22141
|
+
var VERSION = "v2";
|
|
20990
22142
|
var WIDGETS = {
|
|
20991
22143
|
results: {
|
|
20992
22144
|
uri: `ui://widget/results-${VERSION}.html`,
|
|
@@ -21145,7 +22297,7 @@ function show(d) {
|
|
|
21145
22297
|
if (!d) return; data = d;
|
|
21146
22298
|
if (d.workspace) workspace = d.workspace;
|
|
21147
22299
|
const root = document.getElementById("root"); root.textContent = "";
|
|
21148
|
-
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat());
|
|
22300
|
+
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat(3));
|
|
21149
22301
|
resize();
|
|
21150
22302
|
}
|
|
21151
22303
|
(async () => {
|
|
@@ -21233,16 +22385,16 @@ function withIdempotency(api2, approvalId) {
|
|
|
21233
22385
|
}
|
|
21234
22386
|
};
|
|
21235
22387
|
}
|
|
21236
|
-
var
|
|
22388
|
+
var text2 = (value) => [
|
|
21237
22389
|
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }
|
|
21238
22390
|
];
|
|
21239
22391
|
function ok(result, extra) {
|
|
21240
22392
|
const value = result ?? { ok: true };
|
|
21241
22393
|
const structured = value && typeof value === "object" && !Array.isArray(value) ? value : { items: value };
|
|
21242
|
-
return { content:
|
|
22394
|
+
return { content: text2(value), structuredContent: { ...structured, ...extra } };
|
|
21243
22395
|
}
|
|
21244
22396
|
function fail(message) {
|
|
21245
|
-
return { content:
|
|
22397
|
+
return { content: text2({ error: message }), isError: true };
|
|
21246
22398
|
}
|
|
21247
22399
|
function errorMessage2(e) {
|
|
21248
22400
|
if (e instanceof SignInRequiredError) return "The sign-in has expired. Reconnect CMO.so and try again.";
|
|
@@ -21276,8 +22428,8 @@ function createMcpServer(deps) {
|
|
|
21276
22428
|
return deps.access ? deps.access(domain, load) : load();
|
|
21277
22429
|
}
|
|
21278
22430
|
async function resolveWorkspace(t, given) {
|
|
21279
|
-
const
|
|
21280
|
-
if (
|
|
22431
|
+
const named2 = typeof given === "string" ? given.trim().toLowerCase() : "";
|
|
22432
|
+
if (named2) return named2.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
21281
22433
|
const list2 = (await me()).workspaces ?? [];
|
|
21282
22434
|
if (list2.length === 1) return list2[0]?.domain ?? null;
|
|
21283
22435
|
if (t && isAccountTool(t)) return null;
|
|
@@ -21310,17 +22462,32 @@ function createMcpServer(deps) {
|
|
|
21310
22462
|
const t = toolByName.get(name);
|
|
21311
22463
|
if (!t || excluded(name)) return fail(`There is no tool called ${name}.`);
|
|
21312
22464
|
try {
|
|
21313
|
-
|
|
22465
|
+
let approval = null;
|
|
22466
|
+
if (isWrite(t) && args.confirm === true) {
|
|
22467
|
+
if (deps.previewOnly)
|
|
22468
|
+
return fail(
|
|
22469
|
+
"Don't confirm this yourself: the user confirms changes in the CMO.so app. Tell them what the preview will do and stop."
|
|
22470
|
+
);
|
|
22471
|
+
const o = signer.open(typeof args.confirmation === "string" ? args.confirmation : void 0, principal);
|
|
22472
|
+
if (!o.ok)
|
|
22473
|
+
return fail(
|
|
22474
|
+
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."
|
|
22475
|
+
);
|
|
22476
|
+
if (o.approval.calls.length !== 1 || o.approval.calls[0]?.name !== t.name)
|
|
22477
|
+
return fail("This confirmation is for a different action. Get a new preview for this one.");
|
|
22478
|
+
approval = o.approval;
|
|
22479
|
+
}
|
|
22480
|
+
const domain = approval ? approval.domain : await resolveWorkspace(t, args.workspace);
|
|
21314
22481
|
const access = await loadAccess(domain);
|
|
21315
22482
|
if (!allowed(t, access))
|
|
21316
22483
|
return fail(
|
|
21317
22484
|
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"}.`
|
|
21318
22485
|
);
|
|
21319
|
-
const callArgs = toolArgs(args);
|
|
21320
|
-
const ctx = { api: api2, domain };
|
|
22486
|
+
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
22487
|
+
const ctx = { api: api2, domain, client: "mcp" };
|
|
21321
22488
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
21322
|
-
|
|
21323
|
-
|
|
22489
|
+
if (!approval) {
|
|
22490
|
+
const calls = [{ name: t.name, args: callArgs }];
|
|
21324
22491
|
const summary2 = t.confirm?.(callArgs, "en") ?? t.description;
|
|
21325
22492
|
const cost2 = await previewCost(api2, domain, t);
|
|
21326
22493
|
return ok({
|
|
@@ -21329,26 +22496,19 @@ function createMcpServer(deps) {
|
|
|
21329
22496
|
risk: t.risk,
|
|
21330
22497
|
workspace: domain,
|
|
21331
22498
|
confirmation: signer.sign({ calls, principal, domain }),
|
|
21332
|
-
next: "Show the preview to the user. If they agree, call this tool again with
|
|
22499
|
+
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."
|
|
21333
22500
|
});
|
|
21334
22501
|
}
|
|
21335
|
-
const v = signer.verify(typeof args.confirmation === "string" ? args.confirmation : void 0, {
|
|
21336
|
-
calls,
|
|
21337
|
-
principal,
|
|
21338
|
-
domain
|
|
21339
|
-
});
|
|
21340
|
-
if (!v.ok)
|
|
21341
|
-
return fail(
|
|
21342
|
-
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."
|
|
21343
|
-
);
|
|
21344
22502
|
const summary = t.confirm?.(callArgs, "en") ?? t.description;
|
|
21345
22503
|
try {
|
|
21346
|
-
const result = await t.run({ api: withIdempotency(api2,
|
|
21347
|
-
|
|
22504
|
+
const result = await t.run({ api: withIdempotency(api2, approval.id), domain, client: "mcp" }, callArgs);
|
|
22505
|
+
const added = t.name === "website_add" ? result?.domain : null;
|
|
22506
|
+
const where = typeof added === "string" && added ? added : domain;
|
|
22507
|
+
await deps.audit?.({ domain: where, actions: [{ tool: t.name, summary, ok: true, action_id: approval.id }] }).catch(() => void 0);
|
|
21348
22508
|
return ok(result);
|
|
21349
22509
|
} catch (e) {
|
|
21350
22510
|
const error2 = errorMessage2(e);
|
|
21351
|
-
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id:
|
|
22511
|
+
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id: approval.id }] }).catch(() => void 0);
|
|
21352
22512
|
return fail(error2);
|
|
21353
22513
|
}
|
|
21354
22514
|
} catch (e) {
|
|
@@ -21595,8 +22755,8 @@ function errorText(e) {
|
|
|
21595
22755
|
return e instanceof Error ? e.message : String(e);
|
|
21596
22756
|
}
|
|
21597
22757
|
async function workspaceFor(api2, opts, required2 = true) {
|
|
21598
|
-
const
|
|
21599
|
-
if (
|
|
22758
|
+
const named2 = opts.workspace ?? process.env.CMO_WORKSPACE ?? (await settings.get()).workspace;
|
|
22759
|
+
if (named2) return named2.trim().toLowerCase();
|
|
21600
22760
|
const list2 = (await api2.me()).workspaces ?? [];
|
|
21601
22761
|
if (list2.length === 1) return list2[0]?.domain ?? null;
|
|
21602
22762
|
if (!required2) return null;
|
|
@@ -21649,9 +22809,9 @@ async function runTool(api2, name, args, opts) {
|
|
|
21649
22809
|
const preview = [say2, c, domain && t.need.resource !== "account" ? `(${domain})` : ""].filter(Boolean).join(" ");
|
|
21650
22810
|
if (opts.dryRun) return { ran: false, preview };
|
|
21651
22811
|
if (!opts.yes && !await ask(preview)) throw new CliError("Nothing changed.", 1);
|
|
21652
|
-
return { ran: true, result: await t.run({ api: withIdempotency2(api2), domain }, args), domain };
|
|
22812
|
+
return { ran: true, result: await t.run({ api: withIdempotency2(api2), domain, client: "cli" }, args), domain };
|
|
21653
22813
|
}
|
|
21654
|
-
return { ran: true, result: await t.run({ api: api2, domain }, args), domain };
|
|
22814
|
+
return { ran: true, result: await t.run({ api: api2, domain, client: "cli" }, args), domain };
|
|
21655
22815
|
}
|
|
21656
22816
|
function parsePairs(pairs) {
|
|
21657
22817
|
const out = {};
|
|
@@ -21835,30 +22995,47 @@ posts.command("ls", { isDefault: true }).description("planned, draft or publishe
|
|
|
21835
22995
|
posts.command("show <id>").description("one post in full").action(act(async (opts, id) => tool2(opts, "post_get", { id })));
|
|
21836
22996
|
posts.command("publish <id>").description("publish a draft now (uses a post credit)").action(act(async (opts, id) => tool2(opts, "post_publish_now", { id })));
|
|
21837
22997
|
posts.command("delete <id>").description("delete a draft").action(act(async (opts, id) => tool2(opts, "draft_delete", { id })));
|
|
21838
|
-
var social = program2.command("social").description("your LinkedIn
|
|
22998
|
+
var social = program2.command("social").description("your social posts: LinkedIn, your Facebook Page, Instagram and your Google Business Profile");
|
|
22999
|
+
var CHANNELS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
23000
|
+
var channelsOpt = (value) => {
|
|
23001
|
+
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).map((v) => v === "google" || v === "gbp" ? "google_business" : v).filter((v) => CHANNELS.includes(v));
|
|
23002
|
+
return list2.length ? list2 : void 0;
|
|
23003
|
+
};
|
|
21839
23004
|
social.command("ls", { isDefault: true }).description("drafts to review and queued posts").action(
|
|
21840
23005
|
act(
|
|
21841
23006
|
async (opts) => tool2(opts, "social_list", {}, (v) => {
|
|
21842
23007
|
const s = v;
|
|
21843
|
-
const
|
|
23008
|
+
const live = (s.channels ?? []).filter((c) => c.connected);
|
|
23009
|
+
const head = !s.connected ? "No social channel is connected (LinkedIn, a Facebook Page, Instagram or a Google Business Profile)." : s.posting_paused ? "Posting is paused on every channel." : live.length ? live.map((c) => `${c.channel}: ${c.paused ? "paused" : "on"}${c.account ? ` (${c.account})` : ""}`).join(" \xB7 ") : "Posting is on.";
|
|
21844
23010
|
return `${head}
|
|
21845
|
-
${table(s.queue ?? [], ["id", "status", "eta", "text"])}`;
|
|
23011
|
+
${table(s.queue ?? [], ["id", "channel", "status", "eta", "text"])}`;
|
|
21846
23012
|
})
|
|
21847
23013
|
)
|
|
21848
23014
|
);
|
|
21849
23015
|
social.command("approve <ids...>").description("approve drafts: they go out at the next posting times").action(act(async (opts, list2) => tool2(opts, "social_approve", { ids: ids2(list2) })));
|
|
21850
23016
|
social.command("skip <id>").description("skip a draft").action(act(async (opts, id) => tool2(opts, "social_skip", { id: ids2([id])[0] })));
|
|
21851
23017
|
social.command("restore <id>").description("bring a skipped draft back").action(act(async (opts, id) => tool2(opts, "social_restore", { id: ids2([id])[0] })));
|
|
21852
|
-
social.command("draft <topic...>").description("add a
|
|
23018
|
+
social.command("draft <topic...>").description("add a post idea for Maggie to write (every connected channel unless --channel)").option("--summary <text>", "what the post should say").option("--channel <names>", "linkedin, facebook, instagram, google_business (comma-separated)").action(
|
|
21853
23019
|
act(
|
|
21854
|
-
async (opts, topic) => tool2(opts, "social_idea_add", {
|
|
23020
|
+
async (opts, topic) => tool2(opts, "social_idea_add", {
|
|
23021
|
+
title: topic.join(" "),
|
|
23022
|
+
...opts.summary ? { summary: opts.summary } : {},
|
|
23023
|
+
...channelsOpt(opts.channel) ? { channels: channelsOpt(opts.channel) } : {}
|
|
23024
|
+
})
|
|
21855
23025
|
)
|
|
21856
23026
|
);
|
|
21857
23027
|
for (const [cmd, paused] of [
|
|
21858
23028
|
["pause", true],
|
|
21859
23029
|
["resume", false]
|
|
21860
23030
|
])
|
|
21861
|
-
social.command(cmd).description(`${cmd} posting
|
|
23031
|
+
social.command(cmd).description(`${cmd} posting (every channel, or --channel linkedin | facebook | instagram | google_business)`).option("--channel <name>", "only this channel").action(
|
|
23032
|
+
act(
|
|
23033
|
+
async (opts) => tool2(opts, "social_set_paused", {
|
|
23034
|
+
paused,
|
|
23035
|
+
...channelsOpt(opts.channel)?.[0] ? { channel: channelsOpt(opts.channel)?.[0] } : {}
|
|
23036
|
+
})
|
|
23037
|
+
)
|
|
23038
|
+
);
|
|
21862
23039
|
var autopilot = program2.command("autopilot").description("the blog autopilot");
|
|
21863
23040
|
autopilot.command("status", { isDefault: true }).description("on or paused, posts per day, where it publishes").action(act(async (opts) => tool2(opts, "publish_settings_get")));
|
|
21864
23041
|
for (const [cmd, paused] of [
|
|
@@ -21931,6 +23108,47 @@ program2.command("tools").description("every action Maggie can take (run one wit
|
|
|
21931
23108
|
);
|
|
21932
23109
|
})
|
|
21933
23110
|
);
|
|
23111
|
+
var FEEDBACK_TYPES = ["bug", "quality", "idea", "praise", "other"];
|
|
23112
|
+
var feedback = program2.command("feedback").description('tell the cmo.so team what to fix or build: cmo feedback "<one line>" [--type bug]');
|
|
23113
|
+
feedback.command("send <summary...>", { isDefault: true }).description("send feedback (asks first; --yes to skip)").addOption(new Option("-t, --type <type>", "what it is").choices(FEEDBACK_TYPES).default("other")).option("-d, --details <text>", "what happened, in more words").option("--expected <text>", "for a bug: what you expected").option("--actual <text>", "for a bug: what happened instead").action(
|
|
23114
|
+
act(
|
|
23115
|
+
async (opts, words) => {
|
|
23116
|
+
const summary = words.join(" ").trim();
|
|
23117
|
+
if (!summary)
|
|
23118
|
+
throw new CliError('Say what it is about: cmo feedback "Maggie made 14 ideas when I asked for 7"');
|
|
23119
|
+
await tool2(
|
|
23120
|
+
opts,
|
|
23121
|
+
"feedback_send",
|
|
23122
|
+
{
|
|
23123
|
+
type: opts.type,
|
|
23124
|
+
summary,
|
|
23125
|
+
...opts.details ? { details: opts.details } : {},
|
|
23126
|
+
...opts.expected ? { expected: opts.expected } : {},
|
|
23127
|
+
...opts.actual ? { actual: opts.actual } : {}
|
|
23128
|
+
},
|
|
23129
|
+
() => "Sent. Thank you! See the team's answer with: cmo feedback ls"
|
|
23130
|
+
);
|
|
23131
|
+
}
|
|
23132
|
+
)
|
|
23133
|
+
);
|
|
23134
|
+
feedback.command("ls").description("what you sent, its status and the team's reply").action(
|
|
23135
|
+
act(async (opts) => {
|
|
23136
|
+
const { items } = await api().feedback.mine();
|
|
23137
|
+
print(
|
|
23138
|
+
items,
|
|
23139
|
+
opts.json,
|
|
23140
|
+
() => items.length ? table(
|
|
23141
|
+
items.map((f) => ({
|
|
23142
|
+
sent: f.created_at.slice(0, 10),
|
|
23143
|
+
type: f.type,
|
|
23144
|
+
status: f.status.replace("_", " ") + (f.unread ? " (new reply)" : ""),
|
|
23145
|
+
summary: f.summary,
|
|
23146
|
+
reply: f.reply || "-"
|
|
23147
|
+
}))
|
|
23148
|
+
) : 'Nothing sent yet. Try: cmo feedback "\u2026"'
|
|
23149
|
+
);
|
|
23150
|
+
})
|
|
23151
|
+
);
|
|
21934
23152
|
program2.command("run <tool> [args...]").description("run any tool, e.g. cmo run gsc_queries_get period=30d filter=low_ctr limit=10").option("--args <json>", "arguments as JSON instead of key=value").action(
|
|
21935
23153
|
act(async (opts, name, pairs = []) => {
|
|
21936
23154
|
let args;
|