@cmo-so/cli 0.7.6 → 0.8.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 (2) hide show
  1. package/dist/cmo.mjs +686 -28
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -5005,12 +5005,12 @@ var require_limitNumber = __commonJS({
5005
5005
  "use strict";
5006
5006
  Object.defineProperty(exports, "__esModule", { value: true });
5007
5007
  var codegen_1 = require_codegen();
5008
- var ops = codegen_1.operators;
5008
+ var ops2 = codegen_1.operators;
5009
5009
  var KWDs = {
5010
- maximum: { okStr: "<=", ok: ops.LTE, fail: ops.GT },
5011
- minimum: { okStr: ">=", ok: ops.GTE, fail: ops.LT },
5012
- exclusiveMaximum: { okStr: "<", ok: ops.LT, fail: ops.GTE },
5013
- exclusiveMinimum: { okStr: ">", ok: ops.GT, fail: ops.LTE }
5010
+ maximum: { okStr: "<=", ok: ops2.LTE, fail: ops2.GT },
5011
+ minimum: { okStr: ">=", ok: ops2.GTE, fail: ops2.LT },
5012
+ exclusiveMaximum: { okStr: "<", ok: ops2.LT, fail: ops2.GTE },
5013
+ exclusiveMinimum: { okStr: ">", ok: ops2.GT, fail: ops2.LTE }
5014
5014
  };
5015
5015
  var error2 = {
5016
5016
  message: ({ keyword, schemaCode }) => (0, codegen_1.str)`must be ${KWDs[keyword].okStr} ${schemaCode}`,
@@ -7090,12 +7090,12 @@ var require_limit = __commonJS({
7090
7090
  exports.formatLimitDefinition = void 0;
7091
7091
  var ajv_1 = require_ajv();
7092
7092
  var codegen_1 = require_codegen();
7093
- var ops = codegen_1.operators;
7093
+ var ops2 = codegen_1.operators;
7094
7094
  var KWDs = {
7095
- formatMaximum: { okStr: "<=", ok: ops.LTE, fail: ops.GT },
7096
- formatMinimum: { okStr: ">=", ok: ops.GTE, fail: ops.LT },
7097
- formatExclusiveMaximum: { okStr: "<", ok: ops.LT, fail: ops.GTE },
7098
- formatExclusiveMinimum: { okStr: ">", ok: ops.GT, fail: ops.LTE }
7095
+ formatMaximum: { okStr: "<=", ok: ops2.LTE, fail: ops2.GT },
7096
+ formatMinimum: { okStr: ">=", ok: ops2.GTE, fail: ops2.LT },
7097
+ formatExclusiveMaximum: { okStr: "<", ok: ops2.LT, fail: ops2.GTE },
7098
+ formatExclusiveMinimum: { okStr: ">", ok: ops2.GT, fail: ops2.LTE }
7099
7099
  };
7100
7100
  var error2 = {
7101
7101
  message: ({ keyword, schemaCode }) => (0, codegen_1.str)`should be ${KWDs[keyword].okStr} ${schemaCode}`,
@@ -7353,6 +7353,23 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
7353
7353
  apps: () => request("/v1/me/apps"),
7354
7354
  revokeApp: (grantId) => request(`/v1/me/apps/${encodeURIComponent(grantId)}`, { method: "DELETE" }),
7355
7355
  invoices: () => request("/v1/me/invoices"),
7356
+ /** Standing permissions: one of your keys may run some writes on one website without a preview, up to a daily cap. */
7357
+ standing: () => request(
7358
+ "/v1/me/standing"
7359
+ ),
7360
+ /** From another key than `key_id` (409/422 otherwise); replaces any earlier one for that key and website. */
7361
+ grantStanding: (p) => request("/v1/me/standing", {
7362
+ method: "POST",
7363
+ headers: { "Content-Type": "application/json" },
7364
+ body: JSON.stringify(p)
7365
+ }),
7366
+ revokeStanding: (id) => request(`/v1/me/standing/${id}`, { method: "DELETE" }),
7367
+ /** The MCP, just before a write: records one use when the key's permission covers it. */
7368
+ claimStanding: (p) => request("/v1/me/standing/claim", {
7369
+ method: "POST",
7370
+ headers: { "Content-Type": "application/json" },
7371
+ body: JSON.stringify(p)
7372
+ }),
7356
7373
  /**
7357
7374
  * Take a website off your own list (archives your claim; reversible on ai.cmo.so). Nothing changes for
7358
7375
  * anyone else; a team member also leaves the team.
@@ -7936,9 +7953,7 @@ function domainApi(api2, domain) {
7936
7953
  /** Disconnect the website's LinkedIn account. */
7937
7954
  disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
7938
7955
  /** Published articles a LinkedIn post can be made from. */
7939
- socialSources: (search = "") => api2.request(
7940
- `${base}/social/sources?search=${encodeURIComponent(search)}`
7941
- ),
7956
+ socialSources: (search = "") => api2.request(`${base}/social/sources?search=${encodeURIComponent(search)}`),
7942
7957
  /** Maggie writes a post about one article for each channel; `failed` lists a channel that could not be written while the others were. */
7943
7958
  socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
7944
7959
  source_post_id: sourcePostId,
@@ -8145,9 +8160,9 @@ function domainApi(api2, domain) {
8145
8160
  /** The last time posts delivered by API pull were found in the website's sitemap (gives them their address). */
8146
8161
  sitemapMatch: () => api2.request(`${base}/rewrites/sitemap-match`),
8147
8162
  /** Read the sitemap (found on its own when blank) and match API-pull posts to their live address. */
8148
- runSitemapMatch: (sitemapUrl) => post(
8163
+ runSitemapMatch: (sitemapUrl2) => post(
8149
8164
  `${base}/rewrites/sitemap-match`,
8150
- sitemapUrl ? { sitemap_url: sitemapUrl } : {}
8165
+ sitemapUrl2 ? { sitemap_url: sitemapUrl2 } : {}
8151
8166
  ),
8152
8167
  rewritePolicy: () => api2.request(`${base}/rewrite-policy`),
8153
8168
  setRewritePolicy: (p) => put(`${base}/rewrite-policy`, p),
@@ -8164,7 +8179,37 @@ function domainApi(api2, domain) {
8164
8179
  startPromptCheck: (prompt) => post(`${base}/visibility/prompt-check`, { prompt }),
8165
8180
  promptCheck: (taskId) => api2.request(`${base}/visibility/prompt-check/${encodeURIComponent(taskId)}`),
8166
8181
  report: (range = "30d") => api2.request(`${base}/report?range=${range}`),
8167
- platform: () => api2.request(`${base}/platform`)
8182
+ platform: () => api2.request(`${base}/platform`),
8183
+ // Ops Autopilot (cmo-so docs/prd/ops-autopilot.md): Maggie's daily operations on the website.
8184
+ opsAutopilot: () => api2.request(`${base}/ops-autopilot`),
8185
+ setOpsAutopilot: (s) => put(`${base}/ops-autopilot`, s),
8186
+ // The report inbox: Maggie's reports and agents' reports.
8187
+ reports: (q2 = {}) => {
8188
+ const params = new URLSearchParams();
8189
+ if (q2.kind) params.set("kind", q2.kind);
8190
+ if (q2.unread) params.set("unread", "true");
8191
+ if (q2.limit) params.set("limit", String(q2.limit));
8192
+ const qs = params.toString();
8193
+ return api2.request(`${base}/reports${qs ? `?${qs}` : ""}`);
8194
+ },
8195
+ inboxReport: (id) => api2.request(`${base}/reports/${id}`),
8196
+ saveReport: (r) => post(`${base}/reports`, r),
8197
+ readAllReports: () => post(`${base}/reports/read-all`, {}),
8198
+ // Ops Autopilot: ideas the weekly clean-up archived (restorable), post health, Google reviews.
8199
+ ideaCleanup: (limit = 50) => api2.request(`${base}/idea-cleanup?limit=${limit}`),
8200
+ restoreIdeaCleanup: (id) => post(`${base}/idea-cleanup/${id}/restore`, {}),
8201
+ postHealth: (limit = 30) => api2.request(`${base}/post-health?limit=${limit}`),
8202
+ googleReviews: (q2 = {}) => {
8203
+ const params = new URLSearchParams();
8204
+ if (q2.status) params.set("status", q2.status);
8205
+ if (q2.limit) params.set("limit", String(q2.limit));
8206
+ const qs = params.toString();
8207
+ return api2.request(
8208
+ `${base}/google-business/reviews${qs ? `?${qs}` : ""}`
8209
+ );
8210
+ },
8211
+ redraftGoogleReview: (id) => post(`${base}/google-business/reviews/${id}/draft`, {}),
8212
+ replyGoogleReview: (id, text3) => post(`${base}/google-business/reviews/${id}/reply`, text3 === void 0 ? {} : { text: text3 })
8168
8213
  };
8169
8214
  }
8170
8215
 
@@ -8284,8 +8329,84 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
8284
8329
  };
8285
8330
  }
8286
8331
 
8332
+ // ../../packages/api-client/src/sitemaps.ts
8333
+ var SITEMAPS_MAX = 20;
8334
+ var SITEMAP_LABEL_MAX = 80;
8335
+ var SITEMAP_OWNERS = ["own", "competitor", "other"];
8336
+ var SITEMAP_USES = {
8337
+ own: ["post_ideas", "channel_ideas", "internal_links", "post_matching"],
8338
+ competitor: ["post_ideas", "channel_ideas"],
8339
+ other: ["post_ideas", "channel_ideas"]
8340
+ };
8341
+ function sitemapUrl(input) {
8342
+ let v = input.trim();
8343
+ if (!v || /^[a-z][a-z0-9+.-]*:(?!\/\/|\d)/i.test(v)) return null;
8344
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) v = `https://${v}`;
8345
+ let u;
8346
+ try {
8347
+ u = new URL(v);
8348
+ } catch {
8349
+ return null;
8350
+ }
8351
+ if (u.protocol !== "http:" && u.protocol !== "https:" || u.username || u.password) return null;
8352
+ if (!u.hostname.includes(".") || /\s/.test(v)) return null;
8353
+ u.protocol = "https:";
8354
+ return u.toString();
8355
+ }
8356
+ function sitemapUses(owner, uses) {
8357
+ const allowed2 = SITEMAP_USES[owner];
8358
+ const kept = allowed2.filter((u) => uses?.includes(u));
8359
+ return kept.length ? kept : [...allowed2];
8360
+ }
8361
+ function onWebsite(url, domain) {
8362
+ let host;
8363
+ try {
8364
+ host = new URL(url).hostname.toLowerCase();
8365
+ } catch {
8366
+ return false;
8367
+ }
8368
+ const d = domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^www\./, "");
8369
+ return Boolean(d) && (host === d || host.endsWith(`.${d}`));
8370
+ }
8371
+ function cleanSitemap(entry, domain) {
8372
+ const url = sitemapUrl(entry.url ?? "");
8373
+ if (!url) return "The sitemap must be a web address starting with http:// or https://.";
8374
+ let owner = SITEMAP_OWNERS.includes(entry.owner) ? entry.owner : "other";
8375
+ if (owner === "own" && domain && !onWebsite(url, domain)) owner = "other";
8376
+ const label = (entry.label ?? "").trim().slice(0, SITEMAP_LABEL_MAX);
8377
+ const out = { ...entry, url, owner, label: label || null, uses: sitemapUses(owner, entry.uses) };
8378
+ if (!out.id) delete out.id;
8379
+ return out;
8380
+ }
8381
+ function checkSitemaps(list2) {
8382
+ if (list2.length > SITEMAPS_MAX) return `Up to ${SITEMAPS_MAX} sitemaps. Remove one first.`;
8383
+ const seen = /* @__PURE__ */ new Set();
8384
+ for (const s of list2) {
8385
+ const k = s.url.trim().toLowerCase();
8386
+ if (seen.has(k)) return `${s.url} is already in the list.`;
8387
+ seen.add(k);
8388
+ }
8389
+ return null;
8390
+ }
8391
+ function sitemapCheckLine(check) {
8392
+ if (!check || check.status === "unchecked") return "Checked when you save";
8393
+ if (check.status === "error") return check.error?.trim() || "Couldn't read this sitemap.";
8394
+ const n = check.url_count;
8395
+ const found = n == null ? "Read" : `Found ${n.toLocaleString("en-GB")} ${n === 1 ? "page" : "pages"}`;
8396
+ const at = check.checked_at ? new Date(check.checked_at) : null;
8397
+ const when = at && !Number.isNaN(at.getTime()) ? at.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }) : null;
8398
+ return when ? `${found} \xB7 checked ${when}` : found;
8399
+ }
8400
+
8287
8401
  // ../../packages/domain/src/copy/maggie.ts
8288
8402
  var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
8403
+ var CHANNEL_EN = {
8404
+ linkedin: "LinkedIn",
8405
+ facebook: "Facebook",
8406
+ instagram: "Instagram",
8407
+ google_business: "Google Business"
8408
+ };
8409
+ var CHANNEL_ZH = { ...CHANNEL_EN, google_business: "Google \u5546\u5BB6" };
8289
8410
  var named = (title, id) => {
8290
8411
  if (title != null && String(title).trim()) return ` ${q(title)}`;
8291
8412
  const s = String(id ?? "").trim();
@@ -8371,6 +8492,22 @@ var confirmCopy = {
8371
8492
  en: (a) => `Remove the competitor ${q(a.name)}.`,
8372
8493
  "zh-Hant": (a) => `\u522A\u9664\u7AF6\u722D\u5C0D\u624B ${q(a.name)}\u3002`
8373
8494
  },
8495
+ sitemap_upsert: {
8496
+ en: (a) => {
8497
+ const whose = { own: "your site's ", competitor: "a competitor's ", other: "another site's " }[a.owner];
8498
+ const label = a.label ? ` (${q(a.label)})` : "";
8499
+ return a.url ? `Save ${whose ?? "the "}sitemap ${a.url}${label}.` : `Change the sitemap${label}: ${keys(a).filter((k) => k !== "id").join(", ")}.`;
8500
+ },
8501
+ "zh-Hant": (a) => {
8502
+ const whose = { own: "\u81EA\u5DF1\u7DB2\u7AD9\u7684", competitor: "\u7AF6\u722D\u5C0D\u624B\u7684", other: "\u5176\u4ED6\u7DB2\u7AD9\u7684" }[a.owner];
8503
+ const label = a.label ? `\uFF08${q(a.label)}\uFF09` : "";
8504
+ return a.url ? `\u5132\u5B58${whose ?? ""} sitemap ${a.url}${label}\u3002` : `\u4FEE\u6539 sitemap${label}\uFF1A${keys(a).filter((k) => k !== "id").join("\u3001")}\u3002`;
8505
+ }
8506
+ },
8507
+ sitemap_remove: {
8508
+ en: (a) => a.url ?? a.id ? `Remove the sitemap ${a.url ?? a.id}.` : "Remove a sitemap.",
8509
+ "zh-Hant": (a) => a.url ?? a.id ? `\u522A\u9664 sitemap ${a.url ?? a.id}\u3002` : "\u522A\u9664\u4E00\u500B sitemap\u3002"
8510
+ },
8374
8511
  brand_research_update: {
8375
8512
  en: (a) => `Update your market notes: ${keys(a).join(", ") || "nothing"}.`,
8376
8513
  "zh-Hant": (a) => `\u66F4\u65B0\u5E02\u5834\u8CC7\u6599\uFF1A${keys(a).join("\u3001") || "\u6C92\u6709"}\u3002`
@@ -8453,6 +8590,48 @@ var confirmCopy = {
8453
8590
  en: (a) => a.paused ? "Pause publishing blog posts." : "Resume publishing blog posts.",
8454
8591
  "zh-Hant": (a) => a.paused ? "\u66AB\u505C\u767C\u4F48\u90E8\u843D\u683C\u6587\u7AE0\u3002" : "\u6062\u5FA9\u767C\u4F48\u90E8\u843D\u683C\u6587\u7AE0\u3002"
8455
8592
  },
8593
+ ops_autopilot_set: {
8594
+ en: (a) => {
8595
+ const f = Object.entries(a.features ?? {});
8596
+ const on2 = f.filter(([, v]) => v).map(([k]) => k.replace(/_/g, " "));
8597
+ const off = f.filter(([, v]) => !v).map(([k]) => k.replace(/_/g, " "));
8598
+ const parts = [
8599
+ on2.length ? `turn on ${on2.join(", ")}` : "",
8600
+ off.length ? `turn off ${off.join(", ")}` : "",
8601
+ a.weekly_email === true ? "email the Monday report to the owner" : "",
8602
+ a.weekly_email === false ? "stop emailing the Monday report" : ""
8603
+ ].filter(Boolean);
8604
+ return `Ops Autopilot: ${parts.join("; ") || "no change"}.`;
8605
+ },
8606
+ "zh-Hant": (a) => {
8607
+ const f = Object.entries(a.features ?? {});
8608
+ const on2 = f.filter(([, v]) => v).map(([k]) => k);
8609
+ const off = f.filter(([, v]) => !v).map(([k]) => k);
8610
+ const parts = [
8611
+ on2.length ? `\u958B\u555F ${on2.join("\u3001")}` : "",
8612
+ off.length ? `\u95DC\u9589 ${off.join("\u3001")}` : "",
8613
+ a.weekly_email === true ? "\u628A\u9031\u4E00\u5831\u544A\u5BC4\u7D66\u7DB2\u7AD9\u64C1\u6709\u8005" : "",
8614
+ a.weekly_email === false ? "\u4E0D\u518D\u5BC4\u9031\u4E00\u5831\u544A" : ""
8615
+ ].filter(Boolean);
8616
+ return `Ops Autopilot\uFF1A${parts.join("\uFF1B") || "\u6C92\u6709\u8B8A\u66F4"}\u3002`;
8617
+ }
8618
+ },
8619
+ idea_restore_cleanup: {
8620
+ en: (a) => `Bring back the archived idea #${a.id} to the waiting list.`,
8621
+ "zh-Hant": (a) => `\u628A\u5DF2\u6B78\u6A94\u7684 idea #${a.id} \u653E\u56DE\u7B49\u5F85\u5217\u8868\u3002`
8622
+ },
8623
+ gbp_review_draft: {
8624
+ en: (a) => `Write a new reply draft for Google review #${a.id} (one AI generation). Nothing is posted.`,
8625
+ "zh-Hant": (a) => `\u70BA Google \u8A55\u50F9 #${a.id} \u91CD\u5BEB\u56DE\u8986\u8349\u7A3F\uFF08\u7528\u4E00\u6B21 AI \u751F\u6210\uFF09\uFF0C\u4E0D\u6703\u767C\u4F48\u3002`
8626
+ },
8627
+ gbp_review_reply: {
8628
+ en: (a) => `Post ${a.text ? `the reply ${q(a.text)}` : "Maggie's draft"} under Google review #${a.id}. Everyone who sees the review will see it.`,
8629
+ "zh-Hant": (a) => `\u5728 Google \u8A55\u50F9 #${a.id} \u4E0B\u767C\u4F48${a.text ? `\u56DE\u8986 ${q(a.text)}` : " Maggie \u7684\u8349\u7A3F"}\uFF0C\u770B\u5230\u9019\u5247\u8A55\u50F9\u7684\u4EBA\u90FD\u770B\u5F97\u5230\u3002`
8630
+ },
8631
+ report_save: {
8632
+ en: (a) => `Save the report ${q(a.title)} to the website's inbox (the owner gets a notification).`,
8633
+ "zh-Hant": (a) => `\u628A\u5831\u544A ${q(a.title)} \u5B58\u9032\u7DB2\u7AD9\u7684\u6536\u4EF6\u5323\uFF08\u64C1\u6709\u8005\u6703\u6536\u5230\u901A\u77E5\uFF09\u3002`
8634
+ },
8456
8635
  posts_per_day_set: {
8457
8636
  en: (a) => `Publish ${a.posts_per_day} blog post${Number(a.posts_per_day) === 1 ? "" : "s"} a day.`,
8458
8637
  "zh-Hant": (a) => `\u6BCF\u5929\u767C\u4F48 ${a.posts_per_day} \u7BC7\u90E8\u843D\u683C\u6587\u7AE0\u3002`
@@ -8627,8 +8806,8 @@ var confirmCopy = {
8627
8806
  "zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
8628
8807
  },
8629
8808
  social_approve_all: {
8630
- 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.",
8631
- "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"
8809
+ en: (a) => `Approve every ${a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} ` : ""}post waiting for your OK${a.channel ? "" : ", on every channel"}, including any held for a picture check. They go out one by one at each channel's posting times, where people can see them.`,
8810
+ "zh-Hant": (a) => `\u6279\u51C6\u6240\u6709\u7B49\u4F60\u78BA\u8A8D\u7684${a.channel ? ` ${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\uFF08\u6240\u6709\u6E20\u9053\uFF09"}\u8CBC\u6587\uFF0C\u5305\u62EC\u56E0\u5716\u7247\u6AA2\u67E5\u800C\u66AB\u505C\u7684\u3002\u5B83\u5011\u6703\u5728\u5404\u6E20\u9053\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u9010\u7BC7\u767C\u51FA\uFF0C\u5927\u5BB6\u90FD\u770B\u5F97\u5230\u3002`
8632
8811
  },
8633
8812
  series_create: {
8634
8813
  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.`,
@@ -8697,6 +8876,18 @@ var confirmCopy = {
8697
8876
  en: (a) => `Disconnect ${q(a.app)} from your account. It stops working now and has to be connected again.`,
8698
8877
  "zh-Hant": (a) => `\u5F9E\u4F60\u7684\u5E33\u865F\u65B7\u958B ${q(a.app)}\uFF0C\u5B83\u6703\u7ACB\u5373\u505C\u7528\uFF0C\u4E4B\u5F8C\u8981\u91CD\u65B0\u9023\u63A5\u3002`
8699
8878
  },
8879
+ standing_permission_set: {
8880
+ en: (a) => `Let the key ${q(a.key_name)} run ${Object.entries(a.tools ?? {}).map(([tool3, cap]) => `${tool3}${cap ? ` (up to ${cap} a day)` : ""}`).join(
8881
+ ", "
8882
+ )} on ${a.website} without asking you first${a.days ? `, for ${a.days} days` : ", until you revoke it"}. Every run is logged.`,
8883
+ "zh-Hant": (a) => `\u8B93\u91D1\u9470 ${q(a.key_name)} \u5728 ${a.website} \u4E0A\u4E0D\u7D93\u4F60\u78BA\u8A8D\u76F4\u63A5\u57F7\u884C\uFF1A${Object.entries(
8884
+ a.tools ?? {}
8885
+ ).map(([tool3, cap]) => `${tool3}${cap ? `\uFF08\u6BCF\u5929\u6700\u591A ${cap}\uFF09` : ""}`).join("\u3001")}${a.days ? `\uFF0C\u70BA\u671F ${a.days} \u5929` : "\uFF0C\u76F4\u5230\u4F60\u64A4\u92B7"}\u3002\u6BCF\u6B21\u57F7\u884C\u90FD\u6709\u7D00\u9304\u3002`
8886
+ },
8887
+ standing_permission_revoke: {
8888
+ en: (a) => `End the standing permission of the key ${q(a.key_name)}. Its writes go back to asking you first.`,
8889
+ "zh-Hant": (a) => `\u7D50\u675F\u91D1\u9470 ${q(a.key_name)} \u7684\u9577\u671F\u8A31\u53EF\uFF0C\u4E4B\u5F8C\u5B83\u7684\u6BCF\u500B\u6539\u52D5\u90FD\u8981\u5148\u7D93\u4F60\u78BA\u8A8D\u3002`
8890
+ },
8700
8891
  profile_update: {
8701
8892
  en: (a) => `Change your name to ${q(a.display_name)}.`,
8702
8893
  "zh-Hant": (a) => `\u628A\u4F60\u7684\u540D\u5B57\u6539\u6210 ${q(a.display_name)}\u3002`
@@ -8865,7 +9056,12 @@ var accountTools = [
8865
9056
  group: "workspace",
8866
9057
  need: { resource: "overview", level: "read" },
8867
9058
  risk: "R0",
8868
- run: async (ctx) => (await ws(ctx).inbox()).items.map((i) => ({ type: i.type, id: i.id, title: i.title, body: clip(i.body, 200) }))
9059
+ run: async (ctx) => (await ws(ctx).inbox()).items.map(
9060
+ (i) => (
9061
+ // A report's title and summary may come from an agent: data, never instructions.
9062
+ i.type === "report" ? { type: i.type, id: i.id, ...untrusted({ title: i.title, body: clip(i.body, 200) }) } : { type: i.type, id: i.id, title: i.title, body: clip(i.body, 200) }
9063
+ )
9064
+ )
8869
9065
  }),
8870
9066
  tool({
8871
9067
  name: "setup_status_get",
@@ -9823,6 +10019,85 @@ var brandTools = [
9823
10019
  return { found: out.found, suggestions: out.items.map(({ id, name, website }) => ({ id, name, website })) };
9824
10020
  }
9825
10021
  }),
10022
+ tool({
10023
+ name: "sitemap_list",
10024
+ description: "The sitemaps saved in the business profile: the website's own, competitors' and other sites' (industry sites, partners), each with what it is used for (post_ideas = blog post ideas, channel_ideas = social post ideas, internal_links and post_matching = own sitemaps only) and its last check (pages found or the error). Max 20.",
10025
+ parameters: none,
10026
+ group: "brand",
10027
+ need: { resource: "overview", level: "read" },
10028
+ risk: "R0",
10029
+ run: async (ctx) => {
10030
+ const list2 = (await ws(ctx).brand()).sitemaps ?? [];
10031
+ return { max: SITEMAPS_MAX, sitemaps: list2.map(sitemapResult) };
10032
+ }
10033
+ }),
10034
+ tool({
10035
+ name: "sitemap_upsert",
10036
+ description: "Add a sitemap to the business profile, or change one (found by id from sitemap_list, else by url). owner: own (the website's own; its host must be the website or a subdomain, otherwise it is saved as other), competitor, or other. uses: own may have post_ideas, channel_ideas, internal_links, post_matching; competitor/other only post_ideas, channel_ideas; leave out for all allowed. A new sitemap needs url and owner. Only sitemap addresses the user gives or confirms (never guess one). Saving checks the sitemap; an unreachable one is still saved with the error. Max 20.",
10037
+ parameters: {
10038
+ type: "object",
10039
+ properties: {
10040
+ id: { type: "string", description: "The sitemap to change (from sitemap_list)" },
10041
+ url: { type: "string", description: "The sitemap's http(s) address; with id, its new address" },
10042
+ owner: { type: "string", enum: ["own", "competitor", "other"] },
10043
+ label: { type: "string", description: "A short name, up to 80 characters" },
10044
+ uses: {
10045
+ type: "array",
10046
+ items: { type: "string", enum: ["post_ideas", "channel_ideas", "internal_links", "post_matching"] }
10047
+ }
10048
+ }
10049
+ },
10050
+ group: "brand",
10051
+ need: write2,
10052
+ risk: "W1",
10053
+ run: async (ctx, args) => {
10054
+ const list2 = (await ws(ctx).brand()).sitemaps ?? [];
10055
+ const i = findSitemap(list2, args.id, args.id ? void 0 : args.url);
10056
+ if (args.id && i < 0) throw new Error(`No sitemap with id ${args.id}. Use sitemap_list.`);
10057
+ const old = i >= 0 ? list2[i] : void 0;
10058
+ if (!old && (!args.url || !args.owner)) throw new Error("A new sitemap needs url and owner.");
10059
+ const owner = args.owner ?? old?.owner ?? "other";
10060
+ const entry = cleanSitemap(
10061
+ {
10062
+ ...old,
10063
+ url: args.url ?? old?.url ?? "",
10064
+ owner,
10065
+ label: args.label ?? old?.label ?? null,
10066
+ // A new owner without uses gets that owner's defaults.
10067
+ uses: args.uses ?? (args.owner && args.owner !== old?.owner ? [] : old?.uses ?? [])
10068
+ },
10069
+ ctx.domain ?? void 0
10070
+ );
10071
+ if (typeof entry === "string") throw new Error(entry);
10072
+ if (old && sitemapUrl(old.url) !== entry.url) delete entry.check;
10073
+ const next = old ? list2.map((x, n) => n === i ? entry : x) : [...list2, entry];
10074
+ const problem = checkSitemaps(next);
10075
+ if (problem) throw new Error(problem);
10076
+ const saved = (await ws(ctx).updateBrand({ sitemaps: next })).sitemaps ?? next;
10077
+ const mine = saved[findSitemap(saved, void 0, entry.url)];
10078
+ return {
10079
+ saved: mine ? sitemapResult(mine) : sitemapResult(entry),
10080
+ ...(mine?.owner ?? entry.owner) !== owner ? { note: "Not on this website, so it was saved as another site's sitemap (owner other)." } : {},
10081
+ sitemaps: saved.length
10082
+ };
10083
+ }
10084
+ }),
10085
+ tool({
10086
+ name: "sitemap_remove",
10087
+ description: "Remove a sitemap from the business profile (by id from sitemap_list, or by url). Maggie stops reading it; ideas already found stay.",
10088
+ parameters: { type: "object", properties: { id: { type: "string" }, url: { type: "string" } } },
10089
+ group: "brand",
10090
+ need: write2,
10091
+ risk: "W1",
10092
+ run: async (ctx, { id, url }) => {
10093
+ if (!id && !url) throw new Error("Say which sitemap: id or url.");
10094
+ const list2 = (await ws(ctx).brand()).sitemaps ?? [];
10095
+ const i = findSitemap(list2, id, url);
10096
+ if (i < 0) throw new Error(`No sitemap ${id ?? url} was found. Use sitemap_list.`);
10097
+ const saved = (await ws(ctx).updateBrand({ sitemaps: list2.filter((_, n) => n !== i) })).sitemaps ?? [];
10098
+ return { removed: list2[i]?.url, sitemaps: saved.map((s) => s.url) };
10099
+ }
10100
+ }),
9826
10101
  tool({
9827
10102
  name: "brand_research_update",
9828
10103
  description: "Change the profile's market notes: market_research (text), additional_information (anything else Maggie should know) or the SWOT list (each {name: Strengths|Weaknesses|Opportunities|Threats, description}). A list replaces the whole list.",
@@ -9958,6 +10233,22 @@ function brandGaps(b) {
9958
10233
  check("industries", b.industries, "the industry, for examples and wording");
9959
10234
  return { complete: gaps.length === 0, gaps };
9960
10235
  }
10236
+ function findSitemap(list2, id, url) {
10237
+ if (id) return list2.findIndex((s) => s.id === id);
10238
+ const u = url ? (sitemapUrl(url) ?? url).toLowerCase() : "";
10239
+ return u ? list2.findIndex((s) => (sitemapUrl(s.url) ?? s.url).toLowerCase() === u) : -1;
10240
+ }
10241
+ function sitemapResult(s) {
10242
+ return {
10243
+ id: s.id ?? null,
10244
+ url: s.url,
10245
+ owner: s.owner,
10246
+ label: s.label ?? null,
10247
+ uses: s.uses,
10248
+ check: s.check?.status ?? "unchecked",
10249
+ status: sitemapCheckLine(s.check)
10250
+ };
10251
+ }
9961
10252
 
9962
10253
  // ../../packages/tools/src/feedback.ts
9963
10254
  var account = { resource: "account", level: "read" };
@@ -10110,6 +10401,11 @@ var insightTools = [
10110
10401
 
10111
10402
  // ../../packages/tools/src/me.ts
10112
10403
  var account2 = { resource: "account", level: "read" };
10404
+ var STANDING_TOOLS = ["idea_add", "ideas_add_list", "idea_priority", "rewrite_request"];
10405
+ function standingUnits(name, args) {
10406
+ if (name === "ideas_add_list") return Math.max(1, Array.isArray(args.ideas) ? args.ideas.length : 1);
10407
+ return 1;
10408
+ }
10113
10409
  var meTools = [
10114
10410
  tool({
10115
10411
  name: "api_keys_list",
@@ -10152,6 +10448,54 @@ var meTools = [
10152
10448
  risk: "W3",
10153
10449
  run: async ({ api: api2 }, { grant_id }) => api2.account.revokeApp(String(grant_id))
10154
10450
  }),
10451
+ tool({
10452
+ name: "standing_permissions_list",
10453
+ description: "Standing permissions on the user's API keys: which key (a scheduled agent's) may run which writes on which website without a preview, each tool's daily cap and how much is used today.",
10454
+ parameters: none,
10455
+ group: "account",
10456
+ need: account2,
10457
+ risk: "R0",
10458
+ run: async ({ api: api2 }) => api2.account.standing()
10459
+ }),
10460
+ tool({
10461
+ name: "standing_permission_set",
10462
+ description: `Let one of the user's own API keys (from api_keys_list; a scheduled agent's, not the one in use) run some writes on one website without a preview, each up to a daily cap: ${STANDING_TOOLS.join(", ")}. Replaces any earlier one for that key and website. Leave a cap empty for the default (ideas 10 a day, priority changes 25, rewrites 2). Every run is in the action log; standing_permission_revoke ends it.`,
10463
+ parameters: {
10464
+ type: "object",
10465
+ properties: {
10466
+ key_id: { type: "integer" },
10467
+ key_name: { type: "string", description: "The key's name, for the card" },
10468
+ website: { type: "string", description: "The website (domain)" },
10469
+ tools: {
10470
+ type: "object",
10471
+ description: "Daily cap per tool; include only the tools to allow (0 or empty for the default).",
10472
+ properties: Object.fromEntries(STANDING_TOOLS.map((name) => [name, { type: "integer" }]))
10473
+ },
10474
+ days: { type: "integer", description: "How long it lasts (1-366); leave out for until revoked" }
10475
+ },
10476
+ required: ["key_id", "key_name", "website", "tools"]
10477
+ },
10478
+ group: "account",
10479
+ need: account2,
10480
+ risk: "W3",
10481
+ run: async ({ api: api2 }, { key_id, website, tools: tools2, days }) => api2.account.grantStanding({ key_id: Number(key_id), website: String(website), tools: tools2, days: days ?? null })
10482
+ }),
10483
+ tool({
10484
+ name: "standing_permission_revoke",
10485
+ description: "End a standing permission (from standing_permissions_list): its key goes back to previews for every write.",
10486
+ parameters: {
10487
+ type: "object",
10488
+ properties: {
10489
+ id: { type: "integer" },
10490
+ key_name: { type: "string", description: "The key's name, for the card" }
10491
+ },
10492
+ required: ["id", "key_name"]
10493
+ },
10494
+ group: "account",
10495
+ need: account2,
10496
+ risk: "W2",
10497
+ run: async ({ api: api2 }, { id }) => api2.account.revokeStanding(Number(id))
10498
+ }),
10155
10499
  tool({
10156
10500
  name: "invoices_list",
10157
10501
  description: "The user's latest invoices across the subscriptions they pay for: date, amount, status and a link to view or download each.",
@@ -10292,6 +10636,212 @@ var memoryTools = [
10292
10636
  ];
10293
10637
  var MEMORY_TOOLS = new Set(memoryTools.map((t) => t.name));
10294
10638
 
10639
+ // ../../packages/tools/src/ops.ts
10640
+ var overview = { resource: "overview", level: "read" };
10641
+ var autopilotWrite = { resource: "autopilot", level: "write" };
10642
+ var reportWrite = { resource: "report", level: "write" };
10643
+ var OPS_FEATURES = [
10644
+ "top_up",
10645
+ "precheck",
10646
+ "retry",
10647
+ "weekly_report",
10648
+ "gsc_ideas",
10649
+ "ai_visibility",
10650
+ "idea_cleanup",
10651
+ "competitor_watch",
10652
+ "insight_memory",
10653
+ "post_health",
10654
+ "gbp_reviews"
10655
+ ];
10656
+ var opsTools = [
10657
+ tool({
10658
+ name: "ops_autopilot_get",
10659
+ description: "Maggie's daily operations on the website (Ops Autopilot): whether it is available, and each feature with on/off, when it runs and how its last run went. Features: top_up (fills each social channel's empty slots for the week every morning), precheck (checks each social post before it goes out: no links or phone numbers on Google, a picture on Instagram, no placeholders or promised rankings; holds it as a draft if not), retry (tries a failed post once more), weekly_report (Monday report in the inbox), gsc_ideas (Search Console queries near page one become blog ideas), ai_visibility (weekly AI answer check), idea_cleanup (archives stale low-priority ideas weekly, restorable), competitor_watch (competitors' new articles become ideas), insight_memory (what performed best is remembered), post_health (checks new posts once they're live), gbp_reviews (new Google reviews with reply drafts).",
10660
+ parameters: none,
10661
+ group: "workspace",
10662
+ need: overview,
10663
+ risk: "R0",
10664
+ run: async (ctx) => ws(ctx).opsAutopilot()
10665
+ }),
10666
+ tool({
10667
+ name: "ops_autopilot_set",
10668
+ description: "Switch Ops Autopilot features on or off for this website, and whether the Monday report is also emailed to the website's owner. Only features from ops_autopilot_get; a feature that isn't available yet can be switched but won't run.",
10669
+ parameters: {
10670
+ type: "object",
10671
+ properties: {
10672
+ features: {
10673
+ type: "object",
10674
+ description: `Feature \u2192 true (on) or false (off). Features: ${OPS_FEATURES.join(", ")}`,
10675
+ properties: Object.fromEntries(OPS_FEATURES.map((f) => [f, { type: "boolean" }]))
10676
+ },
10677
+ weekly_email: { type: "boolean", description: "Also email the Monday report to the website's owner" }
10678
+ }
10679
+ },
10680
+ group: "workspace",
10681
+ need: autopilotWrite,
10682
+ risk: "W2",
10683
+ idempotent: true,
10684
+ run: async (ctx, { features, weekly_email }) => {
10685
+ const known = Object.fromEntries(
10686
+ Object.entries(features ?? {}).filter(([k]) => OPS_FEATURES.includes(k)).map(([k, v]) => [k, Boolean(v)])
10687
+ );
10688
+ return ws(ctx).setOpsAutopilot({
10689
+ ...Object.keys(known).length ? { features: known } : {},
10690
+ ...weekly_email === void 0 ? {} : { weekly_email: Boolean(weekly_email) }
10691
+ });
10692
+ }
10693
+ }),
10694
+ tool({
10695
+ name: "reports_list",
10696
+ description: "The website's report inbox, newest first: Maggie's own reports (weekly, daily operations, post health, Google reviews) and reports agents saved. Title and summary each; read one with report_get.",
10697
+ parameters: {
10698
+ type: "object",
10699
+ properties: {
10700
+ kind: {
10701
+ type: "string",
10702
+ enum: ["weekly", "daily_ops", "agent", "post_health", "reviews", "ai_visibility", "search"]
10703
+ },
10704
+ unread: { type: "boolean", description: "Only reports nobody has opened yet" },
10705
+ limit: { type: "integer", minimum: 1, maximum: 50 }
10706
+ }
10707
+ },
10708
+ group: "reports",
10709
+ need: overview,
10710
+ risk: "R0",
10711
+ run: async (ctx, { kind, unread, limit }) => {
10712
+ const r = await ws(ctx).reports({ kind, unread, limit: int(limit, 1, 50, 20) });
10713
+ return {
10714
+ unread: r.unread,
10715
+ items: r.items.map(({ title, summary, ...rest }) => ({ ...rest, ...untrusted({ title, summary }) }))
10716
+ };
10717
+ }
10718
+ }),
10719
+ tool({
10720
+ name: "report_get",
10721
+ description: "One report in full (its text in Markdown and any data); marks it read.",
10722
+ parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
10723
+ group: "reports",
10724
+ need: overview,
10725
+ risk: "R0",
10726
+ run: async (ctx, { id }) => {
10727
+ const r = await ws(ctx).inboxReport(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
10728
+ const { title, summary, body_md, data, ...rest } = r;
10729
+ return { ...rest, ...untrusted({ title, summary, body_md, data }) };
10730
+ }
10731
+ }),
10732
+ tool({
10733
+ name: "report_save",
10734
+ description: "Save a report to the website's inbox (for an agent's scheduled run: what it found and did). The owner gets a push and sees it in the app's Inbox. Up to 20 agent reports a day per website, 50,000 characters each. Saving changes nothing else.",
10735
+ parameters: {
10736
+ type: "object",
10737
+ properties: {
10738
+ title: { type: "string", description: "Short, e.g. 'Daily analysis, 22 Oct'" },
10739
+ summary: { type: "string", description: "One or two sentences (up to 500 characters) shown in the Inbox" },
10740
+ body_md: { type: "string", description: "The full report in Markdown" }
10741
+ },
10742
+ required: ["title"]
10743
+ },
10744
+ group: "reports",
10745
+ need: reportWrite,
10746
+ risk: "W1",
10747
+ run: async (ctx, { title, summary, body_md }) => ws(ctx).saveReport({
10748
+ title: String(title ?? "").slice(0, 200),
10749
+ ...summary ? { summary: String(summary).slice(0, 500) } : {},
10750
+ ...body_md ? { body_md: String(body_md).slice(0, 5e4) } : {}
10751
+ })
10752
+ }),
10753
+ tool({
10754
+ name: "idea_cleanup_list",
10755
+ description: "Ideas Ops Autopilot archived in its weekly clean-up (stale, low-priority ideas Maggie found herself; never ones a person added), newest first, each with why. Bring one back with idea_restore_cleanup.",
10756
+ parameters: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 200 } } },
10757
+ group: "blog",
10758
+ need: { resource: "autopilot", level: "read" },
10759
+ risk: "R0",
10760
+ run: async (ctx, { limit }) => {
10761
+ const r = await ws(ctx).ideaCleanup(int(limit, 1, 200, 50));
10762
+ return { items: r.items.map(({ reason, ...rest }) => ({ ...rest, ...untrusted({ reason }) })) };
10763
+ }
10764
+ }),
10765
+ tool({
10766
+ name: "idea_restore_cleanup",
10767
+ description: "Bring an idea the weekly clean-up archived back to the waiting list (id from idea_cleanup_list).",
10768
+ parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
10769
+ group: "blog",
10770
+ need: autopilotWrite,
10771
+ risk: "W1",
10772
+ idempotent: true,
10773
+ run: async (ctx, { id }) => ws(ctx).restoreIdeaCleanup(int(id, 1, Number.MAX_SAFE_INTEGER, 0))
10774
+ }),
10775
+ tool({
10776
+ name: "post_health_get",
10777
+ description: "Checks of the blog posts that went live in the last week (Ops Autopilot, daily): whether each page opens, and has a title, a meta description, a canonical link to itself, an og:image and working links on the site. Each with its issues in plain words.",
10778
+ parameters: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 100 } } },
10779
+ group: "reports",
10780
+ need: { resource: "report", level: "read" },
10781
+ risk: "R0",
10782
+ run: async (ctx, { limit }) => ws(ctx).postHealth(int(limit, 1, 100, 30))
10783
+ }),
10784
+ tool({
10785
+ name: "gbp_reviews_list",
10786
+ description: "Reviews of the website's Google Business profile (read every 6 hours by Ops Autopilot), newest first, each with Maggie's reply draft or the reply already sent. status: none (no reply yet), drafted (a draft waits to be sent), sent.",
10787
+ parameters: {
10788
+ type: "object",
10789
+ properties: {
10790
+ status: { type: "string", enum: ["none", "drafted", "sent", "failed"] },
10791
+ limit: { type: "integer", minimum: 1, maximum: 100 }
10792
+ }
10793
+ },
10794
+ group: "social",
10795
+ need: { resource: "autopilot", level: "read" },
10796
+ risk: "R0",
10797
+ run: async (ctx, { status, limit }) => {
10798
+ const r = await ws(ctx).googleReviews({ status, limit: int(limit, 1, 100, 30) });
10799
+ return {
10800
+ connected: r.connected,
10801
+ waiting: r.waiting,
10802
+ items: r.items.map(({ reviewer, comment, ...rest }) => ({ ...rest, ...untrusted({ reviewer, comment }) }))
10803
+ };
10804
+ }
10805
+ }),
10806
+ tool({
10807
+ name: "gbp_review_draft",
10808
+ description: "Ask Maggie for a new reply draft to one Google review (one AI generation). Nothing is posted.",
10809
+ parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
10810
+ group: "social",
10811
+ need: autopilotWrite,
10812
+ risk: "W2",
10813
+ gates: ["LLM"],
10814
+ run: async (ctx, { id }) => {
10815
+ const r = await ws(ctx).redraftGoogleReview(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
10816
+ const { reviewer, comment, ...rest } = r;
10817
+ return { ...rest, ...untrusted({ reviewer, comment }) };
10818
+ }
10819
+ }),
10820
+ tool({
10821
+ name: "gbp_review_reply",
10822
+ description: "Post the reply to one Google review on the website's Google Business profile: the draft, or your own text (up to 1,000 characters). Public: everyone who sees the review sees the reply. A review can be answered once here.",
10823
+ parameters: {
10824
+ type: "object",
10825
+ properties: {
10826
+ id: { type: "integer" },
10827
+ text: { type: "string", description: "The reply; leave out to send the draft" }
10828
+ },
10829
+ required: ["id"]
10830
+ },
10831
+ group: "social",
10832
+ need: autopilotWrite,
10833
+ risk: "W3",
10834
+ run: async (ctx, { id, text: text3 }) => {
10835
+ const r = await ws(ctx).replyGoogleReview(
10836
+ int(id, 1, Number.MAX_SAFE_INTEGER, 0),
10837
+ text3 === void 0 ? void 0 : String(text3).slice(0, 1e3)
10838
+ );
10839
+ const { reviewer, comment, ...rest } = r;
10840
+ return { ...rest, ...untrusted({ reviewer, comment }) };
10841
+ }
10842
+ })
10843
+ ];
10844
+
10295
10845
  // ../../packages/tools/src/publishing.ts
10296
10846
  var read3 = { resource: "autopilot", level: "read" };
10297
10847
  var write4 = { resource: "autopilot", level: "write" };
@@ -11329,12 +11879,18 @@ var socialTools = [
11329
11879
  }),
11330
11880
  tool({
11331
11881
  name: "social_sources_list",
11332
- description: "Published articles a social post can be made from (optionally matching a search).",
11882
+ description: "Published articles a social post can be made from (optionally matching a search), newest first, up to 30: each with when it was published and used_on, the channels it was shared to in the last 30 days with the latest date (empty = not shared lately; an article is free to share again 30 days after its last post).",
11333
11883
  parameters: { type: "object", properties: { search: { type: "string" } } },
11334
11884
  group: "social",
11335
11885
  need: read5,
11336
11886
  risk: "R0",
11337
- run: async (ctx, { search }) => (await ws(ctx).socialSources(search ?? "")).items.slice(0, 15).map((s) => ({ id: s.id, ...untrusted({ title: s.title }), url: s.url }))
11887
+ run: async (ctx, { search }) => (await ws(ctx).socialSources(search ?? "")).items.slice(0, 30).map((s) => ({
11888
+ id: s.id,
11889
+ ...untrusted({ title: s.title }),
11890
+ url: s.url,
11891
+ published_at: s.published_at ?? null,
11892
+ used_on: s.used_on ?? {}
11893
+ }))
11338
11894
  }),
11339
11895
  tool({
11340
11896
  name: "social_from_article",
@@ -12310,6 +12866,7 @@ var tools = [
12310
12866
  ...accountTools,
12311
12867
  ...meTools,
12312
12868
  ...reportTools,
12869
+ ...opsTools,
12313
12870
  ...insightTools,
12314
12871
  ...brandTools,
12315
12872
  ...blogTools,
@@ -15711,7 +16268,7 @@ var program = new Command();
15711
16268
  // package.json
15712
16269
  var package_default = {
15713
16270
  name: "@cmo-so/cli",
15714
- version: "0.7.6",
16271
+ version: "0.8.0",
15715
16272
  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.",
15716
16273
  type: "module",
15717
16274
  license: "MIT",
@@ -15918,6 +16475,9 @@ function print(value, json, human) {
15918
16475
  // src/mcp.ts
15919
16476
  import { randomBytes } from "node:crypto";
15920
16477
 
16478
+ // ../../packages/mcp/src/index.ts
16479
+ import { randomUUID as randomUUID2 } from "node:crypto";
16480
+
15921
16481
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/util.js
15922
16482
  var util_exports = {};
15923
16483
  __export(util_exports, {
@@ -25875,6 +26435,7 @@ var CONTROL_ARGS = ["workspace", "confirm", "confirmation"];
25875
26435
  var NOT_ON_MCP = /* @__PURE__ */ new Set(["billing_link"]);
25876
26436
  var DESTRUCTIVE = /_(remove|delete|discard|revoke)$/;
25877
26437
  var isAccountTool = (t) => t.need.resource === "account";
26438
+ var isStanding = (name) => STANDING_TOOLS.includes(name);
25878
26439
  function inputSchema(t) {
25879
26440
  const props = { ...t.parameters.properties ?? {} };
25880
26441
  props.workspace = WORKSPACE;
@@ -25943,7 +26504,9 @@ var OPEN_WORLD = /* @__PURE__ */ new Set([
25943
26504
  "seo_audit_run",
25944
26505
  "ai_visibility_prompt_check",
25945
26506
  "idea_add",
25946
- "competitor_suggestions_find"
26507
+ "competitor_suggestions_find",
26508
+ // Saving makes CMO.so fetch the sitemap from its website.
26509
+ "sitemap_upsert"
25947
26510
  ]);
25948
26511
  var OVERWRITES = /* @__PURE__ */ new Set([
25949
26512
  "ai_visibility_keywords_set",
@@ -26014,9 +26577,10 @@ function createMcpServer(deps) {
26014
26577
  { capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: INSTRUCTIONS }
26015
26578
  );
26016
26579
  server.setRequestHandler(ListToolsRequestSchema, async () => {
26017
- const keyAccess = (await me().catch(() => null))?.key?.access ?? null;
26580
+ const key = (await me().catch(() => null))?.key;
26581
+ const keyAccess = key?.access ?? null;
26018
26582
  return {
26019
- tools: offered().filter((t) => !(isWrite(t) && keyAccess === "read")).map((t) => ({
26583
+ tools: offered().filter((t) => !(isWrite(t) && keyAccess === "read")).filter((t) => !(isWrite(t) && key?.standing && !isStanding(t.name))).map((t) => ({
26020
26584
  name: t.name,
26021
26585
  title: t.name.replace(/_/g, " "),
26022
26586
  description: description(t),
@@ -26032,6 +26596,11 @@ function createMcpServer(deps) {
26032
26596
  const t = toolByName.get(name);
26033
26597
  if (!t || excluded(name)) return fail(`There is no tool called ${name}.`);
26034
26598
  try {
26599
+ const agentKey = isWrite(t) && (await me().catch(() => null))?.key?.standing === true;
26600
+ if (agentKey && (args.confirm === true || !isStanding(t.name)))
26601
+ return fail(
26602
+ "This is an agent's key: it can only make the changes its standing permission covers, and can't confirm anything itself. Leave other changes for the person."
26603
+ );
26035
26604
  let approval = null;
26036
26605
  if (isWrite(t) && args.confirm === true) {
26037
26606
  if (deps.previewOnly)
@@ -26056,6 +26625,23 @@ function createMcpServer(deps) {
26056
26625
  const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
26057
26626
  const ctx = { api: api2, domain, client: "mcp" };
26058
26627
  if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
26628
+ let standing = null;
26629
+ if (!approval && !deps.previewOnly && domain && isStanding(t.name)) {
26630
+ const claim2 = await api2.account?.claimStanding({ website: domain, tool: t.name, units: standingUnits(t.name, callArgs) }).catch(() => null);
26631
+ if (claim2?.allowed) {
26632
+ standing = claim2.permission_id;
26633
+ approval = {
26634
+ id: randomUUID2(),
26635
+ calls: [{ name: t.name, args: callArgs }],
26636
+ principal,
26637
+ domain,
26638
+ expires: Date.now()
26639
+ };
26640
+ } else if (agentKey) {
26641
+ const why = claim2?.reason === "daily_cap" ? `today's limit for ${t.name} is used up (${claim2.daily_cap})` : claim2?.reason === "tool_not_included" ? `the standing permission doesn't include ${t.name}` : claim2 ? `there is no standing permission for ${domain} (${claim2.reason})` : "the permission check failed; try again later";
26642
+ return fail(`Not done: ${why}.`);
26643
+ }
26644
+ }
26059
26645
  if (!approval) {
26060
26646
  const calls = [{ name: t.name, args: callArgs }];
26061
26647
  const summary2 = t.confirm?.(callArgs, "en") ?? t.description;
@@ -26069,7 +26655,7 @@ function createMcpServer(deps) {
26069
26655
  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."
26070
26656
  });
26071
26657
  }
26072
- const summary = t.confirm?.(callArgs, "en") ?? t.description;
26658
+ const summary = `${t.confirm?.(callArgs, "en") ?? t.description}${standing ? ` (standing permission #${standing})` : ""}`;
26073
26659
  try {
26074
26660
  const result = await t.run({ api: withIdempotency(api2, approval.id), domain, client: "mcp" }, callArgs);
26075
26661
  const added = t.name === "website_add" ? result?.domain : null;
@@ -26298,7 +26884,7 @@ async function serveStdio(version2) {
26298
26884
  }
26299
26885
 
26300
26886
  // src/run.ts
26301
- import { randomUUID as randomUUID2 } from "node:crypto";
26887
+ import { randomUUID as randomUUID3 } from "node:crypto";
26302
26888
  import { createInterface } from "node:readline/promises";
26303
26889
  var CliError = class extends Error {
26304
26890
  constructor(message, code = 2) {
@@ -26356,7 +26942,7 @@ async function cost(api2, domain, t) {
26356
26942
  return costText({ aiGenerations: ai, aiLeft, credits, creditsLeft }, "en");
26357
26943
  }
26358
26944
  function withIdempotency2(api2) {
26359
- const base = randomUUID2();
26945
+ const base = randomUUID3();
26360
26946
  let n = 0;
26361
26947
  return {
26362
26948
  ...api2,
@@ -26524,6 +27110,78 @@ program2.command("results").description("results for the last 7 or 30 days").add
26524
27110
  })
26525
27111
  )
26526
27112
  );
27113
+ program2.command("reports").description("the report inbox: Maggie's reports and your agents' reports").option("--unread", "only reports nobody has opened yet").action(
27114
+ act(
27115
+ async (opts) => tool2(opts, "reports_list", { unread: Boolean(opts.unread) }, (v) => {
27116
+ const r = v;
27117
+ return `${r.unread} unread
27118
+ ${table(
27119
+ r.items.map((i) => ({
27120
+ id: i.id,
27121
+ kind: i.kind,
27122
+ created: (i.created_at ?? "").slice(0, 16),
27123
+ title: i.untrusted?.title
27124
+ })),
27125
+ ["id", "kind", "created", "title"]
27126
+ )}`;
27127
+ })
27128
+ )
27129
+ );
27130
+ program2.command("report <id>").description("read one report (marks it read)").action(
27131
+ act(
27132
+ async (opts, id) => tool2(opts, "report_get", { id: Number(id) }, (v) => {
27133
+ const r = v;
27134
+ return `# ${r.untrusted.title}
27135
+
27136
+ ${r.untrusted.body_md ?? r.untrusted.summary ?? ""}`;
27137
+ })
27138
+ )
27139
+ );
27140
+ program2.command("reviews").description("Google Business reviews and Maggie's reply drafts").addOption(new Option("--status <status>", "none, drafted or sent").choices(["none", "drafted", "sent"])).action(
27141
+ act(
27142
+ async (opts) => tool2(opts, "gbp_reviews_list", opts.status ? { status: opts.status } : {}, (v) => {
27143
+ const r = v;
27144
+ if (!r.connected) return "No Google Business profile connected for this website.";
27145
+ return `${r.waiting} waiting for a reply
27146
+ ${table(
27147
+ r.items.map((i) => ({
27148
+ id: i.id,
27149
+ stars: i.rating ?? "",
27150
+ from: i.untrusted?.reviewer ?? "",
27151
+ reply: i.reply_status
27152
+ })),
27153
+ ["id", "stars", "from", "reply"]
27154
+ )}`;
27155
+ })
27156
+ )
27157
+ );
27158
+ var ops = program2.command("ops").description("Ops Autopilot: Maggie's daily operations on the website");
27159
+ ops.command("ls", { isDefault: true }).description("each feature, on or off, and its last run").action(
27160
+ act(
27161
+ async (opts) => tool2(opts, "ops_autopilot_get", {}, (v) => {
27162
+ const r = v;
27163
+ if (!r.available) return "Ops Autopilot isn't available for this website yet.";
27164
+ return table(
27165
+ r.features.map((f) => ({
27166
+ feature: f.feature,
27167
+ on: f.on ? "on" : "off",
27168
+ when: f.when ?? "",
27169
+ last: f.last_run ? `${f.last_run.status} ${(f.last_run.at ?? "").slice(0, 16)}` : ""
27170
+ })),
27171
+ ["feature", "on", "when", "last"]
27172
+ );
27173
+ })
27174
+ )
27175
+ );
27176
+ ops.command("set <feature> <state>").description("switch one feature on or off (e.g. cmo ops set top_up off)").action(
27177
+ act(async (opts, feature, state) => {
27178
+ if (!["on", "off"].includes(state)) throw new CliError("State is on or off.", 2);
27179
+ return tool2(opts, "ops_autopilot_set", { features: { [feature]: state === "on" } }, (v) => {
27180
+ const f = v.features.find((x) => x.feature === feature);
27181
+ return f ? `${feature}: ${f.on ? "on" : "off"}` : "Saved.";
27182
+ });
27183
+ })
27184
+ );
26527
27185
  var ideas = program2.command("ideas").description("blog post ideas");
26528
27186
  ideas.command("ls", { isDefault: true }).description("ideas waiting to be planned").option("--page <n>", "page", "1").action(act(async (opts) => tool2(opts, "ideas_list", { page: Number(opts.page) })));
26529
27187
  ideas.command("add <urlOrText...>").description("add an idea from a URL or a sentence").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "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.",
5
5
  "type": "module",
6
6
  "license": "MIT",