npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@edithatogo/substack-cli

v0.1.0

Published

[![CI](https://github.com/edithatogo/substack-cli-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/edithatogo/substack-cli-ts/actions/workflows/ci.yml) [![Coverage](https://img.shields.io/badge/coverage-91%25-brightgreen)](https://github.com/edi

Readme

substack-cli

CI Coverage 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 — 17 tools, 2 resources, 2 prompts for AI agents
  • 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 (≥91% coverage) → Mutation

Installation

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

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

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

# 5. Schedule
substack-cli schedule post.md --at "2026-06-01T09:00:00Z" --yes
  • Final publish and schedule button flows.

Setup

npm install
npm run build
npm test
npm run quality

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 draft examples\basic.md --dry-run
node dist\cli.js draft examples\basic.md --transport auto
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 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
node dist\cli.js schedule examples\basic.md --at 2026-05-01T09:00:00Z --yes
node dist\cli.js schedule examples\basic.md --at 2026-05-01T09:00:00Z --transport auto --yes
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 the same prepublish validation first and stop early if the payload is not compatible. Use --trace-out to capture a local JSON review artifact for later comparison.

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")

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> | Captions via data-caption, alt, 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
  }
}