@cmo-so/mcp 0.1.0 → 0.2.0

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