Files
amethyst/cli/README.md
T
Claude fa48d7188f 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.
2026-04-25 15:48:32 +00:00

177 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Amy — Amethyst CLI
`amy` is the non-interactive command-line face of Amethyst. It speaks the
same Nostr protocol as the Android and Desktop apps, shares the same
`quartz` and `commons` code, and aims to eventually expose every feature
the GUI offers as a command you can script.
Amy exists for three audiences at once:
1. **Humans** using Amethyst from a terminal or remote shell.
2. **Agents / LLMs** driving a Nostr account through a deterministic,
JSON-typed interface — no interactive prompts, no screen scraping.
3. **Interop test harnesses** that put Amethyst side-by-side with the
other ~100 Nostr clients publishing and consuming the same events.
Any flow that is tested in the Amethyst app should be reproducible
through `amy` — that's the bar.
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).
---
## Output contract
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.
- **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`.
- **Exit codes are the real signal.**
- `0` — success
- `1` — runtime error
- `2` — bad arguments
- `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/`. 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
in [DEVELOPMENT.md](./DEVELOPMENT.md).
---
## Local event store — the source of truth
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 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.
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 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)
ctx.relaysOf(pubKey) // latest kind:10002 (NIP-65)
ctx.contactsOf(pubKey) // latest kind:3 (NIP-02)
ctx.dmInboxOf(pubKey) // latest kind:10050 (NIP-17 DM inbox)
ctx.keyPackageRelaysOf(pubKey) // latest kind:10051 (MIP-00 KP relays)
ctx.cachedRelayListsOf(pubKey) // RecipientRelayFetcher.Lists 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`](./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
with completely disjoint relay configurations can still marmot each
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 |
|---|---|---|
| kind:30443 (our own KeyPackage) | `key_package` bucket → NIP-65 outbox → any configured | — |
| kind:30443 (someone else's KeyPackage) | — | Their kind:10051 → their kind:10002 write → our bootstrap pool |
| kind:10051 / 10050 / 10002 (our own lists) | All configured relays (broadcast) | — |
| kind:10051 / 10050 / 10002 (someone else's) | — | Our bootstrap pool = configured relays Amethyst defaults |
| kind:1059 Welcome gift wrap (kind:444 inside) | Recipient's kind:10050 → their kind:10002 read → `DefaultDMRelayList` → our outbox | — |
| 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
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
events.
---
## On-disk layout
```
~/.amy/ ← root, follows $HOME
├── current # marker file written by `amy use NAME`
├── shared/
│ └── events-store/ # FsEventStore — every observed Nostr event
│ ├── events/<aa>/<bb>/… # canonical kind:0 / 3 / 10002 / 10050 / 10051 / 1 / 5 / 1059 / …
│ ├── replaceable/<k>/… # one slot per (kind, pubkey) for kind:0/3/10000-19999
│ ├── addressable/… # one slot per (kind, pubkey, d-tag) for kind:30000-39999
│ ├── idx/ # hardlink indexes (kind / author / owner / tag / fts / expires_at)
│ └── tombstones/ # NIP-09 / NIP-62 enforcement
├── alice/ # one dir per account (e.g. created by `amy --account alice init`)
│ ├── identity.json # nsec/npub/hex — the account
│ ├── state.json # sync cursors (giftWrapSince, groupSince)
│ ├── aliases.json # local name → npub map (init writes a self-entry)
│ └── marmot/
│ ├── keypackages.bundle # MLS KeyPackage bundles (NostrSignerInternal)
│ └── groups/
│ ├── <gid>.mls # MLS group state per group
│ └── <gid>.log # decrypted inner events (one JSON per line)
└── bob/ ... # additional accounts sit alongside
```
All files are plain JSON or framed binary — human-inspectable, easy to
diff across two accounts. Two accounts on the same machine share
`~/.amy/shared/events-store/`, so a public event observed once doesn't
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`.
---
## Where to go next
- **[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, …).