docs(cli): slim README, refresh DEVELOPMENT + ROADMAP for USAGE.md split

USAGE.md is now the single home for "how do I use amy" — install,
quick start, examples, command reference, troubleshooting. README
was carrying all of that PLUS the public-contract material; with
USAGE in place it can collapse to just the contract.

README.md (338 → 176 lines):
- Keep: 1-paragraph intro (three audiences), output contract, local
  event-store explainer, relay-routing rules, on-disk layout, cross-
  references to USAGE/DEVELOPMENT/ROADMAP.
- Drop: install, quick start, command reference table, global flags
  table, account-management verbs, troubleshooting (all in USAGE).

DEVELOPMENT.md:
- Architecture diagram updated to reflect new files (Aliases.kt,
  UseCommand.kt, ProfileCommands.kt, DmCommands.kt, NotesCommands /
  PostCommand / FeedCommand, MarmotResetCommand, StoreCommands,
  SecureFileIO, secrets/ subtree).
- "Keep three things in sync" pointers redirected from README's
  command table to USAGE.md.
- Testing table loses the duplicate "Interop with other clients" row
  (already covered by the harness row above).
- Cross-reference list at the top now mentions USAGE.md.

ROADMAP.md:
- Cross-reference list adds USAGE.md.
- Parity matrix marked  for items shipped on this branch:
  notes post + feed (PostCommand/FeedCommand), profile show+edit
  (ProfileCommands), DMs (already ).
- Reactions split into " in groups, 🆕 elsewhere" since
  marmot message react is shipped but outer-event reactions aren't.
- Order-of-operations entries marked  where relevant.
This commit is contained in:
Claude
2026-04-25 15:48:32 +00:00
parent c7d7d88a6c
commit fa48d7188f
3 changed files with 83 additions and 235 deletions
+46 -208
View File
@@ -15,11 +15,11 @@ Amy exists for three audiences at once:
Any flow that is tested in the Amethyst app should be reproducible
through `amy` — that's the bar.
> Today Amy covers identity, relay config, account bootstrap, and
> Marmot / MLS group chat (MIP-00 / NIP-445). Everything else from the
> Android app is on the roadmap — see [ROADMAP.md](./ROADMAP.md).
>
> To extend Amy, see [DEVELOPMENT.md](./DEVELOPMENT.md).
This file documents the **public contract** and the on-disk layout. For
hands-on instructions and worked examples, see
[USAGE.md](./USAGE.md). To extend amy, see
[DEVELOPMENT.md](./DEVELOPMENT.md). For what's coming, see
[ROADMAP.md](./ROADMAP.md).
---
@@ -30,9 +30,8 @@ What every caller — user, script, agent, CI — can rely on:
- **Default stdout is human-readable text.** A YAML-ish render of the
underlying result map. Friendly at a terminal; no shape promises.
- **`--json` is the machine contract. One line. One object.** Stable
snake_case keys. Pipe it into `jq`, parse it from Python, hand it
to an agent. Pass `--json` anywhere before the subcommand:
`amy --json whoami`, `amy --account alice --json marmot group list`.
snake_case keys. Pipe it into `jq`, parse it from Python, hand it to
an agent. Pass `--json` anywhere before the subcommand.
- **stderr is for humans.** Progress, warnings, per-relay ACK traces.
Safe to discard. Errors land here too: `error: <code>: <detail>` by
default, or JSON `{"error":"…","detail":"…"}` under `--json`.
@@ -43,12 +42,11 @@ What every caller — user, script, agent, CI — can rely on:
- `124``await` timed out
- **No interactive prompts, ever.** Passwords, names, keys — all flags.
- **`~/.amy/` is the whole world.** Per-account dirs hold identity,
cursors, MLS state, and aliases at `~/.amy/<account>/`; every
observed Nostr event lands in `~/.amy/shared/events-store/` (one
cache for every account on the machine). Delete to reset; copy to
move. Tests isolate by overriding `$HOME` for the amy subprocess
(`HOME=/tmp/run.123 amy --account alice …`) — same convention `git`,
`gpg`, and `npm` use.
cursors, MLS state, and aliases at `~/.amy/<account>/`; every observed
Nostr event lands in `~/.amy/shared/events-store/`. Delete to reset;
copy to move. Tests isolate by overriding `$HOME` for the amy
subprocess (`HOME=/tmp/run.123 amy --account alice …`) — same
convention `git`, `gpg`, and `npm` use.
Only the `--json` shape and the exit codes are public API. The default
text format is allowed to change between releases. The rationale lives
@@ -58,25 +56,25 @@ in [DEVELOPMENT.md](./DEVELOPMENT.md).
## Local event store — the source of truth
Every Nostr event Amy observes is verified (NIP-01 id + signature
Every Nostr event amy observes is verified (NIP-01 id + signature
check) and persisted to a file-backed store at
`~/.amy/shared/events-store/` (one store per machine, shared across
every account in `~/.amy/`). That includes:
- events received from any relay subscription (`amy feed`, `amy dm
list`, `amy keypackage publish`, group sync, …),
- events Amy generates and publishes itself,
- events received from any relay subscription (`amy notes feed`,
`amy dm list`, `amy marmot key-package publish`, group sync, …),
- events amy generates and publishes itself,
- inner events unwrapped from NIP-59 gift wraps.
Malformed events are dropped before reaching command code. Persistence
is best-effort — if the store fails (full disk, permissions), the
relay subscription still works, but the event is not cached.
is best-effort — if the store fails (full disk, permissions), the relay
subscription still works, but the event is not cached.
The store is the authoritative cache of everything Amy has seen:
The store is the authoritative cache of everything amy has seen:
profile metadata, relay lists (NIP-65 and NIP-02), gift wraps, group
events, follow lists, etc. Commands that need any of these should read
from the store first and only fall back to a relay fetch on miss.
Three convenience helpers exist on `Context`:
events, follow lists, etc. Commands that need any of these read from
the store first and only fall back to a relay fetch on miss. Three
convenience helpers exist on `Context`:
```kotlin
ctx.profileOf(pubKey) // latest kind:0 (NIP-01)
@@ -87,177 +85,25 @@ ctx.keyPackageRelaysOf(pubKey) // latest kind:10051 (MIP-00 KP relays)
ctx.cachedRelayListsOf(pubKey) // RecipientRelayFetcher.Lists from cache
```
Commands that already read these cache-first:
- `amy profile show` — `--refresh` to bypass.
- `amy feed --following` — local kind:3 served via slot lookup; falls
back to a relay drain on first run.
- `amy dm send` — recipient's kind:10050 / 10051 / 10002 served from
cache before falling back to `RecipientRelayFetcher`.
- `amy marmot key-package check` and `amy marmot await key-package`
— same recipient-relay lookup, cache-first.
- `amy marmot group add` — invitee relay lists served from cache.
The store implements every feature of the Quartz SQLite store —
NIP-01 replaceable / addressable uniqueness, NIP-09 deletion
tombstones, NIP-40 expiration, NIP-50 search, NIP-62 right-to-vanish,
NIP-91 multi-tag AND. See `cli/plans/2026-04-24-file-event-store-*.md`
for the design and `quartz/.../store/fs/FsEventStore.kt` for the
implementation. The on-disk layout is plain JSON files under shard
directories, intentionally inspectable with `ls`, `cat`, `jq`,
`grep`, `find`, `rsync`, and `git`.
To manage the store directly:
```sh
# raw inspection
find ~/.amy/shared/events-store/events -name '*.json' | head
jq . ~/.amy/shared/events-store/replaceable/0/<pubkey>.json
# delete a specific event (tombstone NOT installed — see below)
rm ~/.amy/shared/events-store/events/<aa>/<bb>/<id>.json
```
Deleting an event file is treated as a deliberate "I never saw this"
by Amy. The store tolerates external edits: dangling index entries are
skipped at query time and can be cleaned up with `compact()` /
`scrub()` from the API.
---
## Install
Until Amy ships as a signed native binary (see
[cli/plans/2026-04-21-cli-distribution.md](./plans/2026-04-21-cli-distribution.md)),
run it from source:
```bash
# One-shot run — positional args go after `--args`, quoted as one string
./gradlew :cli:run --quiet --args="whoami"
# Or build a runnable distribution and use the generated launch script
./gradlew :cli:installDist
./cli/build/install/amy/bin/amy whoami
```
The `installDist` tree under `cli/build/install/amy/` is self-contained
(JVM launcher + jars) and is what downstream packaging will wrap.
**Requirements:** JDK 21.
---
## Quick start
```bash
# 1. Create the alice account (~/.amy/alice/) with a full Amethyst-style
# bootstrap: keypair, default NIP-65 / inbox / key-package relays,
# nine bootstrap events.
amy --account alice create
# 2. Publish a fresh MLS KeyPackage so others can invite you.
amy --account alice marmot key-package publish
# 3. Create a group, invite someone, send a message.
amy --account alice marmot group create --name "Test Group"
amy --account alice marmot group add <GID> npub1...bob
amy --account alice marmot message send <GID> "hello"
# 4. On the receiving side, in ~/.amy/bob/.
amy --account bob marmot await group --name "Test Group" --timeout 60
amy --account bob marmot message list <GID>
```
With one account you can drop the flag (auto-pick); with several, pin
one with `amy use alice` and the same commands work flagless. Compose
with `jq` by adding `--json` to switch stdout from human text to a
parseable single-line JSON object:
```bash
GID=$(amy --account alice --json marmot group create --name "Test" | jq -r .group_id)
```
For an interop-test script template, see
[DEVELOPMENT.md § Testing](./DEVELOPMENT.md#testing). The runnable
harnesses live under [`cli/tests/`](./tests/README.md) —
`cli/tests/marmot/` for MLS group messaging vs whitenoise-rs,
`cli/tests/dm/` for NIP-17 DMs between two `amy` clients.
---
## Command reference
Run `amy --help` for the canonical list. As of today:
| Verb | Summary |
|---|---|
| `init [--nsec NSEC]` | Create or import a bare identity. Does not publish anything. |
| `create [--name NAME]` | Provision a full account + publish the nine Amethyst bootstrap events. |
| `login KEY [--password X] [--private]` | Import `nsec` / `ncryptsec` / BIP-39 mnemonic / `npub` / `nprofile` / hex / NIP-05. Read-only when no secret material is supplied. |
| `whoami` | Print the active account's name + npub. |
| `use NAME` | Pin NAME as the active account (`~/.amy/current`). `use --clear` removes the pin; `use` prints the pin and the available accounts. |
| `profile show [PUBKEY] [--refresh] [--timeout SECS]` | Print kind:0 metadata. Default reads from the local store (cache-first); `--refresh` forces a relay drain. PUBKEY accepts `npub` / `nprofile` / hex / `name@domain.tld`; omit for self. |
| `profile edit --name X [--display-name X] …` | Build + publish a new kind:0 starting from the current cached metadata (or fetched if missing). |
| `relay add URL [--type T]` | `T = nip65 \| inbox \| key_package \| all`. |
| `relay list` | Dump configured relays by bucket. |
| `relay publish-lists` | Publish kind:10002 (NIP-65) + kind:10050 (DM inbox) + kind:10051 (KeyPackage relay list). |
| `marmot key-package publish` | Publish a fresh MLS KeyPackage (kind:30443) to the configured `key_package` bucket (fallback: NIP-65 outbox). |
| `marmot key-package check NPUB` | Look up NPUB's kind:10051 / kind:10002 on bootstrap relays, then fetch their KeyPackage from those relays. |
| `marmot group create [--name NAME]` | New empty group with you as sole admin. |
| `marmot group list` | All groups you're a member of. |
| `marmot group show GID` | Full group state (members, admins, epoch, metadata). |
| `marmot group members GID` | Members only. |
| `marmot group admins GID` | Admins only. |
| `marmot group add GID NPUB [NPUB…]` | Fetch KeyPackages for the npubs and commit an add. |
| `marmot group rename GID NAME` | Commit a metadata change. |
| `marmot group promote GID NPUB` | Make an existing member an admin. |
| `marmot group demote GID NPUB` | Revoke admin. |
| `marmot group remove GID NPUB` | Remove a member. |
| `marmot group leave GID` | Self-remove. |
| `marmot message send GID TEXT` | Publish a kind:9 inner event into the group. |
| `marmot message list GID [--limit N]` | Decrypted inner events, oldest first. |
| `marmot await key-package NPUB` | Block until a KeyPackage is seen on NPUB's advertised relays (kind:10051 / kind:10002). |
| `marmot await group --name NAME` | Block until we're added to a group with that name. |
| `marmot await member GID NPUB` | Block until NPUB is in GID's member set. |
| `marmot await admin GID NPUB` | Block until NPUB is an admin of GID. |
| `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 [--allow-fallback]` | Send a NIP-17 gift-wrapped text DM (kind:14 inside kind:1059). Default delivers only to the recipient's kind:10050 (per NIP-17); pass `--allow-fallback` to fall back to kind:10002 read marker → bootstrap pool. |
| `dm send-file RECIPIENT --file PATH --server URL [--mime-type M] [--allow-fallback]` | Encrypt the local file with a fresh AES-GCM cipher, upload the ciphertext to the Blossom server, then publish a kind:15 NIP-17 file message referencing the returned URL. The auto-detected hash, size, dimensions, and blurhash from the upload are folded into the event. The response also surfaces the encryption key + nonce so the same blob can be re-shared without re-uploading. |
| `dm send-file RECIPIENT URL --key HEX --nonce HEX [--mime-type M] [--hash H] [--original-hash H] [--size N] [--dim WxH] [--blurhash S] [--allow-fallback]` | Reference-mode variant: the file is already uploaded; `--key`/`--nonce` carry the AES-GCM material that recipients use to decrypt the bytes at `URL`. Useful when the upload happened elsewhere or to re-publish a previously-uploaded blob. |
| `dm list [--peer NPUB] [--since TS] [--limit N] [--timeout SECS]` | Drain and decrypt gift wraps on our inbox relays. Returns kind:14 (text) and kind:15 (file) messages with a `type` discriminator. 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 (matches text content for kind:14, URL for kind:15). Timeout exits 124. |
All `await` verbs accept `--timeout SECS` (default 30). Timeout exits 124
so scripts can distinguish "condition never happened" from "the command
itself crashed".
### Global flags
- `--account ACCOUNT` — pick which account under `~/.amy/<account>/` to
operate on. Required only when more than one account exists; with a
single account amy auto-picks. Override with `amy use NAME` to
pin one as the default. ACCOUNT must match `[a-zA-Z0-9_-]{1,64}`.
- `--json` — switch stdout to the machine-readable contract.
- `--secret-backend auto|keychain|ncryptsec|plaintext` — where to
persist the private key.
- `--passphrase-file PATH` — passphrase for the `ncryptsec` backend.
- `--help` / `-h` — usage summary.
### Account-management verbs
- `amy use NAME` — pin NAME as the active account (`~/.amy/current`).
- `amy use --clear` — remove the pin.
- `amy use` — print the current pin and the list of available accounts.
NIP-91 multi-tag AND. See
[`cli/plans/2026-04-24-file-event-store-*.md`](./plans/) for the design
and `quartz/.../store/fs/FsEventStore.kt` for the implementation. The
on-disk layout is plain JSON files under shard directories,
intentionally inspectable with `ls`, `cat`, `jq`, `grep`, `find`,
`rsync`, and `git`. Deleting an event file is treated as a deliberate
"I never saw this" by amy; dangling indexes are skipped at query time
and can be cleaned up with `amy store scrub` / `amy store compact`.
---
## Relay routing
Amy follows the Marmot protocol's per-event routing rules so two users
amy follows the Marmot protocol's per-event routing rules so two users
with completely disjoint relay configurations can still marmot each
other. No event ever ships blindly to "our configured relays" — Amy
other. No event ever ships blindly to "our configured relays" — amy
looks up the right relay set per event per recipient.
| Event | Publish to | Fetch from |
@@ -270,12 +116,12 @@ looks up the right relay set per event per recipient.
| kind:1059 gift wraps addressed to us | — | Our kind:10050 |
| kind:445 Group Event (Commit / Proposal / chat) | Group's MIP-01 `relays` field | Same |
**Bootstrap pool**: when Amy needs to discover a user it's never talked
**Bootstrap pool**: when amy needs to discover a user it's never talked
to, it queries `configured relays Amethyst's default NIP-65 set
Amethyst's default DM-inbox set`. These defaults come from
`commons.defaults.AmethystDefaults` and match what the Android/Desktop
UI publishes to on first run, so any fresh Amethyst account is
reachable via the bootstrap pool even before Amy has seen any of their
reachable via the bootstrap pool even before amy has seen any of their
events.
---
@@ -312,27 +158,19 @@ get re-stored per account.
The local relay configuration (kind:10002 / 10050 / 10051) is **not** a
separate file — it lives in the shared `events-store/` as signed events
owned by the account that wrote them. `amy relay add` builds + signs +
ingests a new relay-list event; `amy relay list` reads URLs straight out
of the latest event for each kind; `amy relay publish-lists` broadcasts
those events to upstream relays. There is no `relays.json`.
ingests a new relay-list event; `amy relay list` reads URLs straight
out of the latest event for each kind; `amy relay publish-lists`
broadcasts those events to upstream relays. There is no `relays.json`.
---
## Troubleshooting
## Where to go next
- **`no identity`** — run `init`, `create`, or `login` first, or pass a
different `--account`.
- **`no account at ~/.amy`** — there's no account dir yet; create one
with `amy --account <name> init`.
- **`multiple accounts in ~/.amy (...)`** — disambiguate with `--account`
on the next call, or pin a default via `amy use NAME`.
- **`not_member`** — the group GID is unknown to this account. Run
`marmot group list` to confirm, or `marmot await group --name …` to
wait for an invite to arrive.
- **Hang on a network verb** — Amy connects to the relays advertised
in your local kind:10002 / 10050 / 10051 events; inspect with
`amy relay list`. Every network-bound operation has a timeout — use
`--timeout` for `await`, or wrap the whole command in `timeout(1)`
if you're scripting.
- **Nothing seems to publish** — inspect stderr; each publish prints
per-relay `OK` / `REJECT` via the `[cli] …` traces.
- **[USAGE.md](./USAGE.md)** — install, quick start, worked examples,
the command reference, troubleshooting.
- **[DEVELOPMENT.md](./DEVELOPMENT.md)** — design principles,
architecture, how to add a command without breaking the contract.
- **[ROADMAP.md](./ROADMAP.md)** — north-star goal and the parity
matrix tracking what's left to extract from the Android app.
- **[`plans/`](./plans/)** — design docs for cross-cutting work
(CLI distribution, file-backed event store, NIP-17 DMs, …).