@flowmails/cli

中文文档
0.1.1发布于 2026/11/01MITES Module

The flowmails CLI — login, test, addresses, watch, wait. Built on @flowmails/flowmails-sdk.

原始使用文档
文档目录19 节
  1. Install
  2. The SDK it needs
  3. The human does the one-time onboarding
  4. flowmails login — store a credential
  5. Credential types
  6. flowmails test — the connection check
  7. Step 3 is a liveness check, not a send
  8. The verdict
  9. Sending for real is opt-in
  10. Machine-readable output
  11. flowmails addresses — the mailboxes you own
  12. flowmails watch <addr> — tail the inbound log
  13. flowmails wait <addr> — wait for a message
  14. Resuming after a disconnect
  15. Files
  16. Command surface
  17. Exit codes
  18. What this package is not
  19. Development

@flowmails/cli — the flowmails command

The Flowmails CLI: a thin, scriptable shell around the official SDK (@flowmails/flowmails-sdk). It exists so an agent — or a human — can check a connection, look at the mailboxes an account owns, tail the inbound log and wait for a message without hand-rolling HTTP.

It implements AP-06 of docs/agent-platform.md (§6.3, §10.4).


Install

npm i -g @flowmails/cli

The binary is called flowmails:

flowmails --help

The SDK it needs

The CLI is built on @flowmails/flowmails-sdk and keeps it external — the published binary imports the SDK at runtime rather than carrying it inside the bundle. The published manifest therefore pins an exact SDK version, and the CLI needs:

@flowmails/cli0.1.1
@flowmails/flowmails-sdk0.2.1 (exact)

flowmails --version prints the CLI's version on standard output and the SDK it needs on standard error; flowmails --help states it too.

The pin is a contract, not a formality: 0.2.0 is the newest SDK on the public registry, and it has no fm.workerVersion() — the only way an agent can ask "is my receive worker new enough to fill my event log?". A CLI pinned to 0.2.0 cannot answer that question for its user (audit P0-4, 2026-10-31).

Publish order matters. The SDK must reach npm before the CLI, because the CLI's install pulls the exact version the manifest names. From the repo root:

pnpm run publish:sdk   # @flowmails/flowmails-sdk 0.2.1
pnpm run publish:cli   # @flowmails/cli 0.1.1

To work on the CLI itself, install it from a checkout instead:

# from the repo root
pnpm install
pnpm --filter @flowmails/cli build

# put `flowmails` on your PATH (a symlink into the checkout)
cd packages/cli && npm link

Without linking, run the bundle straight from the checkout:

node packages/cli/dist/cli.js --help

The build needs the SDK's bundle, because the CLI is built on it:

pnpm --filter @flowmails/flowmails-sdk build   # builds packages/sdk/dist
pnpm --filter @flowmails/cli build             # bundles packages/cli/dist

Both build and typecheck in this package run the SDK build first, so pnpm --filter @flowmails/cli build on its own is enough.

Check it:

flowmails --help

The human does the one-time onboarding

The CLI cannot onboard an account, and neither can an agent. These steps are dashboard-only, by design:

  • create the Flowmails account and sign in;
  • connect a Cloudflare account;
  • add and verify a domain (the DNS records are yours);
  • deploy the per-account receive worker;
  • mint the API key itself.

Do them once at https://flowmails.net/dashboard/onboarding**, then create a key at https://flowmails.net/dashboard/settings/api-keys** and hand it to the CLI. After that, everything the CLI does works from the command line — including creating a mailbox and waiting for the first mail.

flowmails login — store a credential

The device flow is not implemented yet (AP-07). There is no device endpoint to talk to, so flowmails login does not authenticate for you: it stores a key you paste. It says so every time it runs — the CLI never pretends to be logged in when it is not.

flowmails login                      # prompts for the key (input is not echoed)
flowmails login --key fm_…           # non-interactive
cat key.txt | flowmails login        # non-interactive, from stdin

The key is validated by shape and stored in the config file with mode 0600. No command ever prints a raw key — output shows a short prefix (fm_012…) and nothing else.

Credential types

PrefixWhat it is
fm_…The SDK key. One tenant, bound to one domain for sending.
agc_…The agent credential. The same tenant plus a scope list.

Both are accepted by flowmails login.

flowmails test — the connection check

This is the first thing the install instructions tell you to run, and the first signal in the "time to first agent action" metric. It is quiet (one line on stdout, the steps on stderr) and decisive (the exit code means something).

flowmails test
# flowmails test: OK — credential fm_012…, domain yourapp.com, API alive (no message sent), domain can send

It checks four things:

  1. Auth — the credential is accepted by the platform.
  2. The domain — which domain the account owns, and which one the key is bound to. (The platform does not expose the binding to a scoped credential — GET /api/v1/me is the SDK key's surface — so with several domains the CLI says "unknown" rather than guessing.)
  3. API liveness — the send origin answers for this credential. No mail is sent.
  4. The send capability — whether the bound domain can actually send. The platform reports it on the agent's domain projection (canSend + sendingState) and on GET /api/v1/me.

Step 3 is a liveness check, not a send

The probe reads the status of a message id that does not exist, so a 404 is the answer that proves the API is reachable. That is all it proves. The command used to call it "send reachability" and print flowmails test: OK for a domain whose sends failed 100% of the time — a false positive on the first command a new user runs (audit P0-2, 2026-10-31).

So the claim is now the claim: the check says "API alive", and the capability is read from the platform.

The verdict

VerdictExitMeaning
OK0Authenticated, the API is alive, and the bound domain can send.
UNVERIFIED0Authenticated, the API is alive, but the platform does not report whether the domain can send (an older backend). This is not a pass — verify with flowmails test --send-to <addr>.
FAILED1Something is broken: the credential was rejected, the platform was unreachable, or the bound domain cannot send. The message names the domain.

A domain that cannot send never prints flowmails test: OK.

flowmails test
# ✓ auth ok (fm_012…)
# ✓ domains: mysoul.fun
# ✓ bound domain: mysoul.fun
# ✓ API alive (no message sent)
# ✓ domain mysoul.fun cannot send (not_onboarded)
# ✗ mysoul.fun cannot send (sending state: not_onboarded)
# flowmails test: FAILED — fm_012…, mysoul.fun cannot send (sending state: not_onboarded)
# $? = 1

(The first five lines are the progress trail on stderr; the FAILED line is the summary, also on stderr. With --json the summary is one JSON object on stdout instead.)

Sending for real is opt-in

Nothing is sent by default. A real message is only sent when you ask for one:

flowmails test --send-to [email protected]

--from <local@domain> overrides the sender address when the account owns several domains. A send that the platform recorded as failed (status: "failed", audit P0-1) fails the check too, with the platform's reason when it gives one.

Machine-readable output

flowmails test --json
# {"ok":true,"verdict":"ok","credential":"fm_012…","domains":[…],"boundDomain":"yourapp.com",
#  "api":{"alive":true},"send":{"canSend":true,"state":"ready","verifiedBy":"capability","sent":null}}

verdict is ok, unverified or failed; send.canSend is null when the platform does not report it.

flowmails addresses — the mailboxes you own

flowmails addresses
# yourapp.com (catchall, ready)
#   [email protected]

flowmails addresses --json
# {"domains":[{"id":"dom_1","name":"yourapp.com","routingMode":"catchall","ready":true,…}]}

On a catchall domain every local part delivers; on a rules domain only the listed ones do — the listing tells you which.

flowmails watch <addr> — tail the inbound log

flowmails watch [email protected]
{"id":"evt_…","messageId":"<…>","event":"inbound.received","recipient":"[email protected]","sender":"[email protected]","subject":"Your invoice","createdAt":"…"}

One JSON object per line, newline-delimited, so it pipes straight into jq or a line reader. The event carries routing facts and the subject — never a body; fetch the body with the SDK's messages.get().

flowmails wait <addr> — wait for a message

flowmails wait [email protected]
# <3f8a…@stripe.example>          # the resolved SMTP Message-ID

flowmails wait [email protected] --subject invoice --sender [email protected] --timeout 5m

The wait is durable on the platform: it answers immediately, it resolves when a matching message lands, and it survives the CLI being killed. Standard output carries exactly one thing — the resolved message id — so the command can be used in a substitution:

id=$(flowmails wait [email protected])

The events observed while waiting are JSON lines on stderr.

Expiry is not a failure. A wait that reaches its deadline without a matching message exits 3 (expired); a wait that could not do its job exits 1 (failure). They used to be the same code, which left a script unable to tell "timed out" from "broken" (audit P1-6, 2026-10-31).

Resuming after a disconnect

watch and wait persist their cursor — the id of the last event they printed — to the state file, and resume from it on the next run:

  • the cursor is an event id, never a timestamp (two events can land in the same millisecond, and a cursor that can skip a row loses mail);
  • it is written after each event is printed, so a crash between two events replays at most the last one.

That is at-least-once delivery, and it is the right trade: the alternative is silently losing mail. If your consumer needs exactly-once, dedupe on messageId.

A failed page (a dropped socket, a 502) does not end the command: the error is reported on stderr and the next poll resumes from the cursor that is already on disk. A cursor the platform does not know is a different failure — the platform answers 400, not an empty page, so watch stops and says to delete the state file rather than spinning or silently replaying the whole log. wait additionally persists the wait id, so a reconnect resumes the same wait instead of opening a second one.

Files

PathModeWhat it holds
~/.config/flowmails/config.json0600The credential, and optional baseURL / platformURL overrides.
~/.config/flowmails/state.json0600The cursors (and the wait id) per command and mailbox.

The directory is created with mode 0700. Both paths are overridable:

flowmails --config /path/to/config.json test
FLOWMAILS_CONFIG=/path/to/config.json flowmails test
FLOWMAILS_STATE=/path/to/state.json flowmails watch [email protected]

$XDG_CONFIG_HOME is honoured for the default location.

Command surface

flowmails login [--key <key>]              Store a credential (paste-key fallback)
flowmails test [--send-to <addr>]          Connection check: auth, domain, API liveness, send capability
flowmails addresses [--json]               List the account's domains and mailboxes
flowmails watch <addr>                     Tail inbound events as JSON lines
flowmails wait <addr> [--subject <s>] [--sender <s>] [--timeout <ms>]
                                            Wait for a message and print its id

Global options: --config <path>, --state <path>, --json, -h/--help, -v/--version.

flowmails <command> --help prints that command's own help — its options and the exit-code table.

--timeout accepts milliseconds (60000) or a duration (30s, 5m, 1h).

Exit codes

The exit codes are a contract: a script branches on them, so a code that means two things is a bug.

CodeMeaning
0Success — the command did what it was asked to do.
1Failure — the credential was rejected, the platform was unreachable, the bound domain cannot send, or the command hit an error.
2Usage error — the arguments did not parse.
3Expired — flowmails wait reached its deadline without a matching message.

3 is separate from 1 on purpose: "timed out" and "broken" are different answers, and a script that cannot tell them apart retries a broken setup forever and gives up on a healthy mailbox that simply had no mail (audit P1-6, 2026-10-31).

flowmails test exits 0 for both OK and UNVERIFIED — nothing is known to be broken in the second case — so read the verdict in the summary (OK / UNVERIFIED / FAILED), not just the exit code.

What this package is not

  • It is not an onboarding tool. See the boundary above.
  • It is not an MCP server (that is AP-09) and it does not expose the ten agent tools — it is a command-line surface over the same SDK.
  • It does not implement the device flow (AP-07).
  • It does not expose per-tool scopes (AP-03) — the SDK key is tenant-wide read plus domain-bound send.

Development

pnpm --filter @flowmails/cli typecheck
pnpm --filter @flowmails/cli build
node --test ./test/cli-*.test.mjs   # from the repo root

The command logic lives in small modules under src/commands/, and the SDK client is injected through the runtime — so the tests drive the commands without a network, without a process and without the SDK's bundle.

版本与发布记录

@flowmails/cli 共发布 3 个版本,首次发布于 2026/10/31;过去 12 个月发布了 3 个版本。

版本发布日期距今
0.1.1latest2026/11/012 天前
0.1.02026/10/313 天前
0.0.0-stage2026/10/313 天前

关于 @flowmails/cli 的常见问题

@flowmails/cli 是什么?

该流程邮件CLI -登录,测试,地址,观察,等待。建立在@flowmail /flowmail -sdk。

如何安装 @flowmails/cli?

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

@flowmails/cli 支持 TypeScript 吗?

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

@flowmails/cli 支持 ESM 还是 CommonJS?

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

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

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

@flowmails/cli 可以商用吗?

@flowmails/cli 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@flowmails/cli 还在维护吗?

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