@cmo-so/cli 0.1.0 → 0.2.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 +996 -178
- 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"];
|
|
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,23 @@ 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
|
+
publishSocialNow: (id) => post(`${base}/social/${id}/publish`),
|
|
7596
|
+
/** Take a posted LinkedIn post down (its likes and comments go with it). */
|
|
7597
|
+
deletePostedSocial: (id) => api2.request(`${base}/social/${id}/published`, { method: "DELETE" }),
|
|
7598
|
+
/** Change a posted LinkedIn post and put it up again: the old one comes down, the new text goes out now. */
|
|
7599
|
+
republishSocial: (id, socialCopy) => post(`${base}/social/${id}/republish`, socialCopy ? { social_copy: socialCopy } : {}),
|
|
7466
7600
|
restoreSocial: (id) => post(`${base}/social/${id}/restore`),
|
|
7467
7601
|
pauseSocial: () => post(`${base}/social/pause`),
|
|
7468
7602
|
/**
|
|
@@ -7508,6 +7642,67 @@ function domainApi(api2, domain) {
|
|
|
7508
7642
|
/** Maggie drafts the first LinkedIn post (1 credit): from a few lines, or one of your services. */
|
|
7509
7643
|
socialFirstPost: (input) => post(`${base}/social/setup/first-post`, input),
|
|
7510
7644
|
socialSetupDone: () => post(`${base}/social/setup/done`),
|
|
7645
|
+
// LinkedIn v2 (docs/prd/linkedin-journey-v2.md): Maggie's plan instead of the step-by-step setup.
|
|
7646
|
+
/** What is set up now, as a plan (no AI). */
|
|
7647
|
+
socialSetupPlan: () => api2.request(`${base}/social/setup/plan`),
|
|
7648
|
+
/** Maggie prepares what's missing (logo, look, voice, writing, frequency); `force` redoes it. Free. */
|
|
7649
|
+
socialSetupAuto: (timezone, force = false) => post(`${base}/social/setup/auto`, {
|
|
7650
|
+
...timezone ? { timezone } : {},
|
|
7651
|
+
...force ? { force: true } : {}
|
|
7652
|
+
}),
|
|
7653
|
+
socialSetupFrequency: (preset, timezone) => api2.request(`${base}/social/setup/frequency`, {
|
|
7654
|
+
method: "PUT",
|
|
7655
|
+
headers: { "Content-Type": "application/json" },
|
|
7656
|
+
body: JSON.stringify(timezone ? { preset, timezone } : { preset })
|
|
7657
|
+
}),
|
|
7658
|
+
/** "Looks good, start": setup done, and Maggie writes the first week's posts. */
|
|
7659
|
+
socialSetupConfirm: () => post(`${base}/social/setup/confirm`),
|
|
7660
|
+
/** OK every post waiting for you (not a series that hasn't been approved), on one channel or all. */
|
|
7661
|
+
approveAllSocial: (channel) => post(
|
|
7662
|
+
`${base}/social/approve-all${channel ? `?channel=${channel}` : ""}`
|
|
7663
|
+
),
|
|
7664
|
+
/** A Facebook sign-in link: after it the user lands on returnTo with ?facebook=connected (or =error). */
|
|
7665
|
+
connectFacebook: (returnTo = "close") => post(`${base}/social/facebook/connect`, { return_to: returnTo }),
|
|
7666
|
+
/** The Facebook Pages you connected, and the one this website posts to. */
|
|
7667
|
+
facebookPages: () => api2.request(`${base}/social/facebook/pages`),
|
|
7668
|
+
chooseFacebookPage: (connectionId) => api2.request(
|
|
7669
|
+
`${base}/social/facebook/page`,
|
|
7670
|
+
{
|
|
7671
|
+
method: "PUT",
|
|
7672
|
+
headers: { "Content-Type": "application/json" },
|
|
7673
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7674
|
+
}
|
|
7675
|
+
),
|
|
7676
|
+
disconnectFacebook: (connectionId) => api2.request(`${base}/social/facebook/${connectionId}`, { method: "DELETE" }),
|
|
7677
|
+
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7678
|
+
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7679
|
+
/** How often one channel posts. */
|
|
7680
|
+
setChannelFrequency: (channel, preset) => api2.request(
|
|
7681
|
+
`${base}/social/channels/${channel}/frequency`,
|
|
7682
|
+
{ method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ preset }) }
|
|
7683
|
+
),
|
|
7684
|
+
/** Start a new picture for the post in the background (one AI generation); `replace` stops the one
|
|
7685
|
+
* already running; `instruction` (up to 300 characters) steers it: "gold border, our logo". */
|
|
7686
|
+
regenerateSocialImage: (id, opts = {}) => post(`${base}/social/${id}/image/regenerate`, {
|
|
7687
|
+
...opts.replace ? { replace: true } : {},
|
|
7688
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
|
|
7689
|
+
}),
|
|
7690
|
+
/** Your own photo (base64 JPEG) as the post's picture: no AI, no charge; only before it goes out. */
|
|
7691
|
+
uploadSocialPhoto: (id, photo) => post(`${base}/social/${id}/image`, { photo }),
|
|
7692
|
+
/** Stop the picture being made (it's thrown away). */
|
|
7693
|
+
stopSocialImage: () => api2.request(`${base}/social/image-job`, { method: "DELETE" }),
|
|
7694
|
+
/** Maggie changes the post as asked, in 3-255 characters (one AI generation). */
|
|
7695
|
+
rewriteSocial: (id, instruction) => post(`${base}/social/${id}/rewrite`, { instruction }),
|
|
7696
|
+
/** Maggie plans and writes a series; it waits for one OK. */
|
|
7697
|
+
/** A series of `count` (1-7) posts on one theme; they are ordinary posts (Review), labelled with the series. */
|
|
7698
|
+
createSocialSeries: (input) => post(`${base}/social/series`, input),
|
|
7699
|
+
approveSocialSeries: (id) => post(`${base}/social/series/${id}/approve`),
|
|
7700
|
+
/** A post from photos (and a line about them): it waits for your OK like any other. */
|
|
7701
|
+
socialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7702
|
+
photos,
|
|
7703
|
+
brief: brief2,
|
|
7704
|
+
...channels ? { channels } : {}
|
|
7705
|
+
}),
|
|
7511
7706
|
/** Disconnect the website's LinkedIn account. */
|
|
7512
7707
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
7513
7708
|
/** Published articles a LinkedIn post can be made from. */
|
|
@@ -7515,9 +7710,16 @@ function domainApi(api2, domain) {
|
|
|
7515
7710
|
`${base}/social/sources?search=${encodeURIComponent(search)}`
|
|
7516
7711
|
),
|
|
7517
7712
|
/** Maggie writes a LinkedIn draft about one article. */
|
|
7518
|
-
socialFromPost: (sourcePostId) => post(`${base}/social/from-post`, {
|
|
7713
|
+
socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7714
|
+
source_post_id: sourcePostId,
|
|
7715
|
+
...channels ? { channels } : {}
|
|
7716
|
+
}),
|
|
7519
7717
|
/** Drafts for the next 1-7 days from the best recent articles. */
|
|
7520
|
-
socialPlan: (days, timezone) => post(`${base}/social/plan`, {
|
|
7718
|
+
socialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7719
|
+
days,
|
|
7720
|
+
timezone,
|
|
7721
|
+
...channels ? { channels } : {}
|
|
7722
|
+
}),
|
|
7521
7723
|
/** Campaign ideas waiting to be written (this person's, newest campaign first). */
|
|
7522
7724
|
socialIdeas: (page = 1, perPage = 20) => api2.request(
|
|
7523
7725
|
`${base}/social/ideas?page=${page}&per_page=${perPage}`
|
|
@@ -7537,14 +7739,35 @@ function domainApi(api2, domain) {
|
|
|
7537
7739
|
`${base}/social/campaigns/${id}/start`,
|
|
7538
7740
|
options
|
|
7539
7741
|
),
|
|
7540
|
-
/**
|
|
7541
|
-
|
|
7742
|
+
/**
|
|
7743
|
+
* Maggie writes the post for this idea (1 credit); it waits in Upcoming.
|
|
7744
|
+
* With `written` ({ linkedin: { body } }, from socialIdeaBrief), the user's own
|
|
7745
|
+
* assistant wrote it: saved through the same checks, for the same credit.
|
|
7746
|
+
*/
|
|
7747
|
+
writeSocialIdea: (id, written, channels) => post(
|
|
7748
|
+
`${base}/social/ideas/${id}/write`,
|
|
7749
|
+
written || channels ? { ...written ? { written } : {}, ...channels ? { channels } : {} } : void 0
|
|
7750
|
+
),
|
|
7751
|
+
/** What Maggie would write this idea's post from: the prompt, JSON shape and length limits (free). */
|
|
7752
|
+
socialIdeaBrief: (id) => api2.request(`${base}/social/ideas/${id}/brief`),
|
|
7542
7753
|
/** Change a LinkedIn post's text before it goes out. */
|
|
7543
7754
|
updateSocial: (id, socialCopy) => api2.request(`${base}/social/${id}`, {
|
|
7544
7755
|
method: "PATCH",
|
|
7545
7756
|
headers: { "Content-Type": "application/json" },
|
|
7546
7757
|
body: JSON.stringify({ social_copy: socialCopy })
|
|
7547
7758
|
}),
|
|
7759
|
+
/** The post's link: it replaces the old one in the text, and the preview card shows the new page. */
|
|
7760
|
+
setSocialLink: (id, link) => api2.request(`${base}/social/${id}`, {
|
|
7761
|
+
method: "PATCH",
|
|
7762
|
+
headers: { "Content-Type": "application/json" },
|
|
7763
|
+
body: JSON.stringify({ link })
|
|
7764
|
+
}),
|
|
7765
|
+
/** Go out as the post's image, or as a link preview. */
|
|
7766
|
+
setSocialPublishMode: (id, mode) => api2.request(`${base}/social/${id}`, {
|
|
7767
|
+
method: "PATCH",
|
|
7768
|
+
headers: { "Content-Type": "application/json" },
|
|
7769
|
+
body: JSON.stringify({ publish_mode: mode })
|
|
7770
|
+
}),
|
|
7548
7771
|
resumeSocial: () => post(`${base}/social/resume`),
|
|
7549
7772
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7550
7773
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
@@ -7574,6 +7797,8 @@ function domainApi(api2, domain) {
|
|
|
7574
7797
|
planCampaign: (id) => post(`${base}/social/campaigns/${id}/plan`),
|
|
7575
7798
|
pauseCampaign: (id) => post(`${base}/social/campaigns/${id}/pause`),
|
|
7576
7799
|
resumeCampaign: (id) => post(`${base}/social/campaigns/${id}/resume`),
|
|
7800
|
+
/** End a series: its posts that haven't gone out are dropped; published ones stay on LinkedIn. */
|
|
7801
|
+
endCampaign: (id) => post(`${base}/social/campaigns/${id}/end`),
|
|
7577
7802
|
setApprovalMode: (mode) => api2.request(`${base}/social/approval-mode`, {
|
|
7578
7803
|
method: "PUT",
|
|
7579
7804
|
headers: { "Content-Type": "application/json" },
|
|
@@ -7590,6 +7815,22 @@ function domainApi(api2, domain) {
|
|
|
7590
7815
|
}),
|
|
7591
7816
|
/** What Maggie changed for you on this website, newest first (docs/18 MT-SEC-6). */
|
|
7592
7817
|
maggieActions: (limit = 30) => api2.request(`${base}/maggie/actions?limit=${limit}`),
|
|
7818
|
+
// Maggie Memory (docs/21): the user's own memory on this website.
|
|
7819
|
+
maggieMemory: () => api2.request(`${base}/maggie/memory`),
|
|
7820
|
+
maggieMemorySettings: (s) => put(`${base}/maggie/memory/settings`, s),
|
|
7821
|
+
addMaggieMemory: (item2) => post(`${base}/maggie/memory/items`, item2),
|
|
7822
|
+
updateMaggieMemory: (id, patch) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, {
|
|
7823
|
+
method: "PATCH",
|
|
7824
|
+
headers: { "Content-Type": "application/json" },
|
|
7825
|
+
body: JSON.stringify(patch)
|
|
7826
|
+
}),
|
|
7827
|
+
deleteMaggieMemory: (id) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, { method: "DELETE" }),
|
|
7828
|
+
/** Learn from recent activity now; `full` rebuilds the learned items from scratch. */
|
|
7829
|
+
learnMaggieMemory: (o = {}) => post(`${base}/maggie/memory/learn`, o),
|
|
7830
|
+
/** Forget what was learned (one channel, or all); the user's own items stay. */
|
|
7831
|
+
clearMaggieMemory: (channel) => api2.request(`${base}/maggie/memory${channel ? `?channel=${encodeURIComponent(channel)}` : ""}`, {
|
|
7832
|
+
method: "DELETE"
|
|
7833
|
+
}),
|
|
7593
7834
|
// docs/19 P3: autopilot cadence and publish-now.
|
|
7594
7835
|
autopilot: () => api2.request(`${base}/autopilot`),
|
|
7595
7836
|
/** Posts per day for pushed blogs (one of `choices`, up to the plan's limit). */
|
|
@@ -7753,6 +7994,11 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
|
|
|
7753
7994
|
|
|
7754
7995
|
// ../../packages/domain/src/copy/maggie.ts
|
|
7755
7996
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
7997
|
+
var named = (title, id) => {
|
|
7998
|
+
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
7999
|
+
const s = String(id ?? "").trim();
|
|
8000
|
+
return s && s.length <= 8 ? ` #${s}` : "";
|
|
8001
|
+
};
|
|
7756
8002
|
var list = (xs, n = 5) => {
|
|
7757
8003
|
const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
|
|
7758
8004
|
return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
|
|
@@ -7835,41 +8081,45 @@ var confirmCopy = {
|
|
|
7835
8081
|
"zh-Hant": (a) => `\u70BA ${list(a.target_audience)} \u5EFA\u7ACB\u54C1\u724C\u8A9E\u6C23 ${q(a.name ?? "\u65B0\u8A9E\u6C23")}\u3002`
|
|
7836
8082
|
},
|
|
7837
8083
|
brand_voice_update_style: {
|
|
7838
|
-
en: (a) => `Change how the brand voice
|
|
7839
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u54C1\u724C\u8A9E\u6C23
|
|
8084
|
+
en: (a) => `Change how the brand voice${named(a.name, a.id)} writes: ${keys(a).filter((k) => k !== "id" && k !== "name").join(", ")}.`,
|
|
8085
|
+
"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
8086
|
},
|
|
7841
8087
|
brand_voice_set_default: {
|
|
7842
|
-
en: (a) => `Make
|
|
7843
|
-
"zh-Hant": (a) => `\u628A
|
|
8088
|
+
en: (a) => `Make${named(a.name, a.id)} your default brand voice.`,
|
|
8089
|
+
"zh-Hant": (a) => `\u628A${named(a.name, a.id)} \u8A2D\u70BA\u9810\u8A2D\u54C1\u724C\u8A9E\u6C23\u3002`
|
|
7844
8090
|
},
|
|
7845
8091
|
brand_voice_delete: {
|
|
7846
|
-
en: (a) => `Delete the brand voice
|
|
7847
|
-
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23
|
|
8092
|
+
en: (a) => `Delete the brand voice${named(a.name, a.id)}. This can't be undone.`,
|
|
8093
|
+
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23${named(a.name, a.id)}\uFF0C\u522A\u9664\u5F8C\u7121\u6CD5\u6062\u5FA9\u3002`
|
|
7848
8094
|
},
|
|
7849
8095
|
// ------------------------------------------------------------------- blog
|
|
7850
8096
|
draft_update: {
|
|
7851
|
-
en: (a) => `Change the draft
|
|
7852
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8349\u7A3F
|
|
8097
|
+
en: (a) => `Change the draft${named(a.title_for_card, a.id)}: ${keys(a).filter((k) => !["id", "title_for_card"].includes(k)).join(", ")}.`,
|
|
8098
|
+
"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
8099
|
},
|
|
7854
8100
|
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
|
|
8101
|
+
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.`,
|
|
8102
|
+
"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`
|
|
8103
|
+
},
|
|
8104
|
+
draft_rewrite_save: {
|
|
8105
|
+
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.`,
|
|
8106
|
+
"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
8107
|
},
|
|
7858
8108
|
draft_delete: {
|
|
7859
|
-
en: (a) => `Delete the draft
|
|
7860
|
-
"zh-Hant": (a) => `\u522A\u9664\u8349\u7A3F
|
|
8109
|
+
en: (a) => `Delete the draft${named(a.title, a.id)}. Its idea is dropped too and the credit is not refunded.`,
|
|
8110
|
+
"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
8111
|
},
|
|
7862
8112
|
plan_move: {
|
|
7863
|
-
en: (a) => `Move
|
|
7864
|
-
"zh-Hant": (a) => `\u628A
|
|
8113
|
+
en: (a) => `Move${named(a.title, a.id)} ${a.to === "top" ? "to the top of" : a.to} the plan.`,
|
|
8114
|
+
"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
8115
|
},
|
|
7866
8116
|
plan_update: {
|
|
7867
|
-
en: (a) => `Change the planned post
|
|
7868
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0
|
|
8117
|
+
en: (a) => `Change the planned post${named(a.title_for_card, a.id)}.`,
|
|
8118
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0${named(a.title_for_card, a.id)}\u3002`
|
|
7869
8119
|
},
|
|
7870
8120
|
plan_remove: {
|
|
7871
|
-
en: (a) => `Take
|
|
7872
|
-
"zh-Hant": (a) => `\u628A
|
|
8121
|
+
en: (a) => `Take${named(a.title, a.id)} out of the plan (its idea can be restored).`,
|
|
8122
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u79FB\u51FA\u8A08\u5283\uFF08idea \u53EF\u4EE5\u6062\u5FA9\uFF09\u3002`
|
|
7873
8123
|
},
|
|
7874
8124
|
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
8125
|
ideas_generate: {
|
|
@@ -7881,24 +8131,24 @@ var confirmCopy = {
|
|
|
7881
8131
|
"zh-Hant": (a) => `\u5F9E ${a.sitemap_url || "\u4F60\u7684 sitemap"} \u5C0E\u5165\u6700\u591A ${a.limit || 10} \u500B idea\u3002`
|
|
7882
8132
|
},
|
|
7883
8133
|
idea_update: {
|
|
7884
|
-
en: (a) => `Change the idea
|
|
7885
|
-
"zh-Hant": (a) => `\u4FEE\u6539 idea
|
|
8134
|
+
en: (a) => `Change the idea${named(a.title_for_card, a.id)}.`,
|
|
8135
|
+
"zh-Hant": (a) => `\u4FEE\u6539 idea${named(a.title_for_card, a.id)}\u3002`
|
|
7886
8136
|
},
|
|
7887
8137
|
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"}
|
|
8138
|
+
en: (a) => `${Number(a.delta) > 0 ? "Raise" : "Lower"} the priority of${named(a.title, a.id)}.`,
|
|
8139
|
+
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
|
|
7890
8140
|
},
|
|
7891
8141
|
idea_plan: {
|
|
7892
|
-
en: (a) => `Add
|
|
7893
|
-
"zh-Hant": (a) => `\u628A
|
|
8142
|
+
en: (a) => `Add${named(a.title, a.id)} to the end of the plan.`,
|
|
8143
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u52A0\u5230\u8A08\u5283\u6700\u5F8C\u3002`
|
|
7894
8144
|
},
|
|
7895
8145
|
idea_discard: {
|
|
7896
|
-
en: (a) => `Remove the idea
|
|
7897
|
-
"zh-Hant": (a) => `\u522A\u9664 idea
|
|
8146
|
+
en: (a) => `Remove the idea${named(a.title, a.id)}.`,
|
|
8147
|
+
"zh-Hant": (a) => `\u522A\u9664 idea${named(a.title, a.id)}\u3002`
|
|
7898
8148
|
},
|
|
7899
8149
|
idea_restore: {
|
|
7900
|
-
en: (a) => `Bring back the idea
|
|
7901
|
-
"zh-Hant": (a) => `\u6062\u5FA9 idea
|
|
8150
|
+
en: (a) => `Bring back the idea${named(a.title, a.id)}.`,
|
|
8151
|
+
"zh-Hant": (a) => `\u6062\u5FA9 idea${named(a.title, a.id)}\u3002`
|
|
7902
8152
|
},
|
|
7903
8153
|
autopilot_set_paused: {
|
|
7904
8154
|
en: (a) => a.paused ? "Pause publishing blog posts." : "Resume publishing blog posts.",
|
|
@@ -7909,8 +8159,8 @@ var confirmCopy = {
|
|
|
7909
8159
|
"zh-Hant": (a) => `\u6BCF\u5929\u767C\u4F48 ${a.posts_per_day} \u7BC7\u90E8\u843D\u683C\u6587\u7AE0\u3002`
|
|
7910
8160
|
},
|
|
7911
8161
|
post_publish_now: {
|
|
7912
|
-
en: (a) => `Publish
|
|
7913
|
-
"zh-Hant": (a) => `\u7ACB\u5373\u628A
|
|
8162
|
+
en: (a) => `Publish${named(a.title, a.id)} to your website now. Everyone can see it once it's out.`,
|
|
8163
|
+
"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
8164
|
},
|
|
7915
8165
|
rewrite_request: {
|
|
7916
8166
|
en: (a) => `Rewrite the live article ${a.url}${a.instructions ? ` (${String(a.instructions).slice(0, 80)})` : ""}. The published page will change.`,
|
|
@@ -7924,11 +8174,11 @@ var confirmCopy = {
|
|
|
7924
8174
|
social_approve: {
|
|
7925
8175
|
en: (a) => {
|
|
7926
8176
|
const n = ids(a);
|
|
7927
|
-
return n.length > 1 ? `Approve ${n.length} LinkedIn posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes
|
|
8177
|
+
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
8178
|
},
|
|
7929
8179
|
"zh-Hant": (a) => {
|
|
7930
8180
|
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\
|
|
8181
|
+
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
8182
|
}
|
|
7933
8183
|
},
|
|
7934
8184
|
social_skip: { en: (a) => `Skip LinkedIn post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E LinkedIn \u8CBC\u6587 #${a.id}\u3002` },
|
|
@@ -7936,21 +8186,29 @@ var confirmCopy = {
|
|
|
7936
8186
|
en: (a) => `Bring back LinkedIn post #${a.id}.`,
|
|
7937
8187
|
"zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
|
|
7938
8188
|
},
|
|
8189
|
+
social_image_new: {
|
|
8190
|
+
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" : ""}.`,
|
|
8191
|
+
"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`
|
|
8192
|
+
},
|
|
8193
|
+
social_image_stop: {
|
|
8194
|
+
en: () => "Stop the LinkedIn picture being made; the post keeps the one it had.",
|
|
8195
|
+
"zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684 LinkedIn \u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
|
|
8196
|
+
},
|
|
7939
8197
|
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\
|
|
8198
|
+
en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the top of" : a.to} Upcoming.`,
|
|
8199
|
+
"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
8200
|
},
|
|
7943
8201
|
social_set_paused: {
|
|
7944
8202
|
en: (a) => a.paused ? "Pause all LinkedIn posting." : "Resume LinkedIn posting.",
|
|
7945
8203
|
"zh-Hant": (a) => a.paused ? "\u66AB\u505C\u6240\u6709 LinkedIn \u767C\u6587\u3002" : "\u6062\u5FA9 LinkedIn \u767C\u6587\u3002"
|
|
7946
8204
|
},
|
|
7947
8205
|
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\
|
|
8206
|
+
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.`,
|
|
8207
|
+
"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
8208
|
},
|
|
7951
8209
|
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`
|
|
8210
|
+
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.`,
|
|
8211
|
+
"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
8212
|
},
|
|
7955
8213
|
social_idea_add: {
|
|
7956
8214
|
en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
|
|
@@ -7965,12 +8223,30 @@ var confirmCopy = {
|
|
|
7965
8223
|
"zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
|
|
7966
8224
|
},
|
|
7967
8225
|
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`
|
|
8226
|
+
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.`,
|
|
8227
|
+
"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`
|
|
8228
|
+
},
|
|
8229
|
+
social_idea_save: {
|
|
8230
|
+
en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
|
|
8231
|
+
"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
8232
|
},
|
|
7971
8233
|
campaign_start: {
|
|
7972
|
-
en: (a) => `Start the
|
|
7973
|
-
"zh-Hant": (a) => `${a.start_date ? `\u5F9E ${a.start_date}` : "\u5F9E\u4ECA\u5929"}\u958B\u59CB\
|
|
8234
|
+
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.`,
|
|
8235
|
+
"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`
|
|
8236
|
+
},
|
|
8237
|
+
// ---------------------------------------------------------- Maggie Memory
|
|
8238
|
+
memory_remember: {
|
|
8239
|
+
en: (a) => `Remember${a.channel === "blog" ? " for blog posts" : a.channel === "linkedin" ? " for LinkedIn" : ""}: ${String(a.text ?? "").slice(0, 160)}`,
|
|
8240
|
+
"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)}`
|
|
8241
|
+
},
|
|
8242
|
+
memory_forget: {
|
|
8243
|
+
en: (a) => `Forget${a.text_for_card ? ` ${q(a.text_for_card)}` : " this"} from your Maggie Memory.`,
|
|
8244
|
+
"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`
|
|
8245
|
+
},
|
|
8246
|
+
// ---------------------------------------------------------- Help Maggie grow
|
|
8247
|
+
feedback_send: {
|
|
8248
|
+
en: (a) => `Send this to the cmo.so team: ${q(String(a.summary ?? "").slice(0, 200))}`,
|
|
8249
|
+
"zh-Hant": (a) => `\u628A\u9019\u5247\u56DE\u994B\u9001\u7D66 cmo.so \u5718\u968A\uFF1A${q(String(a.summary ?? "").slice(0, 200))}`
|
|
7974
8250
|
},
|
|
7975
8251
|
social_voice_learn: {
|
|
7976
8252
|
en: () => "Learn your LinkedIn voice from this post and save it as your default.",
|
|
@@ -8000,17 +8276,29 @@ var confirmCopy = {
|
|
|
8000
8276
|
en: () => "Make a link for you to connect LinkedIn.",
|
|
8001
8277
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8002
8278
|
},
|
|
8279
|
+
social_approve_all: {
|
|
8280
|
+
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.",
|
|
8281
|
+
"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"
|
|
8282
|
+
},
|
|
8283
|
+
series_create: {
|
|
8284
|
+
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.`,
|
|
8285
|
+
"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`
|
|
8286
|
+
},
|
|
8287
|
+
series_approve: {
|
|
8288
|
+
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.`,
|
|
8289
|
+
"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`
|
|
8290
|
+
},
|
|
8003
8291
|
campaign_create: {
|
|
8004
|
-
en: (a) => `
|
|
8005
|
-
"zh-Hant": (a) => `\
|
|
8292
|
+
en: (a) => `Plan the LinkedIn series ${q(a.name)}.`,
|
|
8293
|
+
"zh-Hant": (a) => `\u898F\u5283 LinkedIn \u7CFB\u5217 ${q(a.name)}\u3002`
|
|
8006
8294
|
},
|
|
8007
8295
|
campaign_pause: {
|
|
8008
|
-
en: (a) => `${a.paused ? "Pause" : "Resume"} the
|
|
8009
|
-
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\
|
|
8296
|
+
en: (a) => `${a.paused ? "Pause" : "Resume"} the series${named(a.name, a.id)}.`,
|
|
8297
|
+
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\u7CFB\u5217${named(a.name, a.id)}\u3002`
|
|
8010
8298
|
},
|
|
8011
8299
|
social_approval_mode_set: {
|
|
8012
|
-
en: (a) => a.mode === "auto" ? "Post LinkedIn
|
|
8013
|
-
"zh-Hant": (a) => a.mode === "auto" ? "LinkedIn \
|
|
8300
|
+
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.",
|
|
8301
|
+
"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
8302
|
},
|
|
8015
8303
|
// ---------------------------------------------------------------- reports
|
|
8016
8304
|
gsc_sync: {
|
|
@@ -8115,7 +8403,7 @@ function ws(ctx) {
|
|
|
8115
8403
|
if (!ctx.domain) throw new Error("No website is selected yet. Add one first.");
|
|
8116
8404
|
return domainApi(ctx.api, ctx.domain);
|
|
8117
8405
|
}
|
|
8118
|
-
var clip = (
|
|
8406
|
+
var clip = (text3, n = 280) => typeof text3 === "string" ? text3.length > n ? `${text3.slice(0, n)}\u2026` : text3 : text3 ?? null;
|
|
8119
8407
|
var plain = (html) => String(html ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
8120
8408
|
var untrusted = (value) => ({ untrusted: value });
|
|
8121
8409
|
function upsert(list2, item2, key) {
|
|
@@ -8463,9 +8751,9 @@ var blogTools = [
|
|
|
8463
8751
|
id: { type: "string" },
|
|
8464
8752
|
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8465
8753
|
instruction: { type: "string", description: "What to change, for mode edit" },
|
|
8466
|
-
title: { type: "string", description: "
|
|
8754
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8467
8755
|
},
|
|
8468
|
-
required: ["id", "mode"]
|
|
8756
|
+
required: ["id", "mode", "title"]
|
|
8469
8757
|
},
|
|
8470
8758
|
group: "blog",
|
|
8471
8759
|
need: write,
|
|
@@ -8481,13 +8769,73 @@ var blogTools = [
|
|
|
8481
8769
|
return { saved: true, lost_links: out.lost_links, ...untrusted({ preview: clip(out.markdown, 600) }) };
|
|
8482
8770
|
}
|
|
8483
8771
|
}),
|
|
8772
|
+
tool({
|
|
8773
|
+
name: "draft_rewrite_brief",
|
|
8774
|
+
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.",
|
|
8775
|
+
parameters: {
|
|
8776
|
+
type: "object",
|
|
8777
|
+
properties: {
|
|
8778
|
+
id: { type: "string" },
|
|
8779
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8780
|
+
instruction: { type: "string", description: "What to change, for mode edit" }
|
|
8781
|
+
},
|
|
8782
|
+
required: ["id", "mode"]
|
|
8783
|
+
},
|
|
8784
|
+
group: "blog",
|
|
8785
|
+
need: read,
|
|
8786
|
+
risk: "R0",
|
|
8787
|
+
assistantWrites: true,
|
|
8788
|
+
run: async (ctx, { id, mode, instruction }) => {
|
|
8789
|
+
const p = await ws(ctx).post(id);
|
|
8790
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8791
|
+
if (mode === "edit" && !instruction?.trim()) throw new Error("Say what to change.");
|
|
8792
|
+
return {
|
|
8793
|
+
id: p.id,
|
|
8794
|
+
title: p.title,
|
|
8795
|
+
mode,
|
|
8796
|
+
task: rewriteTask(mode, instruction),
|
|
8797
|
+
links_to_keep: links(p.markdown).length,
|
|
8798
|
+
...untrusted({ markdown: p.markdown })
|
|
8799
|
+
};
|
|
8800
|
+
}
|
|
8801
|
+
}),
|
|
8802
|
+
tool({
|
|
8803
|
+
name: "draft_rewrite_save",
|
|
8804
|
+
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).",
|
|
8805
|
+
parameters: {
|
|
8806
|
+
type: "object",
|
|
8807
|
+
properties: {
|
|
8808
|
+
id: { type: "string" },
|
|
8809
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8810
|
+
markdown: { type: "string", description: "The whole rewritten post" },
|
|
8811
|
+
title: { type: "string", description: "The draft's title, for the approval card" }
|
|
8812
|
+
},
|
|
8813
|
+
required: ["id", "mode", "markdown"]
|
|
8814
|
+
},
|
|
8815
|
+
group: "blog",
|
|
8816
|
+
need: write,
|
|
8817
|
+
risk: "W1",
|
|
8818
|
+
assistantWrites: true,
|
|
8819
|
+
run: async (ctx, { id, mode, markdown }) => {
|
|
8820
|
+
const api2 = ws(ctx);
|
|
8821
|
+
const p = await api2.post(id);
|
|
8822
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8823
|
+
const check2 = checkRewrite(p.markdown, String(markdown ?? ""), mode === "fix" ? "fix" : "edit");
|
|
8824
|
+
if (check2.error) throw new Error(check2.error);
|
|
8825
|
+
await api2.updatePost(id, { markdown });
|
|
8826
|
+
return { saved: true, lost_links: check2.lost };
|
|
8827
|
+
}
|
|
8828
|
+
}),
|
|
8484
8829
|
tool({
|
|
8485
8830
|
name: "draft_delete",
|
|
8486
8831
|
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
8832
|
parameters: {
|
|
8488
8833
|
type: "object",
|
|
8489
|
-
properties: {
|
|
8490
|
-
|
|
8834
|
+
properties: {
|
|
8835
|
+
id: { type: "string" },
|
|
8836
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8837
|
+
},
|
|
8838
|
+
required: ["id", "title"]
|
|
8491
8839
|
},
|
|
8492
8840
|
group: "blog",
|
|
8493
8841
|
need: write,
|
|
@@ -8502,9 +8850,9 @@ var blogTools = [
|
|
|
8502
8850
|
properties: {
|
|
8503
8851
|
id: { type: "string", description: "The planned post's id" },
|
|
8504
8852
|
to: { type: "string", enum: moves },
|
|
8505
|
-
title: { type: "string", description: "
|
|
8853
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8506
8854
|
},
|
|
8507
|
-
required: ["id", "to"]
|
|
8855
|
+
required: ["id", "to", "title"]
|
|
8508
8856
|
},
|
|
8509
8857
|
group: "blog",
|
|
8510
8858
|
need: write,
|
|
@@ -8535,8 +8883,11 @@ var blogTools = [
|
|
|
8535
8883
|
description: "Take a planned post out of the plan; its idea can be restored later.",
|
|
8536
8884
|
parameters: {
|
|
8537
8885
|
type: "object",
|
|
8538
|
-
properties: {
|
|
8539
|
-
|
|
8886
|
+
properties: {
|
|
8887
|
+
id: { type: "string" },
|
|
8888
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8889
|
+
},
|
|
8890
|
+
required: ["id", "title"]
|
|
8540
8891
|
},
|
|
8541
8892
|
group: "blog",
|
|
8542
8893
|
need: write,
|
|
@@ -8593,9 +8944,9 @@ var blogTools = [
|
|
|
8593
8944
|
need: write,
|
|
8594
8945
|
risk: "W2",
|
|
8595
8946
|
gates: ["LLM"],
|
|
8596
|
-
run: async (ctx, { url, text:
|
|
8597
|
-
if (!url && !
|
|
8598
|
-
return ws(ctx).addIdea(url ? { url } : { text:
|
|
8947
|
+
run: async (ctx, { url, text: text3 }) => {
|
|
8948
|
+
if (!url && !text3) throw new Error("Need a link or a sentence.");
|
|
8949
|
+
return ws(ctx).addIdea(url ? { url } : { text: text3 });
|
|
8599
8950
|
}
|
|
8600
8951
|
}),
|
|
8601
8952
|
tool({
|
|
@@ -8613,7 +8964,7 @@ var blogTools = [
|
|
|
8613
8964
|
need: write,
|
|
8614
8965
|
risk: "W2",
|
|
8615
8966
|
gates: ["LLM"],
|
|
8616
|
-
run: async (ctx, { count, brief }) => ws(ctx).generateIdeas(int(count, 1, 10, 3),
|
|
8967
|
+
run: async (ctx, { count, brief: brief2 }) => ws(ctx).generateIdeas(int(count, 1, 10, 3), brief2)
|
|
8617
8968
|
}),
|
|
8618
8969
|
tool({
|
|
8619
8970
|
name: "ideas_import_sitemap",
|
|
@@ -8658,9 +9009,9 @@ var blogTools = [
|
|
|
8658
9009
|
properties: {
|
|
8659
9010
|
id: { type: "string" },
|
|
8660
9011
|
delta: { type: "integer", minimum: -1, maximum: 1 },
|
|
8661
|
-
title: { type: "string", description: "
|
|
9012
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8662
9013
|
},
|
|
8663
|
-
required: ["id", "delta"]
|
|
9014
|
+
required: ["id", "delta", "title"]
|
|
8664
9015
|
},
|
|
8665
9016
|
group: "blog",
|
|
8666
9017
|
need: write,
|
|
@@ -8672,8 +9023,11 @@ var blogTools = [
|
|
|
8672
9023
|
description: "Add a waiting idea to the end of the plan (the plan holds at most 10).",
|
|
8673
9024
|
parameters: {
|
|
8674
9025
|
type: "object",
|
|
8675
|
-
properties: {
|
|
8676
|
-
|
|
9026
|
+
properties: {
|
|
9027
|
+
id: { type: "string" },
|
|
9028
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9029
|
+
},
|
|
9030
|
+
required: ["id", "title"]
|
|
8677
9031
|
},
|
|
8678
9032
|
group: "blog",
|
|
8679
9033
|
need: write,
|
|
@@ -8685,8 +9039,11 @@ var blogTools = [
|
|
|
8685
9039
|
description: "Remove one idea so it won't be written (it can be restored).",
|
|
8686
9040
|
parameters: {
|
|
8687
9041
|
type: "object",
|
|
8688
|
-
properties: {
|
|
8689
|
-
|
|
9042
|
+
properties: {
|
|
9043
|
+
id: { type: "string" },
|
|
9044
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9045
|
+
},
|
|
9046
|
+
required: ["id", "title"]
|
|
8690
9047
|
},
|
|
8691
9048
|
group: "blog",
|
|
8692
9049
|
need: write,
|
|
@@ -8698,8 +9055,11 @@ var blogTools = [
|
|
|
8698
9055
|
description: "Bring back a discarded idea (ideas_list status discarded).",
|
|
8699
9056
|
parameters: {
|
|
8700
9057
|
type: "object",
|
|
8701
|
-
properties: {
|
|
8702
|
-
|
|
9058
|
+
properties: {
|
|
9059
|
+
id: { type: "string" },
|
|
9060
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9061
|
+
},
|
|
9062
|
+
required: ["id", "title"]
|
|
8703
9063
|
},
|
|
8704
9064
|
group: "blog",
|
|
8705
9065
|
need: write,
|
|
@@ -8716,6 +9076,24 @@ var blogTools = [
|
|
|
8716
9076
|
run: async (ctx, { paused }) => paused ? ws(ctx).pauseAutopilot() : ws(ctx).resumeAutopilot()
|
|
8717
9077
|
})
|
|
8718
9078
|
];
|
|
9079
|
+
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.";
|
|
9080
|
+
var LINK = /\]\((\S+?)(?:\s+"[^"]*")?\)/g;
|
|
9081
|
+
var links = (markdown) => [...markdown.matchAll(LINK)].map((m) => m[1]);
|
|
9082
|
+
function rewriteTask(mode, instruction) {
|
|
9083
|
+
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 ?? ""}
|
|
9084
|
+
Change only what that needs; keep the rest as it is. ${KEEP}`;
|
|
9085
|
+
}
|
|
9086
|
+
function checkRewrite(before, after, mode) {
|
|
9087
|
+
const lost = links(before).filter((u) => !after.includes(u)).length;
|
|
9088
|
+
if (after.length < before.length * (mode === "fix" ? 0.8 : 0.3))
|
|
9089
|
+
return {
|
|
9090
|
+
error: "The new version is much shorter than the draft, so nothing was saved. Send the whole post.",
|
|
9091
|
+
lost
|
|
9092
|
+
};
|
|
9093
|
+
if (mode === "fix" && lost)
|
|
9094
|
+
return { error: `The fixed version dropped ${lost} link or image, so nothing was saved. Keep every one.`, lost };
|
|
9095
|
+
return { lost };
|
|
9096
|
+
}
|
|
8719
9097
|
|
|
8720
9098
|
// ../../packages/tools/src/brand.ts
|
|
8721
9099
|
var write2 = { resource: "overview", level: "write" };
|
|
@@ -9038,8 +9416,11 @@ var brandTools = [
|
|
|
9038
9416
|
description: "Delete one of the user's brand voices. Cannot be undone; put its name on the card.",
|
|
9039
9417
|
parameters: {
|
|
9040
9418
|
type: "object",
|
|
9041
|
-
properties: {
|
|
9042
|
-
|
|
9419
|
+
properties: {
|
|
9420
|
+
id: { type: "string" },
|
|
9421
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9422
|
+
},
|
|
9423
|
+
required: ["id", "name"]
|
|
9043
9424
|
},
|
|
9044
9425
|
group: "brand",
|
|
9045
9426
|
need: write2,
|
|
@@ -9066,6 +9447,65 @@ function brandGaps(b) {
|
|
|
9066
9447
|
return { complete: gaps.length === 0, gaps };
|
|
9067
9448
|
}
|
|
9068
9449
|
|
|
9450
|
+
// ../../packages/tools/src/feedback.ts
|
|
9451
|
+
var account = { resource: "account", level: "read" };
|
|
9452
|
+
var TYPES = ["bug", "quality", "idea", "praise", "other"];
|
|
9453
|
+
var text = (v, n) => String(v ?? "").trim().slice(0, n);
|
|
9454
|
+
var feedbackTools = [
|
|
9455
|
+
tool({
|
|
9456
|
+
name: "feedback_send",
|
|
9457
|
+
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.",
|
|
9458
|
+
parameters: {
|
|
9459
|
+
type: "object",
|
|
9460
|
+
properties: {
|
|
9461
|
+
type: {
|
|
9462
|
+
type: "string",
|
|
9463
|
+
enum: TYPES,
|
|
9464
|
+
description: "bug: did something wrong \xB7 quality: works but not good enough \xB7 idea: a new feature \xB7 praise \xB7 other"
|
|
9465
|
+
},
|
|
9466
|
+
summary: { type: "string", description: "One sentence, \u2264200 characters" },
|
|
9467
|
+
details: { type: "string", description: "What happened, \u22644000 characters" },
|
|
9468
|
+
expected: { type: "string", description: "For a bug: what the user expected" },
|
|
9469
|
+
actual: { type: "string", description: "For a bug: what happened instead" }
|
|
9470
|
+
},
|
|
9471
|
+
required: ["type", "summary"]
|
|
9472
|
+
},
|
|
9473
|
+
group: "account",
|
|
9474
|
+
need: account,
|
|
9475
|
+
risk: "W1",
|
|
9476
|
+
run: async (ctx, a) => {
|
|
9477
|
+
const f = await ctx.api.feedback.send({
|
|
9478
|
+
type: TYPES.find((t) => t === a.type) ?? "other",
|
|
9479
|
+
summary: text(a.summary, 200),
|
|
9480
|
+
...a.details ? { details: text(a.details, 4e3) } : {},
|
|
9481
|
+
...a.expected ? { expected: text(a.expected, 1e3) } : {},
|
|
9482
|
+
...a.actual ? { actual: text(a.actual, 1e3) } : {},
|
|
9483
|
+
domain: ctx.domain,
|
|
9484
|
+
source: ctx.client ?? "chat"
|
|
9485
|
+
});
|
|
9486
|
+
return { sent: true, id: f.id, status: f.status };
|
|
9487
|
+
}
|
|
9488
|
+
}),
|
|
9489
|
+
tool({
|
|
9490
|
+
name: "feedback_list",
|
|
9491
|
+
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.",
|
|
9492
|
+
parameters: none,
|
|
9493
|
+
group: "account",
|
|
9494
|
+
need: account,
|
|
9495
|
+
risk: "R0",
|
|
9496
|
+
run: async (ctx) => ({
|
|
9497
|
+
items: (await ctx.api.feedback.mine()).items.slice(0, 20).map((f) => ({
|
|
9498
|
+
type: f.type,
|
|
9499
|
+
summary: clip(f.summary, 200),
|
|
9500
|
+
status: f.status,
|
|
9501
|
+
reply: clip(f.reply, 600),
|
|
9502
|
+
new_reply: f.unread,
|
|
9503
|
+
sent: f.created_at
|
|
9504
|
+
}))
|
|
9505
|
+
})
|
|
9506
|
+
})
|
|
9507
|
+
];
|
|
9508
|
+
|
|
9069
9509
|
// ../../packages/tools/src/insights.ts
|
|
9070
9510
|
var read2 = { resource: "report", level: "read" };
|
|
9071
9511
|
var write3 = { resource: "report", level: "write" };
|
|
@@ -9157,14 +9597,14 @@ var insightTools = [
|
|
|
9157
9597
|
];
|
|
9158
9598
|
|
|
9159
9599
|
// ../../packages/tools/src/me.ts
|
|
9160
|
-
var
|
|
9600
|
+
var account2 = { resource: "account", level: "read" };
|
|
9161
9601
|
var meTools = [
|
|
9162
9602
|
tool({
|
|
9163
9603
|
name: "api_keys_list",
|
|
9164
9604
|
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
9605
|
parameters: none,
|
|
9166
9606
|
group: "account",
|
|
9167
|
-
need:
|
|
9607
|
+
need: account2,
|
|
9168
9608
|
risk: "R0",
|
|
9169
9609
|
run: async ({ api: api2 }) => (await api2.account.keys()).items
|
|
9170
9610
|
}),
|
|
@@ -9173,7 +9613,7 @@ var meTools = [
|
|
|
9173
9613
|
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
9614
|
parameters: none,
|
|
9175
9615
|
group: "account",
|
|
9176
|
-
need:
|
|
9616
|
+
need: account2,
|
|
9177
9617
|
risk: "R0",
|
|
9178
9618
|
run: async ({ api: api2 }) => (await api2.account.apps()).items.map((a) => ({
|
|
9179
9619
|
grant_id: a.grant_id,
|
|
@@ -9196,7 +9636,7 @@ var meTools = [
|
|
|
9196
9636
|
required: ["grant_id", "app"]
|
|
9197
9637
|
},
|
|
9198
9638
|
group: "account",
|
|
9199
|
-
need:
|
|
9639
|
+
need: account2,
|
|
9200
9640
|
risk: "W3",
|
|
9201
9641
|
run: async ({ api: api2 }, { grant_id }) => api2.account.revokeApp(String(grant_id))
|
|
9202
9642
|
}),
|
|
@@ -9205,7 +9645,7 @@ var meTools = [
|
|
|
9205
9645
|
description: "The user's latest invoices across the subscriptions they pay for: date, amount, status and a link to view or download each.",
|
|
9206
9646
|
parameters: none,
|
|
9207
9647
|
group: "billing",
|
|
9208
|
-
need:
|
|
9648
|
+
need: account2,
|
|
9209
9649
|
risk: "R0",
|
|
9210
9650
|
run: async ({ api: api2 }) => (await api2.account.invoices()).items.slice(0, 15).map((i) => ({
|
|
9211
9651
|
website: i.domain,
|
|
@@ -9221,7 +9661,7 @@ var meTools = [
|
|
|
9221
9661
|
description: "Change the user's display name (the only profile field Maggie changes; email and password are on ai.cmo.so).",
|
|
9222
9662
|
parameters: { type: "object", properties: { display_name: { type: "string" } }, required: ["display_name"] },
|
|
9223
9663
|
group: "account",
|
|
9224
|
-
need:
|
|
9664
|
+
need: account2,
|
|
9225
9665
|
risk: "W1",
|
|
9226
9666
|
idempotent: true,
|
|
9227
9667
|
run: async ({ api: api2 }, { display_name }) => api2.account.rename(String(display_name).slice(0, 80))
|
|
@@ -9231,7 +9671,7 @@ var meTools = [
|
|
|
9231
9671
|
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
9672
|
parameters: none,
|
|
9233
9673
|
group: "account",
|
|
9234
|
-
need:
|
|
9674
|
+
need: account2,
|
|
9235
9675
|
risk: "R0",
|
|
9236
9676
|
run: async ({ api: api2 }) => api2.account.notifications()
|
|
9237
9677
|
}),
|
|
@@ -9247,13 +9687,99 @@ var meTools = [
|
|
|
9247
9687
|
}
|
|
9248
9688
|
},
|
|
9249
9689
|
group: "account",
|
|
9250
|
-
need:
|
|
9690
|
+
need: account2,
|
|
9251
9691
|
risk: "W1",
|
|
9252
9692
|
idempotent: true,
|
|
9253
9693
|
run: async ({ api: api2 }, prefs) => api2.account.setNotifications(prefs)
|
|
9254
9694
|
})
|
|
9255
9695
|
];
|
|
9256
9696
|
|
|
9697
|
+
// ../../packages/tools/src/memory.ts
|
|
9698
|
+
var own = { resource: "overview", level: "read" };
|
|
9699
|
+
var SECTIONS = ["prefer", "always", "never"];
|
|
9700
|
+
var KINDS = ["tone", "length", "structure", "format", "wording", "topic", "image", "cta", "approval", "other"];
|
|
9701
|
+
var brief = (m) => ({
|
|
9702
|
+
enabled: m.enabled,
|
|
9703
|
+
channels: Object.fromEntries(
|
|
9704
|
+
MEMORY_CHANNELS.map((ch) => [
|
|
9705
|
+
ch,
|
|
9706
|
+
{
|
|
9707
|
+
summary: m.channels[ch]?.summary ?? "",
|
|
9708
|
+
items: (m.channels[ch]?.items ?? []).map((i) => ({
|
|
9709
|
+
id: i.id,
|
|
9710
|
+
section: i.section,
|
|
9711
|
+
kind: i.kind,
|
|
9712
|
+
text: clip(i.text, 200),
|
|
9713
|
+
source: i.source
|
|
9714
|
+
}))
|
|
9715
|
+
}
|
|
9716
|
+
])
|
|
9717
|
+
)
|
|
9718
|
+
});
|
|
9719
|
+
var memoryTools = [
|
|
9720
|
+
tool({
|
|
9721
|
+
name: "memory_get",
|
|
9722
|
+
description: "The user's Maggie Memory on this website: their saved taste per channel (general, blog, linkedin, facebook) 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.",
|
|
9723
|
+
parameters: none,
|
|
9724
|
+
group: "memory",
|
|
9725
|
+
need: own,
|
|
9726
|
+
risk: "R0",
|
|
9727
|
+
run: async (ctx) => brief(await ws(ctx).maggieMemory())
|
|
9728
|
+
}),
|
|
9729
|
+
tool({
|
|
9730
|
+
name: "memory_remember",
|
|
9731
|
+
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.',
|
|
9732
|
+
parameters: {
|
|
9733
|
+
type: "object",
|
|
9734
|
+
properties: {
|
|
9735
|
+
channel: { type: "string", enum: [...MEMORY_CHANNELS] },
|
|
9736
|
+
section: {
|
|
9737
|
+
type: "string",
|
|
9738
|
+
enum: SECTIONS,
|
|
9739
|
+
description: "prefer: a leaning \xB7 always: do it every time \xB7 never: don't do or use it"
|
|
9740
|
+
},
|
|
9741
|
+
text: { type: "string", description: "The whole rule, \u2264160 characters, e.g. \u201CNever use emojis.\u201D" },
|
|
9742
|
+
kind: { type: "string", enum: KINDS }
|
|
9743
|
+
},
|
|
9744
|
+
required: ["channel", "section", "text"]
|
|
9745
|
+
},
|
|
9746
|
+
group: "memory",
|
|
9747
|
+
need: own,
|
|
9748
|
+
risk: "W1",
|
|
9749
|
+
run: async (ctx, { channel, section, text: text3, kind }) => {
|
|
9750
|
+
const ch = MEMORY_CHANNELS.find((c) => c === channel) ?? "general";
|
|
9751
|
+
const sec = SECTIONS.includes(String(section)) ? section : "prefer";
|
|
9752
|
+
const m = await ws(ctx).addMaggieMemory({
|
|
9753
|
+
channel: ch,
|
|
9754
|
+
section: sec,
|
|
9755
|
+
text: String(text3 ?? "").trim().slice(0, 160),
|
|
9756
|
+
...kind && KINDS.includes(kind) ? { kind } : {}
|
|
9757
|
+
});
|
|
9758
|
+
return { saved: true, enabled: m.enabled };
|
|
9759
|
+
}
|
|
9760
|
+
}),
|
|
9761
|
+
tool({
|
|
9762
|
+
name: "memory_forget",
|
|
9763
|
+
description: "Remove one item from the user's Maggie Memory (ids from memory_get).",
|
|
9764
|
+
parameters: {
|
|
9765
|
+
type: "object",
|
|
9766
|
+
properties: {
|
|
9767
|
+
id: { type: "string" },
|
|
9768
|
+
text_for_card: { type: "string", description: "The item's text, for the card" }
|
|
9769
|
+
},
|
|
9770
|
+
required: ["id"]
|
|
9771
|
+
},
|
|
9772
|
+
group: "memory",
|
|
9773
|
+
need: own,
|
|
9774
|
+
risk: "W1",
|
|
9775
|
+
run: async (ctx, { id }) => {
|
|
9776
|
+
await ws(ctx).deleteMaggieMemory(String(id));
|
|
9777
|
+
return { forgotten: true };
|
|
9778
|
+
}
|
|
9779
|
+
})
|
|
9780
|
+
];
|
|
9781
|
+
var MEMORY_TOOLS = new Set(memoryTools.map((t) => t.name));
|
|
9782
|
+
|
|
9257
9783
|
// ../../packages/tools/src/publishing.ts
|
|
9258
9784
|
var read3 = { resource: "autopilot", level: "read" };
|
|
9259
9785
|
var write4 = { resource: "autopilot", level: "write" };
|
|
@@ -9329,7 +9855,7 @@ var publishingTools = [
|
|
|
9329
9855
|
}),
|
|
9330
9856
|
tool({
|
|
9331
9857
|
name: "rewrite_request",
|
|
9332
|
-
description: "Rewrite one
|
|
9858
|
+
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
9859
|
parameters: {
|
|
9334
9860
|
type: "object",
|
|
9335
9861
|
properties: {
|
|
@@ -9660,8 +10186,19 @@ var reportTools = [
|
|
|
9660
10186
|
var read5 = { resource: "autopilot", level: "read" };
|
|
9661
10187
|
var write5 = { resource: "autopilot", level: "write" };
|
|
9662
10188
|
var moves2 = ["up", "down", "top"];
|
|
10189
|
+
var CHANNEL_KEYS = ["linkedin", "facebook"];
|
|
10190
|
+
var channelsParam = {
|
|
10191
|
+
type: "array",
|
|
10192
|
+
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10193
|
+
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."
|
|
10194
|
+
};
|
|
10195
|
+
var channelsOf = (value) => {
|
|
10196
|
+
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
10197
|
+
return list2.length ? list2 : void 0;
|
|
10198
|
+
};
|
|
9663
10199
|
var item = (i) => ({
|
|
9664
10200
|
id: i.id,
|
|
10201
|
+
channel: i.provider || "linkedin",
|
|
9665
10202
|
status: i.status,
|
|
9666
10203
|
eta: i.eta ?? null,
|
|
9667
10204
|
scheduled_at: i.scheduled_at,
|
|
@@ -9674,7 +10211,7 @@ var item = (i) => ({
|
|
|
9674
10211
|
var socialTools = [
|
|
9675
10212
|
tool({
|
|
9676
10213
|
name: "social_list",
|
|
9677
|
-
description: "The user's LinkedIn:
|
|
10214
|
+
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
10215
|
parameters: none,
|
|
9679
10216
|
group: "social",
|
|
9680
10217
|
need: read5,
|
|
@@ -9683,6 +10220,14 @@ var socialTools = [
|
|
|
9683
10220
|
const s = await ws(ctx).social();
|
|
9684
10221
|
return {
|
|
9685
10222
|
connected: s.connected,
|
|
10223
|
+
channels: (s.channels ?? []).map((c) => ({
|
|
10224
|
+
channel: c.key,
|
|
10225
|
+
connected: c.connected,
|
|
10226
|
+
account: c.account?.display_name ?? null,
|
|
10227
|
+
paused: c.paused,
|
|
10228
|
+
needs_page: c.needs_page,
|
|
10229
|
+
per_week: c.frequency?.per_week ?? null
|
|
10230
|
+
})),
|
|
9686
10231
|
account: s.linkedin ? { name: s.linkedin.display_name, status: s.linkedin.status } : null,
|
|
9687
10232
|
posting_paused: s.posting_paused,
|
|
9688
10233
|
approval_mode: s.approval_mode,
|
|
@@ -9704,7 +10249,7 @@ var socialTools = [
|
|
|
9704
10249
|
}),
|
|
9705
10250
|
tool({
|
|
9706
10251
|
name: "social_update_copy",
|
|
9707
|
-
description: "Change the text of a
|
|
10252
|
+
description: "Change the text of a social post that hasn't gone out yet (a draft or queued post).",
|
|
9708
10253
|
parameters: {
|
|
9709
10254
|
type: "object",
|
|
9710
10255
|
properties: { id: { type: "integer" }, text: { type: "string", description: "The whole new post text" } },
|
|
@@ -9714,8 +10259,8 @@ var socialTools = [
|
|
|
9714
10259
|
need: write5,
|
|
9715
10260
|
risk: "W1",
|
|
9716
10261
|
gates: ["VER"],
|
|
9717
|
-
run: async (ctx, { id, text:
|
|
9718
|
-
const out = await ws(ctx).updateSocial(Number(id), String(
|
|
10262
|
+
run: async (ctx, { id, text: text3 }) => {
|
|
10263
|
+
const out = await ws(ctx).updateSocial(Number(id), String(text3 ?? ""));
|
|
9719
10264
|
return { id: out.id, status: out.status };
|
|
9720
10265
|
}
|
|
9721
10266
|
}),
|
|
@@ -9767,6 +10312,56 @@ var socialTools = [
|
|
|
9767
10312
|
gates: ["VER"],
|
|
9768
10313
|
run: async (ctx, { id }) => ws(ctx).restoreSocial(Number(id))
|
|
9769
10314
|
}),
|
|
10315
|
+
tool({
|
|
10316
|
+
name: "social_image_new",
|
|
10317
|
+
description: "Make a new picture for a LinkedIn post that hasn't gone out (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: if another is being made this fails with image_busy; only pass replace: true when the user agrees to stop that one. 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.",
|
|
10318
|
+
parameters: {
|
|
10319
|
+
type: "object",
|
|
10320
|
+
properties: {
|
|
10321
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10322
|
+
replace: { type: "boolean", description: "Stop the picture being made for another post first" },
|
|
10323
|
+
instruction: {
|
|
10324
|
+
type: "string",
|
|
10325
|
+
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."
|
|
10326
|
+
}
|
|
10327
|
+
},
|
|
10328
|
+
required: ["id"]
|
|
10329
|
+
},
|
|
10330
|
+
group: "social",
|
|
10331
|
+
need: write5,
|
|
10332
|
+
risk: "W2",
|
|
10333
|
+
gates: ["VER", "CR"],
|
|
10334
|
+
run: async (ctx, { id, replace, instruction }) => {
|
|
10335
|
+
const out = await ws(ctx).regenerateSocialImage(Number(id), {
|
|
10336
|
+
replace: replace === true,
|
|
10337
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10338
|
+
});
|
|
10339
|
+
return {
|
|
10340
|
+
id: out.id,
|
|
10341
|
+
image_job: out.image_job,
|
|
10342
|
+
note: "Making it in the background; it shows on the post when ready."
|
|
10343
|
+
};
|
|
10344
|
+
}
|
|
10345
|
+
}),
|
|
10346
|
+
tool({
|
|
10347
|
+
name: "social_image_status",
|
|
10348
|
+
description: "Whether a new LinkedIn picture is being made on this website (which post, since when), or how the last one ended (done, failed with why, stopped) and who drew it (chatgpt or gemini).",
|
|
10349
|
+
parameters: none,
|
|
10350
|
+
group: "social",
|
|
10351
|
+
need: read5,
|
|
10352
|
+
risk: "R0",
|
|
10353
|
+
run: async (ctx) => ({ image_job: (await ws(ctx).social()).image_job ?? null })
|
|
10354
|
+
}),
|
|
10355
|
+
tool({
|
|
10356
|
+
name: "social_image_stop",
|
|
10357
|
+
description: "Stop the LinkedIn picture being made on this website; the post keeps the picture it had.",
|
|
10358
|
+
parameters: none,
|
|
10359
|
+
group: "social",
|
|
10360
|
+
need: write5,
|
|
10361
|
+
risk: "W1",
|
|
10362
|
+
gates: ["VER"],
|
|
10363
|
+
run: async (ctx) => ws(ctx).stopSocialImage()
|
|
10364
|
+
}),
|
|
9770
10365
|
tool({
|
|
9771
10366
|
name: "social_move",
|
|
9772
10367
|
description: "Move a queued LinkedIn post up, down or to the front among the user's own posts.",
|
|
@@ -9783,16 +10378,24 @@ var socialTools = [
|
|
|
9783
10378
|
}),
|
|
9784
10379
|
tool({
|
|
9785
10380
|
name: "social_set_paused",
|
|
9786
|
-
description: "Pause or resume
|
|
9787
|
-
parameters: {
|
|
10381
|
+
description: "Pause or resume the user's social posting: one channel (linkedin or facebook), or every channel when no channel is given.",
|
|
10382
|
+
parameters: {
|
|
10383
|
+
type: "object",
|
|
10384
|
+
properties: { paused: { type: "boolean" }, channel: { type: "string", enum: CHANNEL_KEYS } },
|
|
10385
|
+
required: ["paused"]
|
|
10386
|
+
},
|
|
9788
10387
|
group: "social",
|
|
9789
10388
|
need: write5,
|
|
9790
10389
|
risk: "W1",
|
|
9791
|
-
run: async (ctx, { paused }) =>
|
|
10390
|
+
run: async (ctx, { paused, channel }) => {
|
|
10391
|
+
const key = channelsOf(channel ? [channel] : [])?.[0];
|
|
10392
|
+
if (key) return ws(ctx).setChannelPaused(key, Boolean(paused));
|
|
10393
|
+
return paused ? ws(ctx).pauseSocial() : ws(ctx).resumeSocial();
|
|
10394
|
+
}
|
|
9792
10395
|
}),
|
|
9793
10396
|
tool({
|
|
9794
10397
|
name: "social_sources_list",
|
|
9795
|
-
description: "Published articles a
|
|
10398
|
+
description: "Published articles a social post can be made from (optionally matching a search).",
|
|
9796
10399
|
parameters: { type: "object", properties: { search: { type: "string" } } },
|
|
9797
10400
|
group: "social",
|
|
9798
10401
|
need: read5,
|
|
@@ -9801,29 +10404,34 @@ var socialTools = [
|
|
|
9801
10404
|
}),
|
|
9802
10405
|
tool({
|
|
9803
10406
|
name: "social_from_article",
|
|
9804
|
-
description: "Maggie writes a
|
|
10407
|
+
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).",
|
|
9805
10408
|
parameters: {
|
|
9806
10409
|
type: "object",
|
|
9807
|
-
properties: {
|
|
9808
|
-
|
|
10410
|
+
properties: {
|
|
10411
|
+
source_id: { type: "string" },
|
|
10412
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10413
|
+
channels: channelsParam
|
|
10414
|
+
},
|
|
10415
|
+
required: ["source_id", "title"]
|
|
9809
10416
|
},
|
|
9810
10417
|
group: "social",
|
|
9811
10418
|
need: write5,
|
|
9812
10419
|
risk: "W2",
|
|
9813
10420
|
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) };
|
|
10421
|
+
run: async (ctx, { source_id, channels }) => {
|
|
10422
|
+
const out = await ws(ctx).socialFromPost(String(source_id), channelsOf(channels));
|
|
10423
|
+
return { id: out.id, status: out.status, queued: Boolean(out.queued), text: clip(out.social_copy, 300) };
|
|
9817
10424
|
}
|
|
9818
10425
|
}),
|
|
9819
10426
|
tool({
|
|
9820
10427
|
name: "social_plan_days",
|
|
9821
|
-
description: "Maggie writes
|
|
10428
|
+
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).",
|
|
9822
10429
|
parameters: {
|
|
9823
10430
|
type: "object",
|
|
9824
10431
|
properties: {
|
|
9825
10432
|
days: { type: "integer", minimum: 1, maximum: 7 },
|
|
9826
|
-
timezone: { type: "string", description: "IANA timezone, if known" }
|
|
10433
|
+
timezone: { type: "string", description: "IANA timezone, if known" },
|
|
10434
|
+
channels: channelsParam
|
|
9827
10435
|
},
|
|
9828
10436
|
required: ["days"]
|
|
9829
10437
|
},
|
|
@@ -9831,9 +10439,9 @@ var socialTools = [
|
|
|
9831
10439
|
need: write5,
|
|
9832
10440
|
risk: "W2",
|
|
9833
10441
|
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) };
|
|
10442
|
+
run: async (ctx, { days, timezone, channels }) => {
|
|
10443
|
+
const out = await ws(ctx).socialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels));
|
|
10444
|
+
return { written: out.items.length, queued: Boolean(out.queued), items: out.items.map(item) };
|
|
9837
10445
|
}
|
|
9838
10446
|
}),
|
|
9839
10447
|
tool({
|
|
@@ -9862,21 +10470,21 @@ var socialTools = [
|
|
|
9862
10470
|
}),
|
|
9863
10471
|
tool({
|
|
9864
10472
|
name: "social_idea_add",
|
|
9865
|
-
description: "Add an idea for a
|
|
10473
|
+
description: "Add an idea for a social post (not written yet; writing it later uses a credit per channel).",
|
|
9866
10474
|
parameters: {
|
|
9867
10475
|
type: "object",
|
|
9868
|
-
properties: { title: { type: "string" }, summary: { type: "string" } },
|
|
10476
|
+
properties: { title: { type: "string" }, summary: { type: "string" }, channels: channelsParam },
|
|
9869
10477
|
required: ["title"]
|
|
9870
10478
|
},
|
|
9871
10479
|
group: "social",
|
|
9872
10480
|
need: write5,
|
|
9873
10481
|
risk: "W1",
|
|
9874
10482
|
gates: ["VER", "LI"],
|
|
9875
|
-
run: async (ctx, idea) => ws(ctx).addSocialIdea(idea)
|
|
10483
|
+
run: async (ctx, { channels, ...idea }) => ws(ctx).addSocialIdea({ ...idea, channels: channelsOf(channels) })
|
|
9876
10484
|
}),
|
|
9877
10485
|
tool({
|
|
9878
10486
|
name: "social_idea_update",
|
|
9879
|
-
description: "Change a
|
|
10487
|
+
description: "Change a social idea's title or summary.",
|
|
9880
10488
|
parameters: {
|
|
9881
10489
|
type: "object",
|
|
9882
10490
|
properties: { id: { type: "integer" }, title: { type: "string" }, summary: { type: "string" } },
|
|
@@ -9890,11 +10498,14 @@ var socialTools = [
|
|
|
9890
10498
|
}),
|
|
9891
10499
|
tool({
|
|
9892
10500
|
name: "social_idea_delete",
|
|
9893
|
-
description: "Delete a
|
|
10501
|
+
description: "Delete a social idea that hasn't been written.",
|
|
9894
10502
|
parameters: {
|
|
9895
10503
|
type: "object",
|
|
9896
|
-
properties: {
|
|
9897
|
-
|
|
10504
|
+
properties: {
|
|
10505
|
+
id: { type: "integer" },
|
|
10506
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10507
|
+
},
|
|
10508
|
+
required: ["id", "title"]
|
|
9898
10509
|
},
|
|
9899
10510
|
group: "social",
|
|
9900
10511
|
need: write5,
|
|
@@ -9904,21 +10515,79 @@ var socialTools = [
|
|
|
9904
10515
|
}),
|
|
9905
10516
|
tool({
|
|
9906
10517
|
name: "social_idea_write",
|
|
9907
|
-
description: "Maggie writes
|
|
10518
|
+
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.",
|
|
9908
10519
|
parameters: {
|
|
9909
10520
|
type: "object",
|
|
9910
|
-
properties: {
|
|
9911
|
-
|
|
10521
|
+
properties: {
|
|
10522
|
+
id: { type: "integer" },
|
|
10523
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10524
|
+
channels: channelsParam
|
|
10525
|
+
},
|
|
10526
|
+
required: ["id", "title"]
|
|
9912
10527
|
},
|
|
9913
10528
|
group: "social",
|
|
9914
10529
|
need: write5,
|
|
9915
10530
|
risk: "W2",
|
|
9916
10531
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
9917
|
-
run: async (ctx, { id }) => ws(ctx).writeSocialIdea(Number(id))
|
|
10532
|
+
run: async (ctx, { id, channels }) => ws(ctx).writeSocialIdea(Number(id), void 0, channelsOf(channels))
|
|
10533
|
+
}),
|
|
10534
|
+
tool({
|
|
10535
|
+
name: "social_idea_brief",
|
|
10536
|
+
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.",
|
|
10537
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10538
|
+
group: "social",
|
|
10539
|
+
need: read5,
|
|
10540
|
+
risk: "R0",
|
|
10541
|
+
assistantWrites: true,
|
|
10542
|
+
run: async (ctx, { id }) => {
|
|
10543
|
+
const b = await ws(ctx).socialIdeaBrief(Number(id));
|
|
10544
|
+
return {
|
|
10545
|
+
id: Number(id),
|
|
10546
|
+
channels: b.platforms,
|
|
10547
|
+
limits: b.limits,
|
|
10548
|
+
limit: b.limits.linkedin ?? null,
|
|
10549
|
+
json_schema: b.json_schema,
|
|
10550
|
+
// Built by CMO.so from the user's idea, brand voice and template.
|
|
10551
|
+
brief: b.prompt
|
|
10552
|
+
};
|
|
10553
|
+
}
|
|
10554
|
+
}),
|
|
10555
|
+
tool({
|
|
10556
|
+
name: "social_idea_save",
|
|
10557
|
+
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.",
|
|
10558
|
+
parameters: {
|
|
10559
|
+
type: "object",
|
|
10560
|
+
properties: {
|
|
10561
|
+
id: { type: "integer" },
|
|
10562
|
+
posts: {
|
|
10563
|
+
type: "object",
|
|
10564
|
+
description: "One body per channel from the brief (only the channels you wrote)",
|
|
10565
|
+
properties: { linkedin: { type: "string" }, facebook: { type: "string" } }
|
|
10566
|
+
},
|
|
10567
|
+
text: { type: "string", description: "Only a LinkedIn post: its body (older use; prefer posts)" },
|
|
10568
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10569
|
+
},
|
|
10570
|
+
required: ["id", "title"]
|
|
10571
|
+
},
|
|
10572
|
+
group: "social",
|
|
10573
|
+
need: write5,
|
|
10574
|
+
risk: "W2",
|
|
10575
|
+
gates: ["VER", "LI", "CR"],
|
|
10576
|
+
assistantWrites: true,
|
|
10577
|
+
run: async (ctx, { id, text: text3, posts: posts2 }) => {
|
|
10578
|
+
const written = {};
|
|
10579
|
+
for (const [channel, body] of Object.entries(posts2 ?? {})) {
|
|
10580
|
+
if (CHANNEL_KEYS.includes(channel) && String(body ?? "").trim())
|
|
10581
|
+
written[channel] = { body: String(body).trim() };
|
|
10582
|
+
}
|
|
10583
|
+
if (!Object.keys(written).length && String(text3 ?? "").trim()) written.linkedin = { body: String(text3).trim() };
|
|
10584
|
+
if (!Object.keys(written).length) throw new Error("Write the posts first.");
|
|
10585
|
+
return ws(ctx).writeSocialIdea(Number(id), written);
|
|
10586
|
+
}
|
|
9918
10587
|
}),
|
|
9919
10588
|
tool({
|
|
9920
10589
|
name: "campaign_list",
|
|
9921
|
-
description: "The user's
|
|
10590
|
+
description: "The user's social campaigns: ideas written so far, whether ready or started, start day and posts a day.",
|
|
9922
10591
|
parameters: none,
|
|
9923
10592
|
group: "social",
|
|
9924
10593
|
need: read5,
|
|
@@ -9927,16 +10596,16 @@ var socialTools = [
|
|
|
9927
10596
|
}),
|
|
9928
10597
|
tool({
|
|
9929
10598
|
name: "campaign_start",
|
|
9930
|
-
description: "Start a ready campaign (every idea written): its posts go out on
|
|
10599
|
+
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
10600
|
parameters: {
|
|
9932
10601
|
type: "object",
|
|
9933
10602
|
properties: {
|
|
9934
10603
|
id: { type: "integer" },
|
|
9935
10604
|
start_date: { type: "string", description: "YYYY-MM-DD" },
|
|
9936
10605
|
daily_limit: { type: "integer", minimum: 1, maximum: 5 },
|
|
9937
|
-
name: { type: "string", description: "
|
|
10606
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9938
10607
|
},
|
|
9939
|
-
required: ["id"]
|
|
10608
|
+
required: ["id", "name"]
|
|
9940
10609
|
},
|
|
9941
10610
|
group: "social",
|
|
9942
10611
|
need: write5,
|
|
@@ -9984,13 +10653,65 @@ var socialTools = [
|
|
|
9984
10653
|
gates: ["VER"],
|
|
9985
10654
|
run: async (ctx, c) => ws(ctx).createCampaign(c)
|
|
9986
10655
|
}),
|
|
10656
|
+
tool({
|
|
10657
|
+
name: "social_approve_all",
|
|
10658
|
+
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.",
|
|
10659
|
+
parameters: { type: "object", properties: { channel: { type: "string", enum: CHANNEL_KEYS } } },
|
|
10660
|
+
group: "social",
|
|
10661
|
+
need: write5,
|
|
10662
|
+
risk: "W3",
|
|
10663
|
+
gates: ["VER", "LI"],
|
|
10664
|
+
run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
|
|
10665
|
+
}),
|
|
10666
|
+
tool({
|
|
10667
|
+
name: "series_create",
|
|
10668
|
+
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.",
|
|
10669
|
+
parameters: {
|
|
10670
|
+
type: "object",
|
|
10671
|
+
properties: {
|
|
10672
|
+
brief: { type: "string", description: "What the series is about, in the user's words" },
|
|
10673
|
+
count: { type: "integer", minimum: 1, maximum: 7, description: "How many posts (3 if not said)" },
|
|
10674
|
+
channels: channelsParam
|
|
10675
|
+
},
|
|
10676
|
+
required: ["brief"]
|
|
10677
|
+
},
|
|
10678
|
+
group: "social",
|
|
10679
|
+
need: write5,
|
|
10680
|
+
risk: "W2",
|
|
10681
|
+
gates: ["VER", "LI", "LLM"],
|
|
10682
|
+
run: async (ctx, { brief: brief2, count, channels }) => ws(ctx).createSocialSeries({
|
|
10683
|
+
brief: String(brief2 ?? ""),
|
|
10684
|
+
count: Math.max(1, Math.min(7, Number(count) || 3)),
|
|
10685
|
+
channels: channelsOf(channels)
|
|
10686
|
+
})
|
|
10687
|
+
}),
|
|
10688
|
+
tool({
|
|
10689
|
+
name: "series_approve",
|
|
10690
|
+
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).",
|
|
10691
|
+
parameters: {
|
|
10692
|
+
type: "object",
|
|
10693
|
+
properties: {
|
|
10694
|
+
id: { type: "integer" },
|
|
10695
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10696
|
+
},
|
|
10697
|
+
required: ["id"]
|
|
10698
|
+
},
|
|
10699
|
+
group: "social",
|
|
10700
|
+
need: write5,
|
|
10701
|
+
risk: "W3",
|
|
10702
|
+
gates: ["VER", "LI"],
|
|
10703
|
+
run: async (ctx, { id }) => ws(ctx).approveSocialSeries(Number(id))
|
|
10704
|
+
}),
|
|
9987
10705
|
tool({
|
|
9988
10706
|
name: "campaign_plan",
|
|
9989
10707
|
description: "Maggie plans a campaign's LinkedIn post ideas with AI (uses 1 credit).",
|
|
9990
10708
|
parameters: {
|
|
9991
10709
|
type: "object",
|
|
9992
|
-
properties: {
|
|
9993
|
-
|
|
10710
|
+
properties: {
|
|
10711
|
+
id: { type: "integer" },
|
|
10712
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10713
|
+
},
|
|
10714
|
+
required: ["id", "name"]
|
|
9994
10715
|
},
|
|
9995
10716
|
group: "social",
|
|
9996
10717
|
need: write5,
|
|
@@ -10009,9 +10730,9 @@ var socialTools = [
|
|
|
10009
10730
|
properties: {
|
|
10010
10731
|
id: { type: "integer" },
|
|
10011
10732
|
paused: { type: "boolean" },
|
|
10012
|
-
name: { type: "string", description: "
|
|
10733
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10013
10734
|
},
|
|
10014
|
-
required: ["id", "paused"]
|
|
10735
|
+
required: ["id", "paused", "name"]
|
|
10015
10736
|
},
|
|
10016
10737
|
group: "social",
|
|
10017
10738
|
need: write5,
|
|
@@ -10175,6 +10896,19 @@ var socialTools = [
|
|
|
10175
10896
|
const out = await ws(ctx).connectLinkedIn("close");
|
|
10176
10897
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10177
10898
|
}
|
|
10899
|
+
}),
|
|
10900
|
+
tool({
|
|
10901
|
+
name: "facebook_connect_link",
|
|
10902
|
+
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.",
|
|
10903
|
+
parameters: none,
|
|
10904
|
+
group: "social",
|
|
10905
|
+
need: write5,
|
|
10906
|
+
risk: "W1",
|
|
10907
|
+
gates: ["VER"],
|
|
10908
|
+
run: async (ctx) => {
|
|
10909
|
+
const out = await ws(ctx).connectFacebook("close");
|
|
10910
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10911
|
+
}
|
|
10178
10912
|
})
|
|
10179
10913
|
];
|
|
10180
10914
|
|
|
@@ -10325,7 +11059,9 @@ var tools = [
|
|
|
10325
11059
|
...blogTools,
|
|
10326
11060
|
...publishingTools,
|
|
10327
11061
|
...socialTools,
|
|
10328
|
-
...teamTools
|
|
11062
|
+
...teamTools,
|
|
11063
|
+
...memoryTools,
|
|
11064
|
+
...feedbackTools
|
|
10329
11065
|
];
|
|
10330
11066
|
var toolByName = new Map(tools.map((t) => [t.name, t]));
|
|
10331
11067
|
function allowed(t, a) {
|
|
@@ -13477,9 +14213,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
13477
14213
|
helpWidth: context.helpWidth,
|
|
13478
14214
|
outputHasColors: context.hasColors
|
|
13479
14215
|
});
|
|
13480
|
-
const
|
|
13481
|
-
if (context.hasColors) return
|
|
13482
|
-
return this._outputConfiguration.stripColor(
|
|
14216
|
+
const text3 = helper.formatHelp(this, helper);
|
|
14217
|
+
if (context.hasColors) return text3;
|
|
14218
|
+
return this._outputConfiguration.stripColor(text3);
|
|
13483
14219
|
}
|
|
13484
14220
|
/**
|
|
13485
14221
|
* @typedef HelpContext
|
|
@@ -13639,7 +14375,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
13639
14375
|
* @param {(string | Function)} text - string to add, or a function returning a string
|
|
13640
14376
|
* @return {Command} `this` command for chaining
|
|
13641
14377
|
*/
|
|
13642
|
-
addHelpText(position,
|
|
14378
|
+
addHelpText(position, text3) {
|
|
13643
14379
|
const allowedValues = ["beforeAll", "before", "after", "afterAll"];
|
|
13644
14380
|
if (!allowedValues.includes(position)) {
|
|
13645
14381
|
throw new Error(`Unexpected value for position to addHelpText.
|
|
@@ -13648,10 +14384,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
13648
14384
|
const helpEvent = `${position}Help`;
|
|
13649
14385
|
this.on(helpEvent, (context) => {
|
|
13650
14386
|
let helpStr;
|
|
13651
|
-
if (typeof
|
|
13652
|
-
helpStr =
|
|
14387
|
+
if (typeof text3 === "function") {
|
|
14388
|
+
helpStr = text3({ error: context.error, command: context.command });
|
|
13653
14389
|
} else {
|
|
13654
|
-
helpStr =
|
|
14390
|
+
helpStr = text3;
|
|
13655
14391
|
}
|
|
13656
14392
|
if (helpStr) {
|
|
13657
14393
|
context.write(`${helpStr}
|
|
@@ -13718,7 +14454,7 @@ var program = new Command();
|
|
|
13718
14454
|
// package.json
|
|
13719
14455
|
var package_default = {
|
|
13720
14456
|
name: "@cmo-so/cli",
|
|
13721
|
-
version: "0.
|
|
14457
|
+
version: "0.2.0",
|
|
13722
14458
|
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
14459
|
type: "module",
|
|
13724
14460
|
license: "MIT",
|
|
@@ -20962,6 +21698,23 @@ function hmacSigner(secret) {
|
|
|
20962
21698
|
const body = b64(canonical(approval));
|
|
20963
21699
|
return `${body}.${mac(body)}`;
|
|
20964
21700
|
},
|
|
21701
|
+
open(token, principal, now = Date.now()) {
|
|
21702
|
+
if (!token) return { ok: false, reason: "missing" };
|
|
21703
|
+
const [body, sig] = token.split(".");
|
|
21704
|
+
if (!body || !sig) return { ok: false, reason: "bad" };
|
|
21705
|
+
const want = Buffer.from(mac(body));
|
|
21706
|
+
const got = Buffer.from(sig);
|
|
21707
|
+
if (want.length !== got.length || !timingSafeEqual(want, got)) return { ok: false, reason: "bad" };
|
|
21708
|
+
let approval;
|
|
21709
|
+
try {
|
|
21710
|
+
approval = JSON.parse(unb64(body));
|
|
21711
|
+
} catch {
|
|
21712
|
+
return { ok: false, reason: "bad" };
|
|
21713
|
+
}
|
|
21714
|
+
if (!(approval.expires > now)) return { ok: false, reason: "expired" };
|
|
21715
|
+
if (approval.principal !== principal) return { ok: false, reason: "changed" };
|
|
21716
|
+
return { ok: true, approval };
|
|
21717
|
+
},
|
|
20965
21718
|
verify(token, expect, now = Date.now()) {
|
|
20966
21719
|
if (!token) return { ok: false, reason: "missing" };
|
|
20967
21720
|
const [body, sig] = token.split(".");
|
|
@@ -20986,7 +21739,7 @@ var idempotencyKey = (approvalId, call, request) => createHash("sha256").update(
|
|
|
20986
21739
|
|
|
20987
21740
|
// ../../packages/mcp/src/widgets.ts
|
|
20988
21741
|
var WIDGET_MIME = "text/html;profile=mcp-app";
|
|
20989
|
-
var VERSION = "
|
|
21742
|
+
var VERSION = "v2";
|
|
20990
21743
|
var WIDGETS = {
|
|
20991
21744
|
results: {
|
|
20992
21745
|
uri: `ui://widget/results-${VERSION}.html`,
|
|
@@ -21145,7 +21898,7 @@ function show(d) {
|
|
|
21145
21898
|
if (!d) return; data = d;
|
|
21146
21899
|
if (d.workspace) workspace = d.workspace;
|
|
21147
21900
|
const root = document.getElementById("root"); root.textContent = "";
|
|
21148
|
-
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat());
|
|
21901
|
+
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat(3));
|
|
21149
21902
|
resize();
|
|
21150
21903
|
}
|
|
21151
21904
|
(async () => {
|
|
@@ -21233,16 +21986,16 @@ function withIdempotency(api2, approvalId) {
|
|
|
21233
21986
|
}
|
|
21234
21987
|
};
|
|
21235
21988
|
}
|
|
21236
|
-
var
|
|
21989
|
+
var text2 = (value) => [
|
|
21237
21990
|
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }
|
|
21238
21991
|
];
|
|
21239
21992
|
function ok(result, extra) {
|
|
21240
21993
|
const value = result ?? { ok: true };
|
|
21241
21994
|
const structured = value && typeof value === "object" && !Array.isArray(value) ? value : { items: value };
|
|
21242
|
-
return { content:
|
|
21995
|
+
return { content: text2(value), structuredContent: { ...structured, ...extra } };
|
|
21243
21996
|
}
|
|
21244
21997
|
function fail(message) {
|
|
21245
|
-
return { content:
|
|
21998
|
+
return { content: text2({ error: message }), isError: true };
|
|
21246
21999
|
}
|
|
21247
22000
|
function errorMessage2(e) {
|
|
21248
22001
|
if (e instanceof SignInRequiredError) return "The sign-in has expired. Reconnect CMO.so and try again.";
|
|
@@ -21276,8 +22029,8 @@ function createMcpServer(deps) {
|
|
|
21276
22029
|
return deps.access ? deps.access(domain, load) : load();
|
|
21277
22030
|
}
|
|
21278
22031
|
async function resolveWorkspace(t, given) {
|
|
21279
|
-
const
|
|
21280
|
-
if (
|
|
22032
|
+
const named2 = typeof given === "string" ? given.trim().toLowerCase() : "";
|
|
22033
|
+
if (named2) return named2.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
21281
22034
|
const list2 = (await me()).workspaces ?? [];
|
|
21282
22035
|
if (list2.length === 1) return list2[0]?.domain ?? null;
|
|
21283
22036
|
if (t && isAccountTool(t)) return null;
|
|
@@ -21310,17 +22063,32 @@ function createMcpServer(deps) {
|
|
|
21310
22063
|
const t = toolByName.get(name);
|
|
21311
22064
|
if (!t || excluded(name)) return fail(`There is no tool called ${name}.`);
|
|
21312
22065
|
try {
|
|
21313
|
-
|
|
22066
|
+
let approval = null;
|
|
22067
|
+
if (isWrite(t) && args.confirm === true) {
|
|
22068
|
+
if (deps.previewOnly)
|
|
22069
|
+
return fail(
|
|
22070
|
+
"Don't confirm this yourself: the user confirms changes in the CMO.so app. Tell them what the preview will do and stop."
|
|
22071
|
+
);
|
|
22072
|
+
const o = signer.open(typeof args.confirmation === "string" ? args.confirmation : void 0, principal);
|
|
22073
|
+
if (!o.ok)
|
|
22074
|
+
return fail(
|
|
22075
|
+
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."
|
|
22076
|
+
);
|
|
22077
|
+
if (o.approval.calls.length !== 1 || o.approval.calls[0]?.name !== t.name)
|
|
22078
|
+
return fail("This confirmation is for a different action. Get a new preview for this one.");
|
|
22079
|
+
approval = o.approval;
|
|
22080
|
+
}
|
|
22081
|
+
const domain = approval ? approval.domain : await resolveWorkspace(t, args.workspace);
|
|
21314
22082
|
const access = await loadAccess(domain);
|
|
21315
22083
|
if (!allowed(t, access))
|
|
21316
22084
|
return fail(
|
|
21317
22085
|
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
22086
|
);
|
|
21319
|
-
const callArgs = toolArgs(args);
|
|
21320
|
-
const ctx = { api: api2, domain };
|
|
22087
|
+
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
22088
|
+
const ctx = { api: api2, domain, client: "mcp" };
|
|
21321
22089
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
21322
|
-
|
|
21323
|
-
|
|
22090
|
+
if (!approval) {
|
|
22091
|
+
const calls = [{ name: t.name, args: callArgs }];
|
|
21324
22092
|
const summary2 = t.confirm?.(callArgs, "en") ?? t.description;
|
|
21325
22093
|
const cost2 = await previewCost(api2, domain, t);
|
|
21326
22094
|
return ok({
|
|
@@ -21329,26 +22097,19 @@ function createMcpServer(deps) {
|
|
|
21329
22097
|
risk: t.risk,
|
|
21330
22098
|
workspace: domain,
|
|
21331
22099
|
confirmation: signer.sign({ calls, principal, domain }),
|
|
21332
|
-
next: "Show the preview to the user. If they agree, call this tool again with
|
|
22100
|
+
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
22101
|
});
|
|
21334
22102
|
}
|
|
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
22103
|
const summary = t.confirm?.(callArgs, "en") ?? t.description;
|
|
21345
22104
|
try {
|
|
21346
|
-
const result = await t.run({ api: withIdempotency(api2,
|
|
21347
|
-
|
|
22105
|
+
const result = await t.run({ api: withIdempotency(api2, approval.id), domain, client: "mcp" }, callArgs);
|
|
22106
|
+
const added = t.name === "website_add" ? result?.domain : null;
|
|
22107
|
+
const where = typeof added === "string" && added ? added : domain;
|
|
22108
|
+
await deps.audit?.({ domain: where, actions: [{ tool: t.name, summary, ok: true, action_id: approval.id }] }).catch(() => void 0);
|
|
21348
22109
|
return ok(result);
|
|
21349
22110
|
} catch (e) {
|
|
21350
22111
|
const error2 = errorMessage2(e);
|
|
21351
|
-
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id:
|
|
22112
|
+
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id: approval.id }] }).catch(() => void 0);
|
|
21352
22113
|
return fail(error2);
|
|
21353
22114
|
}
|
|
21354
22115
|
} catch (e) {
|
|
@@ -21595,8 +22356,8 @@ function errorText(e) {
|
|
|
21595
22356
|
return e instanceof Error ? e.message : String(e);
|
|
21596
22357
|
}
|
|
21597
22358
|
async function workspaceFor(api2, opts, required2 = true) {
|
|
21598
|
-
const
|
|
21599
|
-
if (
|
|
22359
|
+
const named2 = opts.workspace ?? process.env.CMO_WORKSPACE ?? (await settings.get()).workspace;
|
|
22360
|
+
if (named2) return named2.trim().toLowerCase();
|
|
21600
22361
|
const list2 = (await api2.me()).workspaces ?? [];
|
|
21601
22362
|
if (list2.length === 1) return list2[0]?.domain ?? null;
|
|
21602
22363
|
if (!required2) return null;
|
|
@@ -21649,9 +22410,9 @@ async function runTool(api2, name, args, opts) {
|
|
|
21649
22410
|
const preview = [say2, c, domain && t.need.resource !== "account" ? `(${domain})` : ""].filter(Boolean).join(" ");
|
|
21650
22411
|
if (opts.dryRun) return { ran: false, preview };
|
|
21651
22412
|
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 };
|
|
22413
|
+
return { ran: true, result: await t.run({ api: withIdempotency2(api2), domain, client: "cli" }, args), domain };
|
|
21653
22414
|
}
|
|
21654
|
-
return { ran: true, result: await t.run({ api: api2, domain }, args), domain };
|
|
22415
|
+
return { ran: true, result: await t.run({ api: api2, domain, client: "cli" }, args), domain };
|
|
21655
22416
|
}
|
|
21656
22417
|
function parsePairs(pairs) {
|
|
21657
22418
|
const out = {};
|
|
@@ -21835,30 +22596,46 @@ posts.command("ls", { isDefault: true }).description("planned, draft or publishe
|
|
|
21835
22596
|
posts.command("show <id>").description("one post in full").action(act(async (opts, id) => tool2(opts, "post_get", { id })));
|
|
21836
22597
|
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
22598
|
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
|
|
22599
|
+
var social = program2.command("social").description("your social posts: LinkedIn and your Facebook Page");
|
|
22600
|
+
var channelsOpt = (value) => {
|
|
22601
|
+
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).filter((v) => v === "linkedin" || v === "facebook");
|
|
22602
|
+
return list2.length ? list2 : void 0;
|
|
22603
|
+
};
|
|
21839
22604
|
social.command("ls", { isDefault: true }).description("drafts to review and queued posts").action(
|
|
21840
22605
|
act(
|
|
21841
22606
|
async (opts) => tool2(opts, "social_list", {}, (v) => {
|
|
21842
22607
|
const s = v;
|
|
21843
|
-
const
|
|
22608
|
+
const live = (s.channels ?? []).filter((c) => c.connected);
|
|
22609
|
+
const head = !s.connected ? "No social channel is connected (LinkedIn or a Facebook Page)." : 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
22610
|
return `${head}
|
|
21845
|
-
${table(s.queue ?? [], ["id", "status", "eta", "text"])}`;
|
|
22611
|
+
${table(s.queue ?? [], ["id", "channel", "status", "eta", "text"])}`;
|
|
21846
22612
|
})
|
|
21847
22613
|
)
|
|
21848
22614
|
);
|
|
21849
22615
|
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
22616
|
social.command("skip <id>").description("skip a draft").action(act(async (opts, id) => tool2(opts, "social_skip", { id: ids2([id])[0] })));
|
|
21851
22617
|
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
|
|
22618
|
+
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 or both (comma-separated)").action(
|
|
21853
22619
|
act(
|
|
21854
|
-
async (opts, topic) => tool2(opts, "social_idea_add", {
|
|
22620
|
+
async (opts, topic) => tool2(opts, "social_idea_add", {
|
|
22621
|
+
title: topic.join(" "),
|
|
22622
|
+
...opts.summary ? { summary: opts.summary } : {},
|
|
22623
|
+
...channelsOpt(opts.channel) ? { channels: channelsOpt(opts.channel) } : {}
|
|
22624
|
+
})
|
|
21855
22625
|
)
|
|
21856
22626
|
);
|
|
21857
22627
|
for (const [cmd, paused] of [
|
|
21858
22628
|
["pause", true],
|
|
21859
22629
|
["resume", false]
|
|
21860
22630
|
])
|
|
21861
|
-
social.command(cmd).description(`${cmd} posting
|
|
22631
|
+
social.command(cmd).description(`${cmd} posting (every channel, or --channel linkedin | facebook)`).option("--channel <name>", "only this channel").action(
|
|
22632
|
+
act(
|
|
22633
|
+
async (opts) => tool2(opts, "social_set_paused", {
|
|
22634
|
+
paused,
|
|
22635
|
+
...channelsOpt(opts.channel)?.[0] ? { channel: channelsOpt(opts.channel)?.[0] } : {}
|
|
22636
|
+
})
|
|
22637
|
+
)
|
|
22638
|
+
);
|
|
21862
22639
|
var autopilot = program2.command("autopilot").description("the blog autopilot");
|
|
21863
22640
|
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
22641
|
for (const [cmd, paused] of [
|
|
@@ -21931,6 +22708,47 @@ program2.command("tools").description("every action Maggie can take (run one wit
|
|
|
21931
22708
|
);
|
|
21932
22709
|
})
|
|
21933
22710
|
);
|
|
22711
|
+
var FEEDBACK_TYPES = ["bug", "quality", "idea", "praise", "other"];
|
|
22712
|
+
var feedback = program2.command("feedback").description('tell the cmo.so team what to fix or build: cmo feedback "<one line>" [--type bug]');
|
|
22713
|
+
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(
|
|
22714
|
+
act(
|
|
22715
|
+
async (opts, words) => {
|
|
22716
|
+
const summary = words.join(" ").trim();
|
|
22717
|
+
if (!summary)
|
|
22718
|
+
throw new CliError('Say what it is about: cmo feedback "Maggie made 14 ideas when I asked for 7"');
|
|
22719
|
+
await tool2(
|
|
22720
|
+
opts,
|
|
22721
|
+
"feedback_send",
|
|
22722
|
+
{
|
|
22723
|
+
type: opts.type,
|
|
22724
|
+
summary,
|
|
22725
|
+
...opts.details ? { details: opts.details } : {},
|
|
22726
|
+
...opts.expected ? { expected: opts.expected } : {},
|
|
22727
|
+
...opts.actual ? { actual: opts.actual } : {}
|
|
22728
|
+
},
|
|
22729
|
+
() => "Sent. Thank you! See the team's answer with: cmo feedback ls"
|
|
22730
|
+
);
|
|
22731
|
+
}
|
|
22732
|
+
)
|
|
22733
|
+
);
|
|
22734
|
+
feedback.command("ls").description("what you sent, its status and the team's reply").action(
|
|
22735
|
+
act(async (opts) => {
|
|
22736
|
+
const { items } = await api().feedback.mine();
|
|
22737
|
+
print(
|
|
22738
|
+
items,
|
|
22739
|
+
opts.json,
|
|
22740
|
+
() => items.length ? table(
|
|
22741
|
+
items.map((f) => ({
|
|
22742
|
+
sent: f.created_at.slice(0, 10),
|
|
22743
|
+
type: f.type,
|
|
22744
|
+
status: f.status.replace("_", " ") + (f.unread ? " (new reply)" : ""),
|
|
22745
|
+
summary: f.summary,
|
|
22746
|
+
reply: f.reply || "-"
|
|
22747
|
+
}))
|
|
22748
|
+
) : 'Nothing sent yet. Try: cmo feedback "\u2026"'
|
|
22749
|
+
);
|
|
22750
|
+
})
|
|
22751
|
+
);
|
|
21934
22752
|
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
22753
|
act(async (opts, name, pairs = []) => {
|
|
21936
22754
|
let args;
|