@cmo-so/cli 0.1.0 → 0.3.0

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