From c984083b575721b4dfe7aa9acd2427be64beec5e Mon Sep 17 00:00:00 2001 From: Francesco Di Lorenzo Date: Wed, 29 Jul 2026 14:35:51 +0100 Subject: [PATCH 1/2] feat(typefully): add Substack Notes platform support Typefully shipped Substack Notes (July 27); this adds it to the agent skill and CLI (TYP-7359): - Add substack to POST_PLATFORM_ORDER so it works with --platform, --all, platform auto-selection, and drafts:update platform resolution - Fail fast when thread content (--- separators or --append) targets substack: the API allows a single post per note, and its 422 is less actionable than a CLI-side error - Document the platform in SKILL.md (single-post constraint, 10,000 character limit, images/GIFs only with no video), README, CLAUDE.md, marketplace.json, and the skill changelog Constraints verified against the live API v2 and the backend implementation (SubstackPlatform schema, SUBSTACK_CONFIG media limits, SUBSTACK_NOTE_MAX_CHARS). --- .claude-plugin/marketplace.json | 2 +- CLAUDE.md | 2 +- README.md | 3 +- skills/typefully/CHANGELOG.md | 7 ++ skills/typefully/SKILL.md | 18 ++-- .../references/platforms/x-articles.md | 2 +- skills/typefully/scripts/typefully.js | 22 ++++- tests/drafts.test.js | 87 +++++++++++++++++++ 8 files changed, 128 insertions(+), 15 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5fee332..76d65d6 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ { "name": "typefully", "source": "./skills/typefully", - "description": "Draft, schedule, and manage social media posts across X, LinkedIn, Threads, Bluesky, and Mastodon", + "description": "Draft, schedule, and manage social media posts across X, LinkedIn, Threads, Bluesky, Mastodon, and Substack Notes", "strict": false, "skills": ["./skills/typefully"] } diff --git a/CLAUDE.md b/CLAUDE.md index e07fb89..3bc3f5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -This repository contains AI agent skills for Typefully - markdown files that give AI agents specialized workflows for drafting, scheduling, and managing social media posts across X, LinkedIn, Threads, Bluesky, and Mastodon. +This repository contains AI agent skills for Typefully - markdown files that give AI agents specialized workflows for drafting, scheduling, and managing social media posts across X, LinkedIn, Threads, Bluesky, Mastodon, and Substack Notes. ## Repository Structure diff --git a/README.md b/README.md index bebac74..5cdbad6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Version](https://img.shields.io/badge/version-1.0.0-green.svg)]() [![Typefully API](https://img.shields.io/badge/Typefully-API-3B9AF8)](https://typefully.com/docs/api) -AI agent skills for drafting, scheduling, and managing social media posts across X, LinkedIn, Threads, Bluesky, and Mastodon. Give your AI agent the ability to manage your social media scheduling directly from your IDE or terminal. +AI agent skills for drafting, scheduling, and managing social media posts across X, LinkedIn, Threads, Bluesky, Mastodon, and Substack Notes. Give your AI agent the ability to manage your social media scheduling directly from your IDE or terminal. Built on the [Typefully API](https://typefully.com/docs/api). [Typefully](https://typefully.com) is a writing and scheduling app used by 200k+ top creators and teams to grow on X, LinkedIn, Threads, and Bluesky. @@ -95,6 +95,7 @@ Then test it in your agent to verify the latest local changes behave as expected - Threads - Bluesky - Mastodon +- Substack Notes ## Troubleshooting diff --git a/skills/typefully/CHANGELOG.md b/skills/typefully/CHANGELOG.md index 250eb72..b0d3e4d 100644 --- a/skills/typefully/CHANGELOG.md +++ b/skills/typefully/CHANGELOG.md @@ -4,6 +4,13 @@ All notable user-facing changes to the Typefully skill and its CLI are documente The format is based on Keep a Changelog. +## 2026-07-29 + +### Added + +- Substack Notes support: target it with `--platform substack` on `drafts:create` and `drafts:update`, combine it with other platforms in one draft, and filter comments with `--platform substack`. When connected, it is also included in `--all` and in platform auto-selection. +- Substack Notes take a single post per draft: the CLI now rejects thread content (`---` separators or `--append`) targeting `substack` with a clear error before calling the API. + ## 2026-07-27 ### Added diff --git a/skills/typefully/SKILL.md b/skills/typefully/SKILL.md index 933d3f3..0b591cf 100644 --- a/skills/typefully/SKILL.md +++ b/skills/typefully/SKILL.md @@ -3,16 +3,16 @@ name: typefully description: > Create, schedule, and manage social media posts via Typefully. ALWAYS use this skill when asked to draft, schedule, post, or check tweets, posts, threads, or - social media content for Twitter/X, LinkedIn, Threads, Bluesky, or Mastodon, - or when the user drops a Typefully draft URL such as + social media content for Twitter/X, LinkedIn, Threads, Bluesky, Mastodon, or + Substack Notes, or when the user drops a Typefully draft URL such as https://typefully.com/?a=&d=. -last-updated: 2026-07-09 +last-updated: 2026-07-29 allowed-tools: Bash(./scripts/typefully.js:*) --- # Typefully Skill -Create, schedule, and publish social media content across X, LinkedIn, Threads, Bluesky, and Mastodon using [Typefully](https://typefully.com). Run everything through `./scripts/typefully.js` (Node.js 18+, no dependencies). All commands output JSON. +Create, schedule, and publish social media content across X, LinkedIn, Threads, Bluesky, Mastodon, and Substack Notes using [Typefully](https://typefully.com). Run everything through `./scripts/typefully.js` (Node.js 18+, no dependencies). All commands output JSON. > **Script paths** below are relative to this skill's directory. Resolve them based on where the skill is installed. > @@ -71,8 +71,9 @@ To decide which social set to use: ./scripts/typefully.js drafts:create --text "Your post" ``` -- If `--platform` is omitted, the first connected platform is auto-selected. Named platforms: `x`, `linkedin`, `threads`, `bluesky`, `mastodon`, `x_article`. -- Split a thread with `---` on its own line. +- If `--platform` is omitted, the first connected platform is auto-selected. Named platforms: `x`, `linkedin`, `threads`, `bluesky`, `mastodon`, `substack` (Substack Notes), `x_article`. +- Split a thread with `---` on its own line. Exception: `substack` takes a single post per draft — no threads. For a thread plus a Substack Note, use the one-draft pattern below to give `substack` its own single-post content. +- Substack Notes media: images and GIFs only (up to 6) — video is rejected. - Attach media with `--media`, tags with `--tags`, internal notes with `--scratchpad`, an internal name with `--title`. - Read content from a file with `--file ./post.txt` instead of `--text`. @@ -95,6 +96,8 @@ When content should differ per platform (e.g. an X thread plus a tailored Linked Never create multiple drafts unless the user explicitly wants separate drafts per platform. > `--all` excludes `x_article`. X Articles are standalone and cannot be combined with any other platform — see [`references/platforms/x-articles.md`](references/platforms/x-articles.md). +> +> `--all` includes `substack` when connected. Since Substack Notes take a single post, `--all` with thread content errors — exclude `substack` via `--platform`, or give it its own single-post content with `drafts:update`. ### Scratchpad notes @@ -137,6 +140,7 @@ To publish the URL as plain text with no card, pass `--hide-link-preview`. Suppr |--------------|--------| | "Draft a tweet about X" | `drafts:create --text "..."` | | "Post this to LinkedIn" | `drafts:create --platform linkedin --text "..."` | +| "Post a Substack Note" | `drafts:create --platform substack --text "..."` (single post, no threads) | | "Post to X and LinkedIn" (same content) | `drafts:create --platform x,linkedin --text "..."` | | "X thread + tailored LinkedIn post" | One draft, then `drafts:update` to add the platform | | "What's scheduled?" / "Recent posts?" | `drafts:list --status scheduled` / `--status published` | @@ -292,7 +296,7 @@ Tags are scoped per social set — a tag in one social set doesn't appear in ano ### Character limits -X 280 · LinkedIn 3000 · Threads 500 · Bluesky 300 · Mastodon 500. +X 280 · LinkedIn 3000 · Threads 500 · Bluesky 300 · Mastodon 500 · Substack Notes 10000. ### Draft URLs diff --git a/skills/typefully/references/platforms/x-articles.md b/skills/typefully/references/platforms/x-articles.md index c1eeb33..b244641 100644 --- a/skills/typefully/references/platforms/x-articles.md +++ b/skills/typefully/references/platforms/x-articles.md @@ -7,7 +7,7 @@ Use this guide when creating, updating, scheduling, publishing, or commenting on ## Core Rules - Always use `--platform x_article`. -- `x_article` is standalone. Do not combine it with `x`, LinkedIn, Threads, Bluesky, Mastodon, or `--all`. +- `x_article` is standalone. Do not combine it with `x`, LinkedIn, Threads, Bluesky, Mastodon, Substack Notes, or `--all`. - Use `--content-markdown ` for article content. - Use `--cover-media-id ` to set a cover image. - Use `--cover-media-id null` on update to remove a cover image. diff --git a/skills/typefully/scripts/typefully.js b/skills/typefully/scripts/typefully.js index 0c14f7e..7f25be3 100755 --- a/skills/typefully/scripts/typefully.js +++ b/skills/typefully/scripts/typefully.js @@ -21,7 +21,8 @@ const LOCAL_CONFIG_FILE = path.join(LOCAL_CONFIG_DIR, 'config.json'); const API_KEY_URL = 'https://typefully.com/?settings=api'; const AUTH_FAILURE_MESSAGE = `Authentication failed: Typefully API key is invalid, expired, or lacks access. Run 'typefully.js setup' to configure a valid key.`; const X_ARTICLE_PLATFORM = 'x_article'; -const POST_PLATFORM_ORDER = ['x', 'linkedin', 'threads', 'bluesky', 'mastodon']; +const SUBSTACK_PLATFORM = 'substack'; +const POST_PLATFORM_ORDER = ['x', 'linkedin', 'threads', 'bluesky', 'mastodon', 'substack']; const X_ARTICLE_POST_ONLY_FLAGS = [ ['text', '--text'], ['file', '--file'], @@ -530,6 +531,15 @@ function validateHideLinkPreviewOption(platformList, hideLinkPreview) { } } +// Substack Notes accepts a single post per draft — the API rejects longer +// posts arrays with a 422, so fail fast with a clearer message. +function validateSubstackSinglePost(platformsObj) { + const posts = platformsObj[SUBSTACK_PLATFORM]?.posts; + if (Array.isArray(posts) && posts.length > 1) { + error('substack (Substack Notes) supports a single post per draft — threads are not supported. Use a single post, or target other platforms with --platform.'); + } +} + function hasParsedArg(parsed, key) { return Object.prototype.hasOwnProperty.call(parsed, key); } @@ -1376,6 +1386,8 @@ async function cmdDraftsCreate(args) { platformsObj[platform] = platformConfig; } + + validateSubstackSinglePost(platformsObj); } const body = { platforms: platformsObj }; @@ -1576,6 +1588,7 @@ async function cmdDraftsUpdate(args) { posts: platformPosts, }; } + validateSubstackSinglePost(platformsObj); body.platforms = platformsObj; } @@ -2188,9 +2201,10 @@ COMMANDS: --use-default Required when using default social set with single arg drafts:create [social_set_id] [options] Create a new draft (uses default if ID omitted) - --platform Comma-separated: x,linkedin,threads,bluesky,mastodon + --platform Comma-separated: x,linkedin,threads,bluesky,mastodon,substack or standalone x_article - (auto-selects first connected platform if omitted) + (auto-selects first connected platform if omitted; + substack = Substack Notes, single post only) --all Post to all connected post platforms (excludes x_article) --text Post content (use --- on its own line for threads) --file, -f Read content from file instead of --text @@ -2262,7 +2276,7 @@ COMMANDS: comments:list [options] List comment threads on a draft --social-set-id Social set (uses default if omitted) - --platform Filter by platform: x, linkedin, threads, bluesky, mastodon, x_article + --platform Filter by platform: x, linkedin, threads, bluesky, mastodon, substack, x_article --status Filter by: unresolved (default), resolved, all --limit Max results (default: 10, max: 50) --offset Skip first N results diff --git a/tests/drafts.test.js b/tests/drafts.test.js index 94df7e1..af7545c 100644 --- a/tests/drafts.test.js +++ b/tests/drafts.test.js @@ -251,6 +251,93 @@ describe('drafts', () => { server.assertNoPendingExpectations(); })); + it('drafts:create targets substack with a single post', withCliHarness(async ({ + sandbox, server, baseUrl, apiKey + }) => { + server.expect('POST', '/v2/social-sets/9/drafts', { + assert: (req) => { + authAssertFactory(apiKey)(req); + assert.deepEqual(req.bodyJson, { + platforms: { + substack: { + enabled: true, + posts: [{ text: 'A quick note' }], + }, + }, + }); + }, + json: { id: 'd1' }, + }); + const result = await runCli( + ['drafts:create', '9', '--platform', 'substack', '--text', 'A quick note'], + { cwd: sandbox.cwd, env: { HOME: sandbox.home, TYPEFULLY_API_BASE: baseUrl, TYPEFULLY_API_KEY: apiKey } } + ); + assert.equal(result.code, 0); + assert.deepEqual(parseJsonOrNull(result.stdout), { id: 'd1' }); + server.assertNoPendingExpectations(); + })); + + it('drafts:create --all includes substack when connected', withCliHarness(async ({ + sandbox, server, baseUrl, apiKey + }) => { + server.expect('GET', '/v2/social-sets/55', { + assert: authAssertFactory(apiKey), + json: { id: '55', platforms: { substack: {}, x: {} } }, + }); + + server.expect('POST', '/v2/social-sets/55/drafts', { + assert: (req) => { + authAssertFactory(apiKey)(req); + assert.deepEqual(Object.keys(req.bodyJson.platforms), ['x', 'substack']); + }, + json: { id: 'd1' }, + }); + const result = await runCli( + ['drafts:create', '55', '--all', '--text', 'Hello'], + { cwd: sandbox.cwd, env: { HOME: sandbox.home, TYPEFULLY_API_BASE: baseUrl, TYPEFULLY_API_KEY: apiKey } } + ); + assert.equal(result.code, 0); + server.assertNoPendingExpectations(); + })); + + it('drafts:create errors when thread content targets substack', withCliHarness(async ({ + sandbox, server + }) => { + const result = await runCli( + ['drafts:create', '9', '--platform', 'x,substack', '--text', 'Post one\n---\nPost two'], + { cwd: sandbox.cwd, env: { HOME: sandbox.home, TYPEFULLY_API_KEY: 'typ_test_key' } } + ); + assert.equal(result.code, 1); + assert.deepEqual(parseJsonOrNull(result.stdout), { + error: 'substack (Substack Notes) supports a single post per draft — threads are not supported. Use a single post, or target other platforms with --platform.', + }); + assert.equal(server.requests.length, 0); + })); + + it('drafts:update --append errors when the draft targets substack', withCliHarness(async ({ + sandbox, server, baseUrl, apiKey + }) => { + server.expect('GET', '/v2/social-sets/9/drafts/d1', { + assert: authAssertFactory(apiKey), + json: { + id: 'd1', + platforms: { + substack: { enabled: true, posts: [{ text: 'Existing note' }] }, + }, + }, + }); + const result = await runCli( + ['drafts:update', '9', 'd1', '--append', '--text', 'One more'], + { cwd: sandbox.cwd, env: { HOME: sandbox.home, TYPEFULLY_API_BASE: baseUrl, TYPEFULLY_API_KEY: apiKey } } + ); + assert.equal(result.code, 1); + assert.deepEqual(parseJsonOrNull(result.stdout), { + error: 'substack (Substack Notes) supports a single post per draft — threads are not supported. Use a single post, or target other platforms with --platform.', + }); + server.assertNoPendingExpectations(); + assert.equal(server.requests.length, 1); + })); + it('drafts:create supports standalone X Article markdown and cover media', withCliHarness(async ({ sandbox, server, baseUrl, apiKey }) => { From 8ba99319d057b76d166717c4a09da25d04180c21 Mon Sep 17 00:00:00 2001 From: Francesco Di Lorenzo Date: Wed, 29 Jul 2026 14:55:18 +0100 Subject: [PATCH 2/2] feat(typefully): document Substack Notes link previews and extend --hide-link-preview Backend PR typefully-backend#1802 adds Substack Notes to API v2's link-preview platforms: API-created notes with a URL now publish with a rich link card (skipped when the note has images), and the API accepts hide_link_preview on the substack post. - Add substack to HIDE_LINK_PREVIEW_PLATFORMS so --hide-link-preview applies to Substack Notes alongside LinkedIn and Threads (matching the editor's per-note hide control). - Update SKILL.md link-preview docs and the flags table; add a CHANGELOG entry. Co-Authored-By: Claude Fable 5 --- skills/typefully/CHANGELOG.md | 1 + skills/typefully/SKILL.md | 6 +++--- skills/typefully/scripts/typefully.js | 6 +++--- tests/drafts.test.js | 16 ++++++++++------ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/skills/typefully/CHANGELOG.md b/skills/typefully/CHANGELOG.md index b0d3e4d..ab7b8d4 100644 --- a/skills/typefully/CHANGELOG.md +++ b/skills/typefully/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on Keep a Changelog. - Substack Notes support: target it with `--platform substack` on `drafts:create` and `drafts:update`, combine it with other platforms in one draft, and filter comments with `--platform substack`. When connected, it is also included in `--all` and in platform auto-selection. - Substack Notes take a single post per draft: the CLI now rejects thread content (`---` separators or `--append`) targeting `substack` with a clear error before calling the API. +- Link-preview cards now also publish on Substack Notes: API-created notes containing a URL get a rich link card, matching editor behavior (the card is skipped when the note has images). `--hide-link-preview` now supports `substack` alongside LinkedIn and Threads. ## 2026-07-27 diff --git a/skills/typefully/SKILL.md b/skills/typefully/SKILL.md index 0b591cf..4041e32 100644 --- a/skills/typefully/SKILL.md +++ b/skills/typefully/SKILL.md @@ -109,9 +109,9 @@ When the user asks to add notes, ideas, or context to a draft, use `--scratchpad ### Link previews -When a post contains a URL, Typefully automatically fetches Open Graph metadata for the last URL in the text and publishes a rich link-preview card on **LinkedIn, Threads, and Bluesky**. No flag is needed — it just works. X and Mastodon unfurl links themselves after publishing. +When a post contains a URL, Typefully automatically fetches Open Graph metadata for the last URL in the text and publishes a rich link-preview card on **LinkedIn, Threads, Bluesky, and Substack Notes**. No flag is needed — it just works. X and Mastodon unfurl links themselves after publishing. On Substack Notes the card is skipped when the note has images (images and the link card are mutually exclusive there). -To publish the URL as plain text with no card, pass `--hide-link-preview`. Suppression is supported on **LinkedIn and Threads only** (matching the Typefully editor); the flag errors if neither platform is targeted, and is ignored for other platforms in a mixed-platform draft: +To publish the URL as plain text with no card, pass `--hide-link-preview`. Suppression is supported on **LinkedIn, Threads, and Substack Notes only** (matching the Typefully editor); the flag errors if none of these platforms is targeted, and is ignored for other platforms in a mixed-platform draft: ```bash ./scripts/typefully.js drafts:create --platform linkedin,threads --text "Read this https://example.com" --hide-link-preview @@ -198,7 +198,7 @@ Add any of these flags to a `drafts:create` or `drafts:update` command. The **Ap | `--scratchpad ""` | Attach internal notes (see [Scratchpad notes](#scratchpad-notes)) | create, update | | `--share` | Generate a public share URL | create, update | | `--schedule ` | Schedule or reschedule the draft | create, update | -| `--hide-link-preview` | Suppress the link-preview card (LinkedIn/Threads only — see [Link previews](#link-previews)) | create, update | +| `--hide-link-preview` | Suppress the link-preview card (LinkedIn/Threads/Substack only — see [Link previews](#link-previews)) | create, update | | `--exclude-comment-markers` | Render response without anchors (display only; validation still applies) | update | | `--force-overwrite-comments` | Destructive last resort — see [`comments.md`](references/comments.md) | update | diff --git a/skills/typefully/scripts/typefully.js b/skills/typefully/scripts/typefully.js index 7f25be3..aa75320 100755 --- a/skills/typefully/scripts/typefully.js +++ b/skills/typefully/scripts/typefully.js @@ -514,7 +514,7 @@ function validateXOnlyPostOptions(platformList, { quotePostUrl, disclosures }) { } // Platforms where Typefully lets you suppress the link-preview card (matches the web editor). -const HIDE_LINK_PREVIEW_PLATFORMS = ['linkedin', 'threads']; +const HIDE_LINK_PREVIEW_PLATFORMS = ['linkedin', 'threads', 'substack']; function getHideLinkPreviewFromParsed(parsed) { return Boolean(parsed['hide-link-preview'] || parsed.hide_link_preview); @@ -527,7 +527,7 @@ function addHideLinkPreview(posts, hideLinkPreview) { function validateHideLinkPreviewOption(platformList, hideLinkPreview) { if (hideLinkPreview && !platformList.some(p => HIDE_LINK_PREVIEW_PLATFORMS.includes(p))) { - error('--hide-link-preview is only supported for LinkedIn and Threads posts. Include linkedin or threads in --platform or remove the flag.'); + error('--hide-link-preview is only supported for LinkedIn, Threads, and Substack posts. Include linkedin, threads, or substack in --platform or remove the flag.'); } } @@ -1566,7 +1566,7 @@ async function cmdDraftsUpdate(args) { existing.platforms[p].posts.length > 0 ); if (targets.length === 0) { - error('Cannot apply --hide-link-preview because this draft has no existing LinkedIn or Threads posts'); + error('Cannot apply --hide-link-preview because this draft has no existing LinkedIn, Threads, or Substack posts'); } postsArray = null; platformList = targets; diff --git a/tests/drafts.test.js b/tests/drafts.test.js index af7545c..d811498 100644 --- a/tests/drafts.test.js +++ b/tests/drafts.test.js @@ -819,7 +819,7 @@ describe('drafts', () => { assert.equal(server.requests.length, 0); })); - it('drafts:create applies --hide-link-preview only to LinkedIn and Threads posts', withCliHarness(async ({ + it('drafts:create applies --hide-link-preview only to LinkedIn, Threads, and Substack posts', withCliHarness(async ({ sandbox, server, baseUrl, apiKey }) => { server.expect('POST', '/v2/social-sets/9/drafts', { @@ -839,13 +839,17 @@ describe('drafts', () => { enabled: true, posts: [{ text: 'Read this https://example.com' }], }, + substack: { + enabled: true, + posts: [{ text: 'Read this https://example.com', hide_link_preview: true }], + }, }, }); }, json: { id: 'd1' }, }); const result = await runCli( - ['drafts:create', '9', '--platform', 'linkedin,threads,bluesky', '--text', 'Read this https://example.com', '--hide-link-preview'], + ['drafts:create', '9', '--platform', 'linkedin,threads,bluesky,substack', '--text', 'Read this https://example.com', '--hide-link-preview'], { cwd: sandbox.cwd, env: { HOME: sandbox.home, TYPEFULLY_API_BASE: baseUrl, TYPEFULLY_API_KEY: apiKey } } ); assert.equal(result.code, 0); @@ -853,7 +857,7 @@ describe('drafts', () => { server.assertNoPendingExpectations(); })); - it('drafts:create errors when --hide-link-preview is used without LinkedIn or Threads', withCliHarness(async ({ + it('drafts:create errors when --hide-link-preview is used without LinkedIn, Threads, or Substack', withCliHarness(async ({ sandbox, server }) => { const result = await runCli( @@ -862,7 +866,7 @@ describe('drafts', () => { ); assert.equal(result.code, 1); assert.deepEqual(parseJsonOrNull(result.stdout), { - error: '--hide-link-preview is only supported for LinkedIn and Threads posts. Include linkedin or threads in --platform or remove the flag.', + error: '--hide-link-preview is only supported for LinkedIn, Threads, and Substack posts. Include linkedin, threads, or substack in --platform or remove the flag.', }); assert.equal(server.requests.length, 0); })); @@ -1491,7 +1495,7 @@ describe('drafts', () => { server.assertNoPendingExpectations(); })); - it('drafts:update errors on --hide-link-preview when the draft has no LinkedIn or Threads posts', withCliHarness(async ({ + it('drafts:update errors on --hide-link-preview when the draft has no LinkedIn, Threads, or Substack posts', withCliHarness(async ({ sandbox, server, baseUrl, apiKey }) => { server.expect('GET', '/v2/social-sets/9/drafts/d1', { @@ -1509,7 +1513,7 @@ describe('drafts', () => { ); assert.equal(result.code, 1); assert.deepEqual(parseJsonOrNull(result.stdout), { - error: '--hide-link-preview is only supported for LinkedIn and Threads posts. Include linkedin or threads in --platform or remove the flag.', + error: '--hide-link-preview is only supported for LinkedIn, Threads, and Substack posts. Include linkedin, threads, or substack in --platform or remove the flag.', }); assert.equal(server.requests.length, 1); server.assertNoPendingExpectations();