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

@letterstory/cli

v0.9.0

Published

Spin up and manage Letterstory phantom blogs from your terminal.

Readme

@letterstory/cli

Spin up and manage Letterstory phantom blogs from your terminal. The CLI is a thin client over the Letterstory MCP endpoint (POST /api/mcp) — it wraps the same tool manifest an agent sees, so it never drifts from the API.

Install

The CLI is plain ESM with zero dependencies and needs no build step (Node ≥ 20).

npm install -g @letterstory/cli   # puts `letterstory` (and `phantom`) on your PATH
# …or run it without installing:
npx -p @letterstory/cli letterstory --help

The -p is not optional. This package ships two commands, letterstory and phantom, and npx only infers one for you when a package has a single command or one named after the package — neither is true here, so a bare npx @letterstory/cli exits with could not determine executable to run. Name the command you want (letterstory or phantom) and pass the package with -p.

Contributing to the CLI itself? Run it straight from a checkout instead:

cd cli
npm link          # puts `letterstory` (and `phantom`) on your PATH, pointing at this checkout
# …or run it directly without linking:
node cli/bin/letterstory.mjs --help

Updating

The CLI checks npm for a newer published version at most once every 24 hours (cached alongside your config, so most runs make no extra network call) and prints a one-line nudge on stderr when one's available:

Update available: 0.2.1 → 0.3.0. Run `npm install -g @letterstory/cli@latest` to upgrade.

To upgrade immediately:

npm install -g @letterstory/cli@latest

Set LETTERSTORY_NO_UPDATE_CHECK=1 to disable the check entirely (e.g. in CI).

phantom — the same CLI, Phantomstory-branded

phantom is a ghost-branded entry point for demos and presentations — same binary, same commands, same auth, same guardrails as letterstory, just a different printed name:

phantom deploy create --name "My Blog" --theme gazette
# 👻 phantom — deploy and manage Phantomstory blogs from your terminal

There is no separate implementation to keep in sync: bin/phantom.mjs sets one env var before importing lib/cli.mjs, which swaps the banner and command-hint strings and otherwise runs the exact same code path as letterstory.

Authenticate

Two ways in — pick whichever fits how you're running the CLI.

Sign in via your browser (the default — best for interactive use on your own machine):

letterstory login
# opens your browser to https://app.letterstory.com, or override with --url

This runs a browser-based OAuth 2.1 flow (PKCE, loopback redirect — RFC 8252) against your account. It comes back with a full-access session and a refresh token, so there's no API key to mint or scope up front. Tokens are saved to ~/.letterstory/config.json (mode 600) and refresh automatically. Run letterstory logout to revoke the session and forget it.

Or use a static API key (for CI/automation, or when you want scoped-down access):

You need a Letterstory API key (starts with ls_; legacy lb_ keys still work) with the deployment:read and deployment:write capabilities — mint one in the app under Settings → API keys. Add deployment:domain too if you plan to buy custom domains.

letterstory login --key ls_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# points at https://app.letterstory.com by default; override with --url

Credentials resolve from --key/--url flags, then LETTERSTORY_API_KEY / LETTERSTORY_API_URL, then ~/.letterstory/config.json (written by login, mode 600).

Run letterstory whoami (alias status) any time to confirm which key/url (or OAuth session) resolved and see your company profile. Run letterstory config to see the resolved url and credential source without making a network call.

Spin up a blog

# create a blog and wait until it's live (polls for you)
letterstory deploy create --name "My Blog" --theme gazette

#   Created deployment 7c19c7… (provisioning)
#     … provisioning · building
#     … provisioning · connecting
#   ✓ Live at https://my-blog-7c19c7.letterstory-staging.com

letterstory deploy list
letterstory deploy get <deployment-id>
letterstory deploy update <deployment-id> --name "New Name"
letterstory deploy rebuild <deployment-id>      # after publishing new articles
letterstory deploy diagnostics <deployment-id>  # why is my blog empty?
letterstory deploy delete <deployment-id> --yes

Pass --no-wait to return immediately and poll later with deploy get, or --dry-run on create to print what would be sent without creating anything.

One-shot: bare deploy

For the common case — reserve a blog, price (and optionally buy) a domain, and rebuild, all in one call — skip the subcommand word entirely:

letterstory deploy --domain yourverticalreview.com --buy
#   Reserved phantom blog "Yourverticalreview" (7c19c7…)
#   Priced yourverticalreview.com
#   Registered yourverticalreview.com
#   SSL + DNS provisioned · sitemap + schema generated
#
#   Your phantom blog is live at https://yourverticalreview.com.

letterstory deploy --blog 7c19c7… --rebuild   # rebuild an existing blog instead of creating one
letterstory deploy --domain demo.com --dry-run  # preview without calling the API

Custom domains

letterstory domain check myblog.com                       # price it (no charge)
letterstory domain buy <deployment-id> myblog.com        # buys + attaches the domain

blogs and domains are aliases for deploy and domain — the exact same commands under Phantomstory's vocabulary (blogs ls, blogs new, blogs rm, blogs update, domains buy, etc. all call the identical handler as their deploy/domain counterpart). Use whichever set of verbs you prefer; both are always available from both letterstory and phantom.

Content

letterstory posts list [--limit <n>] [--collection <uuid>]
letterstory posts show <article-id>
letterstory posts new --title "…" --collection <uuid> --body "…"     # or --file <path|->
letterstory posts publish <article-id>
letterstory posts unpublish <article-id>

letterstory published list [--limit <n>] [--collection <uuid>] [--format html|md]
letterstory published show --slug my-post-slug [--format html|md]    # or --id <uuid>

Collections

letterstory collections list
letterstory collections new --name "Launches" --description "…"
letterstory collections update <id> --cadence-target 4 --cadence-period week
letterstory collections delete <id> --yes
letterstory collections assign <article-id> <collection-id>

Landing pages

A landing page is a link plus context describing what lives there. Collections with auto-mention enabled (toggle in Collection Settings) weave relevant landing pages into articles as they're written. Pass the URL as --link--url is the global flag that overrides the API base URL.

letterstory landings list
letterstory landings get <landing-id>
letterstory landings create --link https://example.com/dataset --context "Our 2026 benchmark dataset. Cite it when an article discusses response-time or CSAT metrics."
letterstory landings create --link https://example.com/report --file ./context.md   # context from a file (or --file - for stdin)
letterstory landings update <landing-id> --context "Updated context"
letterstory landings delete <landing-id> --yes

Flows

Run an editorial pass over an article, check on it, and manage the completion webhook:

letterstory flows list
letterstory flows run <flow-id> <article-id>
letterstory flows status <run-id>

letterstory flows webhook get
letterstory flows webhook set --url https://hooks.example.com/x --secret s3cr3t
letterstory flows webhook set --url none        # disable it

Connectors

Publish a finished article out to an external CMS or doc target:

letterstory connectors list
letterstory connectors publish <article-id> --to webflow --target <target-uuid>
letterstory connectors status --connector webflow --publish-id <id>

Strategy & onboarding

letterstory strategy company get
letterstory strategy company set --name "Acme" --domain acme.com --manifesto "…"

letterstory strategy positioning get
letterstory strategy positioning set --topic seo --topic ai --stance "…" --clear-avoid

letterstory strategy competitors list
letterstory strategy competitors add "Rival Co" rival.com

letterstory strategy sitemap --collection <uuid> --url https://x.com/sitemap.xml

letterstory onboarding status
letterstory onboarding step --complete connect_domain

Content series

A series is a coverage obligation, not a keyword bet: "content that looks like this", one post per declared item. Series feed the same planner queue as topic clusters.

letterstory series list
letterstory series create --name "Secrets, tool by tool" \
  --description "One practical secrets guide per IaC tool." \
  --coverage "Pulumi,Terraform,Ansible" --template <key>
letterstory series backfill <series-id> --collection <uuid>

Research and writing kernels

Two different steps, in this order. The research agent reads the live web and writes a sourced outline into the post; a writing kernel writes the draft from the brief it's given and does no retrieval of its own. Both take minutes, so both are start-then-poll:

# 1. Research the topic and write an outline into the post
letterstory research start --article <uuid> --topic "payroll solutions" \
  --url https://example.com/a --must-include "Pricing" --depth fleshed_out
letterstory research status --article <uuid>       # phases, sources, outline
letterstory research cancel --article <uuid>

# …with --gate, the run pauses for you to pick the angle:
letterstory research start --article <uuid> --gate
letterstory research status --article <uuid>       # status: awaiting_direction + direction_options
letterstory research direction --article <uuid> --choice 1
letterstory research direction --article <uuid> --custom "Lead with the hidden costs"
letterstory research direction --article <uuid> --skip     # let the agent decide

# 2. Have a kernel write the draft
letterstory kernel list
letterstory kernel run --article <uuid> --kernel <uuid> [--brief "…" | --brief-file brief.md]
letterstory kernel status <kernel-job-id>

phantom-job create --topic "…" --collection <uuid> runs topic → draft → publish → rebuild end to end, with phantom-job status <job-id> to follow it.

Seers — event-driven signals

A seer watches an external source (a GitHub repo, a Notion workspace, a spec URL, a news or regulation query) and turns what it detects into headline suggestions — or, in auto mode, straight into drafts. Needs seer:read / seer:write capabilities (template:read / template:write for the templates group).

letterstory seers providers                        # the self-describing config catalog — start here
letterstory seers list
letterstory seers create --name "Ship notes" --collection <uuid> --provider github_pr \
  --config '{"owner":"letterstory","repo":"letterbrace"}'
letterstory seers update <seer-id> --mode auto --interval 60
letterstory seers run <seer-id>                    # poll + produce now (minutes; spends AI budget)
letterstory seers events <seer-id>                 # detected signals, dedup aliases, relevance verdicts
letterstory seers dismiss <seer-id> <event-id>
letterstory seers headlines [--collection <uuid>]  # the seer-sourced suggestion feed
letterstory seers held                             # drafts parked pending human review
letterstory seers release <article-id>             # …or: seers discard <article-id>

Reproducible setup — the onboarding path. export writes the org's seers as a manifest; apply is an idempotent, name-keyed upsert from one (it never deletes; seers in the org but missing from the manifest are listed as extras and left alone):

letterstory seers export --file seers.json
letterstory seers apply --file seers.json --dry-run   # print the create/update/ok plan
letterstory seers apply --file seers.json             # apply it

Connections — both providers connect headlessly (no browser required):

letterstory seers connections                      # GitHub + Notion status
letterstory seers connect github --token <pat>     # org-wide token; unlocks private repos
letterstory seers connect notion                   # no token: prints how to mint one + which pages to share
letterstory seers connect notion --token ntn_xxx   # store a Notion internal-integration secret

Connecting Notion. Notion uses an internal integration (the direct analogue of a GitHub PAT), so it's fully scriptable:

  1. Create one at notion.so/my-integrationsNew integration → Internal, with the Read content capability.
  2. Copy the Internal Integration Secret (ntn_… / secret_…).
  3. In Notion, open each page/database the seer should watch → ••• → Connections → add your integration. A Notion integration only sees pages explicitly shared with it — this step is required, or the seer connects but detects nothing.
  4. letterstory seers connect notion --token ntn_…

The app's browser OAuth flow (Seers tab → Connect Notion) is an equivalent alternative, not a requirement.

Remaining gap. seers run executes synchronously server-side (up to ~5 minutes for web-scan providers); there is no start-then-poll variant yet. And create's config takes raw JSON — seers providers is the schema reference the server validates against.

Content templates (the compose structures seers bind via config.template_key):

letterstory templates list                         # custom + read-only built-ins
letterstory templates create --name "Release notes" --angle "What shipped and why" \
  --blocks-file blocks.json
letterstory templates update <template-id> --angle "…"   # unset fields keep their values
letterstory templates delete <template-id> --yes   # bound seers fall back to defaults

Cover images

Which image model draws a post's cover is a CLI decision, not a UI-only one. variants fans one headline out across up to four models and hands back a preview per model; select commits the one you want:

letterstory covers models                          # image models connected to this org
letterstory covers variants --article <uuid> --model bloom --model openai
letterstory covers select --article <uuid> --url <variant-url> --provider bloom

# …or skip the comparison and let the server pick, using the collection's cover template:
letterstory covers generate <article-id> [--regenerate] [--stock-only]

--ref-url <url> (repeatable) conditions the look on existing images — pair it with extract_blog_reference_images to match a site you're mirroring. Each variant costs one image generation, which is why the fan-out is capped at four models.

Insights

Search Console performance, network-wide, per post, or top posts:

letterstory insights site [--period 14d|30d|90d] [--collection <uuid>]
letterstory insights post <article-id> [--period 14d|30d|90d]
letterstory insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]

Anything else

Every Letterstory tool is reachable, not just the ones with a dedicated command group:

letterstory whoami                                 # or: status — verify your key + who it's for
letterstory tools list                             # list all tools (bare `tools` also works)
letterstory tools show list_articles                # one tool's capability + full argument schema
letterstory call list_articles --args '{"limit":5}'
letterstory call ingest_article --args '{"title":"…","content":"…"}'

# `tool` is the same idea with schema-coerced arguments instead of raw JSON:
letterstory tool list_articles --arg limit=5 --arg collection_id=c1
letterstory tool ingest_article --json-args '{"title":"…"}' --arg content="…"
cat article.json | letterstory tool ingest_article --stdin

letterstory mcp                                     # print MCP server config for an agent
letterstory mcp --print-key                         # inline the real key instead of a placeholder

Global flags

| Flag | Meaning | | ------------ | ----------------------------------------------------------------------------------------- | | --json | Machine-readable output | | --quiet | Suppress success chatter on the new command groups (--json implies it) | | --verbose | Log HTTP requests/responses to stderr | | --no-color | Accepted for compatibility; this CLI already prints plain text | | --dry-run | On deploy/blogs create, or bare deploy: print what would happen, don't call the API | | --url | Override the API base URL for one call | | --key | Override the API key for one call | | --help | Show usage | | --version | Print the CLI version |