Files
amethyst/cli/ROADMAP.md
T
Claude 8446827d33 feat(cli): amy emoji — NIP-30 emoji packs and bulk import
amy emoji pack list|show|create|edit|delete, emoji add|remove (public or
--private), emoji import PACK SOURCE from a folder, a .zip or a
`shortcode url` list (images uploaded to --server or the first kind:10063
Blossom server, one new pack version for the whole batch), and the
kind:10030 emoji list via emoji list|use|unuse. Thin over commons
EmojiPackActions / EmojiImport.

cli/tests/emoji/emoji-headless.sh drives every verb against amy serve and
the marmot harness's throwaway Blossom server (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BN6Wdpxj3hd1iJBswebfYb
2026-10-18 03:44:38 +00:00

22 KiB

Amy Roadmap

The live checklist for growing amy from a Marmot test-bed into a full command-line mirror of Amethyst.

How to use this file: update it in the same PR that ships a feature. Move rows between tables, adjust ordering, add non-goals. This is the single source of truth for "what's left".


North-star goal

For every feature of the Amethyst Android app, there is a way to exercise it through amy, with byte-identical on-relay behaviour.

Why:

  • Interop tests against the ~100 other Nostr clients need a reproducible harness that does not require running Android.
  • Agents and LLMs can script real Amethyst flows without a GUI.
  • Regressions in shared logic (signing, encryption, filter building, event parsing) become shell-scriptable.
  • Power users get a command-line Amethyst for free.

Non-goal: Amy is not a second Nostr client implementation. It is a thin assembly layer over quartz + commons. Protocol and business logic in cli/ are bugs.


Parity matrix

Status legend: ✅ shipped · 📦 logic lives in commons/, needs a command · 🆕 needs extraction from amethyst/ first · ⚠️ blocked (see Notes).

Area Status Notes
Identity create / import (nsec, ncryptsec, mnemonic, npub, nprofile, hex, NIP-05) ✅ LoginCommand + Quartz NIP-05 / NIP-06 / NIP-49
Account bootstrap (nine events) ✅ commons/account/AccountBootstrapEvents.kt
Account logoff (amy logoff) — delete key + per-account state + the account's events in the shared store ✅ LogoffCommand. --yes-gated; --keep-events skips the shared-cache purge.
Status overview (amy status) — who is signed in (profile name, npub, signer + can-sign), what each account has saved (follows, relay lists, own events, contacts, Marmot groups + messages, key package, Concord communities, Cashu wallet, DM cursor), whether an account is actually selected, and the machine operator key ✅ StatusCommand + StatusReport + StatusText. Cross-account and read-only: no keychain prompt, no network, and it never creates the event store it reads. Store size/backend is amy store stat.
Relay config — every relay-list bucket (nip65 10002 via outbox/inbox/nip65 nouns with spec read/write merge, dm 10050, key-package 10051, search 10007, private-outbox 10013, blocked 10006, trusted 10089, proxy 10087, indexer 10086, broadcast 10088, favorite 10012) — noun-first relay <noun> add/remove/set/clear/list + fan-out relay add/remove + publish ✅ RelayCommands. Mirrors the Android relay-settings screen. Local relays (device pref) + relay sets (30002) intentionally out of scope.
MLS KeyPackage publish + fetch ✅ commons/marmot/MarmotManager
Marmot group create / add / rename / promote / demote / remove / leave ✅ commons/marmot/
Marmot message send / list ✅ commons/marmot/
await polling (KP / group / member / admin / message / rename / epoch) ✅ AwaitCommands
NIP-01 note publish (amy notes post TEXT) ✅ PostCommand — outbox via RelayCommands configured set.
NIP-13 proof of work (amy notes post --pow N, amy pow check/mine/bench) ✅ PostCommand + PowCommands — mines pre-signature via quartz PoWMiner; pow mine --pubkey covers delegated PoW; pow check applies the commitment cap.
NIP-01 feed read (amy notes feed [--following | --author NPUB | --hashtag TAG]) ✅ FeedCommand; rows carry the shared renderer's fields. Community feeds still pending.
Event view (amy notes show EVENT) ✅ NoteShowCommand over the shared commons/rendering/ EventRendererRegistry + JsonEventFormatter (any kind; specialised 0/1/3/5/6/7/16/1111/9735/10002/10050/30023).
NIP-10 / NIP-22 replies (amy notes reply EVENT TEXT) ✅ NoteActionCommands → commons ReplyActions.reply (kind:1 → NIP-10, anything else → NIP-22 kind:1111), text through the composer's NewMessageTagger + messageTags. Every interaction is routed by commons BroadcastRelayPlanner — the app's EventBroadcaster routing — with the call the app makes for that action.
NIP-02 follow list add / remove / list ✅ FollowCommand — amy follow USER / amy unfollow USER (fetches the freshest kind:3 first).
NIP-09 event deletion (amy delete EVENT…) ✅ DeleteCommand → commons DeletionActions (own events only, 200 targets per kind:5).
NIP-17 DMs send / list / await ✅ DmCommands — reuses Quartz NIP17Factory + RecipientRelayFetcher; filter extracted to commons/relayClient/nip17Dm/. Plan: cli/plans/2026-04-23-nip17-dm.md.
NIP-18 reposts / quotes (amy notes repost / notes quote) ✅ commons RepostAction (kind:6 / 16); a quote is the composer's own pre-filled text (quoteMessage) posted through the composer's tagging.
NIP-25 reactions ✅ amy notes react EVENT via commons ReactionAction; marmot message react covers MLS group reactions.
NIP-29 relay groups (amy relaygroup) ✅ RelayGroupCommands — list/browse/info/create/join/leave/message/edit/invite/put-user/remove-user against a host relay; kind:10009 joined-list kept in sync.
Buzz workspaces (amy buzz) ✅ BuzzCommands — post/read the kind:40002 stream timeline, attest (offline NIP-OA), console (decrypt+aggregate kind:44200 turn metrics via the shared AgentFleetAggregator), personas (kind:30175). Join/leave reuse amy relaygroup (Buzz workspaces are NIP-29 groups).
Buzz agent jobs (amy buzz job / agent serve) ✅ BuzzJobCommands (request/list/show/cancel, kinds 43001-43006) + BuzzAgentCommands (agent serve responder loop: gate on --accept-from, run --exec, report accept/progress/result/error). Correlation + state via the shared BuzzJobAggregator in commons. Schema provisional (43001-43006 reserved upstream). See cli/plans/2026-07-25-buzz-agent-support-channel.md.
NIP-51 lists (bookmarks, mute, follow sets) 🆕 amethyst/model/nip51Lists/
NIP-57 zaps (send) ✅ partial ZapCommand — zap user/zap event build the kind:9734 request and fetch the BOLT11 (zap splits honored, one invoice per recipient); --with NDEBIT auto-pays through a CLINK debit pointer. Receipt (kind:9735) verification still 🆕.
BOLT12 zaps (NIP-B1, kinds 9736/9737/10058) ✅ partial Bolt12Commands + Bolt12SendCommands over shared commons Bolt12ZapActions — bolt12 decode (offer/proof), verify (validate a kind:9736), offer get/set (kind:10058), and the two-step send intent→zap (out-of-band proof, since amy has no NWC rail). Interop harness + NWC-fetched proofs still 🆕.
NIP-65 outbox model queries ✅ OutboxCommand — amy outbox USER [--refresh], cache-first.
CLINK offers + debits (amy offer / amy debit) ✅ OfferCommands + DebitCommands — pointer decode, NIP-05 discover, kind:21001/21002 round-trips, offer pay --with NDEBIT end-to-end settlement. --timeout is SECONDS.
Geochat (Bitchat geohash, ephemeral kind:20000) ✅ GeochatCommands — listen/send/keys with per-geohash throwaway identity + geo-nearest relay routing; doubles as the Bitchat interop harness.
Concord Channels (encrypted communities) ✅ ConcordCommands — 17 sub-verbs (create/list/import/channels/send/read/invite/revoke/join/recover/rekey/roles/role/grant/ban/unban/refound) over shared commons ConcordActions; secrets in concord.json.
NIP-5A nsites + NIP-5D napplets ✅ NsiteCommands + NappletCommands — fetch/publish/serve/list with sha256 + aggregate-hash verification and requires capability reporting.
Release publishing (NIP-82: desktop updater + Zapstore + Orchard) ✅ ReleasesCommands — releases publish signs the desktop, APK and Flatpak asset + release events (zsp's Android event shape, apksig-checked APKs; header-checked Flatpak bundles for Orchard).
Podcasting 2.0 / podstr (amy podcast20) ✅ Podcast20Commands — kind:30078 metadata, 30054 episodes, 30055 trailers, list.
Follows-of-follows (amy fof get/list/sync) ✅ FofCommand — single-hop social proof from the local store (wot kept as deprecation alias).
NIP-85 GrapeRank web-of-trust (amy graperank) ✅ GrapeRankCommand — outbox-model crawl + scoring engine in commons/wot/ (GrapeRank, TrustGraph, TrustGraphBuilder); every score run persists kind:30382 UserAssertionEvent cards to the local store (diffed against prior ranks, kind:5 retractions), publish mirrors that set to the operator relays via NIP-77 up-sync, rank reads cards back, plus register / unregister / providers for the kind:10040 TrustProviderListEvent discovery layer.
NIP-85 Web of Trust network (amy trust sync/status/check/setup/copy) ✅ Thin layer over commons TrustNetworkState (the apps' index, sync and verdict), wot/onboarding/ (Brainstorm sign-up, which publishes the rows Brainstorm serves) and ScoreProviderEdits (copying another user's rows); the consumer side of graperank.
NIP-72 communities 🆕
NIP-78 app-specific data (settings sync) 🆕
Long-form (NIP-23) publish / read 🆕
Live activities / chess (NIP-53 / NIP-64) 🆕
Simple Nostr Objects (DECK-0003) ✅ read-only SnoCommands — sno parse (§1.9 verdict + the rule a refusal broke), sno work (the §8.10 proof of work an avatar owes), sno verify (whether a kind:11333 avatar has paid). Thin over quartz cyberspace/deck0003Sno/. Conformance harness at cli/tests/sno/ diffs all three against the cyberspace project's own sno-reference.py and cyberspace-cli. Publishing/authoring 🆕.
Blossom blobs (NIP-B7) ✅ BlossomCommands — upload/download/list/delete/check/mirror on shared commons BlossomClient; live-server harness at cli/tests/blossom/.
Custom emoji packs (NIP-30) ✅ EmojiCommands + EmojiImportCommand — pack list/show/create/edit/delete, add/remove (public or private), bulk import from a folder, .zip or shortcode url list (Blossom uploads), kind:10030 list/use/unuse. Builders are commons EmojiPackActions (shared with the app's OwnedEmojiPacksState); file naming and zip reading are commons EmojiImport / extractEmojiArchive. Harness at cli/tests/emoji/.
NIP-60 / 61 Cashu wallet + nutzaps ✅ Full surface: cashu wallet {create,show,export-key,destroy}, mint {ping,info}, sync, balance, receive {ln,complete,resume,token,nutzap-sweep}, send {ln,token,nutzap}, maintenance {scrub,restore,migrate-keysets}, mint-rec {show,add,remove} — all on shared commons CashuWalletOps + CashuWalletReader (the exact path the Android wallet runs). Reads project the local store; cashu sync (or --sync) is what fills it, paging every relay to exhaustion so a cap can't truncate the proof set. Interop harness pending. Plan: cli/plans/2026-05-28-cashu-cli.md.
NIP-47 Wallet Connect 🆕
NIP-46 bunker signer ✅ BunkerCommand + NostrConnect + LoginCommand — host (amy bunker[ connect]) and client (amy login bunker:// / --nostrconnect) sides, --perms/--interactive gating, auth_url challenges.
Profile view (amy profile show NPUB) + edit ✅ ProfileCommands. Cache-first; --refresh forces a relay drain.
Thread view (amy notes thread EVENT) ✅ ThreadCommand → commons relayClient/oneshot/ThreadLoader, which runs the app's thread code over a per-run EventCache (OneShotNoteCache): the thread screen's relay filters, ThreadAssembler, ThreadFeedFilter.thread ordering, ThreadLevelCalculator depths.
Notifications feed (amy notifications) ✅ NotificationsCommand → commons relayClient/oneshot/NotificationsLoader over NotificationKinds (filter + "is it for me") + NotificationItem.classify (extracted from Desktop). DMs stay in amy dm list.
Search (NIP-50) ✅ SearchCommand — search user (kind:0) + search note (--kind, --kinds alias) over the kind:10007 search-relay list; default limit 50.
Namecoin NIP-05 resolve (amy namecoin resolve .bit|d/|id/) ✅ NamecoinCommand — reuses Quartz NamecoinNameResolver + ElectrumXClient + the default ElectrumX server set the Android/Desktop apps ship with. Stateless. On-chain name_history + Core RPC backend pending separate PRs.

nak parity — army-knife primitives

Tracking fiatjaf/nak's command surface. amy adapts the verbiage where its own conventions differ (req → one-shot fetch vs streaming subscribe). Stateless verbs run with no account or network.

nak command amy verb Status Notes
decode amy decode ✅ NIP-19/21 → JSON. Quartz Nip19Parser.
encode amy encode ✅ npub/nsec/note/nevent/nprofile/naddr.
verify / validate amy verify ✅ id-hash + signature, reported separately.
key amy key generate|public|encrypt|decrypt|validate ✅ generate/derive + NIP-49 encrypt/decrypt (bidirectionally nak-verified) + validate (npub/hex parse check). expand/combine(MuSig2)/default still 🆕.
event amy event ✅ build/sign an arbitrary event, optional --publish/--relay.
publish amy publish ✅ broadcast a pre-made event JSON (verified first); --file streams a file of events to one relay through quartz's windowed publishStream, with per-reason OK counts and round-trip percentiles.
req (one-shot) amy fetch ✅ filter → collect-until-EOSE, dedupe, sort, cap. --paginate --limit 0 --out FILE exports a whole walk as one event per line. Also accepts a nip19/nip05 code and resolves relays via the outbox model (code hints + author's NIP-65 write relays), like nak's fetch.
req (stream) amy subscribe ✅ filter → live NDJSON stream to stdout.
count amy count ✅ NIP-45, per-relay counts.
encrypt / decrypt amy encrypt|decrypt ✅ raw NIP-44 (default) / NIP-04.
gift amy gift wrap|unwrap ✅ NIP-59 seal+wrap / unwrap+unseal.
relay (NIP-11) amy relay info ✅ stateless NIP-11 doc fetch.
outbox amy outbox ✅ NIP-65 read/write relays, cache-first.
filter amy filter ✅ stateless — assemble + print a filter JSON.
blossom amy blossom ✅ upload/download/list/delete/check/mirror (reuses commons BlossomClient).
nip amy nip ✅ repo-first lookup + Nostr fallback (NipText kind:30817, wiki:30818, long-form:30023); nip list.
kind amy kind ✅ quartz KindNames registry (kind → English label + NIP) covering every event kind quartz defines (280 entries); number lookup + name search.
sync amy sync ✅ NIP-77 Negentropy reconcile with the local store (down/up/both).
git amy git ✅ (events + read) NIP-34: init bootstraps a repo from the local git checkout (announce + state, like ngit init); repo announce (30617) + state (30618), patches (1617), pull requests (1618/1619), issues (1621), NIP-22 comments (1111), NIP-32 labels (1985), status open/applied/closed/draft (1630-1633), GRASP server list (10317); issues/patches/prs/thread reads derive status; apply applies a fetched patch to the local tree (git am); browse/cat/log read git objects over smart-HTTP v2 (quartz GitHttpClient, the same shallow-clone path the Android browser uses). Only git-packfile push (writing objects to clone/GRASP servers) and NIP-34 cover notes (1624: quartz now types and builds them as GitCoverNoteEvent, but amy has no verb yet) are out of scope. Event tag shapes were verified byte-for-byte against the ngit reference implementation and the NIP-34 spec (clone/web as single multi-value tags, issue p-tag for maintainer routing, plain patch/PR r tags); the quartz readers stay tolerant of the legacy repeated form. See quartz/…/nip34Git/GitNip34InteropTest.
podcast amy podcast ✅ NIP-F4 show metadata (10154) + episode publish (54) + list.
bunker amy bunker[ connect] + amy login bunker:///--nostrconnect ✅ NIP-46 remote signer + login, both the bunker:// and nostrconnect:// flows, each direction, plus auth_url challenge handling (client surfaces the URL + keeps waiting). Interop-verified vs real nak.
admin amy admin RELAY METHOD ✅ NIP-86 Relay Management over NIP-98 HTTP auth — full method set (ban/allow pubkey + event, NIP-43 roles + invite claims, kinds, IP block, change name/desc/icon, list-*). Reuses quartz Nip86Client + shared commons Nip86Retriever. Interop-verified against amy serve.
serve amy serve ✅ Embeds geode (the standalone Ktor relay on quartz's relay-server code) — in-memory by default, --db FILE for SQLite, account is admin so amy admin works against it. NIP-86 + NIP-77 included.
wallet (NIP-60 Cashu) amy cashu ✅ See the Cashu row above — full NIP-60/61 wallet + nutzaps.
mcp / fs / spell — 🆕 (niche) MCP server, FUSE mount, MuSig2/FROST; some pull new deps.

Full nak comparison (introspected both binaries)

nak has 34 functional commands (introspected from nak --help). Coverage:

  • Full / equivalent (24): event, req(→fetch+subscribe), fetch (nip19/nip05-hint resolution), filter, count, decode, encode, verify, relay, bunker(+nostrconnect+auth_url), encrypt, decrypt, gift, publish, sync, profile, podcast, nip, kind, blossom, nsite(NIP-5A), admin(NIP-86), serve(geode), wallet(NIP-60/61 Cashu). Protocol-sensitive ones (bunker, sync, key NIP-49, encode/decode, admin) are interop-verified against the real nak binary or amy serve.
  • Partial / adapted (3): key (no expand/combine(MuSig2)/default), git (full NIP-34 event surface — announce/state/patch/PR/issue/comment/status
    • status-deriving reads + GRASP list, plus browse/cat/log reading git objects over smart-HTTP; only packfile push is out of scope), outbox (NIP-65 model vs nak's local hints DB).
  • Missing (6): dekey (NIP-4E), mcp, curl (NIP-98), fs (FUSE), spell (MuSig2/FROST), and validate (event-schema validation).

Design differences (not gaps): amy is a stateful client (accounts, ~/.amy/, shared event store) with a stable JSON contract; nak is a stateless per-invocation tool that prints bare values for shell substitution. amy also has a large surface nak lacks: Marmot/MLS, NIP-29 relay groups (offered side by side with MLS, not substituting for it), Concord communities, geochat, NIP-17 DMs, zaps, CLINK offer/debit, NIP-02 follow, NIP-50 search, nsites/napplets, podcast20, GrapeRank/fof, profile edit, store management, account management.

Cheap remaining wins: the key expand (hex left-pad) and default (print the active account's key) sub-verbs. key combine needs MuSig2.


Order of operations

Proposed sequencing. Each step is one PR. Each step should extract at least one file from amethyst/ into commons/; if it doesn't move anything, re-audit — you're probably duplicating logic.

  1. Event rendering core in commons/commonMain/.../rendering/ — ✅ shipped with renderers for kinds 0 / 1 / 3 / 5 / 6 / 7 / 16 / 1111 / 9735 / 10002 / 10050 / 30023 and the JSON formatter amy emits. The Compose formatter and the Android/Desktop switch-over are the plan's remaining steps. Design: commons/plans/2026-04-21-event-renderer.md.
  2. amy notes post|show|reply|quote|react|repost|thread, amy delete, amy notifications — ✅ shipped: the core social loop outside Marmot.
  3. amy notes feed — --following, --author NPUB, --hashtag TAG ✅; community feeds still pending.
  4. amy follow|unfollow (NIP-02) — ✅ shipped. A standalone follow list view is still pending.
  5. amy dm send|list (NIP-17) — ✅ shipped. Reuses the gift-wrap path also exercised by Marmot.
  6. amy list bookmarks|mute|pin … (NIP-51).
  7. amy zap send|verify (NIP-57) — send ✅ (invoice fetch + optional CLINK auto-pay via --with NDEBIT); receipt verify pending.
  8. Distribution — Homebrew + Scoop + .deb in the same release pipeline as desktop. Plan: cli/plans/2026-04-21-cli-distribution.md.
  9. Test suite — largely in place, two layers:
    • Shell harnesses under cli/tests/ — among them: blossom (live servers), cache, clink, dm, git (NIP-34 vs amy serve), marmot (vs MDK), nests (manual audio-rooms matrix), notes (the social loop vs amy serve), pow, relaygroup, sync, plus the shared headless/ helpers. See cli/tests/README.md. Every relay-backed suite runs against the embedded amy serve relay (geode) — no external relay binary. None run in CI yet (the Marmot ones need Rust for MDK's wn).
    • JVM unit suite at cli/src/test/kotlin/ — Args parsing, exit-code contract, and --json shape tests driving runCli in-process via the amy.home isolation seam.
  10. Everything else in the matrix.

Non-goals

  • Interactive TUI or REPL.
  • Native image (GraalVM) until Quartz has a pure-Kotlin signer fallback — secp256k1-kmp-jni-* needs JNI today.
  • A Gradle dependency on :amethyst or :desktopApp. Ever.
  • Re-implementing any Nostr protocol piece that's already in quartz/ or in another client's library.