@cmo-so/mcp 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-mcp.mjs +922 -161
- package/package.json +1 -1
package/dist/cmo-mcp.mjs
CHANGED
|
@@ -4622,7 +4622,7 @@ var require_core = __commonJS({
|
|
|
4622
4622
|
errorsText(errors = this.errors, { separator = ", ", dataVar = "data" } = {}) {
|
|
4623
4623
|
if (!errors || errors.length === 0)
|
|
4624
4624
|
return "No errors";
|
|
4625
|
-
return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((
|
|
4625
|
+
return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text3, msg) => text3 + separator + msg);
|
|
4626
4626
|
}
|
|
4627
4627
|
$dataMetaSchema(metaSchema, keywordsJsonPointers) {
|
|
4628
4628
|
const rules = this.RULES.all;
|
|
@@ -7199,7 +7199,7 @@ var require_dist = __commonJS({
|
|
|
7199
7199
|
// package.json
|
|
7200
7200
|
var package_default = {
|
|
7201
7201
|
name: "@cmo-so/cli",
|
|
7202
|
-
version: "0.
|
|
7202
|
+
version: "0.2.0",
|
|
7203
7203
|
description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
7204
7204
|
type: "module",
|
|
7205
7205
|
license: "MIT",
|
|
@@ -7283,13 +7283,13 @@ var ApiError = class extends Error {
|
|
|
7283
7283
|
};
|
|
7284
7284
|
|
|
7285
7285
|
// ../../packages/api-client/src/api.ts
|
|
7286
|
-
function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.fetch }) {
|
|
7286
|
+
function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.fetch, streamFetch }) {
|
|
7287
7287
|
const base = baseUrl.replace(/\/+$/, "");
|
|
7288
|
-
async function send(path, init, token) {
|
|
7288
|
+
async function send(path, init, token, via = fetchImpl) {
|
|
7289
7289
|
const headers = new Headers(init.headers);
|
|
7290
7290
|
headers.set("Authorization", `Bearer ${token}`);
|
|
7291
7291
|
headers.set("Accept", "application/json");
|
|
7292
|
-
return
|
|
7292
|
+
return via(`${base}${path}`, { ...init, headers });
|
|
7293
7293
|
}
|
|
7294
7294
|
async function request(path, init = {}) {
|
|
7295
7295
|
let res = await send(path, init, await session2.getAccessToken());
|
|
@@ -7306,15 +7306,32 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
|
|
|
7306
7306
|
}
|
|
7307
7307
|
return data;
|
|
7308
7308
|
}
|
|
7309
|
-
async function
|
|
7309
|
+
async function requestBytes(path) {
|
|
7310
|
+
let res = await send(path, {}, await session2.getAccessToken());
|
|
7311
|
+
if (res.status === 401) {
|
|
7312
|
+
res = await send(path, {}, await session2.getAccessToken({ forceRefresh: true }));
|
|
7313
|
+
if (res.status === 401) {
|
|
7314
|
+
await session2.end();
|
|
7315
|
+
throw new SignInRequiredError();
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
if (!res.ok) {
|
|
7319
|
+
const data = await res.json().catch(() => null);
|
|
7320
|
+
throw new ApiError(res.status, errorMessage(data, res.status), res.headers.get("X-Request-ID"));
|
|
7321
|
+
}
|
|
7322
|
+
return { type: res.headers.get("content-type") ?? "image/jpeg", bytes: await res.arrayBuffer() };
|
|
7323
|
+
}
|
|
7324
|
+
async function stream(path, body, onEvent, signal) {
|
|
7310
7325
|
const init = {
|
|
7311
7326
|
method: "POST",
|
|
7312
7327
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
7313
|
-
body: JSON.stringify(body)
|
|
7328
|
+
body: JSON.stringify(body),
|
|
7329
|
+
...signal ? { signal } : {}
|
|
7314
7330
|
};
|
|
7315
|
-
|
|
7331
|
+
const via = streamFetch ?? fetchImpl;
|
|
7332
|
+
let res = await send(path, init, await session2.getAccessToken(), via);
|
|
7316
7333
|
if (res.status === 401) {
|
|
7317
|
-
res = await send(path, init, await session2.getAccessToken({ forceRefresh: true }));
|
|
7334
|
+
res = await send(path, init, await session2.getAccessToken({ forceRefresh: true }), via);
|
|
7318
7335
|
if (res.status === 401) {
|
|
7319
7336
|
await session2.end();
|
|
7320
7337
|
throw new SignInRequiredError();
|
|
@@ -7384,8 +7401,87 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
|
|
|
7384
7401
|
},
|
|
7385
7402
|
// Ask Maggie (ADR-015): `history` is opaque; send back what `done` returned.
|
|
7386
7403
|
maggie: {
|
|
7387
|
-
chat: (body, onEvent) => stream("/v1/maggie/chat", body, onEvent),
|
|
7388
|
-
usage: (domain) => request(`/v1/maggie/usage${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`)
|
|
7404
|
+
chat: (body, onEvent, signal) => stream("/v1/maggie/chat", body, onEvent, signal),
|
|
7405
|
+
usage: (domain) => request(`/v1/maggie/usage${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`),
|
|
7406
|
+
// The user's own Maggie Bridge (docs/20 route D): Maggie on their ChatGPT plan.
|
|
7407
|
+
bridge: {
|
|
7408
|
+
/** `live: false`: from the pairing alone, without waking the bridge. */
|
|
7409
|
+
status: (opts = {}) => request(`/v1/maggie/bridge${opts.live === false ? "?live=0" : ""}`),
|
|
7410
|
+
/** Pair in one step: CMO.so issues the bridge its own key with this access (read by default). */
|
|
7411
|
+
pair: (body) => request("/v1/maggie/bridge", {
|
|
7412
|
+
method: "POST",
|
|
7413
|
+
headers: { "Content-Type": "application/json" },
|
|
7414
|
+
body: JSON.stringify(body)
|
|
7415
|
+
}),
|
|
7416
|
+
/** Pair with CMO.so's own bridge in one step (accounts on its list); changes you approve by default. */
|
|
7417
|
+
connectHosted: (access) => request("/v1/maggie/bridge/hosted", {
|
|
7418
|
+
method: "POST",
|
|
7419
|
+
headers: { "Content-Type": "application/json" },
|
|
7420
|
+
body: JSON.stringify(access ? { access } : {})
|
|
7421
|
+
}),
|
|
7422
|
+
setEnabled: (enabled) => request("/v1/maggie/bridge/settings", {
|
|
7423
|
+
method: "POST",
|
|
7424
|
+
headers: { "Content-Type": "application/json" },
|
|
7425
|
+
body: JSON.stringify({ enabled })
|
|
7426
|
+
}),
|
|
7427
|
+
/** Pictures on ChatGPT (on) or Gemini only (off). */
|
|
7428
|
+
setImages: (images) => request("/v1/maggie/bridge/settings", {
|
|
7429
|
+
method: "POST",
|
|
7430
|
+
headers: { "Content-Type": "application/json" },
|
|
7431
|
+
body: JSON.stringify({ images })
|
|
7432
|
+
}),
|
|
7433
|
+
remove: () => request("/v1/maggie/bridge", { method: "DELETE" }),
|
|
7434
|
+
/** Start "Sign in with ChatGPT" for one of the bridge's accounts (the active one by default). */
|
|
7435
|
+
connectChatgpt: (account3) => request("/v1/maggie/bridge/chatgpt", {
|
|
7436
|
+
method: "POST",
|
|
7437
|
+
headers: { "Content-Type": "application/json" },
|
|
7438
|
+
body: JSON.stringify(account3 ? { account: account3 } : {})
|
|
7439
|
+
}),
|
|
7440
|
+
disconnectChatgpt: (account3) => request("/v1/maggie/bridge/chatgpt/logout", {
|
|
7441
|
+
method: "POST",
|
|
7442
|
+
headers: { "Content-Type": "application/json" },
|
|
7443
|
+
body: JSON.stringify(account3 ? { account: account3 } : {})
|
|
7444
|
+
}),
|
|
7445
|
+
/** What Maggie may do in CMO.so; rotates the bridge's key when read / read_write changes. */
|
|
7446
|
+
setAccess: (access) => request("/v1/maggie/bridge/access", {
|
|
7447
|
+
method: "POST",
|
|
7448
|
+
headers: { "Content-Type": "application/json" },
|
|
7449
|
+
body: JSON.stringify({ access })
|
|
7450
|
+
}),
|
|
7451
|
+
/** Which model answers and how hard it thinks; null for the default. */
|
|
7452
|
+
setModel: (body) => request("/v1/maggie/bridge/model", {
|
|
7453
|
+
method: "POST",
|
|
7454
|
+
headers: { "Content-Type": "application/json" },
|
|
7455
|
+
body: JSON.stringify(body)
|
|
7456
|
+
}),
|
|
7457
|
+
models: () => request("/v1/maggie/bridge/models"),
|
|
7458
|
+
/** Add a ChatGPT account to the bridge (then connect it). */
|
|
7459
|
+
addAccount: (label) => request("/v1/maggie/bridge/accounts", {
|
|
7460
|
+
method: "POST",
|
|
7461
|
+
headers: { "Content-Type": "application/json" },
|
|
7462
|
+
body: JSON.stringify({ label: label ?? null })
|
|
7463
|
+
}),
|
|
7464
|
+
/** Make one of the bridge's ChatGPT accounts the one that answers. */
|
|
7465
|
+
chooseAccount: (id) => request(`/v1/maggie/bridge/accounts/${encodeURIComponent(id)}/use`, { method: "POST" }),
|
|
7466
|
+
removeAccount: (id) => request(`/v1/maggie/bridge/accounts/${encodeURIComponent(id)}`, { method: "DELETE" })
|
|
7467
|
+
}
|
|
7468
|
+
},
|
|
7469
|
+
// Help Maggie grow (docs/22): api.cmo.so forwards these to feedback.cmo.so.
|
|
7470
|
+
feedback: {
|
|
7471
|
+
send: (body) => request("/v1/feedback", {
|
|
7472
|
+
method: "POST",
|
|
7473
|
+
headers: { "Content-Type": "application/json" },
|
|
7474
|
+
body: JSON.stringify(body)
|
|
7475
|
+
}),
|
|
7476
|
+
/** The user's own feedback, newest first, with the team's replies. */
|
|
7477
|
+
mine: () => request("/v1/feedback/mine"),
|
|
7478
|
+
/** The user has read the team's latest reply or status change. */
|
|
7479
|
+
seen: (id) => request(`/v1/feedback/${encodeURIComponent(id)}/seen`, { method: "POST" }),
|
|
7480
|
+
/** One of the user's feedback photos as a data: URL (an <Image> source on web and native). */
|
|
7481
|
+
photo: async (id, n) => {
|
|
7482
|
+
const { type, bytes } = await requestBytes(`/v1/feedback/${encodeURIComponent(id)}/photos/${n}`);
|
|
7483
|
+
return dataUrl(type, bytes);
|
|
7484
|
+
}
|
|
7389
7485
|
},
|
|
7390
7486
|
// Web Push for this browser (APP-9). `domains` are the workspaces it shows.
|
|
7391
7487
|
push: {
|
|
@@ -7416,8 +7512,25 @@ function errorMessage(data, status) {
|
|
|
7416
7512
|
}
|
|
7417
7513
|
return `Request failed (${status})`;
|
|
7418
7514
|
}
|
|
7515
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7516
|
+
function dataUrl(type, bytes) {
|
|
7517
|
+
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
7518
|
+
const parts = [];
|
|
7519
|
+
let chunk = "";
|
|
7520
|
+
for (let i = 0; i < b.length; i += 3) {
|
|
7521
|
+
const n = (b[i] ?? 0) << 16 | (b[i + 1] ?? 0) << 8 | (b[i + 2] ?? 0);
|
|
7522
|
+
chunk += B64.charAt(n >> 18 & 63) + B64.charAt(n >> 12 & 63) + (i + 1 < b.length ? B64.charAt(n >> 6 & 63) : "=") + (i + 2 < b.length ? B64.charAt(n & 63) : "=");
|
|
7523
|
+
if (chunk.length >= 8192) {
|
|
7524
|
+
parts.push(chunk);
|
|
7525
|
+
chunk = "";
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
parts.push(chunk);
|
|
7529
|
+
return `data:${type.split(";")[0]?.trim() || "image/jpeg"};base64,${parts.join("")}`;
|
|
7530
|
+
}
|
|
7419
7531
|
|
|
7420
7532
|
// ../../packages/api-client/src/domains.ts
|
|
7533
|
+
var MEMORY_CHANNELS = ["general", "blog", "linkedin", "facebook"];
|
|
7421
7534
|
function addWebsite(api2, url) {
|
|
7422
7535
|
return api2.request("/v1/domains", {
|
|
7423
7536
|
method: "POST",
|
|
@@ -7501,6 +7614,20 @@ function domainApi(api2, domain) {
|
|
|
7501
7614
|
email: email2
|
|
7502
7615
|
}),
|
|
7503
7616
|
verifyEmailConfirm: (code) => post(`${base}/verify/email/confirm`, { code }),
|
|
7617
|
+
/**
|
|
7618
|
+
* The key to paste into the AI CMO WordPress plugin: once the plugin is connected on the website, it is
|
|
7619
|
+
* verified. Viewable until then (or until it expires); create:false only reads it, new:true replaces it.
|
|
7620
|
+
*/
|
|
7621
|
+
verifyWordpressKey: (opts = {}) => post(
|
|
7622
|
+
`${base}/verify/wordpress/key`,
|
|
7623
|
+
opts
|
|
7624
|
+
),
|
|
7625
|
+
/** The personalised WordPress setup guide: the website's own name / icon / colour, links into its wp-admin, plugin status. */
|
|
7626
|
+
setupGuide: () => api2.request(`${base}/setup-guide`),
|
|
7627
|
+
/** "Ask us": email the CMO.so team (with the account and website) to verify it by hand. */
|
|
7628
|
+
verifyRequest: (note = "") => post(`${base}/verify/request`, { note }),
|
|
7629
|
+
/** Check now: is the plugin connected on the website with that key? */
|
|
7630
|
+
verifyWordpressCheck: () => post(`${base}/verify/wordpress/check`),
|
|
7504
7631
|
/** The TXT record of the user's own claim (never makes a new one). */
|
|
7505
7632
|
dnsRecord: () => api2.request(`${base}/verify/dns`),
|
|
7506
7633
|
/** Look for the TXT record now; verifies the claim when it is there. */
|
|
@@ -7510,16 +7637,23 @@ function domainApi(api2, domain) {
|
|
|
7510
7637
|
// Writes (need autopilot write on the domain).
|
|
7511
7638
|
addIdea: (input) => post(`${base}/ideas`, input),
|
|
7512
7639
|
/** Ideas built on up to 10 photos (base64 JPEG). The server keeps no copy. */
|
|
7513
|
-
ideasFromPhotos: (photos, count,
|
|
7514
|
-
generateIdeas: (count,
|
|
7640
|
+
ideasFromPhotos: (photos, count, brief2 = "") => post(`${base}/ideas/from-photos`, { photos, count, brief: brief2 }),
|
|
7641
|
+
generateIdeas: (count, brief2 = "") => post(`${base}/ideas/generate`, { count, brief: brief2 }),
|
|
7515
7642
|
/** This website's sitemap, or `source` (a sitemap, site or page) on any website. */
|
|
7516
7643
|
importSitemap: (limit = 10, source) => post(`${base}/ideas/import-sitemap`, {
|
|
7517
7644
|
limit,
|
|
7518
7645
|
...source ? { sitemap_url: source } : {}
|
|
7519
7646
|
}),
|
|
7520
7647
|
discardIdea: (id, restore = false) => post(`${base}/ideas/${encodeURIComponent(id)}/discard${restore ? "?restore=true" : ""}`),
|
|
7648
|
+
/** OK a draft (or retry a failed post): it goes to the front of the queue; the post comes back with its eta. */
|
|
7521
7649
|
approveSocial: (id) => post(`${base}/social/${id}/approve`),
|
|
7522
7650
|
skipSocial: (id) => post(`${base}/social/${id}/skip`),
|
|
7651
|
+
/** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
|
|
7652
|
+
publishSocialNow: (id) => post(`${base}/social/${id}/publish`),
|
|
7653
|
+
/** Take a posted LinkedIn post down (its likes and comments go with it). */
|
|
7654
|
+
deletePostedSocial: (id) => api2.request(`${base}/social/${id}/published`, { method: "DELETE" }),
|
|
7655
|
+
/** Change a posted LinkedIn post and put it up again: the old one comes down, the new text goes out now. */
|
|
7656
|
+
republishSocial: (id, socialCopy) => post(`${base}/social/${id}/republish`, socialCopy ? { social_copy: socialCopy } : {}),
|
|
7523
7657
|
restoreSocial: (id) => post(`${base}/social/${id}/restore`),
|
|
7524
7658
|
pauseSocial: () => post(`${base}/social/pause`),
|
|
7525
7659
|
/**
|
|
@@ -7565,6 +7699,67 @@ function domainApi(api2, domain) {
|
|
|
7565
7699
|
/** Maggie drafts the first LinkedIn post (1 credit): from a few lines, or one of your services. */
|
|
7566
7700
|
socialFirstPost: (input) => post(`${base}/social/setup/first-post`, input),
|
|
7567
7701
|
socialSetupDone: () => post(`${base}/social/setup/done`),
|
|
7702
|
+
// LinkedIn v2 (docs/prd/linkedin-journey-v2.md): Maggie's plan instead of the step-by-step setup.
|
|
7703
|
+
/** What is set up now, as a plan (no AI). */
|
|
7704
|
+
socialSetupPlan: () => api2.request(`${base}/social/setup/plan`),
|
|
7705
|
+
/** Maggie prepares what's missing (logo, look, voice, writing, frequency); `force` redoes it. Free. */
|
|
7706
|
+
socialSetupAuto: (timezone, force = false) => post(`${base}/social/setup/auto`, {
|
|
7707
|
+
...timezone ? { timezone } : {},
|
|
7708
|
+
...force ? { force: true } : {}
|
|
7709
|
+
}),
|
|
7710
|
+
socialSetupFrequency: (preset, timezone) => api2.request(`${base}/social/setup/frequency`, {
|
|
7711
|
+
method: "PUT",
|
|
7712
|
+
headers: { "Content-Type": "application/json" },
|
|
7713
|
+
body: JSON.stringify(timezone ? { preset, timezone } : { preset })
|
|
7714
|
+
}),
|
|
7715
|
+
/** "Looks good, start": setup done, and Maggie writes the first week's posts. */
|
|
7716
|
+
socialSetupConfirm: () => post(`${base}/social/setup/confirm`),
|
|
7717
|
+
/** OK every post waiting for you (not a series that hasn't been approved), on one channel or all. */
|
|
7718
|
+
approveAllSocial: (channel) => post(
|
|
7719
|
+
`${base}/social/approve-all${channel ? `?channel=${channel}` : ""}`
|
|
7720
|
+
),
|
|
7721
|
+
/** A Facebook sign-in link: after it the user lands on returnTo with ?facebook=connected (or =error). */
|
|
7722
|
+
connectFacebook: (returnTo = "close") => post(`${base}/social/facebook/connect`, { return_to: returnTo }),
|
|
7723
|
+
/** The Facebook Pages you connected, and the one this website posts to. */
|
|
7724
|
+
facebookPages: () => api2.request(`${base}/social/facebook/pages`),
|
|
7725
|
+
chooseFacebookPage: (connectionId) => api2.request(
|
|
7726
|
+
`${base}/social/facebook/page`,
|
|
7727
|
+
{
|
|
7728
|
+
method: "PUT",
|
|
7729
|
+
headers: { "Content-Type": "application/json" },
|
|
7730
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7731
|
+
}
|
|
7732
|
+
),
|
|
7733
|
+
disconnectFacebook: (connectionId) => api2.request(`${base}/social/facebook/${connectionId}`, { method: "DELETE" }),
|
|
7734
|
+
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7735
|
+
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7736
|
+
/** How often one channel posts. */
|
|
7737
|
+
setChannelFrequency: (channel, preset) => api2.request(
|
|
7738
|
+
`${base}/social/channels/${channel}/frequency`,
|
|
7739
|
+
{ method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ preset }) }
|
|
7740
|
+
),
|
|
7741
|
+
/** Start a new picture for the post in the background (one AI generation); `replace` stops the one
|
|
7742
|
+
* already running; `instruction` (up to 300 characters) steers it: "gold border, our logo". */
|
|
7743
|
+
regenerateSocialImage: (id, opts = {}) => post(`${base}/social/${id}/image/regenerate`, {
|
|
7744
|
+
...opts.replace ? { replace: true } : {},
|
|
7745
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
|
|
7746
|
+
}),
|
|
7747
|
+
/** Your own photo (base64 JPEG) as the post's picture: no AI, no charge; only before it goes out. */
|
|
7748
|
+
uploadSocialPhoto: (id, photo) => post(`${base}/social/${id}/image`, { photo }),
|
|
7749
|
+
/** Stop the picture being made (it's thrown away). */
|
|
7750
|
+
stopSocialImage: () => api2.request(`${base}/social/image-job`, { method: "DELETE" }),
|
|
7751
|
+
/** Maggie changes the post as asked, in 3-255 characters (one AI generation). */
|
|
7752
|
+
rewriteSocial: (id, instruction) => post(`${base}/social/${id}/rewrite`, { instruction }),
|
|
7753
|
+
/** Maggie plans and writes a series; it waits for one OK. */
|
|
7754
|
+
/** A series of `count` (1-7) posts on one theme; they are ordinary posts (Review), labelled with the series. */
|
|
7755
|
+
createSocialSeries: (input) => post(`${base}/social/series`, input),
|
|
7756
|
+
approveSocialSeries: (id) => post(`${base}/social/series/${id}/approve`),
|
|
7757
|
+
/** A post from photos (and a line about them): it waits for your OK like any other. */
|
|
7758
|
+
socialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7759
|
+
photos,
|
|
7760
|
+
brief: brief2,
|
|
7761
|
+
...channels ? { channels } : {}
|
|
7762
|
+
}),
|
|
7568
7763
|
/** Disconnect the website's LinkedIn account. */
|
|
7569
7764
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
7570
7765
|
/** Published articles a LinkedIn post can be made from. */
|
|
@@ -7572,9 +7767,16 @@ function domainApi(api2, domain) {
|
|
|
7572
7767
|
`${base}/social/sources?search=${encodeURIComponent(search)}`
|
|
7573
7768
|
),
|
|
7574
7769
|
/** Maggie writes a LinkedIn draft about one article. */
|
|
7575
|
-
socialFromPost: (sourcePostId) => post(`${base}/social/from-post`, {
|
|
7770
|
+
socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7771
|
+
source_post_id: sourcePostId,
|
|
7772
|
+
...channels ? { channels } : {}
|
|
7773
|
+
}),
|
|
7576
7774
|
/** Drafts for the next 1-7 days from the best recent articles. */
|
|
7577
|
-
socialPlan: (days, timezone) => post(`${base}/social/plan`, {
|
|
7775
|
+
socialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7776
|
+
days,
|
|
7777
|
+
timezone,
|
|
7778
|
+
...channels ? { channels } : {}
|
|
7779
|
+
}),
|
|
7578
7780
|
/** Campaign ideas waiting to be written (this person's, newest campaign first). */
|
|
7579
7781
|
socialIdeas: (page = 1, perPage = 20) => api2.request(
|
|
7580
7782
|
`${base}/social/ideas?page=${page}&per_page=${perPage}`
|
|
@@ -7594,14 +7796,35 @@ function domainApi(api2, domain) {
|
|
|
7594
7796
|
`${base}/social/campaigns/${id}/start`,
|
|
7595
7797
|
options
|
|
7596
7798
|
),
|
|
7597
|
-
/**
|
|
7598
|
-
|
|
7799
|
+
/**
|
|
7800
|
+
* Maggie writes the post for this idea (1 credit); it waits in Upcoming.
|
|
7801
|
+
* With `written` ({ linkedin: { body } }, from socialIdeaBrief), the user's own
|
|
7802
|
+
* assistant wrote it: saved through the same checks, for the same credit.
|
|
7803
|
+
*/
|
|
7804
|
+
writeSocialIdea: (id, written, channels) => post(
|
|
7805
|
+
`${base}/social/ideas/${id}/write`,
|
|
7806
|
+
written || channels ? { ...written ? { written } : {}, ...channels ? { channels } : {} } : void 0
|
|
7807
|
+
),
|
|
7808
|
+
/** What Maggie would write this idea's post from: the prompt, JSON shape and length limits (free). */
|
|
7809
|
+
socialIdeaBrief: (id) => api2.request(`${base}/social/ideas/${id}/brief`),
|
|
7599
7810
|
/** Change a LinkedIn post's text before it goes out. */
|
|
7600
7811
|
updateSocial: (id, socialCopy) => api2.request(`${base}/social/${id}`, {
|
|
7601
7812
|
method: "PATCH",
|
|
7602
7813
|
headers: { "Content-Type": "application/json" },
|
|
7603
7814
|
body: JSON.stringify({ social_copy: socialCopy })
|
|
7604
7815
|
}),
|
|
7816
|
+
/** The post's link: it replaces the old one in the text, and the preview card shows the new page. */
|
|
7817
|
+
setSocialLink: (id, link) => api2.request(`${base}/social/${id}`, {
|
|
7818
|
+
method: "PATCH",
|
|
7819
|
+
headers: { "Content-Type": "application/json" },
|
|
7820
|
+
body: JSON.stringify({ link })
|
|
7821
|
+
}),
|
|
7822
|
+
/** Go out as the post's image, or as a link preview. */
|
|
7823
|
+
setSocialPublishMode: (id, mode) => api2.request(`${base}/social/${id}`, {
|
|
7824
|
+
method: "PATCH",
|
|
7825
|
+
headers: { "Content-Type": "application/json" },
|
|
7826
|
+
body: JSON.stringify({ publish_mode: mode })
|
|
7827
|
+
}),
|
|
7605
7828
|
resumeSocial: () => post(`${base}/social/resume`),
|
|
7606
7829
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7607
7830
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
@@ -7631,6 +7854,8 @@ function domainApi(api2, domain) {
|
|
|
7631
7854
|
planCampaign: (id) => post(`${base}/social/campaigns/${id}/plan`),
|
|
7632
7855
|
pauseCampaign: (id) => post(`${base}/social/campaigns/${id}/pause`),
|
|
7633
7856
|
resumeCampaign: (id) => post(`${base}/social/campaigns/${id}/resume`),
|
|
7857
|
+
/** End a series: its posts that haven't gone out are dropped; published ones stay on LinkedIn. */
|
|
7858
|
+
endCampaign: (id) => post(`${base}/social/campaigns/${id}/end`),
|
|
7634
7859
|
setApprovalMode: (mode) => api2.request(`${base}/social/approval-mode`, {
|
|
7635
7860
|
method: "PUT",
|
|
7636
7861
|
headers: { "Content-Type": "application/json" },
|
|
@@ -7647,6 +7872,22 @@ function domainApi(api2, domain) {
|
|
|
7647
7872
|
}),
|
|
7648
7873
|
/** What Maggie changed for you on this website, newest first (docs/18 MT-SEC-6). */
|
|
7649
7874
|
maggieActions: (limit = 30) => api2.request(`${base}/maggie/actions?limit=${limit}`),
|
|
7875
|
+
// Maggie Memory (docs/21): the user's own memory on this website.
|
|
7876
|
+
maggieMemory: () => api2.request(`${base}/maggie/memory`),
|
|
7877
|
+
maggieMemorySettings: (s) => put(`${base}/maggie/memory/settings`, s),
|
|
7878
|
+
addMaggieMemory: (item2) => post(`${base}/maggie/memory/items`, item2),
|
|
7879
|
+
updateMaggieMemory: (id, patch) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, {
|
|
7880
|
+
method: "PATCH",
|
|
7881
|
+
headers: { "Content-Type": "application/json" },
|
|
7882
|
+
body: JSON.stringify(patch)
|
|
7883
|
+
}),
|
|
7884
|
+
deleteMaggieMemory: (id) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, { method: "DELETE" }),
|
|
7885
|
+
/** Learn from recent activity now; `full` rebuilds the learned items from scratch. */
|
|
7886
|
+
learnMaggieMemory: (o = {}) => post(`${base}/maggie/memory/learn`, o),
|
|
7887
|
+
/** Forget what was learned (one channel, or all); the user's own items stay. */
|
|
7888
|
+
clearMaggieMemory: (channel) => api2.request(`${base}/maggie/memory${channel ? `?channel=${encodeURIComponent(channel)}` : ""}`, {
|
|
7889
|
+
method: "DELETE"
|
|
7890
|
+
}),
|
|
7650
7891
|
// docs/19 P3: autopilot cadence and publish-now.
|
|
7651
7892
|
autopilot: () => api2.request(`${base}/autopilot`),
|
|
7652
7893
|
/** Posts per day for pushed blogs (one of `choices`, up to the plan's limit). */
|
|
@@ -7810,6 +8051,11 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
|
|
|
7810
8051
|
|
|
7811
8052
|
// ../../packages/domain/src/copy/maggie.ts
|
|
7812
8053
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
8054
|
+
var named = (title, id) => {
|
|
8055
|
+
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
8056
|
+
const s = String(id ?? "").trim();
|
|
8057
|
+
return s && s.length <= 8 ? ` #${s}` : "";
|
|
8058
|
+
};
|
|
7813
8059
|
var list = (xs, n = 5) => {
|
|
7814
8060
|
const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
|
|
7815
8061
|
return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
|
|
@@ -7892,41 +8138,45 @@ var confirmCopy = {
|
|
|
7892
8138
|
"zh-Hant": (a) => `\u70BA ${list(a.target_audience)} \u5EFA\u7ACB\u54C1\u724C\u8A9E\u6C23 ${q(a.name ?? "\u65B0\u8A9E\u6C23")}\u3002`
|
|
7893
8139
|
},
|
|
7894
8140
|
brand_voice_update_style: {
|
|
7895
|
-
en: (a) => `Change how the brand voice
|
|
7896
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u54C1\u724C\u8A9E\u6C23
|
|
8141
|
+
en: (a) => `Change how the brand voice${named(a.name, a.id)} writes: ${keys(a).filter((k) => k !== "id" && k !== "name").join(", ")}.`,
|
|
8142
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u54C1\u724C\u8A9E\u6C23${named(a.name, a.id)} \u7684\u5BEB\u6CD5\uFF1A${keys(a).filter((k) => k !== "id" && k !== "name").join("\u3001")}\u3002`
|
|
7897
8143
|
},
|
|
7898
8144
|
brand_voice_set_default: {
|
|
7899
|
-
en: (a) => `Make
|
|
7900
|
-
"zh-Hant": (a) => `\u628A
|
|
8145
|
+
en: (a) => `Make${named(a.name, a.id)} your default brand voice.`,
|
|
8146
|
+
"zh-Hant": (a) => `\u628A${named(a.name, a.id)} \u8A2D\u70BA\u9810\u8A2D\u54C1\u724C\u8A9E\u6C23\u3002`
|
|
7901
8147
|
},
|
|
7902
8148
|
brand_voice_delete: {
|
|
7903
|
-
en: (a) => `Delete the brand voice
|
|
7904
|
-
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23
|
|
8149
|
+
en: (a) => `Delete the brand voice${named(a.name, a.id)}. This can't be undone.`,
|
|
8150
|
+
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23${named(a.name, a.id)}\uFF0C\u522A\u9664\u5F8C\u7121\u6CD5\u6062\u5FA9\u3002`
|
|
7905
8151
|
},
|
|
7906
8152
|
// ------------------------------------------------------------------- blog
|
|
7907
8153
|
draft_update: {
|
|
7908
|
-
en: (a) => `Change the draft
|
|
7909
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8349\u7A3F
|
|
8154
|
+
en: (a) => `Change the draft${named(a.title_for_card, a.id)}: ${keys(a).filter((k) => !["id", "title_for_card"].includes(k)).join(", ")}.`,
|
|
8155
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8349\u7A3F${named(a.title_for_card, a.id)}\uFF1A${keys(a).filter((k) => !["id", "title_for_card"].includes(k)).join("\u3001")}\u3002`
|
|
7910
8156
|
},
|
|
7911
8157
|
draft_rewrite: {
|
|
7912
|
-
en: (a) => a.mode === "fix" ? `Let Maggie fix the draft
|
|
7913
|
-
"zh-Hant": (a) => a.mode === "fix" ? `\u8B93 Maggie \u4FEE\u6B63\u8349\u7A3F
|
|
8158
|
+
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.`,
|
|
8159
|
+
"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`
|
|
8160
|
+
},
|
|
8161
|
+
draft_rewrite_save: {
|
|
8162
|
+
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.`,
|
|
8163
|
+
"zh-Hant": (a) => `\u5132\u5B58\u4F60${a.mode === "fix" ? "\u4FEE\u6B63" : "\u4FEE\u6539"}\u5F8C\u7684\u8349\u7A3F${named(a.title, a.id)}\uFF08${String(a.markdown ?? "").length} \u5B57\u5143\uFF09\uFF0C\u53D6\u4EE3\u76EE\u524D\u5167\u5BB9\u3002`
|
|
7914
8164
|
},
|
|
7915
8165
|
draft_delete: {
|
|
7916
|
-
en: (a) => `Delete the draft
|
|
7917
|
-
"zh-Hant": (a) => `\u522A\u9664\u8349\u7A3F
|
|
8166
|
+
en: (a) => `Delete the draft${named(a.title, a.id)}. Its idea is dropped too and the credit is not refunded.`,
|
|
8167
|
+
"zh-Hant": (a) => `\u522A\u9664\u8349\u7A3F${named(a.title, a.id)}\uFF0C\u5C0D\u61C9\u7684 idea \u4E5F\u6703\u4E1F\u68C4\uFF0C\u984D\u5EA6\u4E0D\u9000\u3002`
|
|
7918
8168
|
},
|
|
7919
8169
|
plan_move: {
|
|
7920
|
-
en: (a) => `Move
|
|
7921
|
-
"zh-Hant": (a) => `\u628A
|
|
8170
|
+
en: (a) => `Move${named(a.title, a.id)} ${a.to === "top" ? "to the top of" : a.to} the plan.`,
|
|
8171
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} ${a.to === "top" ? "\u79FB\u5230\u8A08\u5283\u6700\u524D" : a.to === "up" ? "\u5728\u8A08\u5283\u88E1\u4E0A\u79FB" : "\u5728\u8A08\u5283\u88E1\u4E0B\u79FB"}\u3002`
|
|
7922
8172
|
},
|
|
7923
8173
|
plan_update: {
|
|
7924
|
-
en: (a) => `Change the planned post
|
|
7925
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0
|
|
8174
|
+
en: (a) => `Change the planned post${named(a.title_for_card, a.id)}.`,
|
|
8175
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0${named(a.title_for_card, a.id)}\u3002`
|
|
7926
8176
|
},
|
|
7927
8177
|
plan_remove: {
|
|
7928
|
-
en: (a) => `Take
|
|
7929
|
-
"zh-Hant": (a) => `\u628A
|
|
8178
|
+
en: (a) => `Take${named(a.title, a.id)} out of the plan (its idea can be restored).`,
|
|
8179
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u79FB\u51FA\u8A08\u5283\uFF08idea \u53EF\u4EE5\u6062\u5FA9\uFF09\u3002`
|
|
7930
8180
|
},
|
|
7931
8181
|
idea_add: { en: (a) => `Add the idea ${q(a.url || a.text)}.`, "zh-Hant": (a) => `\u52A0\u5165 idea ${q(a.url || a.text)}\u3002` },
|
|
7932
8182
|
ideas_generate: {
|
|
@@ -7938,24 +8188,24 @@ var confirmCopy = {
|
|
|
7938
8188
|
"zh-Hant": (a) => `\u5F9E ${a.sitemap_url || "\u4F60\u7684 sitemap"} \u5C0E\u5165\u6700\u591A ${a.limit || 10} \u500B idea\u3002`
|
|
7939
8189
|
},
|
|
7940
8190
|
idea_update: {
|
|
7941
|
-
en: (a) => `Change the idea
|
|
7942
|
-
"zh-Hant": (a) => `\u4FEE\u6539 idea
|
|
8191
|
+
en: (a) => `Change the idea${named(a.title_for_card, a.id)}.`,
|
|
8192
|
+
"zh-Hant": (a) => `\u4FEE\u6539 idea${named(a.title_for_card, a.id)}\u3002`
|
|
7943
8193
|
},
|
|
7944
8194
|
idea_priority: {
|
|
7945
|
-
en: (a) => `${Number(a.delta) > 0 ? "Raise" : "Lower"} the priority of
|
|
7946
|
-
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}
|
|
8195
|
+
en: (a) => `${Number(a.delta) > 0 ? "Raise" : "Lower"} the priority of${named(a.title, a.id)}.`,
|
|
8196
|
+
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
|
|
7947
8197
|
},
|
|
7948
8198
|
idea_plan: {
|
|
7949
|
-
en: (a) => `Add
|
|
7950
|
-
"zh-Hant": (a) => `\u628A
|
|
8199
|
+
en: (a) => `Add${named(a.title, a.id)} to the end of the plan.`,
|
|
8200
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u52A0\u5230\u8A08\u5283\u6700\u5F8C\u3002`
|
|
7951
8201
|
},
|
|
7952
8202
|
idea_discard: {
|
|
7953
|
-
en: (a) => `Remove the idea
|
|
7954
|
-
"zh-Hant": (a) => `\u522A\u9664 idea
|
|
8203
|
+
en: (a) => `Remove the idea${named(a.title, a.id)}.`,
|
|
8204
|
+
"zh-Hant": (a) => `\u522A\u9664 idea${named(a.title, a.id)}\u3002`
|
|
7955
8205
|
},
|
|
7956
8206
|
idea_restore: {
|
|
7957
|
-
en: (a) => `Bring back the idea
|
|
7958
|
-
"zh-Hant": (a) => `\u6062\u5FA9 idea
|
|
8207
|
+
en: (a) => `Bring back the idea${named(a.title, a.id)}.`,
|
|
8208
|
+
"zh-Hant": (a) => `\u6062\u5FA9 idea${named(a.title, a.id)}\u3002`
|
|
7959
8209
|
},
|
|
7960
8210
|
autopilot_set_paused: {
|
|
7961
8211
|
en: (a) => a.paused ? "Pause publishing blog posts." : "Resume publishing blog posts.",
|
|
@@ -7966,8 +8216,8 @@ var confirmCopy = {
|
|
|
7966
8216
|
"zh-Hant": (a) => `\u6BCF\u5929\u767C\u4F48 ${a.posts_per_day} \u7BC7\u90E8\u843D\u683C\u6587\u7AE0\u3002`
|
|
7967
8217
|
},
|
|
7968
8218
|
post_publish_now: {
|
|
7969
|
-
en: (a) => `Publish
|
|
7970
|
-
"zh-Hant": (a) => `\u7ACB\u5373\u628A
|
|
8219
|
+
en: (a) => `Publish${named(a.title, a.id)} to your website now. Everyone can see it once it's out.`,
|
|
8220
|
+
"zh-Hant": (a) => `\u7ACB\u5373\u628A${named(a.title, a.id)} \u767C\u4F48\u5230\u4F60\u7684\u7DB2\u7AD9\uFF0C\u767C\u51FA\u5F8C\u6240\u6709\u4EBA\u90FD\u770B\u5F97\u5230\u3002`
|
|
7971
8221
|
},
|
|
7972
8222
|
rewrite_request: {
|
|
7973
8223
|
en: (a) => `Rewrite the live article ${a.url}${a.instructions ? ` (${String(a.instructions).slice(0, 80)})` : ""}. The published page will change.`,
|
|
@@ -7981,11 +8231,11 @@ var confirmCopy = {
|
|
|
7981
8231
|
social_approve: {
|
|
7982
8232
|
en: (a) => {
|
|
7983
8233
|
const n = ids(a);
|
|
7984
|
-
return n.length > 1 ? `Approve ${n.length} LinkedIn posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes
|
|
8234
|
+
return n.length > 1 ? `Approve ${n.length} LinkedIn posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes out at your next posting times, where your network can see it.` : `Approve LinkedIn post #${n[0]}. It goes out at your next posting time, where your network can see it.`;
|
|
7985
8235
|
},
|
|
7986
8236
|
"zh-Hant": (a) => {
|
|
7987
8237
|
const n = ids(a);
|
|
7988
|
-
return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7 LinkedIn \u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\
|
|
8238
|
+
return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7 LinkedIn \u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\u5728\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002` : `\u6279\u51C6 LinkedIn \u8CBC\u6587 #${n[0]}\u3002\u5B83\u6703\u5728\u4E0B\u4E00\u500B\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
|
|
7989
8239
|
}
|
|
7990
8240
|
},
|
|
7991
8241
|
social_skip: { en: (a) => `Skip LinkedIn post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E LinkedIn \u8CBC\u6587 #${a.id}\u3002` },
|
|
@@ -7993,21 +8243,29 @@ var confirmCopy = {
|
|
|
7993
8243
|
en: (a) => `Bring back LinkedIn post #${a.id}.`,
|
|
7994
8244
|
"zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
|
|
7995
8245
|
},
|
|
8246
|
+
social_image_new: {
|
|
8247
|
+
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" : ""}.`,
|
|
8248
|
+
"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`
|
|
8249
|
+
},
|
|
8250
|
+
social_image_stop: {
|
|
8251
|
+
en: () => "Stop the LinkedIn picture being made; the post keeps the one it had.",
|
|
8252
|
+
"zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684 LinkedIn \u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
|
|
8253
|
+
},
|
|
7996
8254
|
social_move: {
|
|
7997
|
-
en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the
|
|
7998
|
-
"zh-Hant": (a) => `\u628A LinkedIn \u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\
|
|
8255
|
+
en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the top of" : a.to} Upcoming.`,
|
|
8256
|
+
"zh-Hant": (a) => `\u628A LinkedIn \u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\u5373\u5C07\u767C\u51FA\u7684\u6700\u524D" : a.to === "up" ? "\u4E0A\u79FB" : "\u4E0B\u79FB"}\u3002`
|
|
7999
8257
|
},
|
|
8000
8258
|
social_set_paused: {
|
|
8001
8259
|
en: (a) => a.paused ? "Pause all LinkedIn posting." : "Resume LinkedIn posting.",
|
|
8002
8260
|
"zh-Hant": (a) => a.paused ? "\u66AB\u505C\u6240\u6709 LinkedIn \u767C\u6587\u3002" : "\u6062\u5FA9 LinkedIn \u767C\u6587\u3002"
|
|
8003
8261
|
},
|
|
8004
8262
|
social_from_article: {
|
|
8005
|
-
en: (a) => `Write a LinkedIn post about ${q(a.title || a.source_id)}; it
|
|
8006
|
-
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} \u5BEB\u4E00\u7BC7 LinkedIn \u8CBC\u6587\uFF0C\
|
|
8263
|
+
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.`,
|
|
8264
|
+
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} \u5BEB\u4E00\u7BC7 LinkedIn \u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
|
|
8007
8265
|
},
|
|
8008
8266
|
social_plan_days: {
|
|
8009
|
-
en: (a) => `Write LinkedIn posts for the next ${a.days} day${Number(a.days) === 1 ? "" : "s"} from your best articles.`,
|
|
8010
|
-
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684 LinkedIn \u8CBC\u6587\u3002`
|
|
8267
|
+
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.`,
|
|
8268
|
+
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684 LinkedIn \u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
|
|
8011
8269
|
},
|
|
8012
8270
|
social_idea_add: {
|
|
8013
8271
|
en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
|
|
@@ -8022,12 +8280,30 @@ var confirmCopy = {
|
|
|
8022
8280
|
"zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
|
|
8023
8281
|
},
|
|
8024
8282
|
social_idea_write: {
|
|
8025
|
-
en: (a) => `Write the LinkedIn post for idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}.`,
|
|
8026
|
-
"zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB LinkedIn \u8CBC\u6587\u3002`
|
|
8283
|
+
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.`,
|
|
8284
|
+
"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`
|
|
8285
|
+
},
|
|
8286
|
+
social_idea_save: {
|
|
8287
|
+
en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
|
|
8288
|
+
"zh-Hant": (a) => `\u70BA idea #${a.id} \u5132\u5B58\u9019\u5247 LinkedIn \u8CBC\u6587\uFF08\u4E00\u6B21 AI \u751F\u6210\uFF09\uFF1A${q(a.text)}`
|
|
8027
8289
|
},
|
|
8028
8290
|
campaign_start: {
|
|
8029
|
-
en: (a) => `Start the
|
|
8030
|
-
"zh-Hant": (a) => `${a.start_date ? `\u5F9E ${a.start_date}` : "\u5F9E\u4ECA\u5929"}\u958B\u59CB\
|
|
8291
|
+
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.`,
|
|
8292
|
+
"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`
|
|
8293
|
+
},
|
|
8294
|
+
// ---------------------------------------------------------- Maggie Memory
|
|
8295
|
+
memory_remember: {
|
|
8296
|
+
en: (a) => `Remember${a.channel === "blog" ? " for blog posts" : a.channel === "linkedin" ? " for LinkedIn" : ""}: ${String(a.text ?? "").slice(0, 160)}`,
|
|
8297
|
+
"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)}`
|
|
8298
|
+
},
|
|
8299
|
+
memory_forget: {
|
|
8300
|
+
en: (a) => `Forget${a.text_for_card ? ` ${q(a.text_for_card)}` : " this"} from your Maggie Memory.`,
|
|
8301
|
+
"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`
|
|
8302
|
+
},
|
|
8303
|
+
// ---------------------------------------------------------- Help Maggie grow
|
|
8304
|
+
feedback_send: {
|
|
8305
|
+
en: (a) => `Send this to the cmo.so team: ${q(String(a.summary ?? "").slice(0, 200))}`,
|
|
8306
|
+
"zh-Hant": (a) => `\u628A\u9019\u5247\u56DE\u994B\u9001\u7D66 cmo.so \u5718\u968A\uFF1A${q(String(a.summary ?? "").slice(0, 200))}`
|
|
8031
8307
|
},
|
|
8032
8308
|
social_voice_learn: {
|
|
8033
8309
|
en: () => "Learn your LinkedIn voice from this post and save it as your default.",
|
|
@@ -8057,17 +8333,29 @@ var confirmCopy = {
|
|
|
8057
8333
|
en: () => "Make a link for you to connect LinkedIn.",
|
|
8058
8334
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8059
8335
|
},
|
|
8336
|
+
social_approve_all: {
|
|
8337
|
+
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.",
|
|
8338
|
+
"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"
|
|
8339
|
+
},
|
|
8340
|
+
series_create: {
|
|
8341
|
+
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.`,
|
|
8342
|
+
"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`
|
|
8343
|
+
},
|
|
8344
|
+
series_approve: {
|
|
8345
|
+
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.`,
|
|
8346
|
+
"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`
|
|
8347
|
+
},
|
|
8060
8348
|
campaign_create: {
|
|
8061
|
-
en: (a) => `
|
|
8062
|
-
"zh-Hant": (a) => `\
|
|
8349
|
+
en: (a) => `Plan the LinkedIn series ${q(a.name)}.`,
|
|
8350
|
+
"zh-Hant": (a) => `\u898F\u5283 LinkedIn \u7CFB\u5217 ${q(a.name)}\u3002`
|
|
8063
8351
|
},
|
|
8064
8352
|
campaign_pause: {
|
|
8065
|
-
en: (a) => `${a.paused ? "Pause" : "Resume"} the
|
|
8066
|
-
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\
|
|
8353
|
+
en: (a) => `${a.paused ? "Pause" : "Resume"} the series${named(a.name, a.id)}.`,
|
|
8354
|
+
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\u7CFB\u5217${named(a.name, a.id)}\u3002`
|
|
8067
8355
|
},
|
|
8068
8356
|
social_approval_mode_set: {
|
|
8069
|
-
en: (a) => a.mode === "auto" ? "Post LinkedIn
|
|
8070
|
-
"zh-Hant": (a) => a.mode === "auto" ? "LinkedIn \
|
|
8357
|
+
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.",
|
|
8358
|
+
"zh-Hant": (a) => a.mode === "auto" ? "LinkedIn \u8CBC\u6587\u4E0D\u518D\u5148\u554F\u4F60\uFF0C\u5230\u767C\u6587\u6642\u9593\u76F4\u63A5\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002" : "\u6BCF\u7BC7 LinkedIn \u8CBC\u6587\u767C\u51FA\u524D\u5148\u554F\u4F60\u3002"
|
|
8071
8359
|
},
|
|
8072
8360
|
// ---------------------------------------------------------------- reports
|
|
8073
8361
|
gsc_sync: {
|
|
@@ -8172,7 +8460,7 @@ function ws(ctx) {
|
|
|
8172
8460
|
if (!ctx.domain) throw new Error("No website is selected yet. Add one first.");
|
|
8173
8461
|
return domainApi(ctx.api, ctx.domain);
|
|
8174
8462
|
}
|
|
8175
|
-
var clip = (
|
|
8463
|
+
var clip = (text3, n = 280) => typeof text3 === "string" ? text3.length > n ? `${text3.slice(0, n)}\u2026` : text3 : text3 ?? null;
|
|
8176
8464
|
var plain = (html) => String(html ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
8177
8465
|
var untrusted = (value) => ({ untrusted: value });
|
|
8178
8466
|
function upsert(list2, item2, key) {
|
|
@@ -8520,9 +8808,9 @@ var blogTools = [
|
|
|
8520
8808
|
id: { type: "string" },
|
|
8521
8809
|
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8522
8810
|
instruction: { type: "string", description: "What to change, for mode edit" },
|
|
8523
|
-
title: { type: "string", description: "
|
|
8811
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8524
8812
|
},
|
|
8525
|
-
required: ["id", "mode"]
|
|
8813
|
+
required: ["id", "mode", "title"]
|
|
8526
8814
|
},
|
|
8527
8815
|
group: "blog",
|
|
8528
8816
|
need: write,
|
|
@@ -8538,13 +8826,73 @@ var blogTools = [
|
|
|
8538
8826
|
return { saved: true, lost_links: out.lost_links, ...untrusted({ preview: clip(out.markdown, 600) }) };
|
|
8539
8827
|
}
|
|
8540
8828
|
}),
|
|
8829
|
+
tool({
|
|
8830
|
+
name: "draft_rewrite_brief",
|
|
8831
|
+
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.",
|
|
8832
|
+
parameters: {
|
|
8833
|
+
type: "object",
|
|
8834
|
+
properties: {
|
|
8835
|
+
id: { type: "string" },
|
|
8836
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8837
|
+
instruction: { type: "string", description: "What to change, for mode edit" }
|
|
8838
|
+
},
|
|
8839
|
+
required: ["id", "mode"]
|
|
8840
|
+
},
|
|
8841
|
+
group: "blog",
|
|
8842
|
+
need: read,
|
|
8843
|
+
risk: "R0",
|
|
8844
|
+
assistantWrites: true,
|
|
8845
|
+
run: async (ctx, { id, mode, instruction }) => {
|
|
8846
|
+
const p = await ws(ctx).post(id);
|
|
8847
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8848
|
+
if (mode === "edit" && !instruction?.trim()) throw new Error("Say what to change.");
|
|
8849
|
+
return {
|
|
8850
|
+
id: p.id,
|
|
8851
|
+
title: p.title,
|
|
8852
|
+
mode,
|
|
8853
|
+
task: rewriteTask(mode, instruction),
|
|
8854
|
+
links_to_keep: links(p.markdown).length,
|
|
8855
|
+
...untrusted({ markdown: p.markdown })
|
|
8856
|
+
};
|
|
8857
|
+
}
|
|
8858
|
+
}),
|
|
8859
|
+
tool({
|
|
8860
|
+
name: "draft_rewrite_save",
|
|
8861
|
+
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).",
|
|
8862
|
+
parameters: {
|
|
8863
|
+
type: "object",
|
|
8864
|
+
properties: {
|
|
8865
|
+
id: { type: "string" },
|
|
8866
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8867
|
+
markdown: { type: "string", description: "The whole rewritten post" },
|
|
8868
|
+
title: { type: "string", description: "The draft's title, for the approval card" }
|
|
8869
|
+
},
|
|
8870
|
+
required: ["id", "mode", "markdown"]
|
|
8871
|
+
},
|
|
8872
|
+
group: "blog",
|
|
8873
|
+
need: write,
|
|
8874
|
+
risk: "W1",
|
|
8875
|
+
assistantWrites: true,
|
|
8876
|
+
run: async (ctx, { id, mode, markdown }) => {
|
|
8877
|
+
const api2 = ws(ctx);
|
|
8878
|
+
const p = await api2.post(id);
|
|
8879
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8880
|
+
const check2 = checkRewrite(p.markdown, String(markdown ?? ""), mode === "fix" ? "fix" : "edit");
|
|
8881
|
+
if (check2.error) throw new Error(check2.error);
|
|
8882
|
+
await api2.updatePost(id, { markdown });
|
|
8883
|
+
return { saved: true, lost_links: check2.lost };
|
|
8884
|
+
}
|
|
8885
|
+
}),
|
|
8541
8886
|
tool({
|
|
8542
8887
|
name: "draft_delete",
|
|
8543
8888
|
description: "Delete a draft. Its idea is dropped too and the credit it used is not refunded. Ask which one; never delete several at once.",
|
|
8544
8889
|
parameters: {
|
|
8545
8890
|
type: "object",
|
|
8546
|
-
properties: {
|
|
8547
|
-
|
|
8891
|
+
properties: {
|
|
8892
|
+
id: { type: "string" },
|
|
8893
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8894
|
+
},
|
|
8895
|
+
required: ["id", "title"]
|
|
8548
8896
|
},
|
|
8549
8897
|
group: "blog",
|
|
8550
8898
|
need: write,
|
|
@@ -8559,9 +8907,9 @@ var blogTools = [
|
|
|
8559
8907
|
properties: {
|
|
8560
8908
|
id: { type: "string", description: "The planned post's id" },
|
|
8561
8909
|
to: { type: "string", enum: moves },
|
|
8562
|
-
title: { type: "string", description: "
|
|
8910
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8563
8911
|
},
|
|
8564
|
-
required: ["id", "to"]
|
|
8912
|
+
required: ["id", "to", "title"]
|
|
8565
8913
|
},
|
|
8566
8914
|
group: "blog",
|
|
8567
8915
|
need: write,
|
|
@@ -8592,8 +8940,11 @@ var blogTools = [
|
|
|
8592
8940
|
description: "Take a planned post out of the plan; its idea can be restored later.",
|
|
8593
8941
|
parameters: {
|
|
8594
8942
|
type: "object",
|
|
8595
|
-
properties: {
|
|
8596
|
-
|
|
8943
|
+
properties: {
|
|
8944
|
+
id: { type: "string" },
|
|
8945
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8946
|
+
},
|
|
8947
|
+
required: ["id", "title"]
|
|
8597
8948
|
},
|
|
8598
8949
|
group: "blog",
|
|
8599
8950
|
need: write,
|
|
@@ -8650,9 +9001,9 @@ var blogTools = [
|
|
|
8650
9001
|
need: write,
|
|
8651
9002
|
risk: "W2",
|
|
8652
9003
|
gates: ["LLM"],
|
|
8653
|
-
run: async (ctx, { url, text:
|
|
8654
|
-
if (!url && !
|
|
8655
|
-
return ws(ctx).addIdea(url ? { url } : { text:
|
|
9004
|
+
run: async (ctx, { url, text: text3 }) => {
|
|
9005
|
+
if (!url && !text3) throw new Error("Need a link or a sentence.");
|
|
9006
|
+
return ws(ctx).addIdea(url ? { url } : { text: text3 });
|
|
8656
9007
|
}
|
|
8657
9008
|
}),
|
|
8658
9009
|
tool({
|
|
@@ -8670,7 +9021,7 @@ var blogTools = [
|
|
|
8670
9021
|
need: write,
|
|
8671
9022
|
risk: "W2",
|
|
8672
9023
|
gates: ["LLM"],
|
|
8673
|
-
run: async (ctx, { count, brief }) => ws(ctx).generateIdeas(int(count, 1, 10, 3),
|
|
9024
|
+
run: async (ctx, { count, brief: brief2 }) => ws(ctx).generateIdeas(int(count, 1, 10, 3), brief2)
|
|
8674
9025
|
}),
|
|
8675
9026
|
tool({
|
|
8676
9027
|
name: "ideas_import_sitemap",
|
|
@@ -8715,9 +9066,9 @@ var blogTools = [
|
|
|
8715
9066
|
properties: {
|
|
8716
9067
|
id: { type: "string" },
|
|
8717
9068
|
delta: { type: "integer", minimum: -1, maximum: 1 },
|
|
8718
|
-
title: { type: "string", description: "
|
|
9069
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8719
9070
|
},
|
|
8720
|
-
required: ["id", "delta"]
|
|
9071
|
+
required: ["id", "delta", "title"]
|
|
8721
9072
|
},
|
|
8722
9073
|
group: "blog",
|
|
8723
9074
|
need: write,
|
|
@@ -8729,8 +9080,11 @@ var blogTools = [
|
|
|
8729
9080
|
description: "Add a waiting idea to the end of the plan (the plan holds at most 10).",
|
|
8730
9081
|
parameters: {
|
|
8731
9082
|
type: "object",
|
|
8732
|
-
properties: {
|
|
8733
|
-
|
|
9083
|
+
properties: {
|
|
9084
|
+
id: { type: "string" },
|
|
9085
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9086
|
+
},
|
|
9087
|
+
required: ["id", "title"]
|
|
8734
9088
|
},
|
|
8735
9089
|
group: "blog",
|
|
8736
9090
|
need: write,
|
|
@@ -8742,8 +9096,11 @@ var blogTools = [
|
|
|
8742
9096
|
description: "Remove one idea so it won't be written (it can be restored).",
|
|
8743
9097
|
parameters: {
|
|
8744
9098
|
type: "object",
|
|
8745
|
-
properties: {
|
|
8746
|
-
|
|
9099
|
+
properties: {
|
|
9100
|
+
id: { type: "string" },
|
|
9101
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9102
|
+
},
|
|
9103
|
+
required: ["id", "title"]
|
|
8747
9104
|
},
|
|
8748
9105
|
group: "blog",
|
|
8749
9106
|
need: write,
|
|
@@ -8755,8 +9112,11 @@ var blogTools = [
|
|
|
8755
9112
|
description: "Bring back a discarded idea (ideas_list status discarded).",
|
|
8756
9113
|
parameters: {
|
|
8757
9114
|
type: "object",
|
|
8758
|
-
properties: {
|
|
8759
|
-
|
|
9115
|
+
properties: {
|
|
9116
|
+
id: { type: "string" },
|
|
9117
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9118
|
+
},
|
|
9119
|
+
required: ["id", "title"]
|
|
8760
9120
|
},
|
|
8761
9121
|
group: "blog",
|
|
8762
9122
|
need: write,
|
|
@@ -8773,6 +9133,24 @@ var blogTools = [
|
|
|
8773
9133
|
run: async (ctx, { paused }) => paused ? ws(ctx).pauseAutopilot() : ws(ctx).resumeAutopilot()
|
|
8774
9134
|
})
|
|
8775
9135
|
];
|
|
9136
|
+
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.";
|
|
9137
|
+
var LINK = /\]\((\S+?)(?:\s+"[^"]*")?\)/g;
|
|
9138
|
+
var links = (markdown) => [...markdown.matchAll(LINK)].map((m) => m[1]);
|
|
9139
|
+
function rewriteTask(mode, instruction) {
|
|
9140
|
+
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 ?? ""}
|
|
9141
|
+
Change only what that needs; keep the rest as it is. ${KEEP}`;
|
|
9142
|
+
}
|
|
9143
|
+
function checkRewrite(before, after, mode) {
|
|
9144
|
+
const lost = links(before).filter((u) => !after.includes(u)).length;
|
|
9145
|
+
if (after.length < before.length * (mode === "fix" ? 0.8 : 0.3))
|
|
9146
|
+
return {
|
|
9147
|
+
error: "The new version is much shorter than the draft, so nothing was saved. Send the whole post.",
|
|
9148
|
+
lost
|
|
9149
|
+
};
|
|
9150
|
+
if (mode === "fix" && lost)
|
|
9151
|
+
return { error: `The fixed version dropped ${lost} link or image, so nothing was saved. Keep every one.`, lost };
|
|
9152
|
+
return { lost };
|
|
9153
|
+
}
|
|
8776
9154
|
|
|
8777
9155
|
// ../../packages/tools/src/brand.ts
|
|
8778
9156
|
var write2 = { resource: "overview", level: "write" };
|
|
@@ -9095,8 +9473,11 @@ var brandTools = [
|
|
|
9095
9473
|
description: "Delete one of the user's brand voices. Cannot be undone; put its name on the card.",
|
|
9096
9474
|
parameters: {
|
|
9097
9475
|
type: "object",
|
|
9098
|
-
properties: {
|
|
9099
|
-
|
|
9476
|
+
properties: {
|
|
9477
|
+
id: { type: "string" },
|
|
9478
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9479
|
+
},
|
|
9480
|
+
required: ["id", "name"]
|
|
9100
9481
|
},
|
|
9101
9482
|
group: "brand",
|
|
9102
9483
|
need: write2,
|
|
@@ -9123,6 +9504,65 @@ function brandGaps(b) {
|
|
|
9123
9504
|
return { complete: gaps.length === 0, gaps };
|
|
9124
9505
|
}
|
|
9125
9506
|
|
|
9507
|
+
// ../../packages/tools/src/feedback.ts
|
|
9508
|
+
var account = { resource: "account", level: "read" };
|
|
9509
|
+
var TYPES = ["bug", "quality", "idea", "praise", "other"];
|
|
9510
|
+
var text = (v, n) => String(v ?? "").trim().slice(0, n);
|
|
9511
|
+
var feedbackTools = [
|
|
9512
|
+
tool({
|
|
9513
|
+
name: "feedback_send",
|
|
9514
|
+
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.",
|
|
9515
|
+
parameters: {
|
|
9516
|
+
type: "object",
|
|
9517
|
+
properties: {
|
|
9518
|
+
type: {
|
|
9519
|
+
type: "string",
|
|
9520
|
+
enum: TYPES,
|
|
9521
|
+
description: "bug: did something wrong \xB7 quality: works but not good enough \xB7 idea: a new feature \xB7 praise \xB7 other"
|
|
9522
|
+
},
|
|
9523
|
+
summary: { type: "string", description: "One sentence, \u2264200 characters" },
|
|
9524
|
+
details: { type: "string", description: "What happened, \u22644000 characters" },
|
|
9525
|
+
expected: { type: "string", description: "For a bug: what the user expected" },
|
|
9526
|
+
actual: { type: "string", description: "For a bug: what happened instead" }
|
|
9527
|
+
},
|
|
9528
|
+
required: ["type", "summary"]
|
|
9529
|
+
},
|
|
9530
|
+
group: "account",
|
|
9531
|
+
need: account,
|
|
9532
|
+
risk: "W1",
|
|
9533
|
+
run: async (ctx, a) => {
|
|
9534
|
+
const f = await ctx.api.feedback.send({
|
|
9535
|
+
type: TYPES.find((t) => t === a.type) ?? "other",
|
|
9536
|
+
summary: text(a.summary, 200),
|
|
9537
|
+
...a.details ? { details: text(a.details, 4e3) } : {},
|
|
9538
|
+
...a.expected ? { expected: text(a.expected, 1e3) } : {},
|
|
9539
|
+
...a.actual ? { actual: text(a.actual, 1e3) } : {},
|
|
9540
|
+
domain: ctx.domain,
|
|
9541
|
+
source: ctx.client ?? "chat"
|
|
9542
|
+
});
|
|
9543
|
+
return { sent: true, id: f.id, status: f.status };
|
|
9544
|
+
}
|
|
9545
|
+
}),
|
|
9546
|
+
tool({
|
|
9547
|
+
name: "feedback_list",
|
|
9548
|
+
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.",
|
|
9549
|
+
parameters: none,
|
|
9550
|
+
group: "account",
|
|
9551
|
+
need: account,
|
|
9552
|
+
risk: "R0",
|
|
9553
|
+
run: async (ctx) => ({
|
|
9554
|
+
items: (await ctx.api.feedback.mine()).items.slice(0, 20).map((f) => ({
|
|
9555
|
+
type: f.type,
|
|
9556
|
+
summary: clip(f.summary, 200),
|
|
9557
|
+
status: f.status,
|
|
9558
|
+
reply: clip(f.reply, 600),
|
|
9559
|
+
new_reply: f.unread,
|
|
9560
|
+
sent: f.created_at
|
|
9561
|
+
}))
|
|
9562
|
+
})
|
|
9563
|
+
})
|
|
9564
|
+
];
|
|
9565
|
+
|
|
9126
9566
|
// ../../packages/tools/src/insights.ts
|
|
9127
9567
|
var read2 = { resource: "report", level: "read" };
|
|
9128
9568
|
var write3 = { resource: "report", level: "write" };
|
|
@@ -9214,14 +9654,14 @@ var insightTools = [
|
|
|
9214
9654
|
];
|
|
9215
9655
|
|
|
9216
9656
|
// ../../packages/tools/src/me.ts
|
|
9217
|
-
var
|
|
9657
|
+
var account2 = { resource: "account", level: "read" };
|
|
9218
9658
|
var meTools = [
|
|
9219
9659
|
tool({
|
|
9220
9660
|
name: "api_keys_list",
|
|
9221
9661
|
description: "The user's API keys made on ai.cmo.so: name, first characters, read or read-write, last used. Never the key itself; making or revoking keys is done on ai.cmo.so (dashboard_link api_access).",
|
|
9222
9662
|
parameters: none,
|
|
9223
9663
|
group: "account",
|
|
9224
|
-
need:
|
|
9664
|
+
need: account2,
|
|
9225
9665
|
risk: "R0",
|
|
9226
9666
|
run: async ({ api: api2 }) => (await api2.account.keys()).items
|
|
9227
9667
|
}),
|
|
@@ -9230,7 +9670,7 @@ var meTools = [
|
|
|
9230
9670
|
description: "Apps connected to the user's account (ChatGPT, Claude, the cmo.so app\u2026), what they may do and when they were last used. `current` is the one in this conversation.",
|
|
9231
9671
|
parameters: none,
|
|
9232
9672
|
group: "account",
|
|
9233
|
-
need:
|
|
9673
|
+
need: account2,
|
|
9234
9674
|
risk: "R0",
|
|
9235
9675
|
run: async ({ api: api2 }) => (await api2.account.apps()).items.map((a) => ({
|
|
9236
9676
|
grant_id: a.grant_id,
|
|
@@ -9253,7 +9693,7 @@ var meTools = [
|
|
|
9253
9693
|
required: ["grant_id", "app"]
|
|
9254
9694
|
},
|
|
9255
9695
|
group: "account",
|
|
9256
|
-
need:
|
|
9696
|
+
need: account2,
|
|
9257
9697
|
risk: "W3",
|
|
9258
9698
|
run: async ({ api: api2 }, { grant_id }) => api2.account.revokeApp(String(grant_id))
|
|
9259
9699
|
}),
|
|
@@ -9262,7 +9702,7 @@ var meTools = [
|
|
|
9262
9702
|
description: "The user's latest invoices across the subscriptions they pay for: date, amount, status and a link to view or download each.",
|
|
9263
9703
|
parameters: none,
|
|
9264
9704
|
group: "billing",
|
|
9265
|
-
need:
|
|
9705
|
+
need: account2,
|
|
9266
9706
|
risk: "R0",
|
|
9267
9707
|
run: async ({ api: api2 }) => (await api2.account.invoices()).items.slice(0, 15).map((i) => ({
|
|
9268
9708
|
website: i.domain,
|
|
@@ -9278,7 +9718,7 @@ var meTools = [
|
|
|
9278
9718
|
description: "Change the user's display name (the only profile field Maggie changes; email and password are on ai.cmo.so).",
|
|
9279
9719
|
parameters: { type: "object", properties: { display_name: { type: "string" } }, required: ["display_name"] },
|
|
9280
9720
|
group: "account",
|
|
9281
|
-
need:
|
|
9721
|
+
need: account2,
|
|
9282
9722
|
risk: "W1",
|
|
9283
9723
|
idempotent: true,
|
|
9284
9724
|
run: async ({ api: api2 }, { display_name }) => api2.account.rename(String(display_name).slice(0, 80))
|
|
@@ -9288,7 +9728,7 @@ var meTools = [
|
|
|
9288
9728
|
description: "Which app notifications the user gets: LinkedIn posts to review (social_review), LinkedIn posts that failed (social_failed), new blog posts published (published).",
|
|
9289
9729
|
parameters: none,
|
|
9290
9730
|
group: "account",
|
|
9291
|
-
need:
|
|
9731
|
+
need: account2,
|
|
9292
9732
|
risk: "R0",
|
|
9293
9733
|
run: async ({ api: api2 }) => api2.account.notifications()
|
|
9294
9734
|
}),
|
|
@@ -9304,13 +9744,99 @@ var meTools = [
|
|
|
9304
9744
|
}
|
|
9305
9745
|
},
|
|
9306
9746
|
group: "account",
|
|
9307
|
-
need:
|
|
9747
|
+
need: account2,
|
|
9308
9748
|
risk: "W1",
|
|
9309
9749
|
idempotent: true,
|
|
9310
9750
|
run: async ({ api: api2 }, prefs) => api2.account.setNotifications(prefs)
|
|
9311
9751
|
})
|
|
9312
9752
|
];
|
|
9313
9753
|
|
|
9754
|
+
// ../../packages/tools/src/memory.ts
|
|
9755
|
+
var own = { resource: "overview", level: "read" };
|
|
9756
|
+
var SECTIONS = ["prefer", "always", "never"];
|
|
9757
|
+
var KINDS = ["tone", "length", "structure", "format", "wording", "topic", "image", "cta", "approval", "other"];
|
|
9758
|
+
var brief = (m) => ({
|
|
9759
|
+
enabled: m.enabled,
|
|
9760
|
+
channels: Object.fromEntries(
|
|
9761
|
+
MEMORY_CHANNELS.map((ch) => [
|
|
9762
|
+
ch,
|
|
9763
|
+
{
|
|
9764
|
+
summary: m.channels[ch]?.summary ?? "",
|
|
9765
|
+
items: (m.channels[ch]?.items ?? []).map((i) => ({
|
|
9766
|
+
id: i.id,
|
|
9767
|
+
section: i.section,
|
|
9768
|
+
kind: i.kind,
|
|
9769
|
+
text: clip(i.text, 200),
|
|
9770
|
+
source: i.source
|
|
9771
|
+
}))
|
|
9772
|
+
}
|
|
9773
|
+
])
|
|
9774
|
+
)
|
|
9775
|
+
});
|
|
9776
|
+
var memoryTools = [
|
|
9777
|
+
tool({
|
|
9778
|
+
name: "memory_get",
|
|
9779
|
+
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.",
|
|
9780
|
+
parameters: none,
|
|
9781
|
+
group: "memory",
|
|
9782
|
+
need: own,
|
|
9783
|
+
risk: "R0",
|
|
9784
|
+
run: async (ctx) => brief(await ws(ctx).maggieMemory())
|
|
9785
|
+
}),
|
|
9786
|
+
tool({
|
|
9787
|
+
name: "memory_remember",
|
|
9788
|
+
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.',
|
|
9789
|
+
parameters: {
|
|
9790
|
+
type: "object",
|
|
9791
|
+
properties: {
|
|
9792
|
+
channel: { type: "string", enum: [...MEMORY_CHANNELS] },
|
|
9793
|
+
section: {
|
|
9794
|
+
type: "string",
|
|
9795
|
+
enum: SECTIONS,
|
|
9796
|
+
description: "prefer: a leaning \xB7 always: do it every time \xB7 never: don't do or use it"
|
|
9797
|
+
},
|
|
9798
|
+
text: { type: "string", description: "The whole rule, \u2264160 characters, e.g. \u201CNever use emojis.\u201D" },
|
|
9799
|
+
kind: { type: "string", enum: KINDS }
|
|
9800
|
+
},
|
|
9801
|
+
required: ["channel", "section", "text"]
|
|
9802
|
+
},
|
|
9803
|
+
group: "memory",
|
|
9804
|
+
need: own,
|
|
9805
|
+
risk: "W1",
|
|
9806
|
+
run: async (ctx, { channel, section, text: text3, kind }) => {
|
|
9807
|
+
const ch = MEMORY_CHANNELS.find((c) => c === channel) ?? "general";
|
|
9808
|
+
const sec = SECTIONS.includes(String(section)) ? section : "prefer";
|
|
9809
|
+
const m = await ws(ctx).addMaggieMemory({
|
|
9810
|
+
channel: ch,
|
|
9811
|
+
section: sec,
|
|
9812
|
+
text: String(text3 ?? "").trim().slice(0, 160),
|
|
9813
|
+
...kind && KINDS.includes(kind) ? { kind } : {}
|
|
9814
|
+
});
|
|
9815
|
+
return { saved: true, enabled: m.enabled };
|
|
9816
|
+
}
|
|
9817
|
+
}),
|
|
9818
|
+
tool({
|
|
9819
|
+
name: "memory_forget",
|
|
9820
|
+
description: "Remove one item from the user's Maggie Memory (ids from memory_get).",
|
|
9821
|
+
parameters: {
|
|
9822
|
+
type: "object",
|
|
9823
|
+
properties: {
|
|
9824
|
+
id: { type: "string" },
|
|
9825
|
+
text_for_card: { type: "string", description: "The item's text, for the card" }
|
|
9826
|
+
},
|
|
9827
|
+
required: ["id"]
|
|
9828
|
+
},
|
|
9829
|
+
group: "memory",
|
|
9830
|
+
need: own,
|
|
9831
|
+
risk: "W1",
|
|
9832
|
+
run: async (ctx, { id }) => {
|
|
9833
|
+
await ws(ctx).deleteMaggieMemory(String(id));
|
|
9834
|
+
return { forgotten: true };
|
|
9835
|
+
}
|
|
9836
|
+
})
|
|
9837
|
+
];
|
|
9838
|
+
var MEMORY_TOOLS = new Set(memoryTools.map((t) => t.name));
|
|
9839
|
+
|
|
9314
9840
|
// ../../packages/tools/src/publishing.ts
|
|
9315
9841
|
var read3 = { resource: "autopilot", level: "read" };
|
|
9316
9842
|
var write4 = { resource: "autopilot", level: "write" };
|
|
@@ -9386,7 +9912,7 @@ var publishingTools = [
|
|
|
9386
9912
|
}),
|
|
9387
9913
|
tool({
|
|
9388
9914
|
name: "rewrite_request",
|
|
9389
|
-
description: "Rewrite one
|
|
9915
|
+
description: "Rewrite one post or page on the user's website and update it there (same URL). Works for any post on their WordPress, including its blog subdomain (blog.example.com for example.com), whether CMO.so wrote it or not: it is found by its URL. Uses one post credit. Optional instructions (what to change, e.g. 'add 5 FAQs at the end') and target keywords.",
|
|
9390
9916
|
parameters: {
|
|
9391
9917
|
type: "object",
|
|
9392
9918
|
properties: {
|
|
@@ -9717,8 +10243,19 @@ var reportTools = [
|
|
|
9717
10243
|
var read5 = { resource: "autopilot", level: "read" };
|
|
9718
10244
|
var write5 = { resource: "autopilot", level: "write" };
|
|
9719
10245
|
var moves2 = ["up", "down", "top"];
|
|
10246
|
+
var CHANNEL_KEYS = ["linkedin", "facebook"];
|
|
10247
|
+
var channelsParam = {
|
|
10248
|
+
type: "array",
|
|
10249
|
+
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10250
|
+
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."
|
|
10251
|
+
};
|
|
10252
|
+
var channelsOf = (value) => {
|
|
10253
|
+
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
10254
|
+
return list2.length ? list2 : void 0;
|
|
10255
|
+
};
|
|
9720
10256
|
var item = (i) => ({
|
|
9721
10257
|
id: i.id,
|
|
10258
|
+
channel: i.provider || "linkedin",
|
|
9722
10259
|
status: i.status,
|
|
9723
10260
|
eta: i.eta ?? null,
|
|
9724
10261
|
scheduled_at: i.scheduled_at,
|
|
@@ -9731,7 +10268,7 @@ var item = (i) => ({
|
|
|
9731
10268
|
var socialTools = [
|
|
9732
10269
|
tool({
|
|
9733
10270
|
name: "social_list",
|
|
9734
|
-
description: "The user's LinkedIn:
|
|
10271
|
+
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.",
|
|
9735
10272
|
parameters: none,
|
|
9736
10273
|
group: "social",
|
|
9737
10274
|
need: read5,
|
|
@@ -9740,6 +10277,14 @@ var socialTools = [
|
|
|
9740
10277
|
const s = await ws(ctx).social();
|
|
9741
10278
|
return {
|
|
9742
10279
|
connected: s.connected,
|
|
10280
|
+
channels: (s.channels ?? []).map((c) => ({
|
|
10281
|
+
channel: c.key,
|
|
10282
|
+
connected: c.connected,
|
|
10283
|
+
account: c.account?.display_name ?? null,
|
|
10284
|
+
paused: c.paused,
|
|
10285
|
+
needs_page: c.needs_page,
|
|
10286
|
+
per_week: c.frequency?.per_week ?? null
|
|
10287
|
+
})),
|
|
9743
10288
|
account: s.linkedin ? { name: s.linkedin.display_name, status: s.linkedin.status } : null,
|
|
9744
10289
|
posting_paused: s.posting_paused,
|
|
9745
10290
|
approval_mode: s.approval_mode,
|
|
@@ -9761,7 +10306,7 @@ var socialTools = [
|
|
|
9761
10306
|
}),
|
|
9762
10307
|
tool({
|
|
9763
10308
|
name: "social_update_copy",
|
|
9764
|
-
description: "Change the text of a
|
|
10309
|
+
description: "Change the text of a social post that hasn't gone out yet (a draft or queued post).",
|
|
9765
10310
|
parameters: {
|
|
9766
10311
|
type: "object",
|
|
9767
10312
|
properties: { id: { type: "integer" }, text: { type: "string", description: "The whole new post text" } },
|
|
@@ -9771,8 +10316,8 @@ var socialTools = [
|
|
|
9771
10316
|
need: write5,
|
|
9772
10317
|
risk: "W1",
|
|
9773
10318
|
gates: ["VER"],
|
|
9774
|
-
run: async (ctx, { id, text:
|
|
9775
|
-
const out = await ws(ctx).updateSocial(Number(id), String(
|
|
10319
|
+
run: async (ctx, { id, text: text3 }) => {
|
|
10320
|
+
const out = await ws(ctx).updateSocial(Number(id), String(text3 ?? ""));
|
|
9776
10321
|
return { id: out.id, status: out.status };
|
|
9777
10322
|
}
|
|
9778
10323
|
}),
|
|
@@ -9824,6 +10369,56 @@ var socialTools = [
|
|
|
9824
10369
|
gates: ["VER"],
|
|
9825
10370
|
run: async (ctx, { id }) => ws(ctx).restoreSocial(Number(id))
|
|
9826
10371
|
}),
|
|
10372
|
+
tool({
|
|
10373
|
+
name: "social_image_new",
|
|
10374
|
+
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.",
|
|
10375
|
+
parameters: {
|
|
10376
|
+
type: "object",
|
|
10377
|
+
properties: {
|
|
10378
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10379
|
+
replace: { type: "boolean", description: "Stop the picture being made for another post first" },
|
|
10380
|
+
instruction: {
|
|
10381
|
+
type: "string",
|
|
10382
|
+
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."
|
|
10383
|
+
}
|
|
10384
|
+
},
|
|
10385
|
+
required: ["id"]
|
|
10386
|
+
},
|
|
10387
|
+
group: "social",
|
|
10388
|
+
need: write5,
|
|
10389
|
+
risk: "W2",
|
|
10390
|
+
gates: ["VER", "CR"],
|
|
10391
|
+
run: async (ctx, { id, replace, instruction }) => {
|
|
10392
|
+
const out = await ws(ctx).regenerateSocialImage(Number(id), {
|
|
10393
|
+
replace: replace === true,
|
|
10394
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10395
|
+
});
|
|
10396
|
+
return {
|
|
10397
|
+
id: out.id,
|
|
10398
|
+
image_job: out.image_job,
|
|
10399
|
+
note: "Making it in the background; it shows on the post when ready."
|
|
10400
|
+
};
|
|
10401
|
+
}
|
|
10402
|
+
}),
|
|
10403
|
+
tool({
|
|
10404
|
+
name: "social_image_status",
|
|
10405
|
+
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).",
|
|
10406
|
+
parameters: none,
|
|
10407
|
+
group: "social",
|
|
10408
|
+
need: read5,
|
|
10409
|
+
risk: "R0",
|
|
10410
|
+
run: async (ctx) => ({ image_job: (await ws(ctx).social()).image_job ?? null })
|
|
10411
|
+
}),
|
|
10412
|
+
tool({
|
|
10413
|
+
name: "social_image_stop",
|
|
10414
|
+
description: "Stop the LinkedIn picture being made on this website; the post keeps the picture it had.",
|
|
10415
|
+
parameters: none,
|
|
10416
|
+
group: "social",
|
|
10417
|
+
need: write5,
|
|
10418
|
+
risk: "W1",
|
|
10419
|
+
gates: ["VER"],
|
|
10420
|
+
run: async (ctx) => ws(ctx).stopSocialImage()
|
|
10421
|
+
}),
|
|
9827
10422
|
tool({
|
|
9828
10423
|
name: "social_move",
|
|
9829
10424
|
description: "Move a queued LinkedIn post up, down or to the front among the user's own posts.",
|
|
@@ -9840,16 +10435,24 @@ var socialTools = [
|
|
|
9840
10435
|
}),
|
|
9841
10436
|
tool({
|
|
9842
10437
|
name: "social_set_paused",
|
|
9843
|
-
description: "Pause or resume
|
|
9844
|
-
parameters: {
|
|
10438
|
+
description: "Pause or resume the user's social posting: one channel (linkedin or facebook), or every channel when no channel is given.",
|
|
10439
|
+
parameters: {
|
|
10440
|
+
type: "object",
|
|
10441
|
+
properties: { paused: { type: "boolean" }, channel: { type: "string", enum: CHANNEL_KEYS } },
|
|
10442
|
+
required: ["paused"]
|
|
10443
|
+
},
|
|
9845
10444
|
group: "social",
|
|
9846
10445
|
need: write5,
|
|
9847
10446
|
risk: "W1",
|
|
9848
|
-
run: async (ctx, { paused }) =>
|
|
10447
|
+
run: async (ctx, { paused, channel }) => {
|
|
10448
|
+
const key = channelsOf(channel ? [channel] : [])?.[0];
|
|
10449
|
+
if (key) return ws(ctx).setChannelPaused(key, Boolean(paused));
|
|
10450
|
+
return paused ? ws(ctx).pauseSocial() : ws(ctx).resumeSocial();
|
|
10451
|
+
}
|
|
9849
10452
|
}),
|
|
9850
10453
|
tool({
|
|
9851
10454
|
name: "social_sources_list",
|
|
9852
|
-
description: "Published articles a
|
|
10455
|
+
description: "Published articles a social post can be made from (optionally matching a search).",
|
|
9853
10456
|
parameters: { type: "object", properties: { search: { type: "string" } } },
|
|
9854
10457
|
group: "social",
|
|
9855
10458
|
need: read5,
|
|
@@ -9858,29 +10461,34 @@ var socialTools = [
|
|
|
9858
10461
|
}),
|
|
9859
10462
|
tool({
|
|
9860
10463
|
name: "social_from_article",
|
|
9861
|
-
description: "Maggie writes a
|
|
10464
|
+
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).",
|
|
9862
10465
|
parameters: {
|
|
9863
10466
|
type: "object",
|
|
9864
|
-
properties: {
|
|
9865
|
-
|
|
10467
|
+
properties: {
|
|
10468
|
+
source_id: { type: "string" },
|
|
10469
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10470
|
+
channels: channelsParam
|
|
10471
|
+
},
|
|
10472
|
+
required: ["source_id", "title"]
|
|
9866
10473
|
},
|
|
9867
10474
|
group: "social",
|
|
9868
10475
|
need: write5,
|
|
9869
10476
|
risk: "W2",
|
|
9870
10477
|
gates: ["VER", "LI", "LLM"],
|
|
9871
|
-
run: async (ctx, { source_id }) => {
|
|
9872
|
-
const out = await ws(ctx).socialFromPost(String(source_id));
|
|
9873
|
-
return { id: out.id, status: out.status, text: clip(out.social_copy, 300) };
|
|
10478
|
+
run: async (ctx, { source_id, channels }) => {
|
|
10479
|
+
const out = await ws(ctx).socialFromPost(String(source_id), channelsOf(channels));
|
|
10480
|
+
return { id: out.id, status: out.status, queued: Boolean(out.queued), text: clip(out.social_copy, 300) };
|
|
9874
10481
|
}
|
|
9875
10482
|
}),
|
|
9876
10483
|
tool({
|
|
9877
10484
|
name: "social_plan_days",
|
|
9878
|
-
description: "Maggie writes
|
|
10485
|
+
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).",
|
|
9879
10486
|
parameters: {
|
|
9880
10487
|
type: "object",
|
|
9881
10488
|
properties: {
|
|
9882
10489
|
days: { type: "integer", minimum: 1, maximum: 7 },
|
|
9883
|
-
timezone: { type: "string", description: "IANA timezone, if known" }
|
|
10490
|
+
timezone: { type: "string", description: "IANA timezone, if known" },
|
|
10491
|
+
channels: channelsParam
|
|
9884
10492
|
},
|
|
9885
10493
|
required: ["days"]
|
|
9886
10494
|
},
|
|
@@ -9888,9 +10496,9 @@ var socialTools = [
|
|
|
9888
10496
|
need: write5,
|
|
9889
10497
|
risk: "W2",
|
|
9890
10498
|
gates: ["VER", "LI", "LLM"],
|
|
9891
|
-
run: async (ctx, { days, timezone }) => {
|
|
9892
|
-
const out = await ws(ctx).socialPlan(int(days, 1, 7, 3), timezone || "UTC");
|
|
9893
|
-
return { written: out.items.length, items: out.items.map(item) };
|
|
10499
|
+
run: async (ctx, { days, timezone, channels }) => {
|
|
10500
|
+
const out = await ws(ctx).socialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels));
|
|
10501
|
+
return { written: out.items.length, queued: Boolean(out.queued), items: out.items.map(item) };
|
|
9894
10502
|
}
|
|
9895
10503
|
}),
|
|
9896
10504
|
tool({
|
|
@@ -9919,21 +10527,21 @@ var socialTools = [
|
|
|
9919
10527
|
}),
|
|
9920
10528
|
tool({
|
|
9921
10529
|
name: "social_idea_add",
|
|
9922
|
-
description: "Add an idea for a
|
|
10530
|
+
description: "Add an idea for a social post (not written yet; writing it later uses a credit per channel).",
|
|
9923
10531
|
parameters: {
|
|
9924
10532
|
type: "object",
|
|
9925
|
-
properties: { title: { type: "string" }, summary: { type: "string" } },
|
|
10533
|
+
properties: { title: { type: "string" }, summary: { type: "string" }, channels: channelsParam },
|
|
9926
10534
|
required: ["title"]
|
|
9927
10535
|
},
|
|
9928
10536
|
group: "social",
|
|
9929
10537
|
need: write5,
|
|
9930
10538
|
risk: "W1",
|
|
9931
10539
|
gates: ["VER", "LI"],
|
|
9932
|
-
run: async (ctx, idea) => ws(ctx).addSocialIdea(idea)
|
|
10540
|
+
run: async (ctx, { channels, ...idea }) => ws(ctx).addSocialIdea({ ...idea, channels: channelsOf(channels) })
|
|
9933
10541
|
}),
|
|
9934
10542
|
tool({
|
|
9935
10543
|
name: "social_idea_update",
|
|
9936
|
-
description: "Change a
|
|
10544
|
+
description: "Change a social idea's title or summary.",
|
|
9937
10545
|
parameters: {
|
|
9938
10546
|
type: "object",
|
|
9939
10547
|
properties: { id: { type: "integer" }, title: { type: "string" }, summary: { type: "string" } },
|
|
@@ -9947,11 +10555,14 @@ var socialTools = [
|
|
|
9947
10555
|
}),
|
|
9948
10556
|
tool({
|
|
9949
10557
|
name: "social_idea_delete",
|
|
9950
|
-
description: "Delete a
|
|
10558
|
+
description: "Delete a social idea that hasn't been written.",
|
|
9951
10559
|
parameters: {
|
|
9952
10560
|
type: "object",
|
|
9953
|
-
properties: {
|
|
9954
|
-
|
|
10561
|
+
properties: {
|
|
10562
|
+
id: { type: "integer" },
|
|
10563
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10564
|
+
},
|
|
10565
|
+
required: ["id", "title"]
|
|
9955
10566
|
},
|
|
9956
10567
|
group: "social",
|
|
9957
10568
|
need: write5,
|
|
@@ -9961,21 +10572,79 @@ var socialTools = [
|
|
|
9961
10572
|
}),
|
|
9962
10573
|
tool({
|
|
9963
10574
|
name: "social_idea_write",
|
|
9964
|
-
description: "Maggie writes
|
|
10575
|
+
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.",
|
|
9965
10576
|
parameters: {
|
|
9966
10577
|
type: "object",
|
|
9967
|
-
properties: {
|
|
9968
|
-
|
|
10578
|
+
properties: {
|
|
10579
|
+
id: { type: "integer" },
|
|
10580
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10581
|
+
channels: channelsParam
|
|
10582
|
+
},
|
|
10583
|
+
required: ["id", "title"]
|
|
9969
10584
|
},
|
|
9970
10585
|
group: "social",
|
|
9971
10586
|
need: write5,
|
|
9972
10587
|
risk: "W2",
|
|
9973
10588
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
9974
|
-
run: async (ctx, { id }) => ws(ctx).writeSocialIdea(Number(id))
|
|
10589
|
+
run: async (ctx, { id, channels }) => ws(ctx).writeSocialIdea(Number(id), void 0, channelsOf(channels))
|
|
10590
|
+
}),
|
|
10591
|
+
tool({
|
|
10592
|
+
name: "social_idea_brief",
|
|
10593
|
+
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.",
|
|
10594
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10595
|
+
group: "social",
|
|
10596
|
+
need: read5,
|
|
10597
|
+
risk: "R0",
|
|
10598
|
+
assistantWrites: true,
|
|
10599
|
+
run: async (ctx, { id }) => {
|
|
10600
|
+
const b = await ws(ctx).socialIdeaBrief(Number(id));
|
|
10601
|
+
return {
|
|
10602
|
+
id: Number(id),
|
|
10603
|
+
channels: b.platforms,
|
|
10604
|
+
limits: b.limits,
|
|
10605
|
+
limit: b.limits.linkedin ?? null,
|
|
10606
|
+
json_schema: b.json_schema,
|
|
10607
|
+
// Built by CMO.so from the user's idea, brand voice and template.
|
|
10608
|
+
brief: b.prompt
|
|
10609
|
+
};
|
|
10610
|
+
}
|
|
10611
|
+
}),
|
|
10612
|
+
tool({
|
|
10613
|
+
name: "social_idea_save",
|
|
10614
|
+
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.",
|
|
10615
|
+
parameters: {
|
|
10616
|
+
type: "object",
|
|
10617
|
+
properties: {
|
|
10618
|
+
id: { type: "integer" },
|
|
10619
|
+
posts: {
|
|
10620
|
+
type: "object",
|
|
10621
|
+
description: "One body per channel from the brief (only the channels you wrote)",
|
|
10622
|
+
properties: { linkedin: { type: "string" }, facebook: { type: "string" } }
|
|
10623
|
+
},
|
|
10624
|
+
text: { type: "string", description: "Only a LinkedIn post: its body (older use; prefer posts)" },
|
|
10625
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10626
|
+
},
|
|
10627
|
+
required: ["id", "title"]
|
|
10628
|
+
},
|
|
10629
|
+
group: "social",
|
|
10630
|
+
need: write5,
|
|
10631
|
+
risk: "W2",
|
|
10632
|
+
gates: ["VER", "LI", "CR"],
|
|
10633
|
+
assistantWrites: true,
|
|
10634
|
+
run: async (ctx, { id, text: text3, posts }) => {
|
|
10635
|
+
const written = {};
|
|
10636
|
+
for (const [channel, body] of Object.entries(posts ?? {})) {
|
|
10637
|
+
if (CHANNEL_KEYS.includes(channel) && String(body ?? "").trim())
|
|
10638
|
+
written[channel] = { body: String(body).trim() };
|
|
10639
|
+
}
|
|
10640
|
+
if (!Object.keys(written).length && String(text3 ?? "").trim()) written.linkedin = { body: String(text3).trim() };
|
|
10641
|
+
if (!Object.keys(written).length) throw new Error("Write the posts first.");
|
|
10642
|
+
return ws(ctx).writeSocialIdea(Number(id), written);
|
|
10643
|
+
}
|
|
9975
10644
|
}),
|
|
9976
10645
|
tool({
|
|
9977
10646
|
name: "campaign_list",
|
|
9978
|
-
description: "The user's
|
|
10647
|
+
description: "The user's social campaigns: ideas written so far, whether ready or started, start day and posts a day.",
|
|
9979
10648
|
parameters: none,
|
|
9980
10649
|
group: "social",
|
|
9981
10650
|
need: read5,
|
|
@@ -9984,16 +10653,16 @@ var socialTools = [
|
|
|
9984
10653
|
}),
|
|
9985
10654
|
tool({
|
|
9986
10655
|
name: "campaign_start",
|
|
9987
|
-
description: "Start a ready campaign (every idea written): its posts go out on
|
|
10656
|
+
description: "Start a ready campaign (every idea written): its posts go out on its channels in order from start_date (default today), at most daily_limit a day.",
|
|
9988
10657
|
parameters: {
|
|
9989
10658
|
type: "object",
|
|
9990
10659
|
properties: {
|
|
9991
10660
|
id: { type: "integer" },
|
|
9992
10661
|
start_date: { type: "string", description: "YYYY-MM-DD" },
|
|
9993
10662
|
daily_limit: { type: "integer", minimum: 1, maximum: 5 },
|
|
9994
|
-
name: { type: "string", description: "
|
|
10663
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9995
10664
|
},
|
|
9996
|
-
required: ["id"]
|
|
10665
|
+
required: ["id", "name"]
|
|
9997
10666
|
},
|
|
9998
10667
|
group: "social",
|
|
9999
10668
|
need: write5,
|
|
@@ -10041,13 +10710,65 @@ var socialTools = [
|
|
|
10041
10710
|
gates: ["VER"],
|
|
10042
10711
|
run: async (ctx, c) => ws(ctx).createCampaign(c)
|
|
10043
10712
|
}),
|
|
10713
|
+
tool({
|
|
10714
|
+
name: "social_approve_all",
|
|
10715
|
+
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.",
|
|
10716
|
+
parameters: { type: "object", properties: { channel: { type: "string", enum: CHANNEL_KEYS } } },
|
|
10717
|
+
group: "social",
|
|
10718
|
+
need: write5,
|
|
10719
|
+
risk: "W3",
|
|
10720
|
+
gates: ["VER", "LI"],
|
|
10721
|
+
run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
|
|
10722
|
+
}),
|
|
10723
|
+
tool({
|
|
10724
|
+
name: "series_create",
|
|
10725
|
+
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.",
|
|
10726
|
+
parameters: {
|
|
10727
|
+
type: "object",
|
|
10728
|
+
properties: {
|
|
10729
|
+
brief: { type: "string", description: "What the series is about, in the user's words" },
|
|
10730
|
+
count: { type: "integer", minimum: 1, maximum: 7, description: "How many posts (3 if not said)" },
|
|
10731
|
+
channels: channelsParam
|
|
10732
|
+
},
|
|
10733
|
+
required: ["brief"]
|
|
10734
|
+
},
|
|
10735
|
+
group: "social",
|
|
10736
|
+
need: write5,
|
|
10737
|
+
risk: "W2",
|
|
10738
|
+
gates: ["VER", "LI", "LLM"],
|
|
10739
|
+
run: async (ctx, { brief: brief2, count, channels }) => ws(ctx).createSocialSeries({
|
|
10740
|
+
brief: String(brief2 ?? ""),
|
|
10741
|
+
count: Math.max(1, Math.min(7, Number(count) || 3)),
|
|
10742
|
+
channels: channelsOf(channels)
|
|
10743
|
+
})
|
|
10744
|
+
}),
|
|
10745
|
+
tool({
|
|
10746
|
+
name: "series_approve",
|
|
10747
|
+
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).",
|
|
10748
|
+
parameters: {
|
|
10749
|
+
type: "object",
|
|
10750
|
+
properties: {
|
|
10751
|
+
id: { type: "integer" },
|
|
10752
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10753
|
+
},
|
|
10754
|
+
required: ["id"]
|
|
10755
|
+
},
|
|
10756
|
+
group: "social",
|
|
10757
|
+
need: write5,
|
|
10758
|
+
risk: "W3",
|
|
10759
|
+
gates: ["VER", "LI"],
|
|
10760
|
+
run: async (ctx, { id }) => ws(ctx).approveSocialSeries(Number(id))
|
|
10761
|
+
}),
|
|
10044
10762
|
tool({
|
|
10045
10763
|
name: "campaign_plan",
|
|
10046
10764
|
description: "Maggie plans a campaign's LinkedIn post ideas with AI (uses 1 credit).",
|
|
10047
10765
|
parameters: {
|
|
10048
10766
|
type: "object",
|
|
10049
|
-
properties: {
|
|
10050
|
-
|
|
10767
|
+
properties: {
|
|
10768
|
+
id: { type: "integer" },
|
|
10769
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10770
|
+
},
|
|
10771
|
+
required: ["id", "name"]
|
|
10051
10772
|
},
|
|
10052
10773
|
group: "social",
|
|
10053
10774
|
need: write5,
|
|
@@ -10066,9 +10787,9 @@ var socialTools = [
|
|
|
10066
10787
|
properties: {
|
|
10067
10788
|
id: { type: "integer" },
|
|
10068
10789
|
paused: { type: "boolean" },
|
|
10069
|
-
name: { type: "string", description: "
|
|
10790
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10070
10791
|
},
|
|
10071
|
-
required: ["id", "paused"]
|
|
10792
|
+
required: ["id", "paused", "name"]
|
|
10072
10793
|
},
|
|
10073
10794
|
group: "social",
|
|
10074
10795
|
need: write5,
|
|
@@ -10232,6 +10953,19 @@ var socialTools = [
|
|
|
10232
10953
|
const out = await ws(ctx).connectLinkedIn("close");
|
|
10233
10954
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10234
10955
|
}
|
|
10956
|
+
}),
|
|
10957
|
+
tool({
|
|
10958
|
+
name: "facebook_connect_link",
|
|
10959
|
+
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.",
|
|
10960
|
+
parameters: none,
|
|
10961
|
+
group: "social",
|
|
10962
|
+
need: write5,
|
|
10963
|
+
risk: "W1",
|
|
10964
|
+
gates: ["VER"],
|
|
10965
|
+
run: async (ctx) => {
|
|
10966
|
+
const out = await ws(ctx).connectFacebook("close");
|
|
10967
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10968
|
+
}
|
|
10235
10969
|
})
|
|
10236
10970
|
];
|
|
10237
10971
|
|
|
@@ -10382,7 +11116,9 @@ var tools = [
|
|
|
10382
11116
|
...blogTools,
|
|
10383
11117
|
...publishingTools,
|
|
10384
11118
|
...socialTools,
|
|
10385
|
-
...teamTools
|
|
11119
|
+
...teamTools,
|
|
11120
|
+
...memoryTools,
|
|
11121
|
+
...feedbackTools
|
|
10386
11122
|
];
|
|
10387
11123
|
var toolByName = new Map(tools.map((t) => [t.name, t]));
|
|
10388
11124
|
function allowed(t, a) {
|
|
@@ -17441,6 +18177,23 @@ function hmacSigner(secret) {
|
|
|
17441
18177
|
const body = b64(canonical(approval));
|
|
17442
18178
|
return `${body}.${mac(body)}`;
|
|
17443
18179
|
},
|
|
18180
|
+
open(token, principal, now = Date.now()) {
|
|
18181
|
+
if (!token) return { ok: false, reason: "missing" };
|
|
18182
|
+
const [body, sig] = token.split(".");
|
|
18183
|
+
if (!body || !sig) return { ok: false, reason: "bad" };
|
|
18184
|
+
const want = Buffer.from(mac(body));
|
|
18185
|
+
const got = Buffer.from(sig);
|
|
18186
|
+
if (want.length !== got.length || !timingSafeEqual(want, got)) return { ok: false, reason: "bad" };
|
|
18187
|
+
let approval;
|
|
18188
|
+
try {
|
|
18189
|
+
approval = JSON.parse(unb64(body));
|
|
18190
|
+
} catch {
|
|
18191
|
+
return { ok: false, reason: "bad" };
|
|
18192
|
+
}
|
|
18193
|
+
if (!(approval.expires > now)) return { ok: false, reason: "expired" };
|
|
18194
|
+
if (approval.principal !== principal) return { ok: false, reason: "changed" };
|
|
18195
|
+
return { ok: true, approval };
|
|
18196
|
+
},
|
|
17444
18197
|
verify(token, expect, now = Date.now()) {
|
|
17445
18198
|
if (!token) return { ok: false, reason: "missing" };
|
|
17446
18199
|
const [body, sig] = token.split(".");
|
|
@@ -17465,7 +18218,7 @@ var idempotencyKey = (approvalId, call, request) => createHash("sha256").update(
|
|
|
17465
18218
|
|
|
17466
18219
|
// ../../packages/mcp/src/widgets.ts
|
|
17467
18220
|
var WIDGET_MIME = "text/html;profile=mcp-app";
|
|
17468
|
-
var VERSION = "
|
|
18221
|
+
var VERSION = "v2";
|
|
17469
18222
|
var WIDGETS = {
|
|
17470
18223
|
results: {
|
|
17471
18224
|
uri: `ui://widget/results-${VERSION}.html`,
|
|
@@ -17624,7 +18377,7 @@ function show(d) {
|
|
|
17624
18377
|
if (!d) return; data = d;
|
|
17625
18378
|
if (d.workspace) workspace = d.workspace;
|
|
17626
18379
|
const root = document.getElementById("root"); root.textContent = "";
|
|
17627
|
-
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat());
|
|
18380
|
+
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat(3));
|
|
17628
18381
|
resize();
|
|
17629
18382
|
}
|
|
17630
18383
|
(async () => {
|
|
@@ -17712,16 +18465,16 @@ function withIdempotency(api2, approvalId) {
|
|
|
17712
18465
|
}
|
|
17713
18466
|
};
|
|
17714
18467
|
}
|
|
17715
|
-
var
|
|
18468
|
+
var text2 = (value) => [
|
|
17716
18469
|
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }
|
|
17717
18470
|
];
|
|
17718
18471
|
function ok(result, extra) {
|
|
17719
18472
|
const value = result ?? { ok: true };
|
|
17720
18473
|
const structured = value && typeof value === "object" && !Array.isArray(value) ? value : { items: value };
|
|
17721
|
-
return { content:
|
|
18474
|
+
return { content: text2(value), structuredContent: { ...structured, ...extra } };
|
|
17722
18475
|
}
|
|
17723
18476
|
function fail(message) {
|
|
17724
|
-
return { content:
|
|
18477
|
+
return { content: text2({ error: message }), isError: true };
|
|
17725
18478
|
}
|
|
17726
18479
|
function errorMessage2(e) {
|
|
17727
18480
|
if (e instanceof SignInRequiredError) return "The sign-in has expired. Reconnect CMO.so and try again.";
|
|
@@ -17755,8 +18508,8 @@ function createMcpServer(deps) {
|
|
|
17755
18508
|
return deps.access ? deps.access(domain, load) : load();
|
|
17756
18509
|
}
|
|
17757
18510
|
async function resolveWorkspace(t, given) {
|
|
17758
|
-
const
|
|
17759
|
-
if (
|
|
18511
|
+
const named2 = typeof given === "string" ? given.trim().toLowerCase() : "";
|
|
18512
|
+
if (named2) return named2.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
17760
18513
|
const list2 = (await me()).workspaces ?? [];
|
|
17761
18514
|
if (list2.length === 1) return list2[0]?.domain ?? null;
|
|
17762
18515
|
if (t && isAccountTool(t)) return null;
|
|
@@ -17789,17 +18542,32 @@ function createMcpServer(deps) {
|
|
|
17789
18542
|
const t = toolByName.get(name);
|
|
17790
18543
|
if (!t || excluded(name)) return fail(`There is no tool called ${name}.`);
|
|
17791
18544
|
try {
|
|
17792
|
-
|
|
18545
|
+
let approval = null;
|
|
18546
|
+
if (isWrite(t) && args.confirm === true) {
|
|
18547
|
+
if (deps.previewOnly)
|
|
18548
|
+
return fail(
|
|
18549
|
+
"Don't confirm this yourself: the user confirms changes in the CMO.so app. Tell them what the preview will do and stop."
|
|
18550
|
+
);
|
|
18551
|
+
const o = signer.open(typeof args.confirmation === "string" ? args.confirmation : void 0, principal);
|
|
18552
|
+
if (!o.ok)
|
|
18553
|
+
return fail(
|
|
18554
|
+
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."
|
|
18555
|
+
);
|
|
18556
|
+
if (o.approval.calls.length !== 1 || o.approval.calls[0]?.name !== t.name)
|
|
18557
|
+
return fail("This confirmation is for a different action. Get a new preview for this one.");
|
|
18558
|
+
approval = o.approval;
|
|
18559
|
+
}
|
|
18560
|
+
const domain = approval ? approval.domain : await resolveWorkspace(t, args.workspace);
|
|
17793
18561
|
const access = await loadAccess(domain);
|
|
17794
18562
|
if (!allowed(t, access))
|
|
17795
18563
|
return fail(
|
|
17796
18564
|
access.keyAccess === "read" && isWrite(t) ? "This key can only read. Reconnect with write access to make changes." : `You don't have permission to do this on ${domain ?? "this website"}.`
|
|
17797
18565
|
);
|
|
17798
|
-
const callArgs = toolArgs(args);
|
|
17799
|
-
const ctx = { api: api2, domain };
|
|
18566
|
+
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
18567
|
+
const ctx = { api: api2, domain, client: "mcp" };
|
|
17800
18568
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
17801
|
-
|
|
17802
|
-
|
|
18569
|
+
if (!approval) {
|
|
18570
|
+
const calls = [{ name: t.name, args: callArgs }];
|
|
17803
18571
|
const summary2 = t.confirm?.(callArgs, "en") ?? t.description;
|
|
17804
18572
|
const cost = await previewCost(api2, domain, t);
|
|
17805
18573
|
return ok({
|
|
@@ -17808,26 +18576,19 @@ function createMcpServer(deps) {
|
|
|
17808
18576
|
risk: t.risk,
|
|
17809
18577
|
workspace: domain,
|
|
17810
18578
|
confirmation: signer.sign({ calls, principal, domain }),
|
|
17811
|
-
next: "Show the preview to the user. If they agree, call this tool again with
|
|
18579
|
+
next: "Show the preview to the user. If they agree, call this tool again with confirm: true and this confirmation (it runs exactly what was previewed). Nothing has changed yet."
|
|
17812
18580
|
});
|
|
17813
18581
|
}
|
|
17814
|
-
const v = signer.verify(typeof args.confirmation === "string" ? args.confirmation : void 0, {
|
|
17815
|
-
calls,
|
|
17816
|
-
principal,
|
|
17817
|
-
domain
|
|
17818
|
-
});
|
|
17819
|
-
if (!v.ok)
|
|
17820
|
-
return fail(
|
|
17821
|
-
v.reason === "missing" ? "Call this tool without confirm first to get a preview and a confirmation." : v.reason === "expired" ? "The confirmation expired. Get a new preview and ask the user again." : "The confirmation does not match these arguments. Get a new preview and ask the user again."
|
|
17822
|
-
);
|
|
17823
18582
|
const summary = t.confirm?.(callArgs, "en") ?? t.description;
|
|
17824
18583
|
try {
|
|
17825
|
-
const result = await t.run({ api: withIdempotency(api2,
|
|
17826
|
-
|
|
18584
|
+
const result = await t.run({ api: withIdempotency(api2, approval.id), domain, client: "mcp" }, callArgs);
|
|
18585
|
+
const added = t.name === "website_add" ? result?.domain : null;
|
|
18586
|
+
const where = typeof added === "string" && added ? added : domain;
|
|
18587
|
+
await deps.audit?.({ domain: where, actions: [{ tool: t.name, summary, ok: true, action_id: approval.id }] }).catch(() => void 0);
|
|
17827
18588
|
return ok(result);
|
|
17828
18589
|
} catch (e) {
|
|
17829
18590
|
const error2 = errorMessage2(e);
|
|
17830
|
-
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id:
|
|
18591
|
+
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id: approval.id }] }).catch(() => void 0);
|
|
17831
18592
|
return fail(error2);
|
|
17832
18593
|
}
|
|
17833
18594
|
} catch (e) {
|