feat(cli): amy dm send|list|await (NIP-17)

Adds three verbs wired into the top-level dispatch:

- `amy dm send RECIPIENT TEXT` — builds a kind:14 ChatMessageEvent,
  runs it through NIP17Factory to seal and gift-wrap, then publishes
  each wrap to its recipient's DM inbox (kind:10050, fallback kind:10002
  read, final fallback bootstrap). Emits one JSON line with the inner
  kind:14 id, per-recipient wrap ids, and per-relay ACK status.

- `amy dm list [--peer NPUB] [--since TS] [--limit N] [--timeout SECS]`
  — drains gift wraps addressed to us from our inbox relays (or outbox /
  bootstrap fallbacks), unwraps+unseals each one, dedupes by inner id,
  and returns them oldest-first. With no flags it advances the
  giftWrapSince cursor in state.json (matches the Marmot syncIncoming
  convention); with --peer/--since it's a stateless query.

- `amy dm await --peer NPUB --match TEXT [--timeout SECS]` — polls the
  same drain on a 2s tick until a DM from NPUB contains TEXT. Timeout
  exits 124 like the other await verbs.

All three reuse Quartz entirely (NIP17Factory, GiftWrapEvent,
SealedRumorEvent, RecipientRelayFetcher); the only shared piece we
needed in commons — filterGiftWrapsToPubkey — was extracted in the
previous commit. No business logic leaks into cli/.

Plan: cli/plans/2026-04-23-nip17-dm.md.
This commit is contained in:
Claude
2026-04-23 18:23:12 +00:00
parent c43a69b083
commit 15205c24b5
5 changed files with 300 additions and 1 deletions
+3
View File
@@ -137,6 +137,9 @@ Run `amy --help` for the canonical list. As of today:
| `marmot await message GID --match TEXT` | Block until a message containing `TEXT` lands. |
| `marmot await rename GID --name NAME` | Block until GID's name matches. |
| `marmot await epoch GID --min N` | Block until GID's MLS epoch is ≥ N. |
| `dm send RECIPIENT TEXT` | Send a NIP-17 gift-wrapped DM (kind:14 inside kind:1059). Publishes to the recipient's kind:10050 → kind:10002 read → our bootstrap pool. |
| `dm list [--peer NPUB] [--since TS] [--limit N] [--timeout SECS]` | Drain and decrypt gift wraps on our inbox relays. With neither `--peer` nor `--since` the gift-wrap cursor in `state.json` is advanced to the newest message seen. |
| `dm await --peer NPUB --match TEXT [--timeout SECS]` | Block until a DM from NPUB containing TEXT arrives. Timeout exits 124. |
All `await` verbs accept `--timeout SECS` (default 30). Timeout exits 124
so scripts can distinguish "condition never happened" from "the command