Redesign the shared StaticWebsiteCard (used by the feed AND the napplets browse screen) to look like an app entry instead of a manifest dump: square app icon (with a colored monogram fallback), name, a NAPPLET/WEBSITE type label, a short description, and an Open button. The technical details users don't care about — declared capabilities, Blossom servers, source URL — move behind a tap-to-expand "What it can access" disclosure; capabilities are still re-confirmed at the consent prompt when actually used and remain fully manageable in the permissions screen. Add an `icon` tag (NIP-5A/5D) end-to-end: - quartz: IconTag + siteIcon() accessor/builder, NappletManifest.icon(), and an icon param on all four site/napplet build() factories (+ round-trip test). - amy: `--icon URL` on `nsite/napplet publish`, surfaced in the publish output. - card: renders the icon via Coil, monogram fallback when absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Amy — Amethyst CLI
amy is the command-line face of Amethyst.
It speaks the same Nostr protocol as the Android and Desktop apps and shares
their codebase. From a terminal you can post notes, send NIP-17 DMs,
manage MLS group chats, switch identities, and pipe machine-readable JSON
into the rest of your toolbox.
amy is built for three audiences at once: humans at a terminal,
agents/LLMs driving an account through a deterministic JSON interface,
and interop test harnesses pinning Amethyst against the rest of the
Nostr-client ecosystem.
Looking for the architecture and the public-API contract? See DEVELOPMENT.md. For what's coming, see ROADMAP.md.
Install
amy builds from this repository — no package manager yet.
# build the runnable distribution
./gradlew :cli:installDist
# the launch script
./cli/build/install/amy/bin/amy --help
# put it on your PATH if you want
ln -s "$PWD/cli/build/install/amy/bin/amy" ~/.local/bin/amy
Requires JDK 21. All state lives under ~/.amy/ — delete to reset.
Quick start
# 1. Create an account named alice — keypair, default relays, kind:0 metadata,
# everything Amethyst stamps on first run.
amy --account alice create --name "Alice"
# 2. With one account you can drop the flag from now on (auto-pick).
amy whoami
# 3. Post a short note.
amy notes post "hello from amy"
# 4. Send a NIP-17 DM.
amy dm send bob@example.com "hey"
# 5. Read your inbox.
amy dm list
That's the full loop. Add --json to any command if you want a single-line
JSON object instead of human-readable text — same data, machine shape.
Examples
1. Post a note
$ amy notes post "good morning nostr"
event_id: a3c1f9c2…(64 hex)
kind: 1
accepted_by:
- wss://relay.damus.io/
- wss://nos.lol/
rejected_by: (none)
amy notes feed reads recent kind:1 notes from your follows; --limit N
caps the count, --author npub1… narrows to one user.
2. Send a direct message
$ amy dm send npub1uu8m… "lunch friday?"
event_id: 18bd0a7e…
kind: 14
recipients:
- pubkey: e70fb804…
relay_source: kind_10050
relays:
- wss://nostr.wine/
recipients[*].relay_source tells you how amy resolved the recipient's
inbox — kind_10050 is the strict NIP-17 inbox; nip65_read /
bootstrap only fire when you pass --allow-fallback.
3. Read a DM thread
$ amy dm list --peer npub1uu8m… --limit 5
messages:
- event_id: a82f04e1…
author: 71cf3ab2…
type: text
created_at: 2026-04-25 13:42:11Z (8m ago)
content: sounds good
- event_id: 18bd0a7e…
author: e70fb804…
type: text
created_at: 2026-04-25 13:30:02Z (20m ago)
content: lunch friday?
amy dm await --peer NPUB --match TEXT --timeout 60 blocks until a matching
DM arrives — useful in scripts.
4. View a profile
$ amy profile show npub1th9z…
pubkey: 5dca27ae…
found: yes
source: cache
event_id: a041df5a…
created_at: 2026-04-25 13:36:23Z (1h ago)
metadata:
name: Alice
picture: https://example.test/a.png
about: demo identity
nip05: alice@example.test
queried_relays: (none)
source: cache means the local store served the lookup; pass --refresh to
force a relay round-trip. Profiles for name@domain.tld (NIP-05) are
resolved transparently.
5. Create a group, invite someone, send a message
# Mint a group and invite Bob.
GID=$(amy --json marmot group create --name "Lunch Plans" | jq -r .group_id)
amy marmot group add "$GID" npub1...bob
# Send an MLS-encrypted message.
amy marmot message send "$GID" "hello group"
On the other side:
# Bob waits for the invite to land, then sees the message.
amy --account bob marmot await group --name "Lunch Plans" --timeout 60
amy --account bob marmot message list "$GID"
6. Switch between accounts
$ amy whoami
error: bad_args: multiple accounts in /home/me/.amy (alice, bob); pick one with --account <name> or `amy use <name>`
$ amy use bob
current: bob
root: /home/me/.amy
$ amy whoami
name: bob
npub: npub1uu8m…
data_dir: /home/me/.amy/bob
amy use --clear removes the pin; amy --account alice <cmd> overrides
it for one command.
7. Add a relay
$ amy relay add wss://nostr.wine
url: wss://nostr.wine
added_to:
- nip65
- inbox
- key_package
already_present: (none)
$ amy relay publish-lists # broadcast updated kind:10002/10050/10051
Commands
Primitives (stateless — no account or network)
Army-knife verbs that operate purely on their arguments. They never touch
~/.amy/, so they run with zero state — handy for scripting and piping
(amy decode … | jq, … | amy verify).
| Command | What it does |
|---|---|
amy decode ENTITY |
Decode a NIP-19/21 entity (npub/nsec/note/nevent/nprofile/naddr/nrelay/nembed) to JSON. Accepts an optional nostr: prefix. |
amy encode npub HEX / nsec HEX / note ID |
Encode a single 32-byte hex value into the matching NIP-19 entity. |
amy encode nevent ID [--author HEX] [--kind N] [--relay URL[,URL…]] |
Encode an event pointer with optional author/kind/relay hints. |
amy encode nprofile HEX [--relay URL[,URL…]] |
Encode a profile pointer with optional relay hints. |
amy encode naddr --kind N --pubkey HEX --identifier D [--relay URL[,URL…]] |
Encode an addressable-event (a tag) pointer. |
amy verify [EVENT-JSON] |
Check an event's id hash and signature. Reads stdin when the argument is omitted or -. Reports id_ok + signature_ok separately. |
amy key generate |
Mint a fresh keypair (nsec + npub + hex). Does not persist — use init/login for that. |
amy key public NSEC|HEX |
Derive the public key from a secret key. |
amy key encrypt NSEC|HEX --password X |
NIP-49 encrypt a secret key to an ncryptsec1…. |
amy key decrypt NCRYPTSEC --password X |
NIP-49 decrypt back to nsec/hex/npub. |
amy key validate NPUB|HEX |
Parse-check a public key. Prints {valid, pubkey, npub} or {valid:false} — never errors, so scripts branch on the field. |
amy filter [filter flags] |
Assemble and print a NIP-01 filter JSON from the same flags fetch/subscribe use — no query is sent. |
amy nip N / amy nip list |
Look up a NIP — the nostr-protocol/nips repo first, then a Nostr wiki/long-form fallback. list fetches the index. |
amy kind N / amy kind NAME |
Look up an event kind's label + defining NIP (number), or search labels by name. Backed by quartz's KindNames registry. |
amy relay info URL |
Fetch and print a relay's NIP-11 information document. |
Remote signing (NIP-46 bunker)
Two amy processes can talk: one hosts a bunker with its local key; the other logs in through it and signs remotely (events come out authored by the host's key).
| Command | What it does |
|---|---|
amy bunker [--relay URL[,URL…]] [--secret S] [--timeout SECS] |
Run a NIP-46 remote signer for the active local-key account. Prints a bunker://… URI, then services sign / nip04 / nip44 / get_public_key / ping requests until interrupted (or --timeout). |
amy login bunker://PUBKEY?relay=…&secret=… |
Log in through a bunker (signer advertises). Mints a local transport keypair; the account then acts as PUBKEY and every signing/encryption call is delegated to the remote signer. Percent-encoded relay params are decoded. |
amy bunker connect nostrconnect://… |
Client-initiated (NostrConnect) flow, signer side: ack a client's offer (echo its secret) and service its requests. |
amy login --nostrconnect [--relay URL[,URL…]] [--name N] [--timeout SECS] |
Client-initiated flow, client side: print a nostrconnect:// offer, wait for a signer to connect, then persist a bunker account that acts as the signer's key. |
Interop-tested against the real nak binary:
- bunker:// both directions —
amy login bunker://⇄nak bunker, andnak event --sec bunker://⇄amy bunker. - nostrconnect:// client —
amy login --nostrconnect⇄nak bunker connect(amy signs, event authored by nak's key).
Supports connect (secret-checked), get_public_key, get_relays, sign_event, nip04_encrypt/decrypt, nip44_encrypt/decrypt, ping. When a bunker answers with an auth_url challenge, amy prints the authorization URL to stderr and keeps waiting for the real response (open the URL in a browser to authorize).
Example (two terminals, shared $HOME):
# terminal 1 — host alice's key as a bunker
amy --account alice bunker --relay wss://relay.example --secret s3cret
# → bunker://<alice-pubkey>?relay=wss://relay.example/&secret=s3cret
# terminal 2 — bob signs through it
amy --account bob login 'bunker://<alice-pubkey>?relay=wss://relay.example/&secret=s3cret'
amy --account bob event --kind 1 --content "signed remotely" # authored by alice
Raw events
| Command | What it does |
|---|---|
amy event --kind N [--content TEXT] [--tags JSON] [--created-at TS] |
Build + sign an arbitrary event with the active account. Prints the signed event. --tags is a JSON array-of-arrays, e.g. '[["t","nostr"],["e","<id>"]]'. |
amy event … --publish / --relay URL[,URL…] |
As above, then broadcast (to the outbox, or to the given relays). |
amy publish [EVENT-JSON] [--relay URL[,URL…]] |
Broadcast a pre-made signed event (verified first). Reads stdin when the argument is omitted or -. |
Queries
Filter flags are shared by fetch and subscribe: --kind K[,K], --author U[,U] (npub/nprofile/hex), --id ID[,ID] (note/nevent/naddr/hex), --tag e=ID,p=PK,t=hashtag, --since TS, --until TS, --limit N, --search TEXT, --relay URL[,URL…]. Relays default to your outbox, then the bootstrap set.
| Command | What it does |
|---|---|
amy fetch [filter flags] [--timeout SECS] |
One-shot query — collect until every relay sends EOSE (or --timeout, default 8s), dedupe, sort newest-first, print and exit. --limit defaults to 100. |
amy fetch CODE [--timeout SECS] |
Code mode — pass a single nevent/naddr/nprofile/npub/note or name@domain. Resolves relays the outbox way: the hints embedded in the code plus the author's NIP-65 write relays (draining their kind:10002 on a cache miss), exactly how the app opens a shared link. |
amy subscribe [filter flags] [--timeout SECS] |
Live stream — print each matching event as it arrives (NDJSON under --json). Runs until --timeout SECS or until interrupted. |
amy count [filter flags] [--timeout SECS] |
NIP-45 COUNT — per-relay match counts, no event download. |
amy outbox USER [--refresh] [--timeout SECS] |
Show USER's NIP-65 read/write relays (outbox model). Cache-first; --refresh forces a relay drain. |
amy sync --relay URL [filter flags] [--down] [--up] |
NIP-77 Negentropy reconcile between the local store and a relay. --down (default) pulls events we lack; --up pushes events the relay lacks; both for bidirectional. |
Encryption
| Command | What it does |
|---|---|
amy encrypt --to USER [TEXT] [--nip04] |
NIP-44 (default) or NIP-04 encrypt with the active account's key. Reads stdin when TEXT is omitted or -. USER accepts npub/nprofile/hex/NIP-05. |
amy decrypt --from USER [CIPHERTEXT] [--nip04] |
Inverse of encrypt. |
amy gift wrap --to USER [EVENT-JSON] [--relay …] |
NIP-59: seal a signed inner event for USER and wrap it in a kind:1059 gift wrap. Prints the wrap; --relay also broadcasts it. |
amy gift unwrap [GIFTWRAP-JSON] |
Decrypt + unseal a kind:1059 wrap addressed to the active account; prints the inner event. |
Git (NIP-34)
nak's clone/push/pull (git-packfile transport over relays/GRASP) are out of scope — these are the metadata + collaboration events.
| Command | What it does |
|---|---|
amy git announce --name N [--description D] [--clone URL[,URL]] [--web URL[,URL]] [--relay URL[,URL]] [--maintainer HEX[,HEX]] [--hashtag T[,T]] [--earliest-commit C] [--d ID] |
Publish a kind:30617 repository announcement. |
amy git list [USER] |
List a user's repo announcements (defaults to self). |
amy git show NADDR|kind:pubkey:id |
Print one repo announcement (cache-first). |
amy git issue NADDR|coords --subject S [BODY] [--hashtag T[,T]] |
Publish a kind:1621 issue against a repo. BODY from arg or stdin. |
Podcasts (NIP-F4)
| Command | What it does |
|---|---|
amy podcast metadata --title T --image URL --description D [--website URL[,URL]] |
Publish kind:10154 show metadata (replaceable). |
amy podcast publish --title T --description D --audio URL[,URL] [--audio-type MIME] [--image URL] [--content MD] |
Publish a kind:54 episode. |
amy podcast list [USER] [--limit N] |
List a user's show metadata + episodes. |
Blossom blobs (NIP-B7)
| Command | What it does |
|---|---|
amy blossom upload --server URL FILE [--mime-type M] |
Upload a file (BUD-01, authed). Prints the blob URL + sha256. |
amy blossom download URL [--out FILE] |
Download a blob (public). Accepts a full URL, or a HASH plus --server URL. |
amy blossom list --server URL [USER] |
List a user's blobs (BUD-04). USER defaults to the active account. |
amy blossom delete HASH --server URL |
Delete a blob you own (BUD-02). |
amy blossom check --server URL HASH[,HASH] |
HEAD-check the server has each blob; exit 1 if any is missing. |
amy blossom mirror --server URL SOURCE-URL |
Ask the server to mirror a blob from SOURCE-URL (BUD-04). |
Cashu wallet (NIP-60 / NIP-61)
A NIP-60 ecash wallet + NIP-61 nutzaps, driven by the same shared
commons CashuWalletOps / CashuWalletReader the Android wallet runs — so
amy's on-relay events match the app's. NUT-13 counters persist in
~/.amy/<account>/cashu.json. mint ping/info are stateless (no account).
| Command | What it does |
|---|---|
amy cashu wallet create [--mint URL] [--mints a,b] [--privkey HEX] [--relay r1,r2] |
Publish a kind:17375 wallet + kind:10019 nutzap info. Advertises your outbox relays for nutzaps unless --relay overrides. |
amy cashu wallet show |
P2PK pubkey, mints, balance, per-mint balances, proof/history/pending counts. |
amy cashu wallet export-key |
Decrypt and print the wallet's P2PK private key. |
amy cashu wallet destroy |
Withdraw the nutzap advertisement and NIP-09 delete the wallet (leaves token events — the ecash still lives at the mint). |
amy cashu balance [--mint URL] |
Spendable balance from the local store (optionally one mint). |
amy cashu mint ping URL / info URL |
Stateless /v1/info probe (name/pubkey/version) / full DTO. |
amy cashu receive ln SATS [--mint URL] |
Request a mint quote; prints the bolt11 + kind:7374 quote. |
amy cashu receive complete QUOTE_ID / resume QUOTE_ID |
Poll the quote; once the invoice is settled, mint proofs (kind:7375 + kind:7376). |
amy cashu receive token TOKEN |
Redeem a cashuB… token into the wallet. |
amy cashu receive nutzap-sweep [--mint URL] |
Redeem inbound NIP-61 nutzaps locked to your wallet key. |
amy cashu send ln INVOICE [--mint URL] |
Melt proofs to pay a bolt11 (scrubs stale proofs first). |
amy cashu send token SATS [--mint URL] [--memo S] |
Export a cashuB… token of SATS. |
amy cashu send nutzap USER SATS [--zapped EVENT_ID] [--message S] |
Send a P2PK-locked nutzap to USER (resolves their kind:10019). |
amy cashu maintenance scrub [--mint URL] |
NUT-07 + NIP-09 prune of spent proofs. |
amy cashu maintenance restore MINT_URL |
NUT-09 restore unspent proofs from the wallet seed. |
amy cashu maintenance migrate-keysets [--mint URL] |
Consolidate proofs onto each mint's active keyset. |
amy cashu mint-rec show [--author NPUB] / add URL [--dtag X] [--review T] / remove EVENT_ID |
NIP-87 mint recommendations (kind:38000). |
Relay management — admin (NIP-86)
Signs a NIP-98 request with the active account and POSTs it to the relay's
HTTP endpoint. Reuses quartz's Nip86Client and the shared Nip86Retriever
(the same path Amethyst's relay-management screen runs).
| Command | What it does |
|---|---|
amy admin RELAY supported-methods |
List the NIP-86 methods the relay implements. |
amy admin RELAY ban-pubkey HEX [--reason R] / unban-pubkey HEX / list-banned-pubkeys |
Pubkey ban list. |
amy admin RELAY allow-pubkey HEX [--reason R] / unallow-pubkey HEX / list-allowed-pubkeys |
Pubkey allow list. |
amy admin RELAY ban-event ID [--reason R] / allow-event ID / list-banned-events / list-needing-moderation |
Event moderation. |
amy admin RELAY allow-kind N / disallow-kind N / list-allowed-kinds |
Kind allow list. |
amy admin RELAY block-ip IP [--reason R] / unblock-ip IP / list-blocked-ips |
IP block list. |
amy admin RELAY change-name S / change-description S / change-icon URL |
Relay metadata. |
Run a relay — serve
| Command | What it does |
|---|---|
amy serve [--host H] [--port N] [--path P] [--db FILE] [--admin NPUBS] |
Run a Nostr relay by embedding geode (the standalone Ktor relay on quartz's relay-server code). In-memory by default; --db FILE for SQLite. The active account is always an admin, so amy admin ws://host:port … works against it. Blocks until interrupted. |
Identity
| Command | What it does |
|---|---|
amy --account NAME init [--nsec NSEC] |
Create or import a bare keypair. No relay traffic. |
amy --account NAME create [--name X] |
Full Amethyst-style bootstrap: keypair, default relays, kind:0, kind:3, the works. |
amy login KEY [--password X] |
Import an existing identity (nsec/ncryptsec/mnemonic/npub/nprofile/hex/NIP-05). |
amy whoami |
Print the active account's name + npub. |
amy use NAME / --clear / no-arg |
Pin / clear / inspect the active account. |
Social
| Command | What it does |
|---|---|
amy notes post TEXT [--relay URL] |
Publish a kind:1 short text note. |
amy notes feed [--author USER | --following] [--limit N] |
Read recent kind:1 notes (yours, one user's, or your follow set). |
amy profile show [USER] |
Print kind:0 metadata. USER accepts npub/nprofile/hex/NIP-05; defaults to self. |
amy profile edit --name … --about … --picture URL … |
Patch and re-publish your kind:0. |
Direct messages (NIP-17)
| Command | What it does |
|---|---|
amy dm send RECIPIENT TEXT [--allow-fallback] |
Gift-wrap a kind:14 to RECIPIENT. Strict kind:10050 routing by default. |
amy dm send-file RECIPIENT --file PATH --server URL |
Encrypt a local file, upload to a Blossom server, publish a kind:15 referencing it. |
amy dm send-file RECIPIENT URL --key HEX --nonce HEX |
Reference-mode: file already uploaded; just publish the kind:15. |
amy dm list [--peer NPUB] [--since TS] [--limit N] |
Drain and decrypt gift wraps. |
amy dm await --peer NPUB --match TEXT [--timeout SECS] |
Block until a matching DM arrives. |
Groups (Marmot / MLS)
| Command | What it does |
|---|---|
amy marmot key-package publish |
Publish a fresh KeyPackage so others can invite you. |
amy marmot key-package check NPUB |
Look up someone else's KeyPackage on relays. |
amy marmot group create [--name X] |
New empty group with you as sole admin. |
amy marmot group list |
All groups you're a member of. |
amy marmot group show GID |
Members, admins, epoch, metadata. |
amy marmot group add GID NPUB [NPUB…] |
Fetch KeyPackages and invite. |
amy marmot group rename GID NAME |
Commit a metadata change. |
amy marmot group promote / demote / remove GID NPUB |
Admin verbs. |
amy marmot group leave GID |
Self-remove. |
amy marmot message send GID TEXT |
Publish a kind:9 inner event into the group. |
amy marmot message list GID [--limit N] |
Decrypted inner events, oldest first. |
amy marmot message react GID EVENT_ID EMOJI |
Publish a kind:7 reaction. |
amy marmot message delete GID EVENT_ID … |
Publish a kind:5 deletion. |
CLINK Offers
| Command | What it does |
|---|---|
amy offer info NOFFER |
Decode a noffer1… pointer (pubkey, relays, price type/amount). Local, no network. |
amy offer request NOFFER [--amount SATS] [--timeout MS] [--payer-data K=V,…] |
kind:21001 round-trip: publish the request to the pointer's relays and print the returned BOLT11. --amount is required for spontaneous offers; fixed offers default to the pointer's price. --payer-data attaches payer fields (e.g. email=a@b.c) for offers that require them — Lightning.Pub answers "Invalid Offer" (code 1) when they are missing. |
CLINK Debits
| Command | What it does |
|---|---|
amy debit info NDEBIT |
Decode an ndebit1… pointer (pubkey, relays, pointer id, session flag). Local, no network. |
amy debit pay NDEBIT BOLT11 [--amount SATS] [--timeout MS] |
kind:21002 round-trip: ask the pointed-to wallet to pay the invoice; print the preimage or the service's GFY error. |
amy debit budget NDEBIT --amount SATS [--frequency day|week|month] [--timeout MS] |
Authorize a spending budget; omit --frequency for a one-time budget. |
Wait-for-condition (await)
Every await verb blocks until the condition holds, then prints the
matching event/state. All accept --timeout SECS (default 30); on
timeout the exit code is 124 so scripts can tell "didn't happen"
from "command crashed".
| Command | Blocks until… |
|---|---|
amy marmot await key-package NPUB |
NPUB has a KeyPackage discoverable on their advertised relays. |
amy marmot await group --name X |
You've been added to a group with that name. |
amy marmot await member GID NPUB |
NPUB is in GID's member set. |
amy marmot await admin GID NPUB |
NPUB is an admin of GID. |
amy marmot await message GID --match TEXT |
A message containing TEXT lands in GID. |
amy marmot await rename GID --name X |
GID's name matches X. |
amy marmot await epoch GID --min N |
GID's MLS epoch reaches N. |
amy dm await --peer NPUB --match TEXT |
A matching DM from NPUB arrives. |
Relays
| Command | What it does |
|---|---|
amy relay add URL [--type T] |
Add URL to a bucket: nip65, inbox, key_package, or all. |
amy relay list |
Print the configured relays per bucket. |
amy relay publish-lists |
Broadcast your kind:10002 / 10050 / 10051. |
Local store maintenance
| Command | What it does |
|---|---|
amy store stat |
Event count, kind histogram, disk usage, oldest/newest timestamps. |
amy store sweep-expired |
Delete events past their NIP-40 expiration. |
amy store scrub |
Rebuild the index after external edits or a crash. |
amy store compact |
Drop dangling index entries (canonical event already gone). |
Output: text by default, JSON on demand
By default amy writes a YAML-ish, colored, human-readable result to
stdout. Pass --json and stdout becomes a single-line JSON object —
same data, stable snake_case keys, ready for jq:
$ amy --json whoami
{"name":"alice","npub":"npub1th9z…","hex":"5dca27ae…","data_dir":"/home/me/.amy/alice"}
$ amy --json marmot group create --name "Lunch" | jq -r .group_id
ab12cd34…
Errors mirror the same rule. Default:
$ amy marmot group show abc123
error: not_member: abc123 # exit 1
Under --json the error goes to stderr as {"error":"not_member","detail":"abc123"}.
Color auto-disables when stdout is a pipe; force it with CLICOLOR_FORCE=1,
turn it off entirely with NO_COLOR=1.
Exit codes — the real signal for scripts:
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | runtime error (network, permission, NIP rejection, …) |
| 2 | bad arguments |
| 124 | await timed out |
Multi-account workflows
amy is built to host more than one identity per machine. The layout
matches that:
~/.amy/
├── current # marker: which account `amy use NAME` pinned
├── shared/
│ └── events-store/ # one Nostr event store, shared by every account
├── alice/
│ ├── identity.json # keypair (or reference to keychain entry)
│ ├── state.json # sync cursors
│ ├── aliases.json # local name → npub map
│ └── marmot/ # MLS state per group
└── bob/
└── …
Account selection when you don't pass --account:
- If
~/.amy/currentis set, use it. - Else if exactly one account exists, use it (silent auto-pick).
- Else error and list the candidates so you can disambiguate.
amy use NAME writes ~/.amy/current; amy use --clear removes it.
For one-off override, prepend --account NAME to any command.
init and create write a self-entry into aliases.json so you can
refer to your own account by name in future commands. The alias resolver
in recipient slots (amy dm send alice "hi") is on the roadmap.
For the deeper layout (events-store internals, relay-routing rules, the public-contract guarantees) see DEVELOPMENT.md.
For agents and scripts
Three contracts keep amy machine-safe:
- One JSON object per success on stdout under
--json. Stable snake_case keys; keys never disappear silently. - Errors as JSON on stderr under
--json:{"error":"...","detail":"..."}. - Exit codes mean specific things (table above) —
124forawaittimeout in particular lets you distinguish "condition never happened" from "the command itself crashed".
Recipes
# Capture a fresh group's id.
GID=$(amy --json marmot group create --name "ops" | jq -r .group_id)
# Add several members at once and report which KeyPackages were missing.
amy --json marmot group add "$GID" npub1aaa npub1bbb npub1ccc \
| jq -r '.added[] | select(.status != "ok") | "missing: \(.pubkey)"'
# Wait up to 5 minutes for a particular message and capture its event id.
EVT=$(amy --json marmot await message "$GID" --match "deploy starting" --timeout 300 \
| jq -r .event_id)
# Run a command per follow.
amy --json notes feed --following --limit 50 \
| jq -r '.notes[].author' \
| sort -u \
| while read -r author; do
amy --json profile show "$author" | jq -r '.metadata.name // "?"'
done
Test isolation
amy reads $HOME directly to find ~/.amy/, so harnesses isolate the
exact same way git, gpg, npm, and ssh do — by overriding $HOME
for the subprocess:
HOME=$(mktemp -d) amy --account alice init
HOME=$(mktemp -d) amy --account alice marmot group create --name "scratch"
Inside the amy process there's no test mode — it just sees a fresh
~/.amy/ and behaves like a brand-new install.
Troubleshooting
no account at ~/.amy— you haven't created one yet. Runamy --account NAME init(bare keypair) oramy --account NAME create(full Amethyst-style bootstrap).multiple accounts in ~/.amy (alice, bob)— pin one withamy use NAMEor pass--account NAMEper command.current pins 'X' but ~/.amy/X doesn't exist— the active-account marker is stale. Rewrite withamy use OTHERoramy use --clear.no_dm_relays— recipient hasn't published a kind:10050 inbox. Pass--allow-fallbackto fall back to their kind:10002 read marker → bootstrap pool. Or wait for them to publish one.not_member— the group GID is unknown to this account. Runamy marmot group listto see what you're in, orawait group --name Xto wait for an invite.- A network verb hangs — every network verb has a relay timeout.
Inspect what amy is connecting to with
amy relay list. Wrap any command intimeout(1)if you're scripting and want a hard ceiling. - Nothing seems to publish — stderr carries
[cli] …traces with per-relayOK/REJECT. Capture with2> /tmp/amy.logand grep.
Where to go next
- DEVELOPMENT.md — design principles, architecture, the public contract, the local event store, relay routing, full on-disk layout, how to extend amy without breaking it.
- ROADMAP.md — north-star goal and the parity matrix tracking what's left to extract from the Android app.
plans/— design docs for cross-cutting work (CLI distribution, file-backed event store, NIP-17 DMs, …).- Nostr NIPs — the protocol amy speaks.