Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

291 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

substack-cli

npm version npm downloads CI npm npm downloads codecov License Node TypeScript Renovate Dependabot PRs Welcome Mutation testing MCP

TypeScript CLI for publishing local Markdown files to a user-owned Substack publication. Supports dual-transport: HTTP API for automated publishing and browser automation (Playwright + Stagehand) for full editor interaction.


Features

  • Markdown → ProseMirror — Parse Markdown with front matter into Substack-compatible JSON
  • Dual Transport--transport api for API-driven or --transport browser for full editor interaction
  • API Publishing — Create, update, publish, and schedule drafts via Substack's API
  • Browser Automation — Local Chrome or Browserbase remote sessions
  • Media Upload — Upload images via base64 data URLs
  • MCP Server — 27 tools, 7 resources, 2 prompts for AI agents
  • Creator OS — Campaign plans, media/live planning, growth snapshots, and community triage
  • Rich Content — Tables, embeds, paywall, subscribe, code blocks, blockquotes
  • Draft Management — Mappings, optimistic concurrency, section resolution, duplicates
  • Workflow Traces — Capture, review, and compare browser artifacts
  • Quality Gates — Format → Lint → TypeScript → Build → Test with enforced baseline coverage → Mutation

Installation

npm install -g @edithatogo/substack-cli
npx @edithatogo/substack-cli inspect examples/basic.md

Shell Completions

Completions are generated by the installed CLI:

substack-cli completion bash
substack-cli completion zsh
substack-cli completion powershell

For bash or zsh, add source <(substack-cli completion bash) or source <(substack-cli completion zsh) to your shell profile. The npm package also includes scripts/install-completions.sh and scripts/install-completions.ps1 as user-run helpers that generate completions from the installed binary; generated scripts/completions.* files are intentionally not shipped.


Quick Start

# 1. Configure
substack-cli config set-publication https://yourpub.substack.com

# 2. Inspect a Markdown file
substack-cli inspect examples/basic.md

# 3. Create a draft (dry-run first)
substack-cli draft post.md --dry-run
substack-cli draft create post.md --run-log-dir catalogue/run-log

# Inspect or schedule an existing draft by ID
substack-cli draft inspect --draft-id 123
substack-cli draft schedule --draft-id 123 --scheduled-at "2026-06-01T09:00:00Z" --run-log-dir catalogue/run-log

# 4. Publish
substack-cli publish post.md --yes

# 5. Schedule
substack-cli schedule post.md --at "2026-06-01T09:00:00Z" --yes
substack-cli schedule reconcile --schedule-file schedule.json --by title,time
substack-cli batch schedule --schedule-file schedule.json --draft-ids-file draft-ids.txt --dry-run
  • Final publish and schedule button flows.

Setup

npm install
npm run build
npm test
npm run quality

Windows/OneDrive-safe install:

npm install --cache .npm-cache --prefer-online

The repository includes the same npm cache settings in .npmrc, and .npm-cache/ is ignored. This avoids global cache rename collisions and cleanup locks commonly seen in OneDrive or SharePoint-synced workspaces.

Create a local .env from .env.example:

Copy-Item .env.example .env

Set:

  • BROWSERBASE_API_KEY
  • BROWSERBASE_PROJECT_ID
  • SUBSTACK_PUBLICATION_URL
  • SUBSTACK_EMAIL and SUBSTACK_PASSWORD only if you want auth login --auto-login
  • SUBSTACK_COOKIE only if you want to test the internal API adapter without reading the local browser profile

Commands

node dist\cli.js inspect examples\basic.md
node dist\cli.js prepublish examples\basic.md
node dist\cli.js prepublish examples\basic.md --mode schedule --at 2026-05-01T09:00:00Z
node dist\cli.js preflight examples\basic.md --strict
node dist\cli.js preflight examples\basic.md --mode schedule --at 2026-05-01T09:00:00Z --schedule-file schedule.json --draft-id 123
node dist\cli.js draft examples\basic.md --dry-run
node dist\cli.js draft examples\basic.md --transport auto
node dist\cli.js draft create examples\basic.md --dry-run
node dist\cli.js draft inspect --draft-id 123
node dist\cli.js draft schedule --draft-id 123 --scheduled-at 2026-05-01T09:00:00Z --dry-run
node dist\cli.js note create --text-file examples\note.md --dry-run
node dist\cli.js note schedule --text-file examples\note.md --post-url https://example.substack.com/p/post --scheduled-at 2026-05-01T09:00:00Z --dry-run
node dist\cli.js note inspect --note-id 123
node dist\cli.js note batch --schedule-file notes.json --dry-run
node dist\cli.js campaign plan examples\basic.md --publish-at 2026-05-01T09:00:00Z --note-at 2026-05-01T10:00:00Z --channels notes,linkedin --out campaign.json
node dist\cli.js campaign validate --plan campaign.json
node dist\cli.js campaign execute --plan campaign.json --yes
node dist\cli.js campaign report --run-log-dir .substack-cli\run-log
node dist\cli.js media video plan --file video.mp4 --post examples\basic.md
node dist\cli.js media audio plan --file episode.mp3 --post examples\basic.md
node dist\cli.js live plan --title "Launch Q&A" --at 2026-05-01T11:00:00Z --audience paid
node dist\cli.js analytics snapshot --post-url https://example.substack.com/p/post --out snapshot.json --dry-run
node dist\cli.js analytics trend --snapshots-dir .substack-cli\analytics
node dist\cli.js growth report --campaign campaign.json
node dist\cli.js warehouse export --campaign campaign.json --analytics-dir .substack-cli\analytics --run-log-dir .substack-cli\run-log --out-dir .substack-cli\warehouse
node dist\cli.js warehouse attribution --campaign campaign.json --analytics-dir .substack-cli\analytics
node dist\cli.js backup plan --snapshot .substack-cli\backup\snapshot.json --source .substack-cli\warehouse
node dist\cli.js recommendations inspect
node dist\cli.js boost inspect
node dist\cli.js comments triage --post-id 123
node dist\cli.js notes campaign --post-url https://example.substack.com/p/post --schedule-file notes.json
node dist\cli.js config set-publication https://example.substack.com
node dist\cli.js config set-runtime local
node dist\cli.js doctor
node dist\cli.js policy
node dist\cli.js mcp surface
node dist\cli.js mcp summary
node dist\cli.js mcp serve
node dist\cli.js coverage safe-surfaces
node dist\cli.js coverage safe-surface --id native-video-live-automation
node dist\cli.js api auth status --source local-profile
node dist\cli.js api inventory --source local-profile --post-limit 10
node dist\cli.js api payload examples\basic.md
node dist\cli.js api media examples\media.md
node dist\cli.js api draft create examples\basic.md
node dist\cli.js api draft observe --timeout-seconds 180
node dist\cli.js api draft contract .substack-cli\draft-captures\example.json
node dist\cli.js api draft contract-matrix .substack-cli\draft-captures\a.json .substack-cli\draft-captures\b.json
node dist\cli.js api draft contract-matrix .substack-cli\draft-captures\a.json .substack-cli\draft-captures\b.json --out fixtures\draft\matrix.json
node dist\cli.js api draft contract-matrix-compare fixtures\draft\expected.json fixtures\draft\actual.json
node dist\cli.js api draft duplicates examples\basic.md
node dist\cli.js api draft section examples\basic.md
node dist\cli.js api draft inspect examples\basic.md
node dist\cli.js api draft review .substack-cli\draft-captures\example.json
node dist\cli.js api draft compare .substack-cli\draft-captures\expected.json .substack-cli\draft-captures\actual.json
node dist\cli.js api draft fixture .substack-cli\draft-captures\example.json --out fixtures\draft\baseline.json
node dist\cli.js api draft mappings
node dist\cli.js api draft link examples\basic.md --draft-id 123
node dist\cli.js auth status
node dist\cli.js auth login --wait-seconds 120
node dist\cli.js auth login --auto-login --wait-seconds 120
node dist\cli.js auth login --auto-login --pause-before-password --wait-seconds 300

Capture and compare parser fixtures:

node dist\cli.js schema capture examples\basic.md --out fixtures\prosemirror\basic.json
node dist\cli.js schema validate fixtures\prosemirror\basic.json
node dist\cli.js schema compare examples\basic.md fixtures\prosemirror\basic.json

Publishing and scheduling require explicit confirmation:

node dist\cli.js prepublish examples\basic.md
node dist\cli.js publish examples\basic.md --dry-run
node dist\cli.js publish examples\basic.md --review-only --yes --trace-out .substack-cli\publish-traces\review.json
node dist\cli.js publish examples\basic.md --transport browser --yes
node dist\cli.js publish examples\basic.md --yes --run-log-dir .substack-cli\run-log
node dist\cli.js schedule examples\basic.md --at 2026-05-01T09:00:00Z --yes --run-log-dir .substack-cli\run-log
node dist\cli.js schedule examples\basic.md --at 2026-05-01T09:00:00Z --transport auto --yes
node dist\cli.js schedule examples\basic.md --at 2026-05-01T09:00:00Z --schedule-file schedule.json --yes
node dist\cli.js schedule reconcile --schedule-file schedule.json --by title,time
node dist\cli.js batch schedule --schedule-file schedule.json --draft-ids-file draft-ids.txt --dry-run
node dist\cli.js batch schedule --schedule-file schedule.json --draft-ids-file draft-ids.txt --yes --run-log-dir .substack-cli\run-log
node dist\cli.js note create --text-file examples\note.md --yes --run-log-dir .substack-cli\run-log
node dist\cli.js note schedule --text-file examples\note.md --post-url https://example.substack.com/p/post --scheduled-at 2026-05-01T09:00:00Z --yes --run-log-dir .substack-cli\run-log
node dist\cli.js note batch --schedule-file notes.json --yes --run-log-dir .substack-cli\run-log
node dist\cli.js campaign plan examples\basic.md --channels notes,x,youtube --out campaign.json
node dist\cli.js media video plan --file launch.mp4 --post examples\basic.md
node dist\cli.js live plan --title "Reader Q&A" --at 2026-05-01T11:00:00Z --audience subscribers
node dist\cli.js trace review .substack-cli\publish-traces\review.json
node dist\cli.js trace compare .substack-cli\publish-traces\review.json .substack-cli\publish-traces\publish.json
node dist\cli.js trace fixture .substack-cli\publish-traces\review.json --out fixtures\trace\review.json

Publish and schedule commands run prepublish and preflight validation first and stop early if the payload or operational contract is not compatible. Use --trace-out to capture a local JSON review artifact for later comparison. preflight checks the publication target, title, slug format, section/tag/cover readiness, editorial placeholders, schedule parse/future status, optional schedule-file collisions, and dry-run payload printability; --strict escalates optional workflow checks to blockers.

Scheduled notes enforce the covering-note contract before live writes: each item must include the matching postUrl, the note text must mention that URL, the note must be no more than three sentences, and scheduledAt must be a valid timestamp. A note batch file can be a JSON array or { "items": [...] }:

{
  "items": [
    {
      "textFile": "notes/example.md",
      "postUrl": "https://example.substack.com/p/post",
      "scheduledAt": "2026-05-01T09:00:00Z",
      "title": "Example post"
    }
  ]
}

Markdown Markers

Use front matter for metadata:

---
title: "Example post"
subtitle: "Generated from Markdown"
tags: [example, markdown]
audience: everyone
section: original-essays
comments: enabled
---

Media examples:

![Remote alt](https://example.com/image.png "Remote caption")
![Local alt](./assets/local-image.png "Local caption")
<img src="https://example.com/native.png" alt="Native alt" data-caption="Native caption">

inspect prints a media manifest for every image. Markdown image titles and inline HTML data-caption values are recorded there as caption metadata; a normal paragraph after an image stays visible paragraph text and is not treated as native caption metadata.

Supported custom markers:

{{paywall}}
{{subscribe: Subscribe for future posts}}
{{youtube: https://www.youtube.com/watch?v=VIDEO_ID}}
{{embed: https://example.com/article}}
{{podcast: https://open.spotify.com/episode/ID}}

Markdown Feature Support

Supported Nodes (Substack API-compatible)

ProseMirror Node Markdown Source Notes
paragraph Plain text Default block
heading # through ###### Levels 1-6
bulletList / listItem - , * Nested lists supported
orderedList / listItem 1. Nested lists supported
blockquote > Nested blockquotes supported
codeBlock ``` or ```language Language annotation preserved
horizontalRule --- Thematic break
image ![alt](src) or <img> Caption metadata via data-caption or Markdown title
embedNode {{youtube:}}, {{embed:}}, {{podcast:}} URL and embed type stored
paywallDivider {{paywall}} Atom block, no content
subscribeWidget {{subscribe: label}} Label attribute
table / tableRow / tableCell / tableHeader GFM tables Non-resizable, inline marks supported
hardBreak Line break
text Text content

Supported Marks

ProseMirror Mark Markdown Source
bold **text** or __text__
italic *text* or _text_
strike ~~text~~
code `text`
link [text](url)

Unsupported Markdown Features

These constructs pass through the parser but are either not mapped to a named ProseMirror node or may render differently in Substack's editor. The inspect command reports them as compatibility issues before any publish attempt:

Feature Why / Fallback
Image galleries No multi-image group node; single images work independently
Substack native captioned images Captions parsed from data-caption but no separate caption element
Native audio/video All media embeds use embedNode; no dedicated audio/video ProseMirror node
Math / LaTeX ($$, $) No MathExtension registered; falls through as plain text
Callouts / pull quotes No Substack blockquote variant; rendered as standard blockquote
Buttons No Substack button element; would be stripped if HTML is unrecognized
Task lists (- [ ]) Syntax not processed; renders as plain listItem
Mentions (@user) No mention extension
Footnote references No footnote ProseMirror node
Figure / figcaption HTML5 <figure> not handled; children still parsed
Auto-generated heading IDs StarterKit heading extension does not emit id attributes
Image dimensions image node does not carry width/height attrs
Substack editor node names Custom node names (paywallDivider, subscribeWidget, embedNode) may not match Substack's internal schema

The inspect command reports the full list of detected node types, mark types, and any unmapped types in compatibility:

$ substack-cli inspect examples/formatting.md
{
  "compatibility": {
    "ok": true,
    "supportedNodeTypes": ["blockquote", "bulletList", ...],
    "unsupportedIssues": null
  }
}

README window note

On Windows, prefer a repo path outside OneDrive. If OneDrive is unavoidable, use the repo-local .npm-cache/ configured by .npmrc, and set SUBSTACK_CLI_STATE_DIR to a non-synced path such as %LOCALAPPDATA%\substack-cli-state when browser-profile locks or sync cleanup interfere with local runtime state.

About

TypeScript CLI for publishing local Markdown to Substack via browser automation and internal API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages