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:
+23
-16
@@ -2,7 +2,8 @@
|
||||
|
||||
How to touch the `cli/` module without breaking its public contract.
|
||||
|
||||
- What Amy is and what's already shipped: [README.md](./README.md).
|
||||
- What Amy is and the public contract: [README.md](./README.md).
|
||||
- How to use it: [USAGE.md](./USAGE.md).
|
||||
- What to build next and in what order: [ROADMAP.md](./ROADMAP.md).
|
||||
- Plans for cross-cutting work: see this module's `plans/` folder
|
||||
and `commons/plans/` for shared-code work consumed by Amy.
|
||||
@@ -46,25 +47,31 @@ cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/
|
||||
├── Main.kt # argv → subcommand dispatch
|
||||
├── Args.kt # tiny flag parser (no framework)
|
||||
├── Output.kt # text/json mode emitter (--json flag)
|
||||
├── Config.kt # Identity, RunState, DataDir
|
||||
├── Aliases.kt # per-account aliases.json read/write
|
||||
├── Config.kt # Identity, RunState, DataDir (~/.amy layout)
|
||||
├── Context.kt # per-run wiring: signer + NostrClient +
|
||||
│ # MarmotManager + publish/drain/sync helpers
|
||||
├── SecureFileIO.kt # 0600/0700 atomic writes, perm tighten
|
||||
├── stores/FileStores.kt # File-backed MLS / KP / message stores
|
||||
├── secrets/ # SecretStore backends (keychain / ncryptsec / plaintext)
|
||||
└── commands/
|
||||
├── Commands.kt # dispatcher
|
||||
├── Commands.kt # dispatcher tables
|
||||
├── UseCommand.kt # `amy use NAME` — pin active account
|
||||
├── InitCommands.kt # init, whoami
|
||||
├── CreateCommand.kt # full bootstrap (→ commons/account/)
|
||||
├── LoginCommand.kt # nsec/ncryptsec/mnemonic/npub/nprofile/hex/nip05
|
||||
├── RelayCommands.kt # add/list/publish-lists
|
||||
├── ProfileCommands.kt # profile show / edit (kind:0)
|
||||
├── NotesCommands.kt + PostCommand.kt + FeedCommand.kt # kind:1
|
||||
├── DmCommands.kt # NIP-17 dm send / send-file / list / await
|
||||
├── KeyPackageCommands.kt # marmot key-package publish / check
|
||||
├── GroupCommands.kt # marmot group create/list/show/…
|
||||
├── GroupCreateCommand.kt
|
||||
├── GroupReadCommands.kt
|
||||
├── GroupAddMemberCommand.kt
|
||||
├── GroupMembershipCommands.kt
|
||||
├── GroupMetadataCommands.kt
|
||||
├── MessageCommands.kt # marmot message send / list
|
||||
└── AwaitCommands.kt # poll-until-condition helpers
|
||||
├── GroupCommands.kt + GroupCreateCommand.kt
|
||||
│ GroupReadCommands.kt + GroupAddMemberCommand.kt
|
||||
│ GroupMembershipCommands.kt + GroupMetadataCommands.kt
|
||||
├── MessageCommands.kt # marmot message send / list / react / delete
|
||||
├── MarmotResetCommand.kt # destructive wipe of MLS state
|
||||
├── AwaitCommands.kt # poll-until-condition helpers
|
||||
└── StoreCommands.kt # store stat / sweep-expired / scrub / compact
|
||||
```
|
||||
|
||||
**Dependencies:** `:quartz` + `:commons` + kotlinx-coroutines + OkHttp
|
||||
@@ -171,8 +178,9 @@ object NoteCommands {
|
||||
```
|
||||
|
||||
Wire it into `Commands.kt`, add a top-level branch in `Main.kt`'s
|
||||
`dispatch`, and extend `printUsage()`. Keep [README.md](./README.md)'s
|
||||
command table and [ROADMAP.md](./ROADMAP.md)'s parity matrix in sync.
|
||||
`dispatch`, and extend `printUsage()`. Keep the command tour in
|
||||
[USAGE.md](./USAGE.md) and the parity matrix in
|
||||
[ROADMAP.md](./ROADMAP.md) in sync.
|
||||
|
||||
### 4. Output-shape conventions
|
||||
|
||||
@@ -205,8 +213,7 @@ Amy-specific layer still needs its own coverage:
|
||||
| Error / exit-code contract (bad args → 2, await timeout → 124, runtime → 1) | Table-driven tests invoking `main(argv)` with captured stdout/stderr. |
|
||||
| JSON output shape (each command's keys and types under `--json`) | Snapshot tests: run a command with `--json` against a throwaway `$HOME` (`HOME=$(mktemp -d) amy --account X …`), assert the JSON matches a golden file. The default text render has no shape contract and shouldn't be snapshotted. |
|
||||
| File layout on disk (`identity.json`, `events-store/…`, `marmot/groups/*.mls`, `marmot/keypackages.bundle`) | Structural assertions after a command sequence. |
|
||||
| Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`. Each harness spins up a local `nostr-rs-relay` and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple identities (`--account A`, `--account D`, etc.) sharing the same `~/.amy/shared/events-store/` and drives a scenario through them (+ `wn` for Marmot interop against whitenoise-rs). Today there are two suites: `cli/tests/marmot/` (MLS scenarios vs whitenoise-rs) and `cli/tests/dm/` (NIP-17 DM round-trips between two `amy` accounts). |
|
||||
| Interop with other clients | Covered by `cli/tests/marmot/marmot-interop-headless.sh` (drives Amy against whitenoise-rs `wn`/`wnd`). Add new scenarios there or start a new sibling under `cli/tests/`. |
|
||||
| Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`: each spins up a local `nostr-rs-relay` and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple accounts (`--account A`, `--account D`, …) sharing one `~/.amy/shared/events-store/` and drives a scenario through them. Today: `cli/tests/dm/` (NIP-17 DMs between two amy accounts) and `cli/tests/marmot/` (MLS scenarios vs whitenoise-rs `wn`/`wnd`). |
|
||||
|
||||
**What not to test here:** event signing, filter assembly, MLS
|
||||
correctness, NIP-44 encryption. Those belong in `quartz`/`commons`.
|
||||
@@ -241,7 +248,7 @@ a gap — add it to [ROADMAP.md](./ROADMAP.md).
|
||||
|
||||
- Run `./gradlew spotlessApply` before every commit.
|
||||
- Keep three things in sync: `printUsage()` in `Main.kt`, the command
|
||||
table in [README.md](./README.md), and the parity matrix in
|
||||
tour in [USAGE.md](./USAGE.md), and the parity matrix in
|
||||
[ROADMAP.md](./ROADMAP.md). They drift fast.
|
||||
- Never add a Gradle dependency on `:amethyst` or `:desktopApp`. If
|
||||
you need something from there, move it to `commons/` first.
|
||||
|
||||
+46
-208
@@ -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, …).
|
||||
|
||||
+14
-11
@@ -7,7 +7,8 @@ full command-line mirror of Amethyst.
|
||||
feature. Move rows between tables, adjust ordering, add non-goals.
|
||||
This is the single source of truth for "what's left".
|
||||
|
||||
- How the CLI is used today: [README.md](./README.md)
|
||||
- What amy is + the public contract: [README.md](./README.md)
|
||||
- How to use it (examples + walkthroughs): [USAGE.md](./USAGE.md)
|
||||
- How to implement an item: [DEVELOPMENT.md](./DEVELOPMENT.md)
|
||||
- Ongoing design plans: [plans/](./plans/)
|
||||
- Shared work consumed here: [../commons/plans/](../commons/plans/)
|
||||
@@ -48,13 +49,13 @@ Status legend: ✅ shipped · 📦 logic lives in `commons/`, needs a command ·
|
||||
| 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 note publish TEXT`) | 🆕 | Needs a `commons/` builder wrapper. |
|
||||
| NIP-01 feed read (`amy feed home`, `amy feed hashtag #X`, `amy feed profile NPUB`) | 🆕 | Extract `FeedFilter` usage from `amethyst/ui/dal/` into `commons/` entry points. |
|
||||
| NIP-01 note publish (`amy notes post TEXT`) | ✅ | `PostCommand` — outbox via `RelayCommands` configured set. |
|
||||
| NIP-01 feed read (`amy notes feed [--following \| --author NPUB]`) | ✅ | `FeedCommand`. Hashtag / community feeds still pending. |
|
||||
| NIP-02 follow list add / remove / list | 🆕 | Logic in `amethyst/model/nip02FollowLists/`. |
|
||||
| NIP-09 event deletion | 🆕 | Builder exists in quartz. |
|
||||
| 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`](./plans/2026-04-23-nip17-dm.md). |
|
||||
| NIP-18 reposts / quotes | 🆕 | |
|
||||
| NIP-25 reactions | 🆕 | |
|
||||
| NIP-25 reactions | ✅ in groups · 🆕 elsewhere | `marmot message react` covers MLS group reactions; outer-event reactions still pending. |
|
||||
| NIP-51 lists (bookmarks, mute, follow sets) | 🆕 | `amethyst/model/nip51Lists/` |
|
||||
| NIP-57 zaps (send + verify) | 🆕 | Needs LN-URL plumbing; `amethyst/service/lnurl/`. |
|
||||
| NIP-65 outbox model queries | 🆕 | |
|
||||
@@ -65,7 +66,7 @@ Status legend: ✅ shipped · 📦 logic lives in `commons/`, needs a command ·
|
||||
| Blossom uploads (NIP-B7) | 🆕 | |
|
||||
| NIP-47 Wallet Connect | 🆕 | |
|
||||
| NIP-46 bunker signer | 🆕 | Needs a signers abstraction in Amy. |
|
||||
| Profile view (`amy profile show NPUB`) | ⚠️ | Blocked on event-renderer plan in `commons/plans/`. |
|
||||
| Profile view (`amy profile show NPUB`) + edit | ✅ | `ProfileCommands`. Cache-first; `--refresh` forces a relay drain. |
|
||||
| Thread view (`amy thread show EVENT_ID`) | ⚠️ | Same. |
|
||||
| Notifications feed | 🆕 | |
|
||||
| Search (NIP-50) | 🆕 | |
|
||||
@@ -82,14 +83,16 @@ move anything, re-audit — you're probably duplicating logic.
|
||||
with renderers for kinds 0 / 1 / 3 / 6 / 7 / 10002 / 10050.
|
||||
Unblocks all the 🆕 and ⚠️ read-path rows below.
|
||||
Design: `commons/plans/2026-04-21-event-renderer.md`.
|
||||
2. **`amy note publish` / `amy note show` / `amy note react`** —
|
||||
smallest end-to-end write+read loop outside Marmot.
|
||||
3. **`amy feed home|profile|hashtag|thread`** reading through the
|
||||
renderer.
|
||||
2. **`amy notes post` / `amy notes show` / `amy notes react`** —
|
||||
smallest end-to-end write+read loop outside Marmot. Post + feed
|
||||
✅ shipped; `notes show` and outer-event `react` still pending.
|
||||
3. **`amy notes feed home|profile|hashtag|thread`** reading through the
|
||||
renderer. `--following` and `--author NPUB` ✅; hashtag/thread
|
||||
variants still pending.
|
||||
4. **`amy follow add|remove|list`** (NIP-02) — proves extraction of
|
||||
list-building logic from `amethyst/model/`.
|
||||
5. **`amy dm send|list`** (NIP-17) — reuses the gift-wrap path already
|
||||
exercised by Marmot.
|
||||
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).
|
||||
8. **Distribution** — Homebrew + Scoop + `.deb` in the same release
|
||||
|
||||
Reference in New Issue
Block a user