@cmo-so/cli 0.7.7 → 0.8.1
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.
- package/dist/cmo.mjs +527 -25
- 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
|
|
5008
|
+
var ops2 = codegen_1.operators;
|
|
5009
5009
|
var KWDs = {
|
|
5010
|
-
maximum: { okStr: "<=", ok:
|
|
5011
|
-
minimum: { okStr: ">=", ok:
|
|
5012
|
-
exclusiveMaximum: { okStr: "<", ok:
|
|
5013
|
-
exclusiveMinimum: { okStr: ">", ok:
|
|
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
|
|
7093
|
+
var ops2 = codegen_1.operators;
|
|
7094
7094
|
var KWDs = {
|
|
7095
|
-
formatMaximum: { okStr: "<=", ok:
|
|
7096
|
-
formatMinimum: { okStr: ">=", ok:
|
|
7097
|
-
formatExclusiveMaximum: { okStr: "<", ok:
|
|
7098
|
-
formatExclusiveMinimum: { okStr: ">", ok:
|
|
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,
|
|
@@ -8164,7 +8179,39 @@ 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
|
+
/** One social post in full (social_list clips its text). */
|
|
8199
|
+
socialPost: (id) => api2.request(`${base}/social/${id}`),
|
|
8200
|
+
// Ops Autopilot: ideas the weekly clean-up archived (restorable), post health, Google reviews.
|
|
8201
|
+
ideaCleanup: (limit = 50) => api2.request(`${base}/idea-cleanup?limit=${limit}`),
|
|
8202
|
+
restoreIdeaCleanup: (id) => post(`${base}/idea-cleanup/${id}/restore`, {}),
|
|
8203
|
+
postHealth: (limit = 30) => api2.request(`${base}/post-health?limit=${limit}`),
|
|
8204
|
+
googleReviews: (q2 = {}) => {
|
|
8205
|
+
const params = new URLSearchParams();
|
|
8206
|
+
if (q2.status) params.set("status", q2.status);
|
|
8207
|
+
if (q2.limit) params.set("limit", String(q2.limit));
|
|
8208
|
+
const qs = params.toString();
|
|
8209
|
+
return api2.request(
|
|
8210
|
+
`${base}/google-business/reviews${qs ? `?${qs}` : ""}`
|
|
8211
|
+
);
|
|
8212
|
+
},
|
|
8213
|
+
redraftGoogleReview: (id) => post(`${base}/google-business/reviews/${id}/draft`, {}),
|
|
8214
|
+
replyGoogleReview: (id, text3) => post(`${base}/google-business/reviews/${id}/reply`, text3 === void 0 ? {} : { text: text3 })
|
|
8168
8215
|
};
|
|
8169
8216
|
}
|
|
8170
8217
|
|
|
@@ -8355,6 +8402,13 @@ function sitemapCheckLine(check) {
|
|
|
8355
8402
|
|
|
8356
8403
|
// ../../packages/domain/src/copy/maggie.ts
|
|
8357
8404
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
8405
|
+
var CHANNEL_EN = {
|
|
8406
|
+
linkedin: "LinkedIn",
|
|
8407
|
+
facebook: "Facebook",
|
|
8408
|
+
instagram: "Instagram",
|
|
8409
|
+
google_business: "Google Business"
|
|
8410
|
+
};
|
|
8411
|
+
var CHANNEL_ZH = { ...CHANNEL_EN, google_business: "Google \u5546\u5BB6" };
|
|
8358
8412
|
var named = (title, id) => {
|
|
8359
8413
|
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
8360
8414
|
const s = String(id ?? "").trim();
|
|
@@ -8538,6 +8592,48 @@ var confirmCopy = {
|
|
|
8538
8592
|
en: (a) => a.paused ? "Pause publishing blog posts." : "Resume publishing blog posts.",
|
|
8539
8593
|
"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"
|
|
8540
8594
|
},
|
|
8595
|
+
ops_autopilot_set: {
|
|
8596
|
+
en: (a) => {
|
|
8597
|
+
const f = Object.entries(a.features ?? {});
|
|
8598
|
+
const on2 = f.filter(([, v]) => v).map(([k]) => k.replace(/_/g, " "));
|
|
8599
|
+
const off = f.filter(([, v]) => !v).map(([k]) => k.replace(/_/g, " "));
|
|
8600
|
+
const parts = [
|
|
8601
|
+
on2.length ? `turn on ${on2.join(", ")}` : "",
|
|
8602
|
+
off.length ? `turn off ${off.join(", ")}` : "",
|
|
8603
|
+
a.weekly_email === true ? "email the Monday report to the owner" : "",
|
|
8604
|
+
a.weekly_email === false ? "stop emailing the Monday report" : ""
|
|
8605
|
+
].filter(Boolean);
|
|
8606
|
+
return `Ops Autopilot: ${parts.join("; ") || "no change"}.`;
|
|
8607
|
+
},
|
|
8608
|
+
"zh-Hant": (a) => {
|
|
8609
|
+
const f = Object.entries(a.features ?? {});
|
|
8610
|
+
const on2 = f.filter(([, v]) => v).map(([k]) => k);
|
|
8611
|
+
const off = f.filter(([, v]) => !v).map(([k]) => k);
|
|
8612
|
+
const parts = [
|
|
8613
|
+
on2.length ? `\u958B\u555F ${on2.join("\u3001")}` : "",
|
|
8614
|
+
off.length ? `\u95DC\u9589 ${off.join("\u3001")}` : "",
|
|
8615
|
+
a.weekly_email === true ? "\u628A\u9031\u4E00\u5831\u544A\u5BC4\u7D66\u7DB2\u7AD9\u64C1\u6709\u8005" : "",
|
|
8616
|
+
a.weekly_email === false ? "\u4E0D\u518D\u5BC4\u9031\u4E00\u5831\u544A" : ""
|
|
8617
|
+
].filter(Boolean);
|
|
8618
|
+
return `Ops Autopilot\uFF1A${parts.join("\uFF1B") || "\u6C92\u6709\u8B8A\u66F4"}\u3002`;
|
|
8619
|
+
}
|
|
8620
|
+
},
|
|
8621
|
+
idea_restore_cleanup: {
|
|
8622
|
+
en: (a) => `Bring back the archived idea #${a.id} to the waiting list.`,
|
|
8623
|
+
"zh-Hant": (a) => `\u628A\u5DF2\u6B78\u6A94\u7684 idea #${a.id} \u653E\u56DE\u7B49\u5F85\u5217\u8868\u3002`
|
|
8624
|
+
},
|
|
8625
|
+
gbp_review_draft: {
|
|
8626
|
+
en: (a) => `Write a new reply draft for Google review #${a.id} (one AI generation). Nothing is posted.`,
|
|
8627
|
+
"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`
|
|
8628
|
+
},
|
|
8629
|
+
gbp_review_reply: {
|
|
8630
|
+
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.`,
|
|
8631
|
+
"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`
|
|
8632
|
+
},
|
|
8633
|
+
report_save: {
|
|
8634
|
+
en: (a) => `Save the report ${q(a.title)} to the website's inbox (the owner gets a notification).`,
|
|
8635
|
+
"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`
|
|
8636
|
+
},
|
|
8541
8637
|
posts_per_day_set: {
|
|
8542
8638
|
en: (a) => `Publish ${a.posts_per_day} blog post${Number(a.posts_per_day) === 1 ? "" : "s"} a day.`,
|
|
8543
8639
|
"zh-Hant": (a) => `\u6BCF\u5929\u767C\u4F48 ${a.posts_per_day} \u7BC7\u90E8\u843D\u683C\u6587\u7AE0\u3002`
|
|
@@ -8712,8 +8808,8 @@ var confirmCopy = {
|
|
|
8712
8808
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8713
8809
|
},
|
|
8714
8810
|
social_approve_all: {
|
|
8715
|
-
en: () =>
|
|
8716
|
-
"zh-Hant": () =>
|
|
8811
|
+
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.`,
|
|
8812
|
+
"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`
|
|
8717
8813
|
},
|
|
8718
8814
|
series_create: {
|
|
8719
8815
|
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.`,
|
|
@@ -8782,6 +8878,18 @@ var confirmCopy = {
|
|
|
8782
8878
|
en: (a) => `Disconnect ${q(a.app)} from your account. It stops working now and has to be connected again.`,
|
|
8783
8879
|
"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`
|
|
8784
8880
|
},
|
|
8881
|
+
standing_permission_set: {
|
|
8882
|
+
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(
|
|
8883
|
+
", "
|
|
8884
|
+
)} on ${a.website} without asking you first${a.days ? `, for ${a.days} days` : ", until you revoke it"}. Every run is logged.`,
|
|
8885
|
+
"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(
|
|
8886
|
+
a.tools ?? {}
|
|
8887
|
+
).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`
|
|
8888
|
+
},
|
|
8889
|
+
standing_permission_revoke: {
|
|
8890
|
+
en: (a) => `End the standing permission of the key ${q(a.key_name)}. Its writes go back to asking you first.`,
|
|
8891
|
+
"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`
|
|
8892
|
+
},
|
|
8785
8893
|
profile_update: {
|
|
8786
8894
|
en: (a) => `Change your name to ${q(a.display_name)}.`,
|
|
8787
8895
|
"zh-Hant": (a) => `\u628A\u4F60\u7684\u540D\u5B57\u6539\u6210 ${q(a.display_name)}\u3002`
|
|
@@ -8950,7 +9058,12 @@ var accountTools = [
|
|
|
8950
9058
|
group: "workspace",
|
|
8951
9059
|
need: { resource: "overview", level: "read" },
|
|
8952
9060
|
risk: "R0",
|
|
8953
|
-
run: async (ctx) => (await ws(ctx).inbox()).items.map(
|
|
9061
|
+
run: async (ctx) => (await ws(ctx).inbox()).items.map(
|
|
9062
|
+
(i) => (
|
|
9063
|
+
// A report's title and summary may come from an agent: data, never instructions.
|
|
9064
|
+
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) }
|
|
9065
|
+
)
|
|
9066
|
+
)
|
|
8954
9067
|
}),
|
|
8955
9068
|
tool({
|
|
8956
9069
|
name: "setup_status_get",
|
|
@@ -10290,6 +10403,11 @@ var insightTools = [
|
|
|
10290
10403
|
|
|
10291
10404
|
// ../../packages/tools/src/me.ts
|
|
10292
10405
|
var account2 = { resource: "account", level: "read" };
|
|
10406
|
+
var STANDING_TOOLS = ["idea_add", "ideas_add_list", "idea_priority", "rewrite_request"];
|
|
10407
|
+
function standingUnits(name, args) {
|
|
10408
|
+
if (name === "ideas_add_list") return Math.max(1, Array.isArray(args.ideas) ? args.ideas.length : 1);
|
|
10409
|
+
return 1;
|
|
10410
|
+
}
|
|
10293
10411
|
var meTools = [
|
|
10294
10412
|
tool({
|
|
10295
10413
|
name: "api_keys_list",
|
|
@@ -10332,6 +10450,54 @@ var meTools = [
|
|
|
10332
10450
|
risk: "W3",
|
|
10333
10451
|
run: async ({ api: api2 }, { grant_id }) => api2.account.revokeApp(String(grant_id))
|
|
10334
10452
|
}),
|
|
10453
|
+
tool({
|
|
10454
|
+
name: "standing_permissions_list",
|
|
10455
|
+
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.",
|
|
10456
|
+
parameters: none,
|
|
10457
|
+
group: "account",
|
|
10458
|
+
need: account2,
|
|
10459
|
+
risk: "R0",
|
|
10460
|
+
run: async ({ api: api2 }) => api2.account.standing()
|
|
10461
|
+
}),
|
|
10462
|
+
tool({
|
|
10463
|
+
name: "standing_permission_set",
|
|
10464
|
+
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.`,
|
|
10465
|
+
parameters: {
|
|
10466
|
+
type: "object",
|
|
10467
|
+
properties: {
|
|
10468
|
+
key_id: { type: "integer" },
|
|
10469
|
+
key_name: { type: "string", description: "The key's name, for the card" },
|
|
10470
|
+
website: { type: "string", description: "The website (domain)" },
|
|
10471
|
+
tools: {
|
|
10472
|
+
type: "object",
|
|
10473
|
+
description: "Daily cap per tool; include only the tools to allow (0 or empty for the default).",
|
|
10474
|
+
properties: Object.fromEntries(STANDING_TOOLS.map((name) => [name, { type: "integer" }]))
|
|
10475
|
+
},
|
|
10476
|
+
days: { type: "integer", description: "How long it lasts (1-366); leave out for until revoked" }
|
|
10477
|
+
},
|
|
10478
|
+
required: ["key_id", "key_name", "website", "tools"]
|
|
10479
|
+
},
|
|
10480
|
+
group: "account",
|
|
10481
|
+
need: account2,
|
|
10482
|
+
risk: "W3",
|
|
10483
|
+
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 })
|
|
10484
|
+
}),
|
|
10485
|
+
tool({
|
|
10486
|
+
name: "standing_permission_revoke",
|
|
10487
|
+
description: "End a standing permission (from standing_permissions_list): its key goes back to previews for every write.",
|
|
10488
|
+
parameters: {
|
|
10489
|
+
type: "object",
|
|
10490
|
+
properties: {
|
|
10491
|
+
id: { type: "integer" },
|
|
10492
|
+
key_name: { type: "string", description: "The key's name, for the card" }
|
|
10493
|
+
},
|
|
10494
|
+
required: ["id", "key_name"]
|
|
10495
|
+
},
|
|
10496
|
+
group: "account",
|
|
10497
|
+
need: account2,
|
|
10498
|
+
risk: "W2",
|
|
10499
|
+
run: async ({ api: api2 }, { id }) => api2.account.revokeStanding(Number(id))
|
|
10500
|
+
}),
|
|
10335
10501
|
tool({
|
|
10336
10502
|
name: "invoices_list",
|
|
10337
10503
|
description: "The user's latest invoices across the subscriptions they pay for: date, amount, status and a link to view or download each.",
|
|
@@ -10472,6 +10638,236 @@ var memoryTools = [
|
|
|
10472
10638
|
];
|
|
10473
10639
|
var MEMORY_TOOLS = new Set(memoryTools.map((t) => t.name));
|
|
10474
10640
|
|
|
10641
|
+
// ../../packages/tools/src/ops.ts
|
|
10642
|
+
var overview = { resource: "overview", level: "read" };
|
|
10643
|
+
var autopilotWrite = { resource: "autopilot", level: "write" };
|
|
10644
|
+
var reportWrite = { resource: "report", level: "write" };
|
|
10645
|
+
var OPS_FEATURES = [
|
|
10646
|
+
"top_up",
|
|
10647
|
+
"precheck",
|
|
10648
|
+
"retry",
|
|
10649
|
+
"weekly_report",
|
|
10650
|
+
"gsc_ideas",
|
|
10651
|
+
"ai_visibility",
|
|
10652
|
+
"idea_cleanup",
|
|
10653
|
+
"competitor_watch",
|
|
10654
|
+
"insight_memory",
|
|
10655
|
+
"post_health",
|
|
10656
|
+
"gbp_reviews"
|
|
10657
|
+
];
|
|
10658
|
+
var opsTools = [
|
|
10659
|
+
tool({
|
|
10660
|
+
name: "ops_autopilot_get",
|
|
10661
|
+
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).",
|
|
10662
|
+
parameters: none,
|
|
10663
|
+
group: "workspace",
|
|
10664
|
+
need: overview,
|
|
10665
|
+
risk: "R0",
|
|
10666
|
+
run: async (ctx) => ws(ctx).opsAutopilot()
|
|
10667
|
+
}),
|
|
10668
|
+
tool({
|
|
10669
|
+
name: "ops_autopilot_set",
|
|
10670
|
+
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.",
|
|
10671
|
+
parameters: {
|
|
10672
|
+
type: "object",
|
|
10673
|
+
properties: {
|
|
10674
|
+
features: {
|
|
10675
|
+
type: "object",
|
|
10676
|
+
description: `Feature \u2192 true (on) or false (off). Features: ${OPS_FEATURES.join(", ")}`,
|
|
10677
|
+
properties: Object.fromEntries(OPS_FEATURES.map((f) => [f, { type: "boolean" }]))
|
|
10678
|
+
},
|
|
10679
|
+
weekly_email: { type: "boolean", description: "Also email the Monday report to the website's owner" }
|
|
10680
|
+
}
|
|
10681
|
+
},
|
|
10682
|
+
group: "workspace",
|
|
10683
|
+
need: autopilotWrite,
|
|
10684
|
+
risk: "W2",
|
|
10685
|
+
idempotent: true,
|
|
10686
|
+
run: async (ctx, { features, weekly_email }) => {
|
|
10687
|
+
const known = Object.fromEntries(
|
|
10688
|
+
Object.entries(features ?? {}).filter(([k]) => OPS_FEATURES.includes(k)).map(([k, v]) => [k, Boolean(v)])
|
|
10689
|
+
);
|
|
10690
|
+
return ws(ctx).setOpsAutopilot({
|
|
10691
|
+
...Object.keys(known).length ? { features: known } : {},
|
|
10692
|
+
...weekly_email === void 0 ? {} : { weekly_email: Boolean(weekly_email) }
|
|
10693
|
+
});
|
|
10694
|
+
}
|
|
10695
|
+
}),
|
|
10696
|
+
tool({
|
|
10697
|
+
name: "reports_list",
|
|
10698
|
+
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.",
|
|
10699
|
+
parameters: {
|
|
10700
|
+
type: "object",
|
|
10701
|
+
properties: {
|
|
10702
|
+
kind: {
|
|
10703
|
+
type: "string",
|
|
10704
|
+
enum: ["weekly", "daily_ops", "agent", "post_health", "reviews", "ai_visibility", "search"]
|
|
10705
|
+
},
|
|
10706
|
+
unread: { type: "boolean", description: "Only reports nobody has opened yet" },
|
|
10707
|
+
limit: { type: "integer", minimum: 1, maximum: 50 }
|
|
10708
|
+
}
|
|
10709
|
+
},
|
|
10710
|
+
group: "reports",
|
|
10711
|
+
need: overview,
|
|
10712
|
+
risk: "R0",
|
|
10713
|
+
run: async (ctx, { kind, unread, limit }) => {
|
|
10714
|
+
const r = await ws(ctx).reports({ kind, unread, limit: int(limit, 1, 50, 20) });
|
|
10715
|
+
return {
|
|
10716
|
+
unread: r.unread,
|
|
10717
|
+
items: r.items.map(({ title, summary, ...rest }) => ({ ...rest, ...untrusted({ title, summary }) }))
|
|
10718
|
+
};
|
|
10719
|
+
}
|
|
10720
|
+
}),
|
|
10721
|
+
tool({
|
|
10722
|
+
name: "report_get",
|
|
10723
|
+
description: "One report in full (its text in Markdown and any data); marks it read.",
|
|
10724
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10725
|
+
group: "reports",
|
|
10726
|
+
need: overview,
|
|
10727
|
+
risk: "R0",
|
|
10728
|
+
run: async (ctx, { id }) => {
|
|
10729
|
+
const r = await ws(ctx).inboxReport(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
|
|
10730
|
+
const { title, summary, body_md, data, ...rest } = r;
|
|
10731
|
+
return { ...rest, ...untrusted({ title, summary, body_md, data }) };
|
|
10732
|
+
}
|
|
10733
|
+
}),
|
|
10734
|
+
tool({
|
|
10735
|
+
name: "report_save",
|
|
10736
|
+
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.",
|
|
10737
|
+
parameters: {
|
|
10738
|
+
type: "object",
|
|
10739
|
+
properties: {
|
|
10740
|
+
title: { type: "string", description: "Short, e.g. 'Daily analysis, 22 Oct'" },
|
|
10741
|
+
summary: { type: "string", description: "One or two sentences (up to 500 characters) shown in the Inbox" },
|
|
10742
|
+
body_md: { type: "string", description: "The full report in Markdown" }
|
|
10743
|
+
},
|
|
10744
|
+
required: ["title"]
|
|
10745
|
+
},
|
|
10746
|
+
group: "reports",
|
|
10747
|
+
need: reportWrite,
|
|
10748
|
+
risk: "W1",
|
|
10749
|
+
run: async (ctx, { title, summary, body_md }) => ws(ctx).saveReport({
|
|
10750
|
+
title: String(title ?? "").slice(0, 200),
|
|
10751
|
+
...summary ? { summary: String(summary).slice(0, 500) } : {},
|
|
10752
|
+
...body_md ? { body_md: String(body_md).slice(0, 5e4) } : {}
|
|
10753
|
+
})
|
|
10754
|
+
}),
|
|
10755
|
+
tool({
|
|
10756
|
+
name: "idea_cleanup_list",
|
|
10757
|
+
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.",
|
|
10758
|
+
parameters: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 200 } } },
|
|
10759
|
+
group: "blog",
|
|
10760
|
+
need: { resource: "autopilot", level: "read" },
|
|
10761
|
+
risk: "R0",
|
|
10762
|
+
run: async (ctx, { limit }) => {
|
|
10763
|
+
const r = await ws(ctx).ideaCleanup(int(limit, 1, 200, 50));
|
|
10764
|
+
return { items: r.items.map(({ reason, ...rest }) => ({ ...rest, ...untrusted({ reason }) })) };
|
|
10765
|
+
}
|
|
10766
|
+
}),
|
|
10767
|
+
tool({
|
|
10768
|
+
name: "idea_restore_cleanup",
|
|
10769
|
+
description: "Bring an idea the weekly clean-up archived back to the waiting list (id from idea_cleanup_list).",
|
|
10770
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10771
|
+
group: "blog",
|
|
10772
|
+
need: autopilotWrite,
|
|
10773
|
+
risk: "W1",
|
|
10774
|
+
idempotent: true,
|
|
10775
|
+
run: async (ctx, { id }) => ws(ctx).restoreIdeaCleanup(int(id, 1, Number.MAX_SAFE_INTEGER, 0))
|
|
10776
|
+
}),
|
|
10777
|
+
tool({
|
|
10778
|
+
name: "post_health_get",
|
|
10779
|
+
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.",
|
|
10780
|
+
parameters: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 100 } } },
|
|
10781
|
+
group: "reports",
|
|
10782
|
+
need: { resource: "report", level: "read" },
|
|
10783
|
+
risk: "R0",
|
|
10784
|
+
run: async (ctx, { limit }) => ws(ctx).postHealth(int(limit, 1, 100, 30))
|
|
10785
|
+
}),
|
|
10786
|
+
tool({
|
|
10787
|
+
name: "gbp_reviews_list",
|
|
10788
|
+
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.",
|
|
10789
|
+
parameters: {
|
|
10790
|
+
type: "object",
|
|
10791
|
+
properties: {
|
|
10792
|
+
status: { type: "string", enum: ["none", "drafted", "sent", "failed"] },
|
|
10793
|
+
limit: { type: "integer", minimum: 1, maximum: 100 }
|
|
10794
|
+
}
|
|
10795
|
+
},
|
|
10796
|
+
group: "social",
|
|
10797
|
+
need: { resource: "autopilot", level: "read" },
|
|
10798
|
+
risk: "R0",
|
|
10799
|
+
run: async (ctx, { status, limit }) => {
|
|
10800
|
+
const r = await ws(ctx).googleReviews({ status, limit: int(limit, 1, 100, 30) });
|
|
10801
|
+
return {
|
|
10802
|
+
connected: r.connected,
|
|
10803
|
+
waiting: r.waiting,
|
|
10804
|
+
items: r.items.map(({ reviewer, comment, ...rest }) => ({ ...rest, ...untrusted({ reviewer, comment }) }))
|
|
10805
|
+
};
|
|
10806
|
+
}
|
|
10807
|
+
}),
|
|
10808
|
+
tool({
|
|
10809
|
+
name: "gbp_review_draft",
|
|
10810
|
+
description: "Ask Maggie for a new reply draft to one Google review (one AI generation). Nothing is posted.",
|
|
10811
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10812
|
+
group: "social",
|
|
10813
|
+
need: autopilotWrite,
|
|
10814
|
+
risk: "W2",
|
|
10815
|
+
gates: ["LLM"],
|
|
10816
|
+
run: async (ctx, { id }) => {
|
|
10817
|
+
const r = await ws(ctx).redraftGoogleReview(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
|
|
10818
|
+
const { reviewer, comment, ...rest } = r;
|
|
10819
|
+
return { ...rest, ...untrusted({ reviewer, comment }) };
|
|
10820
|
+
}
|
|
10821
|
+
}),
|
|
10822
|
+
tool({
|
|
10823
|
+
name: "gbp_review_reply",
|
|
10824
|
+
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.",
|
|
10825
|
+
parameters: {
|
|
10826
|
+
type: "object",
|
|
10827
|
+
properties: {
|
|
10828
|
+
id: { type: "integer" },
|
|
10829
|
+
text: { type: "string", description: "The reply; leave out to send the draft" }
|
|
10830
|
+
},
|
|
10831
|
+
required: ["id"]
|
|
10832
|
+
},
|
|
10833
|
+
group: "social",
|
|
10834
|
+
need: autopilotWrite,
|
|
10835
|
+
risk: "W3",
|
|
10836
|
+
run: async (ctx, { id, text: text3 }) => {
|
|
10837
|
+
const r = await ws(ctx).replyGoogleReview(
|
|
10838
|
+
int(id, 1, Number.MAX_SAFE_INTEGER, 0),
|
|
10839
|
+
text3 === void 0 ? void 0 : String(text3).slice(0, 1e3)
|
|
10840
|
+
);
|
|
10841
|
+
const { reviewer, comment, ...rest } = r;
|
|
10842
|
+
return { ...rest, ...untrusted({ reviewer, comment }) };
|
|
10843
|
+
}
|
|
10844
|
+
}),
|
|
10845
|
+
tool({
|
|
10846
|
+
name: "social_post_get",
|
|
10847
|
+
description: "One social post in full: its whole text (social_list shortens it to about 300 characters), its pictures, its link, its channel and status, when it goes out, and why it was held before going out, if it was. Use it to check a post's ending (a web address or phone number on Google, a cut-off sentence) before approving or fixing it.",
|
|
10848
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10849
|
+
group: "social",
|
|
10850
|
+
need: { resource: "autopilot", level: "read" },
|
|
10851
|
+
risk: "R0",
|
|
10852
|
+
run: async (ctx, { id }) => {
|
|
10853
|
+
const p = await ws(ctx).socialPost(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
|
|
10854
|
+
return {
|
|
10855
|
+
id: p.id,
|
|
10856
|
+
channel: p.provider,
|
|
10857
|
+
status: p.status,
|
|
10858
|
+
eta: p.eta ?? null,
|
|
10859
|
+
text: p.social_copy,
|
|
10860
|
+
characters: String(p.social_copy ?? "").length,
|
|
10861
|
+
picture: p.image_url ?? null,
|
|
10862
|
+
pictures: p.image_urls ?? [],
|
|
10863
|
+
link: p.cta_url ?? p.source_url ?? null,
|
|
10864
|
+
held: p.held ?? null,
|
|
10865
|
+
failure: p.failure_message ?? null
|
|
10866
|
+
};
|
|
10867
|
+
}
|
|
10868
|
+
})
|
|
10869
|
+
];
|
|
10870
|
+
|
|
10475
10871
|
// ../../packages/tools/src/publishing.ts
|
|
10476
10872
|
var read3 = { resource: "autopilot", level: "read" };
|
|
10477
10873
|
var write4 = { resource: "autopilot", level: "write" };
|
|
@@ -11509,12 +11905,18 @@ var socialTools = [
|
|
|
11509
11905
|
}),
|
|
11510
11906
|
tool({
|
|
11511
11907
|
name: "social_sources_list",
|
|
11512
|
-
description: "Published articles a social post can be made from (optionally matching a search).",
|
|
11908
|
+
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).",
|
|
11513
11909
|
parameters: { type: "object", properties: { search: { type: "string" } } },
|
|
11514
11910
|
group: "social",
|
|
11515
11911
|
need: read5,
|
|
11516
11912
|
risk: "R0",
|
|
11517
|
-
run: async (ctx, { search }) => (await ws(ctx).socialSources(search ?? "")).items.slice(0,
|
|
11913
|
+
run: async (ctx, { search }) => (await ws(ctx).socialSources(search ?? "")).items.slice(0, 30).map((s) => ({
|
|
11914
|
+
id: s.id,
|
|
11915
|
+
...untrusted({ title: s.title }),
|
|
11916
|
+
url: s.url,
|
|
11917
|
+
published_at: s.published_at ?? null,
|
|
11918
|
+
used_on: s.used_on ?? {}
|
|
11919
|
+
}))
|
|
11518
11920
|
}),
|
|
11519
11921
|
tool({
|
|
11520
11922
|
name: "social_from_article",
|
|
@@ -12490,6 +12892,7 @@ var tools = [
|
|
|
12490
12892
|
...accountTools,
|
|
12491
12893
|
...meTools,
|
|
12492
12894
|
...reportTools,
|
|
12895
|
+
...opsTools,
|
|
12493
12896
|
...insightTools,
|
|
12494
12897
|
...brandTools,
|
|
12495
12898
|
...blogTools,
|
|
@@ -15891,7 +16294,7 @@ var program = new Command();
|
|
|
15891
16294
|
// package.json
|
|
15892
16295
|
var package_default = {
|
|
15893
16296
|
name: "@cmo-so/cli",
|
|
15894
|
-
version: "0.
|
|
16297
|
+
version: "0.8.1",
|
|
15895
16298
|
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.",
|
|
15896
16299
|
type: "module",
|
|
15897
16300
|
license: "MIT",
|
|
@@ -16098,6 +16501,9 @@ function print(value, json, human) {
|
|
|
16098
16501
|
// src/mcp.ts
|
|
16099
16502
|
import { randomBytes } from "node:crypto";
|
|
16100
16503
|
|
|
16504
|
+
// ../../packages/mcp/src/index.ts
|
|
16505
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16506
|
+
|
|
16101
16507
|
// ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/util.js
|
|
16102
16508
|
var util_exports = {};
|
|
16103
16509
|
__export(util_exports, {
|
|
@@ -26055,6 +26461,7 @@ var CONTROL_ARGS = ["workspace", "confirm", "confirmation"];
|
|
|
26055
26461
|
var NOT_ON_MCP = /* @__PURE__ */ new Set(["billing_link"]);
|
|
26056
26462
|
var DESTRUCTIVE = /_(remove|delete|discard|revoke)$/;
|
|
26057
26463
|
var isAccountTool = (t) => t.need.resource === "account";
|
|
26464
|
+
var isStanding = (name) => STANDING_TOOLS.includes(name);
|
|
26058
26465
|
function inputSchema(t) {
|
|
26059
26466
|
const props = { ...t.parameters.properties ?? {} };
|
|
26060
26467
|
props.workspace = WORKSPACE;
|
|
@@ -26196,9 +26603,10 @@ function createMcpServer(deps) {
|
|
|
26196
26603
|
{ capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: INSTRUCTIONS }
|
|
26197
26604
|
);
|
|
26198
26605
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
26199
|
-
const
|
|
26606
|
+
const key = (await me().catch(() => null))?.key;
|
|
26607
|
+
const keyAccess = key?.access ?? null;
|
|
26200
26608
|
return {
|
|
26201
|
-
tools: offered().filter((t) => !(isWrite(t) && keyAccess === "read")).map((t) => ({
|
|
26609
|
+
tools: offered().filter((t) => !(isWrite(t) && keyAccess === "read")).filter((t) => !(isWrite(t) && key?.standing && !isStanding(t.name))).map((t) => ({
|
|
26202
26610
|
name: t.name,
|
|
26203
26611
|
title: t.name.replace(/_/g, " "),
|
|
26204
26612
|
description: description(t),
|
|
@@ -26214,6 +26622,11 @@ function createMcpServer(deps) {
|
|
|
26214
26622
|
const t = toolByName.get(name);
|
|
26215
26623
|
if (!t || excluded(name)) return fail(`There is no tool called ${name}.`);
|
|
26216
26624
|
try {
|
|
26625
|
+
const agentKey = isWrite(t) && (await me().catch(() => null))?.key?.standing === true;
|
|
26626
|
+
if (agentKey && (args.confirm === true || !isStanding(t.name)))
|
|
26627
|
+
return fail(
|
|
26628
|
+
"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."
|
|
26629
|
+
);
|
|
26217
26630
|
let approval = null;
|
|
26218
26631
|
if (isWrite(t) && args.confirm === true) {
|
|
26219
26632
|
if (deps.previewOnly)
|
|
@@ -26238,6 +26651,23 @@ function createMcpServer(deps) {
|
|
|
26238
26651
|
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
26239
26652
|
const ctx = { api: api2, domain, client: "mcp" };
|
|
26240
26653
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
26654
|
+
let standing = null;
|
|
26655
|
+
if (!approval && !deps.previewOnly && domain && isStanding(t.name)) {
|
|
26656
|
+
const claim2 = await api2.account?.claimStanding({ website: domain, tool: t.name, units: standingUnits(t.name, callArgs) }).catch(() => null);
|
|
26657
|
+
if (claim2?.allowed) {
|
|
26658
|
+
standing = claim2.permission_id;
|
|
26659
|
+
approval = {
|
|
26660
|
+
id: randomUUID2(),
|
|
26661
|
+
calls: [{ name: t.name, args: callArgs }],
|
|
26662
|
+
principal,
|
|
26663
|
+
domain,
|
|
26664
|
+
expires: Date.now()
|
|
26665
|
+
};
|
|
26666
|
+
} else if (agentKey) {
|
|
26667
|
+
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";
|
|
26668
|
+
return fail(`Not done: ${why}.`);
|
|
26669
|
+
}
|
|
26670
|
+
}
|
|
26241
26671
|
if (!approval) {
|
|
26242
26672
|
const calls = [{ name: t.name, args: callArgs }];
|
|
26243
26673
|
const summary2 = t.confirm?.(callArgs, "en") ?? t.description;
|
|
@@ -26251,7 +26681,7 @@ function createMcpServer(deps) {
|
|
|
26251
26681
|
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."
|
|
26252
26682
|
});
|
|
26253
26683
|
}
|
|
26254
|
-
const summary = t.confirm?.(callArgs, "en") ?? t.description
|
|
26684
|
+
const summary = `${t.confirm?.(callArgs, "en") ?? t.description}${standing ? ` (standing permission #${standing})` : ""}`;
|
|
26255
26685
|
try {
|
|
26256
26686
|
const result = await t.run({ api: withIdempotency(api2, approval.id), domain, client: "mcp" }, callArgs);
|
|
26257
26687
|
const added = t.name === "website_add" ? result?.domain : null;
|
|
@@ -26480,7 +26910,7 @@ async function serveStdio(version2) {
|
|
|
26480
26910
|
}
|
|
26481
26911
|
|
|
26482
26912
|
// src/run.ts
|
|
26483
|
-
import { randomUUID as
|
|
26913
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
26484
26914
|
import { createInterface } from "node:readline/promises";
|
|
26485
26915
|
var CliError = class extends Error {
|
|
26486
26916
|
constructor(message, code = 2) {
|
|
@@ -26538,7 +26968,7 @@ async function cost(api2, domain, t) {
|
|
|
26538
26968
|
return costText({ aiGenerations: ai, aiLeft, credits, creditsLeft }, "en");
|
|
26539
26969
|
}
|
|
26540
26970
|
function withIdempotency2(api2) {
|
|
26541
|
-
const base =
|
|
26971
|
+
const base = randomUUID3();
|
|
26542
26972
|
let n = 0;
|
|
26543
26973
|
return {
|
|
26544
26974
|
...api2,
|
|
@@ -26706,6 +27136,78 @@ program2.command("results").description("results for the last 7 or 30 days").add
|
|
|
26706
27136
|
})
|
|
26707
27137
|
)
|
|
26708
27138
|
);
|
|
27139
|
+
program2.command("reports").description("the report inbox: Maggie's reports and your agents' reports").option("--unread", "only reports nobody has opened yet").action(
|
|
27140
|
+
act(
|
|
27141
|
+
async (opts) => tool2(opts, "reports_list", { unread: Boolean(opts.unread) }, (v) => {
|
|
27142
|
+
const r = v;
|
|
27143
|
+
return `${r.unread} unread
|
|
27144
|
+
${table(
|
|
27145
|
+
r.items.map((i) => ({
|
|
27146
|
+
id: i.id,
|
|
27147
|
+
kind: i.kind,
|
|
27148
|
+
created: (i.created_at ?? "").slice(0, 16),
|
|
27149
|
+
title: i.untrusted?.title
|
|
27150
|
+
})),
|
|
27151
|
+
["id", "kind", "created", "title"]
|
|
27152
|
+
)}`;
|
|
27153
|
+
})
|
|
27154
|
+
)
|
|
27155
|
+
);
|
|
27156
|
+
program2.command("report <id>").description("read one report (marks it read)").action(
|
|
27157
|
+
act(
|
|
27158
|
+
async (opts, id) => tool2(opts, "report_get", { id: Number(id) }, (v) => {
|
|
27159
|
+
const r = v;
|
|
27160
|
+
return `# ${r.untrusted.title}
|
|
27161
|
+
|
|
27162
|
+
${r.untrusted.body_md ?? r.untrusted.summary ?? ""}`;
|
|
27163
|
+
})
|
|
27164
|
+
)
|
|
27165
|
+
);
|
|
27166
|
+
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(
|
|
27167
|
+
act(
|
|
27168
|
+
async (opts) => tool2(opts, "gbp_reviews_list", opts.status ? { status: opts.status } : {}, (v) => {
|
|
27169
|
+
const r = v;
|
|
27170
|
+
if (!r.connected) return "No Google Business profile connected for this website.";
|
|
27171
|
+
return `${r.waiting} waiting for a reply
|
|
27172
|
+
${table(
|
|
27173
|
+
r.items.map((i) => ({
|
|
27174
|
+
id: i.id,
|
|
27175
|
+
stars: i.rating ?? "",
|
|
27176
|
+
from: i.untrusted?.reviewer ?? "",
|
|
27177
|
+
reply: i.reply_status
|
|
27178
|
+
})),
|
|
27179
|
+
["id", "stars", "from", "reply"]
|
|
27180
|
+
)}`;
|
|
27181
|
+
})
|
|
27182
|
+
)
|
|
27183
|
+
);
|
|
27184
|
+
var ops = program2.command("ops").description("Ops Autopilot: Maggie's daily operations on the website");
|
|
27185
|
+
ops.command("ls", { isDefault: true }).description("each feature, on or off, and its last run").action(
|
|
27186
|
+
act(
|
|
27187
|
+
async (opts) => tool2(opts, "ops_autopilot_get", {}, (v) => {
|
|
27188
|
+
const r = v;
|
|
27189
|
+
if (!r.available) return "Ops Autopilot isn't available for this website yet.";
|
|
27190
|
+
return table(
|
|
27191
|
+
r.features.map((f) => ({
|
|
27192
|
+
feature: f.feature,
|
|
27193
|
+
on: f.on ? "on" : "off",
|
|
27194
|
+
when: f.when ?? "",
|
|
27195
|
+
last: f.last_run ? `${f.last_run.status} ${(f.last_run.at ?? "").slice(0, 16)}` : ""
|
|
27196
|
+
})),
|
|
27197
|
+
["feature", "on", "when", "last"]
|
|
27198
|
+
);
|
|
27199
|
+
})
|
|
27200
|
+
)
|
|
27201
|
+
);
|
|
27202
|
+
ops.command("set <feature> <state>").description("switch one feature on or off (e.g. cmo ops set top_up off)").action(
|
|
27203
|
+
act(async (opts, feature, state) => {
|
|
27204
|
+
if (!["on", "off"].includes(state)) throw new CliError("State is on or off.", 2);
|
|
27205
|
+
return tool2(opts, "ops_autopilot_set", { features: { [feature]: state === "on" } }, (v) => {
|
|
27206
|
+
const f = v.features.find((x) => x.feature === feature);
|
|
27207
|
+
return f ? `${feature}: ${f.on ? "on" : "off"}` : "Saved.";
|
|
27208
|
+
});
|
|
27209
|
+
})
|
|
27210
|
+
);
|
|
26709
27211
|
var ideas = program2.command("ideas").description("blog post ideas");
|
|
26710
27212
|
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) })));
|
|
26711
27213
|
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.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|