@cmo-so/cli 0.9.0 → 0.9.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.
Files changed (2) hide show
  1. package/dist/cmo.mjs +86 -16
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -8778,11 +8778,11 @@ var confirmCopy = {
8778
8778
  },
8779
8779
  social_pictures_attach: {
8780
8780
  en: (a) => {
8781
- const n = Array.isArray(a.urls) ? a.urls.length : 0;
8781
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8782
8782
  return `${a.append ? "Add" : "Put"} your ${n} picture${n === 1 ? "" : "s"} ${a.append ? "after the pictures on" : "on"} post #${a.id}${a.append ? "" : ", in place of its picture"} (free). On LinkedIn a set goes out as a swipeable PDF carousel.`;
8783
8783
  },
8784
8784
  "zh-Hant": (a) => {
8785
- const n = Array.isArray(a.urls) ? a.urls.length : 0;
8785
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8786
8786
  return `${a.append ? `\u5728\u8CBC\u6587 #${a.id} \u73FE\u6709\u5716\u7247\u5F8C\u52A0\u4E0A` : `\u628A\u8CBC\u6587 #${a.id} \u7684\u5716\u7247\u63DB\u6210`}\u4F60\u7684 ${n} \u5F35\u5716\u7247\uFF08\u514D\u8CBB\uFF09\u3002\u5728 LinkedIn \u4E0A\uFF0C\u4E00\u7D44\u5716\u7247\u6703\u4EE5\u53EF\u6ED1\u52D5\u7684 PDF \u8F2A\u64AD\u767C\u51FA\u3002`;
8787
8787
  }
8788
8788
  },
@@ -11553,6 +11553,17 @@ var channelsParam = {
11553
11553
  items: { type: "string", enum: CHANNEL_KEYS },
11554
11554
  description: "Which channels to write for: linkedin, facebook, instagram, google_business. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it. YouTube and TikTok take a video: use social_video_add."
11555
11555
  };
11556
+ var FILE_SCHEMA = {
11557
+ type: "object",
11558
+ properties: {
11559
+ download_url: { type: "string" },
11560
+ file_id: { type: "string" },
11561
+ mime_type: { type: "string" },
11562
+ file_name: { type: "string" }
11563
+ },
11564
+ required: ["download_url", "file_id"]
11565
+ };
11566
+ var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
11556
11567
  var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
11557
11568
  var channelsOf = (value) => {
11558
11569
  const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
@@ -11922,27 +11933,35 @@ var socialTools = [
11922
11933
  }),
11923
11934
  tool({
11924
11935
  name: "social_pictures_attach",
11925
- description: "Put the user's own pictures on a post, in order: public https links to image files (JPEG, PNG or WebP), for example slides another assistant made. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. Preview first.",
11936
+ description: "Put the user's own pictures on a post, in order: pictures from this chat (files: ones the user added or that were made here) or public https links to image files (urls), JPEG, PNG or WebP, for example carousel slides. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. If you can pass neither, give the user social_upload_link. Preview first.",
11926
11937
  parameters: {
11927
11938
  type: "object",
11928
11939
  properties: {
11929
11940
  id: { type: "integer", description: "The post (from social_list), not gone out yet" },
11941
+ files: {
11942
+ type: "array",
11943
+ items: FILE_SCHEMA,
11944
+ description: "Pictures from this chat, in the order they should appear"
11945
+ },
11930
11946
  urls: {
11931
11947
  type: "array",
11932
11948
  items: { type: "string" },
11933
- description: "Public https links to the image files, in the order they should appear"
11949
+ description: "Public https links to the image files, in order (after the files, when both are given)"
11934
11950
  },
11935
11951
  append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
11936
11952
  },
11937
- required: ["id", "urls"]
11953
+ required: ["id"]
11938
11954
  },
11955
+ fileParams: ["files"],
11939
11956
  group: "social",
11940
11957
  need: write5,
11941
11958
  risk: "W1",
11942
11959
  gates: ["VER"],
11943
- run: async (ctx, { id, urls, append }) => {
11944
- const list2 = (Array.isArray(urls) ? urls : []).map((u) => String(u).trim()).filter(Boolean);
11945
- if (!list2.length) throw new Error("Give at least one https link to a picture.");
11960
+ run: async (ctx, { id, urls, files, append }) => {
11961
+ const list2 = [...fileLinks(files), ...(Array.isArray(urls) ? urls : []).map((u) => String(u).trim())].filter(
11962
+ Boolean
11963
+ );
11964
+ if (!list2.length) throw new Error("Give the pictures (files from this chat, or https links).");
11946
11965
  if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
11947
11966
  if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
11948
11967
  const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
@@ -11955,30 +11974,57 @@ var socialTools = [
11955
11974
  }),
11956
11975
  tool({
11957
11976
  name: "social_carousel_pdf_attach",
11958
- description: "A LinkedIn post goes out as this PDF: a swipeable document carousel (a public https link to the PDF, up to 300 pages and 20 MB, no password). Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. Preview first.",
11977
+ description: "A LinkedIn post goes out as this PDF: a swipeable document carousel. The PDF from this chat (file: one the user added or that was made here) or a public https link to it (url); up to 300 pages and 20 MB, no password. Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. If you can pass neither, give the user social_upload_link. Preview first.",
11959
11978
  parameters: {
11960
11979
  type: "object",
11961
11980
  properties: {
11962
11981
  id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
11982
+ file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
11963
11983
  url: { type: "string", description: "Public https link to the PDF file" },
11964
11984
  title: {
11965
11985
  type: "string",
11966
11986
  description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
11967
11987
  }
11968
11988
  },
11969
- required: ["id", "url"]
11989
+ required: ["id"]
11970
11990
  },
11991
+ fileParams: ["file"],
11971
11992
  group: "social",
11972
11993
  need: write5,
11973
11994
  risk: "W1",
11974
11995
  gates: ["VER", "LI"],
11975
- run: async (ctx, { id, url, title }) => {
11976
- const link = String(url ?? "").trim();
11977
- if (!link.startsWith("https://")) throw new Error("Give a public https link to the PDF.");
11996
+ run: async (ctx, { id, url, file, title }) => {
11997
+ const link = fileLinks(file)[0] ?? String(url ?? "").trim();
11998
+ if (!link.startsWith("https://"))
11999
+ throw new Error("Give the PDF (a file from this chat, or a public https link).");
11978
12000
  const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
11979
12001
  return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
11980
12002
  }
11981
12003
  }),
12004
+ tool({
12005
+ name: "social_upload_link",
12006
+ description: "A link that opens one post's picture in the CMO.so app, where the user uploads their own photos (up to 20, a carousel on LinkedIn) or, on a computer, a PDF carousel themselves. Use it when you can't pass the files (this chat can't send files to tools, or they are on the user's device). Give the link; nothing changes until they upload.",
12007
+ parameters: {
12008
+ type: "object",
12009
+ properties: { id: { type: "integer", description: "The post (from social_list), not gone out yet" } },
12010
+ required: ["id"]
12011
+ },
12012
+ group: "social",
12013
+ need: read5,
12014
+ risk: "R0",
12015
+ run: async (ctx, { id }) => {
12016
+ const post = await ws(ctx).socialPost(Number(id));
12017
+ if (["published", "publishing", "cancelled"].includes(String(post.status)))
12018
+ throw new Error("That post has already gone out (or was skipped): its pictures can't change.");
12019
+ const site = ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : "";
12020
+ return {
12021
+ id: post.id,
12022
+ channel: post.provider ?? null,
12023
+ url: `https://app.cmo.so/social?picture=${post.id}${site}`,
12024
+ how: post.provider === "linkedin" ? "Open it, then Upload several photos (in order), or on a computer Upload a PDF carousel." : "Open it, then Upload several photos (in order)."
12025
+ };
12026
+ }
12027
+ }),
11982
12028
  tool({
11983
12029
  name: "social_carousel_set",
11984
12030
  description: "How a LinkedIn post with a set of pictures goes out: format pdf (a swipeable PDF carousel made from the pictures, the default) or images (several pictures); and the carousel's title. Preview first.",
@@ -16499,7 +16545,7 @@ var program = new Command();
16499
16545
  // package.json
16500
16546
  var package_default = {
16501
16547
  name: "@cmo-so/cli",
16502
- version: "0.9.0",
16548
+ version: "0.9.1",
16503
16549
  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.",
16504
16550
  type: "module",
16505
16551
  license: "MIT",
@@ -26688,6 +26734,30 @@ function description(t) {
26688
26734
  ].filter(Boolean);
26689
26735
  return `${t.description}${notes.length ? ` (${notes.join("; ")}.)` : ""} Changes data: call without confirm for a preview, then with confirm: true and the confirmation after the user agrees.`;
26690
26736
  }
26737
+ function fileMeta(t) {
26738
+ return t.fileParams?.length ? { "openai/fileParams": t.fileParams } : void 0;
26739
+ }
26740
+ function withFreshFiles(t, signed, now) {
26741
+ if (!t.fileParams?.length) return signed;
26742
+ const fresh = /* @__PURE__ */ new Map();
26743
+ const collect = (v) => {
26744
+ for (const f of Array.isArray(v) ? v : [v])
26745
+ if (f && typeof f === "object" && typeof f.file_id === "string")
26746
+ fresh.set(f.file_id, f);
26747
+ };
26748
+ for (const p of t.fileParams) collect(now[p]);
26749
+ const swap = (v) => {
26750
+ if (Array.isArray(v)) return v.map(swap);
26751
+ if (v && typeof v === "object" && typeof v.file_id === "string") {
26752
+ const f = fresh.get(v.file_id);
26753
+ return f && typeof f.download_url === "string" ? { ...v, download_url: f.download_url } : v;
26754
+ }
26755
+ return v;
26756
+ };
26757
+ const out = { ...signed };
26758
+ for (const p of t.fileParams) if (p in out) out[p] = swap(out[p]);
26759
+ return out;
26760
+ }
26691
26761
  function toolArgs(args) {
26692
26762
  const out = {};
26693
26763
  for (const [k, v] of Object.entries(args ?? {})) if (!CONTROL_ARGS.includes(k) && v !== void 0) out[k] = v;
@@ -26817,7 +26887,7 @@ function createMcpServer(deps) {
26817
26887
  description: description(t),
26818
26888
  inputSchema: inputSchema(t),
26819
26889
  annotations: { title: t.name.replace(/_/g, " "), ...annotations(t) },
26820
- ...deps.widgets && toolMeta(t.name) ? { _meta: toolMeta(t.name) } : {}
26890
+ ...fileMeta(t) || deps.widgets && toolMeta(t.name) ? { _meta: { ...deps.widgets ? toolMeta(t.name) : {}, ...fileMeta(t) } } : {}
26821
26891
  }))
26822
26892
  };
26823
26893
  });
@@ -26853,7 +26923,7 @@ function createMcpServer(deps) {
26853
26923
  return fail(
26854
26924
  access.keyAccess === "read" && isWrite(t) ? "This key can only read. Reconnect with write access to make changes." : `You don't have permission to do this on ${domain ?? "this website"}.`
26855
26925
  );
26856
- const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
26926
+ const callArgs = approval ? withFreshFiles(t, approval.calls[0]?.args ?? {}, toolArgs(args)) : toolArgs(args);
26857
26927
  const ctx = { api: api2, domain, client: "mcp" };
26858
26928
  if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
26859
26929
  let standing = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.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",