@cmo-so/mcp 0.1.0 → 0.3.0

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