skill
substack-cli-ts
TypeScript CLI for publishing local Markdown to Substack via browser automation and internal API
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/edithatogo/substack-cli-ts itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 25, 2026substack-cli
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 apifor API-driven or--transport browserfor 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
npm run build
node dist/cli.js inspect examples/basic.md
The intended npm name is @edithatogo/substack-publisher (binary substack-publisher). Do not treat local install as a hosted npm release.
Shell Completions
Completions are generated by the installed CLI:
substack-publisher completion bash
substack-publisher completion zsh
substack-publisher completion powershell
For bash or zsh, add source <(substack-publisher completion bash) or source <(substack-publisher 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_KEYBROWSERBASE_PROJECT_IDSUBSTACK_PUBLICATION_URLSUBSTACK_EMAILandSUBSTACK_PASSWORDonly if you wantauth login --auto-loginSUBSTACK_COOKIEonly 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
---
Start the article body here. Do not repeat the front-matter title or subtitle as
the first heading or paragraph; those values are written to Substack's dedicated
header fields. The CLI defensively removes exact leading repetitions, but users
and authoring agents should emit each title and subtitle only once.
Media examples:


<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 |  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.