@letterstory/cli

中文文档
0.175.18发布于 2026/11/01UNLICENSEDES Module零依赖

Spin up and manage Letter phantom blogs from your terminal.

原始使用文档
文档目录33 节
  1. Install
  2. Updating
  3. phantom — the same CLI, Phantomstory-branded
  4. Authenticate
  5. Phantomstory — a collection Letter hosts as an independent …
  6. Custom domains
  7. Content
  8. Collections — the content engine, one door
  9. Landing pages
  10. Flows
  11. Connectors
  12. Strategy & onboarding
  13. What needs you — the collection's "needs you" strip, from t…
  14. Offerings — one org, many products
  15. One-shot domain onboarding
  16. Staff: ask the fleet a question
  17. Staff: client patterns and Fable
  18. Staff: kernel experiments
  19. Staff: plan recipes
  20. Staff: the CRM pipeline
  21. Propose, then onboard a phantom
  22. Content series
  23. Arenas and writing kernels
  24. The standalone kernel endpoint
  25. Shredding
  26. Enhancing
  27. Org config
  28. Seers — event-driven signals
  29. Cover images
  30. Insights
  31. Outbound email — mailers, sequences, prospects
  32. Anything else
  33. Global flags

@letterstory/cli

Spin up and manage Letter phantom blogs from your terminal. The CLI is a thin client over the Letter 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 Letter API key (starts with ls_; legacy lb_ keys still work) with the phantom:read and phantom:write capabilities — mint one in the app under Settings → API keys (https://app.letterstory.com/settings?tab=api-keys). Add phantom: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.

Phantomstory — a collection Letter hosts as an independent publication

A phantom is a site we host that renders one of your collections; it never carries your company's name. Phantoms are metered in SITES live at once (phantom balance), not posts.

# provision a site and follow it until it's live (then it configures itself in the background:
# strategy, visuals, writing kernel, cadence, topic map)
letterstory phantom new --name "Saved Query" --config '{"cadence":{"posts_per_week":6}}'

#   Phantom 7c19c7… provisioning — following…
#     … provisioning · deploy
#     … provisioning · connect
#   Live at https://saved-query-7c19c7.letterstory-staging.com. Configuration continues in the background

letterstory phantom list
letterstory phantom get <phantom-id>
letterstory phantom config <phantom-id>                        # the site's file joined to its collection's
letterstory phantom config <phantom-id> --theme commons          # patch the site (identity/theme rebuild it)
letterstory phantom config <phantom-id> --collection-config '{"cadence":{"posts_per_week":4}}'
letterstory phantom config <phantom-id> --configure --dry-run   # what re-configuring would decide, applying nothing
letterstory phantom rebuild <phantom-id>                        # after publishing outside a collection run
letterstory phantom pause <phantom-id>                          # stop new posts and probing; the site stays live (resume to undo)
letterstory phantom resume <phantom-id>
letterstory phantom diagnostics <phantom-id>                    # why is my site empty?
letterstory phantom balance                                     # sites live / granted, and the cadence cap
letterstory phantom spread-looks [--apply]                      # a different look per site; dry run unless --apply
letterstory phantom delete <phantom-id> --yes

Pass --no-wait to return immediately and poll later with phantom get, --no-configure for a bare site, or --dry-run to see the plan without provisioning anything.

Publishing to the site is a collection run on its collection (below). Letter's own launch recipe from a client domain — positioning inferred, a masthead proposed, the first posts seeded — is phantom-onboard (see "Propose, then onboard a phantom").

Custom domains

letterstory phantom domain check myblog.com                     # price it (no charge)
letterstory phantom domain buy <phantom-id> myblog.com --yes    # buys + attaches (spends money; phantom:domain)
letterstory phantom domain connect <phantom-id> myblog.com      # attach one you already own

Content

letterstory posts list [--limit <n>] [--collection <uuid>]
letterstory posts show <article-id>
letterstory posts new --title "…" --collection <uuid> --body "…" [--article-id <uuid>]   # or --file <path|->; --article-id updates that article instead of creating one
letterstory posts new --title "…" --collection <uuid> --body "…" --context "Internal data from 120 accounts: disputes fell 38%."   # your own data: stated in the post with your attribution
letterstory posts update <article-id> --title "…" --summary "…" --tag seo --tag ai   # only what you pass changes; --body/--file for the HTML body
letterstory posts edit <article-id> --instruction "Tighten the intro"                   # AI rewrite, saved as your edit
letterstory posts mentions <article-id>                                                # re-apply the brand-mention & competitor policy
letterstory posts history <article-id>
letterstory posts restore <article-id> <version>                                       # comes back as the newest version
letterstory posts export <article-id> --format pdf --out post.pdf                      # or --format md (prints the Markdown)
letterstory posts comments <article-id>
letterstory posts comments add <article-id> --body "Source?" --quote "grew 40%"
letterstory posts comments update <article-id> <comment-id> --resolve                  # or --reopen, --body "…"
letterstory posts comments resolve-ai <article-id> <comment-id>                        # AI makes the change the comment asks for
letterstory reviewers invite <article-id> --email [email protected] --name "Sam"       # a guest reviewer: link + PIN by email
letterstory posts publish <article-id>
letterstory posts unpublish <article-id>
letterstory posts delete --id <uuid> [--id <uuid> ...]                # permanent; content, history, and any published copy are all removed

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

Collections — the content engine, one door

A collection is a publication with a ONE-FILE configuration (identity, content, cadence, publishing, targets, measurement) that produces and publishes posts on a cadence. collection run produces ONE post: source → write → gate → publish → (hosted) rebuild.

letterstory collection list                                     # public_id = the HTTP door's address
letterstory collection overview                                 # site, state, posts, in Google, AI reads per collection
letterstory collection new --name "Launches" --config-file cfg.json
letterstory collection config <id>                              # print the one file
letterstory collection config <id> --config '{"targets":{"enabled":true,"tier":"ramping"}}'   # patch it

# one post, followed to the end (20–60 min); stops at the gate under manual approval
letterstory collection run --collection <id> --spoke <topic-id>
letterstory collection run --collection <id> --topic "…" --approval manual --no-wait
letterstory collection run --collection <id> --at 2026-10-01T09:00:00Z   # a future slot
letterstory collection run-status <run-id>                      # status, steps, constraints, verification
letterstory collection runs --collection <id> --status ready
letterstory collection approve <run-id> --now
letterstory collection cancel <run-id>
letterstory collection inventory <id>                           # every piece; each finished draft's publish-on-its-own time
letterstory collection links <id>                               # internal-link refresh: on/off, posts due
letterstory collection links <id> --refresh                     # run the next batch now
letterstory collection delete <id> --yes

# the legacy verbs still work under `collections`
letterstory collections update <id> --cadence-target 4 --cadence-period week
letterstory collections assign <article-id> <collection-id>
letterstory collections activate <id> --cadence 6 --approval autopilot
letterstory collections autopen <id> --kernel-family gazette

A direct run spends one of the collection's weekly cadence slots (its posts_per_week, capped by the org's cadence cap) and is refused — never queued silently — once the week is spent; the planner's own slots are the budget being spent as designed. The same door over HTTP: POST /api/collection/ingest/{public_id} (202 + poll_url), GET …/runs/{run_id}, POST …/runs/{run_id}/approve.

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 positioning set --company-kind devtools --stage seed --motion product-led --category "AI visibility"   # the company model; `none` clears a field

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

What needs you — the collection's "needs you" strip, from the terminal

Everything in the strip either applies itself after its countdown or waits for your decision; the same reads and decisions are here.

letterstory suggestions list <collection-id>          # strategy cards + when each applies itself
letterstory suggestions dismiss proposal <id>         # reject one inside its window
letterstory collection inventory <collection-id>      # finished drafts + when each publishes on its own
letterstory collection links <collection-id>          # internal links due for a refresh

# proposed edits to live posts: a mention an AI citation earned, or fresher internal links
letterstory revisions list --collection <collection-id>
letterstory revisions accept <revision-id>                     # every change; live where we publish
letterstory revisions reject <revision-id> --change <change-id> --note "not this one"

Offerings — one org, many products

Context is layered org → offering → collection. An offering is anything the org would name when recommending it ("use Lettershred"). Each layer stores only what it overrides: an omitted field inherits, and --reset <field> puts one back to inheriting. Stances, avoid topics and blocked mentions add across layers (a collection's stances lead in prompts); priority topics (--topic) replace the inherited list, so a product never picks up its siblings' focus.

letterstory offerings list
letterstory offerings create lettershred --name Lettershred --link https://lettershred.com \
  --one-liner "Removes AI watermarks from text" --stance "Detection is an arms race" --topic "AI watermarks"
letterstory offerings update <offering-id> --reset priority_topics      # inherit the org's focus again
letterstory offerings competitor <offering-id> --competitor <competitor-domain-id> --stance avoid

# A collection publishes as its primary offering and may mention the others it lists.
letterstory offerings set-collection <collection-id> --primary <offering-id> --also <other-id>:0.5
letterstory offerings set-collection <collection-id> --none               # the org itself again

# Why is this collection writing what it's writing? (every field + the layer that set it)
letterstory offerings collection <collection-id>
letterstory offerings collection-context <collection-id> --stance "…" --avoid "pricing" --framing "…"

# The planner picks an offering per topic; override one pick (or --offering none to clear).
letterstory offerings topic <topic-id> --offering <offering-id>

One-shot domain onboarding

A bare company domain in, a fully-configured organization out — as ONE durable job that composes the existing engines: the main blog (imported from its sitemap or a connected CMS, content areas, topic map, SEO, series, company GEO probing, headlines), org visuals inferred from the site, the main blog's Enhance flow configured (new posts are enhanced; imported posts are left as published), a phantom provisioned + configured + seeded with posts that are actually written and published, and Seers in suggest mode when they fit. The job finishes from a readback: it waits (default 2h) for the seed posts to publish and the first GEO probe to land, then reports what landed.

# On the org behind your key (its registered domain is the source):
letterstory phantom-onboard domain --about "payroll for platform teams" --posts 3 --watch
letterstory phantom-onboard domain --sitemap https://acme.com/sitemap.xml --seers skip --no-visuals
letterstory phantom-onboard domain --attach <job-id> --watch     # watch a job started elsewhere
letterstory phantom-onboard domain --status                      # the org's latest domain onboarding

# Staff: the key exchange (create the org first) needs a STAFF key with org:create —
# mint your OWN, don't share one. --watch makes the whole recipe ONE command: the
# freshly-minted org key is used only in this process's memory to poll to completion.
LETTERSTORY_STAFF_API_KEY=<your staff key with org:create> \
  letterstory admin create-org --domain acme.com --onboard --watch --about "payroll for platform teams"

# Without --watch, it prints the org id, a fresh ORG-scoped API key (once), and the job
# id — watch it yourself with:
#   LETTERSTORY_API_KEY=<the printed key> letterstory phantom-onboard domain --attach <job-id> --watch

Everything the parent job does is also its own tool for existing orgs: ensure_collection_enhance_flow, provision_org_visuals, propose_seers (--args '{"apply":true}').

Staff: ask the fleet a question

# Needs a STAFF key with the fleet:query capability. The answer is derived from
# read-only SQL across every org -- nothing here can write, and credential columns
# are unreadable, so this is safe to point at production because it cannot be
# anything else.
LETTERSTORY_STAFF_API_KEY=<your staff key with fleet:query> \
  letterstory admin ask "which writing kernels are hooked up to the most clients?"

# --sql prints every query behind the answer, which is what makes a scripted answer
# checkable; --json emits the whole result (answer + steps) for a caller that wants
# the queries as data.
letterstory admin ask "which clients published nothing in the last 30 days?" --sql

This is the headless half of /superadmin/ask — same function, same schema — so a long-running agent can watch the fleet on a schedule without a browser.

Staff: client patterns and Fable

# What separates clients whose AI visibility moves (docs/architecture/client-patterns.md):
# every client against itself on share of voice, mention rate and page access, with what we
# did for it. Needs fleet:read; `run` and `ask` spend Fable tokens and need fleet:query.
letterstory admin patterns --period 30d
letterstory admin patterns chain --dimension arenaVersion --outcome cited14   # what each kernel/arena/research change did
letterstory admin patterns reports            # Fable's weekly reports and runs, newest first
letterstory admin patterns report <id>        # one report's markdown
letterstory admin patterns run                # a report now (takes minutes)
letterstory admin patterns ask "do answer-gap posts help paying clients too?"

The headless half of Admin → Client performance → Patterns: same functions, same schemas.

Staff: kernel experiments

# Needs a STAFF key with the tuning:experiment capability. A key always acts as an
# agent: it can propose, plan, note, spend and conclude, never approve a plan.
export LETTERSTORY_STAFF_API_KEY=<your staff key with tuning:experiment>

letterstory admin tuning-context --q "colons"          # what the agent knows (JSON)
letterstory admin tuning kernels                       # every catalog kernel: live version, writer, candidates
letterstory admin tuning kernel <service-kernel-id> --version live   # one version's kernel file (JSON)
letterstory admin tuning candidate <service-kernel-id> --file hazel.json --note "Echo as the writer"
letterstory admin tuning compare --title "Echo vs live" --candidates Hazel:21,Aspen:20
letterstory admin tuning comparison <id>               # per-measure verdicts (better / worse / within noise / refused)
letterstory admin tuning picks <id>                    # the blind queue; then: admin tuning pick <id> --cell <key> --pick left|right|tie
letterstory admin tuning reading-list <id> --n 10      # your next blind pairs as links (Text A / Text B), to read off the screen; pick in the console
letterstory admin tuning pair <id> --cell <key>        # one pair named: live and candidate, both texts, the measures per arm (a key reads every pair)
letterstory admin tuning bank                                     # the eval bank: every eval (rules and drafts), instances, labels
letterstory admin tuning bank-pool <eval-id>                      # draw an eval's pool from our shipped runs and the human corpus
letterstory admin tuning bank-judge <eval-id> --prompt-file judge.md --mode score --threshold 4   # a judge version (or --live: the rule's own)
letterstory admin tuning bank-score <eval-id> --version 2         # score it over the pool; prints agreement on held-out labels
letterstory admin tuning bank-try <eval-id> --version 2           # the judge as a trial rule in candidates, and a comparison against live
letterstory admin tuning invite --email [email protected]        # invite an editor (Pick and Eval bank only); `people`, `revoke <id>`
letterstory admin tuning assign --pick <comparison-id> --instructions "Read for the opening"   # a task for every editor (--to <email> for one)
letterstory admin tuning assign --label bank-draft-x-not-y --target 50                      # editors cut or keep 50 of this eval's sentences
letterstory admin tuning side-by-side <id>,<id>,<id> [--cell <key>]   # several writers as one board: each one's measures against live; with --cell, every writer's text on that input
letterstory admin tuning candidates --kernels all --patch '{"writer":"echo/echo"}' --note "Echo"   # the same change as a candidate of each kernel
letterstory admin tuning session-new --title "Echo round 1" --kernel Hazel --input-file brief.md --kind outline [--version 22] [--versions 23,29]   # --versions: saved candidates, a trial each
letterstory admin tuning propose <session> --text "Echo with rarity on; live with the shred after" --create   # trials from words, over the dials
letterstory admin tuning trial-run <trial> --repeats 2  # runs advance on the server; poll `admin tuning session <id> --texts`
letterstory admin tuning trial-save <trial> --note "…"  # a trial as a candidate version (never live); then `admin tuning compare`
letterstory admin tuning promote <experiment-id> --versions <service-kernel-id>:21   # from a concluded "ship" decision
letterstory admin experiments list --q colons          # what we've tried
letterstory admin experiments list --primitive write --stage rarity   # what we learned about one stage
letterstory admin experiments new "A different writer, voice rewritten, rarity rewrite off" --primitive write   # an investigation in Write's notebook
letterstory admin experiments new "We use too many colons" --as "claude-code"
letterstory admin experiments update <id> --plan-file plan.json --budget 120
# ...a person approves the plan on the investigation's page (approval gates spending a budget; a person's
# investigation needs none and is concluded straight away)...
letterstory admin experiments spend <id> 54
letterstory admin tuning-import lab/results/colons/publish/<brief>.json --experiment <id>
letterstory admin experiments conclude <id> --decision drop \
  --conclusion "Colons fell 60%; P(machine) did not move; quality held." \
  --evidence lab:"kernels lab/results/colons/report.md" --stages rules

The ledger behind Kernel Tuning's Experiments tab (docs/architecture/kernel-experiment-agent.md): every question asked of the kernels, how it was tested, and what we learned and why.

Staff: plan recipes

# Needs a STAFF key with recipes:read (reads) and recipes:write (writes): mint it on
# /superadmin/recipes → "Your agent's key". Recipes are global: one library behind every
# account's Plan. The same routes and functions as that page.
export LETTERSTORY_STAFF_API_KEY=<your staff key with recipes:read,recipes:write>

letterstory admin recipes vocab                          # what a recipe can say: tabs, fields, moves, outcomes, kinds, primitives
letterstory admin recipes export --out recipes.json      # the library as a file
letterstory admin recipes import recipes.json --dry-run  # what would be created / changed / retired
letterstory admin recipes import recipes.json            # write it (matched by id, else tab and name)
letterstory admin recipes save --tab build --name "Docs it already writes" --look-at "…" --when-to-use "…" --company-types devtools
letterstory admin letterpress settings                   # the one judgement's weights per ask; per lens its mode (off / shadow / live) and bar
letterstory admin letterpress flip fix shadow            # run the one judgement beside today's on every Fix pass, recorded, nothing shown changes
letterstory admin letterpress set weights.fix.prior 0.3  # one weight; the version bumps, the previous body is kept
letterstory admin recipes stats                          # per tab and recipe: shown, dismissed, and why
letterstory admin recipes save <id> --disable           # retire one (delete <id> --yes removes it)

Staff: the CRM pipeline

# Needs a STAFF key with crm:read (reads) and crm:write (writes). The same routes the
# crm.letter.dev front end calls with a staff JWT; any /api/superadmin/crm/* route takes
# the key as x-staff-api-key (GET → crm:read, anything else → crm:write).
export LETTERSTORY_STAFF_API_KEY=<your staff key with crm:read,crm:write>

letterstory admin crm signals --status new             # the Inbound feed (unmatched signals)
letterstory admin crm record --source bookface --kind message \
  --title "Asked about phantoms on Bookface" --name "Jane Doe" --company Acme --domain acme.com
# Re-running that is a no-op: with no --external-id the intake derives an idempotency key from
# the signal's content, so a retried agent loop cannot fill the feed with the same observation.
# Pass --external-id when the source has a stable id (a thread id, a tweet id), or to record that
# an otherwise identical signal happened AGAIN.
letterstory admin crm record --source twitter --kind social_engagement \
  --title "Liked our launch tweet" --name "Pat" --linkedin https://linkedin.com/in/pat
letterstory admin crm signal <signal-id> qualify       # link/create the account
letterstory admin crm signal <signal-id> dismiss       # the easy delete (kept, marked)
letterstory admin crm add "Acme" --domain acme.com     # quick-add; an existing account says "already exists as <name> (<domain>)"
letterstory admin crm accounts --stage demo_scheduled  # with likelihood
letterstory admin crm score <account-id>               # recompute; prints the factors
letterstory admin crm slack-channels [--all]           # Watchdog channels + the account each is linked to
letterstory admin crm link-slack <account-id> C0123ABC # the CRM's watch-only Slack link (`none` unlinks)
letterstory admin crm import leads.csv --dry-run       # bulk-import leads: what would be created/matched
letterstory admin crm import leads.csv --tag "YC W26"  # …and for real (dedupes by domain, then company name)
letterstory admin crm capture thread.png --context "this is for tryordinal.com" --dry-run
                                                       # a screenshot → a lead: what it read, which account
letterstory admin crm capture thread.png --context "this is for tryordinal.com"  # …and for real
letterstory admin crm capture --text-file notes.txt    # pasted text or notes work the same way
letterstory admin crm capture thread.png --retry       # sent before, some entries didn't save: write only what's missing

# One-click onboarding of a won client (key needs crm:write + fleet:onboard + org:create)
letterstory admin crm onboard <account-id> --domain acme.com \
  --site "Where fintech compliance is heading" --site "" \
  --slack [email protected] --invite [email protected]:admin --invite [email protected]
letterstory admin crm onboarding <account-id>          # step-by-step progress (sites refreshed)
letterstory admin crm retry-onboarding <onboarding-id> # re-run only the failed steps
letterstory admin crm onboard-plan <account-id>        # what the calls say: sites, Slack/app people, paid?
letterstory admin crm payment-link <account-id>        # Stripe Payment Link at the deal amount (billing:write)
letterstory admin crm payment-link <account-id> --plan growth   # with Lago signups on: a card-only start link, Lago bills the plan

onboard runs org → link → phantom sites → Slack channel → checklist → app invites → stage in one call. Each site is a real (not test) fleet build; --site "" lets the engine infer its beat from the client's homepage. --disclose 2 (or --disclose all) turns on sponsor disclosure ("safe mode") for that site, and --disclose-note "…" records the client's words as the reason; onboard-plan flags the sites whose calls asked for it. onboard-plan reads the account's recorded calls (newest first, cached per set of calls; --refresh re-reads) and says how many sites, what each is about, who to invite, and whether they've paid. payment-link stamps the account's org on the Stripe subscription, so paying through it binds and marks the account paid by itself. With Lago signups on (LETTERBRACE_LAGO_SIGNUP), it is a start link of ours instead: the client saves a card, Lago charges the plan (--plan, default the one priced at the deal amount; --ratio, --why as for admin billing lago provision), and no Stripe subscription is made. The account needs an org.

Every trigger — Cal.com bookings, Circleback recordings, Letterhome dinner RSVPs, RB2B visits, and what an agent records here — goes through one intake (ingestSignal, src/lib/crm/signals-intake.ts), so an agent's signal dedupes against a webhook's exactly as two webhooks do. Qualified kinds (booking, call recording, dinner, message, form) link or create the account; unqualified ones (a visit, a like) only attach to an existing account and otherwise wait in the Inbound feed.

Propose, then onboard a phantom

The Fleet wizard's Plan and Name screens as two commands: pick the beat, then the name, before anything is created. Both persist nothing and spend nothing (domain checks are read-only). Save the proposal as JSON; the chosen plan travels by --plan-file + --plan-id, so its direction AND its content areas are pinned on the site exactly as approved.

# Step one: the beat. Omit the domain to use your org's own.
letterstory phantom-onboard propose acme.com --json > proposal.json
letterstory phantom-onboard propose acme.com --about "bank regulators on agentic AI"   # variations on a beat

# Step two: the name — candidate mastheads for ONE plan, each with its own priced domains.
letterstory phantom-onboard mastheads acme.com --plan-file proposal.json --plan-id plan-2
jq '.topicPlans[1]' proposal.json | letterstory phantom-onboard mastheads acme.com --plan-file -

# Then onboard with the plan (direction + pillars), the masthead and, if any, the domain.
letterstory phantom-onboard acme.com --plan-file proposal.json --plan-id plan-2 \
  --name "The Compliance Wire" --domain compliancewire.com --domain-mode buy_now --real
# (drives plan_phantom; the seeded posts are collection runs — poll them with `collection run-status`)

propose reads the client's homepage and returns distinct editorial angles (topicPlans), the single fallback plan, the client's existing publications with an overlap warning, and the org's custom-domain headroom. mastheads returns names that never carry the client's own name. Without --plan-file, phantom-onboard <domain> still plans from the homepage alone.

To review a plan and then apply that plan, hand it back with --accept-plan. A bare onboarding computes the plan twice — once for the dry run you read, once inside the job — and the second masthead proposal need not match the first, so the name committed into the URL would not be the one you approved. --accept-plan pins it, implies --accept, and skips the second homepage read.

letterstory phantom-onboard acme.com --json > plan.json     # dry run: read it, edit nothing
letterstory phantom-onboard acme.com --accept-plan plan.json
jq '.plan' plan.json | letterstory phantom-onboard acme.com --accept-plan -

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>

Arenas and writing kernels

Two different steps, in this order. An arena — an adversarial cluster of research agents — reads the live web and writes a sourced, fact-checked outline (into the post when you pass --article, otherwise back on the run); 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 — hosted on a post (outline written into it) or standalone
letterstory arena run --article <uuid> --topic "payroll solutions" \
  --source https://example.com/a --must-include "Pricing" --depth fleshed_out
letterstory arena run --topic "payroll solutions" --no-wait   # standalone; prints the run id
letterstory arena run-status <run-id>                  # phases, sources, outline, verification
letterstory arena cancel <run-id>
letterstory arena balance                              # arena runs left (direct runs only)

# …with --gate, the run pauses for you to pick the angle:
letterstory arena run --article <uuid> --gate
letterstory arena run-status <run-id>                  # status: awaiting_direction + direction_options
letterstory arena direction <run-id> --choice 1
letterstory arena direction <run-id> --custom "Lead with the hidden costs"
letterstory arena direction <run-id> --skip            # let the agents decide
letterstory arena run --topic "payroll solutions" --direction "Lead with the hidden costs"   # no gate, no vote

# Watching runs (what an agent monitoring the account polls):
letterstory arena runs --status awaiting_direction     # the gates waiting on you
letterstory arena runs --since 2026-10-02T00:00:00Z --limit 50
letterstory arena runs --before <next_cursor>          # the next (older) page
letterstory arena latest --article <uuid>              # the post's latest run, whoever started it

# What research decided each site is — and your call on one (research never overrides it):
letterstory arena roles --rival                        # competitors research found that you never listed
letterstory arena role set reviewsite.com --role context
letterstory arena role set rival.com --role company --company "Rival" --rival
letterstory arena role reset rival.com                 # hand it back to research

# Arenas are callable endpoints with one-file defaults (the org's default arena is "Letter"):
letterstory arena list
letterstory arena endpoint create --name "Docs research" --defaults '{"depth":"barebones"}'
letterstory arena endpoint config <arena-id> [--file cfg.json]
letterstory arena endpoint update <arena-id> --name "Docs research v2"   # creator or org admin only
letterstory arena endpoint delete <arena-id> --yes

# The research agent as ONE versioned file (like a kernel file; schema at /api/schema/arena):
letterstory arena file <arena-id> > arena.json            # the file the arena runs now
letterstory arena file <arena-id> --patch '{"rules":{"context-figures":"measure"}}' --note "try it"
letterstory arena file <arena-id> --file arena.json        # save a whole file as the next version
letterstory arena file <arena-id> --file arena.json --candidate  # save beside live; runs nothing yet
letterstory arena promote <arena-id> <version> --yes       # ship a candidate, or roll back
letterstory arena versions <arena-id>                      # your copy's versions and our releases
letterstory arena pin <arena-id> <version-id|none>         # pin one of our releases, or follow live
letterstory arena offer <arena-id> [--accept]              # our newer version, over your own changes

# Ledes: a prospect's context in, ready-to-send opening lines out (chosen from a
# hand-approved library, never generated; the top choice is flagged). Synchronous.
letterstory lede create --name "Clay outbound table"
letterstory lede run <endpoint-id> --first-name Jane --company Acme --company-url acme.com --years 3
letterstory lede run <endpoint-id> --email [email protected] --tone direct --candidates 3
letterstory lede run <endpoint-id> --company Acme --default-lede "Came across {{company}} this week."
letterstory lede runs --limit 20
letterstory lede balance
letterstory lede endpoint config <endpoint-id> [--file cfg.json]

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

kernel run (and the submit_kernel_run tool behind it) was removed on 2026-09-27. Its one job is kernel write <endpoint-id> --article <uuid>: with no --body/--file it writes from the article's own content, and kernel status still polls the job it starts. Its --brief/--brief-file flags have no replacement: the input is --body/--file, and guidance that is not content goes in --directives.

The standalone kernel endpoint

kernel write and kernel polish hit the standalone door: prose comes back, nothing is saved into an article, and it is metered in kernel-run credits rather than the AI-usage gate.

letterstory kernel endpoint create --name "Writer"
letterstory kernel endpoint delete <endpoint-id> --yes

letterstory kernel write <endpoint-id> --file outline.md \
  --read-as outline --person first --author "Ada" --shredder <endpoint-id>
# Follows the run and prints the ARTICLE when it lands. --no-wait returns the run id instead.

Shredding

letterstory shredder run <endpoint-id> --file post.md --format markdown
# Queues, follows the job, prints the finished DOCUMENT. Exits non-zero if the job lands
# as error or degraded, so a script can branch on it without parsing the payload.

letterstory shredder run <endpoint-id> --file post.md --no-wait   # just queue it
letterstory shredder job <job-id>                                 # poll it yourself

# The shredder as ONE versioned file (like a kernel file; schema at /api/schema/shredder):
letterstory shredder file <endpoint-id> > shredder.json       # the file the endpoint shreds with
letterstory shredder file <endpoint-id> --patch '{"rewriters":{"excludeWriterFamily":true}}'
letterstory shredder file <endpoint-id> --file s.json --candidate  # save beside live; shreds nothing yet
letterstory shredder promote <endpoint-id> <version> --yes     # ship a candidate, or roll back
letterstory shredder versions <endpoint-id>                    # your copy's versions and our releases
letterstory shredder pin <endpoint-id> <version-id|none>       # pin one of our releases, or follow live
letterstory shredder offer <endpoint-id> [--accept]            # our newer version, over your own changes
letterstory shredder families [--hours 6]                      # how each rewriter family is doing

Both follow loops poll every 15–20 s. LETTERSTORY_POLL_INTERVAL_MS overrides that — set it to 0 in CI so a test does not wait on a real interval.

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

Enhancing

The passes that run after the writer — visuals, internal links, external links (citations), metadata — as one versioned file per collection (models and settings), each run recorded.

letterstory enhance run <article-id> --pass internal_links    # run one pass on a post now
letterstory enhance runs --article <article-id>               # what each pass did (also --auto-pen, --outcome skipped)
letterstory enhance get <run-id>                              # one run: version, models, what it added, cost
letterstory enhance file <collection-id> > enhancer.json      # the file the collection's passes run
letterstory enhance file <collection-id> --patch '{"internalLinks":{"maxPerPost":3}}'
letterstory enhance versions <collection-id>                  # your copy's versions and our releases
letterstory enhance pin <collection-id> <version-id|none>     # pin one of our releases, or follow live
letterstory enhance offer <collection-id> [--accept]          # our newer version, over your own changes
letterstory autopen run --article <id> --skip finish,metadata # leave passes out of one run

Org config

An org's whole configuration as one JSON bundle (export_org_config; docs/architecture/org-config.md): settings, primitives and data, keyed by name, live primitive files, no runs or credentials.

letterstory org export > acme.json                 # the bundle — the same document MCP and the API return
letterstory org export --sections primitives.kernels --sections data.collections
letterstory org export --split acme/               # one file per record, for keeping config in git
letterstory org join acme/ > acme.json             # rebuild the identical bundle from the directory
letterstory org fields data.collections            # what each setting means: label, path, how it's edited, its gate
letterstory org diff acme.json                     # what applying the bundle would change: + add, ~ change, - only in the org
letterstory org apply acme.json --dry-run           # what apply would write; then drop --dry-run to apply
letterstory org apply acme.json --prune --yes      # also delete records the bundle lacks
letterstory org git connect acme/config --path letterstory/org.json   # keep the config in GitHub: each new commit is planned and posted to Slack
letterstory org git connect acme/config --mode apply                   # …or applied (deletes only with --prune)
letterstory org git status                         # the connected repo and its last sync
letterstory org git sync [--force]                 # sync now instead of waiting for the 15-minute poll

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-integrations → New 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

Primitive compositions (a named bundle of kernel/shredder/template/seer references — docs/formats/compositions.md; not a primitive, no endpoint or run. Needs composition:read / composition:write):

letterstory compositions list
letterstory compositions create --name "Regulation-watch site kit" \
  --content '{"kernel_family":"Cedar","shred":true,"template_key":"tmpl_howto"}' \
  --phantom '{"theme":"gazette"}' \
  --seer '{"name":"AI regulation watch","provider":"regulation_change","mode":"auto","config":{"query":"AI regulation"}}'
letterstory compositions resolve comp_9f1a2b3c4d5e     # preview, read-only — costs nothing
letterstory compositions update <composition-id> --clear-seer
letterstory compositions delete <composition-id> --yes  # non-destructive to phantoms already built from it
letterstory phantom create --name "EU AI Act Weekly" --composition '{"key":"comp_9f1a2b3c4d5e"}'

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]

AI visibility and page access — the Measure tabs' reads, for scripts and agents:

# The Measure Overview: how often AI answers name you and cite your pages (rates
# with 95% intervals and gated trends), share of voice, search for your site and
# the phantom network kept apart, and each ranking target's standing. --part
# (repeatable) returns only those blocks: audience, ai, shareOfVoice, pageAccess,
# search, actions, targets — `ai` is the slow one on a large org.
letterstory insights summary [--period 14d|30d|90d] [--part <block>]...

# GEO → Competitors: every tracked brand's share of mentions (one per brand per
# answer) on the window, whether each is moving (the same question on the same
# engine in both windows — the only up/down read), the question × engine cells
# behind it, the line per engine, each question's top rival, and every dated
# change under the number. --period sets the windows: 14d → last 2 complete
# weeks vs the 2 before, 30d → 4 vs 4, 90d → 13 vs 13.
letterstory insights competitors [--period 14d|30d|90d]

# The AI Visibility Bank: which questions named you, cited your pages, or both (a
# breakthrough). All time unless --period; --source picks the phantom network
# (default), your marketing site, or the whole portfolio; --phantom narrows to
# specific phantom sites (repeatable); --answers caps receipts per question.
letterstory insights bank [--source phantoms|marketing|all] [--phantom <uuid>]... [--period 14d|30d|90d] [--answers <n>]

# Page access: AI answer engines, search crawlers and confirmed human readers on
# your phantom sites; --collection narrows to the site(s) a collection publishes.
letterstory insights access [--period 14d|30d|90d] [--collection <uuid>]

Outbound email — mailers, sequences, prospects

The cold-email stack: sending mailers (real Google mailboxes on domains we own), sequences (multi-step flows sent from them, with a review queue) and prospects (who you write to). Writes here can SEND email or spend money, so they refuse unknown flags instead of ignoring them. Scopes: mailer:read / mailer:manage / mailer:provision, sequence:read / sequence:write, prospect:read / prospect:write (the write scopes are admin-only keys).

# Mailers
letterstory mail vercel-key set --token-file - --team team_AbC123   # optional: buy domains on YOUR Vercel team (token on stdin)
letterstory mail registrant --first-name Dana --last-name Lee --email [email protected] --phone "(415) 555-0100" \
  --address "1 Main St" --city Oakland --state CA --zip 94607 --country "United States"   # WHOIS owner; any phone format
letterstory mailers quote acme-mail.com                      # price it first: nothing is bought
letterstory mailers launch acme-mail.com --mailer "hello:Hello Team" --forward-to [email protected] --yes
letterstory mailers domains                                  # every domain: setup job, mailers, warmup
letterstory mailers list [--domain acme.com]                 # status + daily send limit
letterstory mailers domain <owned-domain-id>                 # provisioning stage, DNS, mailers on it
letterstory mailers retry <owned-domain-id>                  # resume a failed setup from the step it stopped at
letterstory mailers messages <mailer-id> [--folder inbox|sent|all] [--limit 25]   # envelopes
letterstory mailers message <mailer-id> <uid> --folder inbox # one message, plain-text body
letterstory mailers forward <mailer-id> --to [email protected]    # or --clear
letterstory mailers warmup start|pause <mailer-id>           # 14+ days; ready once the warmup score reaches 85
letterstory mailers limit <mailer-id> 40                     # 0..500; refused while warming
letterstory mailers retire <mailer-id>                       # disable; history kept
letterstory mailers retire <mailer-id> --release-mailbox --yes   # also delete the hosted mailbox (irreversible)
letterstory mailers suppressions [list] [--reason bounce] [--limit 100]
letterstory mailers suppressions add [email protected] --reason manual --note "asked to stop"
letterstory mailers suppressions remove [email protected]         # only 'manual' entries; opt-outs stay

# Org-level mail settings
letterstory mail connections                                 # connected mailboxes, key + postal address status
letterstory mail postal-address "123 Main St, Portland, OR 97201"   # CAN-SPAM footer

# Sequences (approving or enrolling SENDS)
letterstory sequences list | get <id> | create | update <id> | pause <id> | resume <id>
letterstory sequences enroll <id> --list <list-id>           # or --email [email protected] ...
letterstory sequences review list | approve <item-id> | skip <item-id> | edit <item-id> --subject "…"
letterstory sequences links create --days 2                  # no-login phone review link, shown once
letterstory sequences update <id> --test-mode                # internal accounts only: walks as live, sends nothing

# Dispatches (the review desks a sequence queues to)
letterstory dispatches list | create --name "Founders" | rename <id> --name "VIPs" | delete <id> --yes

# Prospects
letterstory prospects lists | ls --list <list-id> | get <id> | import --csv-file leads.csv --list-name "Q4"
letterstory prospects source --description "heads of growth at seed-stage fintech" --list-name "Fintech"
letterstory prospects openers <list-id> --new-group "Fintech openers"   # print an opening line per row
letterstory prospects columns add "Cake image" | columns delete <column-id> --yes
letterstory prospects delete <prospect-id>... --yes

Connecting a bring-your-own Gmail mailbox needs a browser (Google consent), so it is done in the app; letterstory mail connections shows what is connected.

Anything else

Every Letter 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

FlagMeaning
--jsonMachine-readable output
--quietSuppress success chatter on the new command groups (--json implies it)
--verboseLog HTTP requests/responses to stderr
--no-colorAccepted for compatibility; this CLI already prints plain text
--dry-runOn deploy/blogs create, or bare deploy: print what would happen, don't call the API
--urlOverride the API base URL for one call
--keyOverride the API key for one call
--helpShow usage
--versionPrint the CLI version

版本与发布记录

@letterstory/cli 共发布 274 个版本,首次发布于 2026/07/20;过去 12 个月发布了 274 个版本。

版本发布日期距今
0.175.18latest2026/11/011 天前
0.175.172026/11/012 天前
0.175.162026/11/012 天前
0.175.152026/11/012 天前
0.175.142026/11/012 天前
0.175.132026/11/012 天前

关于 @letterstory/cli 的常见问题

@letterstory/cli 是什么?

从您的终端启动和管理Letter虚拟博客。

如何安装 @letterstory/cli?

在项目目录运行 npm install @letterstory/cli,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 phantom、letterstory,可以用 npx phantom 直接运行。

@letterstory/cli 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/letterstory__cli 是否存在。

@letterstory/cli 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@letterstory/cli 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@letterstory/cli 可以商用吗?

@letterstory/cli 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@letterstory/cli 还在维护吗?

最新版本 0.175.18 发布于 2026/11/01(1 天前);至今共发布 274 个版本,其中过去 12 个月发布了 274 个;首次发布于 2026/07/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。