@cmo-so/cli 0.1.0 → 0.2.0

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