Commit Graph
25 Commits
Author SHA1 Message Date
Claude 188746485d docs(cli): correct nak parity tally (24 full / 3 partial / 7 missing)
Re-introspected the real nak binary: 34 functional commands. Fixes the
stale count — adds nsite to full (amy has NsiteCommands), corrects missing
to 7, and clarifies group/nip29 is an intentional MLS/Marmot divergence,
not a gap. Drops the stale "validate" cheap-win (key validate shipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 14:45:14 +00:00
Claude 9a5e9090c0 feat(cli): amy key validate + fetch nip19/nip05 outbox resolution
- `key validate PUBKEY` — nak's `key validate`: parse an npub or 64-hex
  pubkey and report {valid, pubkey, npub}; never errors on bad input
  (reports valid:false) so scripts branch on the field.
- `fetch <nevent|naddr|nprofile|npub|note|nip05>` — code mode that resolves
  relays the way the Android app opens a shared link: the relay hints
  embedded in the nip19 code UNION the author's NIP-65 write (outbox)
  relays, draining the author's kind:10002 on a cache miss. This is nak's
  `fetch` (nip19-hint resolution); filter mode is unchanged.

Verified: key validate accepts fiatjaf's npub/hex and rejects garbage +
bad-checksum npubs; `fetch <npub>` drained the author's kind:10002,
queried their actual advertised write relays, and returned kind:0.

nak parity: 23 full / 3 partial / 6 missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 01:06:01 +00:00
Claude 47cc76d9f1 feat(cli): amy admin (NIP-86) + serve (embed geode relay)
Two new nak-parity commands:

- `amy admin RELAY METHOD [args]` — NIP-86 Relay Management API over NIP-98
  HTTP auth. Full method set: ban/unban + allow/unallow pubkey, ban/allow
  event, allow/disallow kind, block/unblock IP, change name/description/icon,
  and all list-* queries. Reuses quartz's Nip86Client (request build + NIP-98
  auth + parse) and the Nip86Retriever HTTP path — extracted from amethyst to
  commons/jvmAndroid so amy and the Android relay-management screen share it.

- `amy serve [--host --port --path --db --admin]` — runs a Nostr relay by
  embedding geode (the standalone Ktor relay on quartz's relay-server code).
  In-memory by default (ephemeral, like nak serve); --db FILE for SQLite. The
  account's own pubkey is always an admin, so `amy admin` works against it out
  of the box. cli gains a :geode dependency (geode depends only on :quartz) and
  kotlinx-serialization-json (to render NIP-86 JSON results).

Verified end-to-end: `amy serve` + `amy admin ws://127.0.0.1:PORT
supported-methods|change-name|ban-pubkey|list-banned-pubkeys` round-trip
cleanly over real HTTP + NIP-98 against the live geode relay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 00:19:29 +00:00
Claude 42a82c7e8b feat(cli): amy cashu receive/send/maintenance/mint-rec tiers
Complete the Cashu command surface, every verb a thin wrapper over the
shared commons CashuWalletOps the Android wallet runs:

  cashu receive ln SATS [--mint]      start mint, return bolt11 + kind:7374
  cashu receive complete QUOTE_ID     poll mint; on settle, mint proofs
  cashu receive resume QUOTE_ID       alias of complete
  cashu receive token TOKEN           redeem a cashuB token
  cashu receive nutzap-sweep [--mint] redeem inbound NIP-61 nutzaps
  cashu send ln INVOICE [--mint]      melt to a bolt11 (scrubs first)
  cashu send token SATS [--mint --memo]  export a cashuB token (scrubs first)
  cashu send nutzap USER SATS [--zapped --message]  P2PK-locked nutzap
  cashu maintenance scrub [--mint]    NUT-07 + NIP-09 prune spent proofs
  cashu maintenance restore MINT_URL  NUT-09 restore from seed
  cashu maintenance migrate-keysets [--mint]  consolidate onto active keyset
  cashu mint-rec show [--author] / add URL [--dtag --review] / remove ID

- scrubStaleProofs extracted into CashuWalletOps so Android's
  CashuWalletState.scrubLocallyStaleProofs and amy share one impl.
- Context.cashuRestore mirrors CashuWalletState.restoreFromMint (seed +
  NUT-13 counter bump); Context.cashuSeed warms the per-run seed.
- receive complete recovers the mint amount by decoding the quote's
  bolt11 (kind:7374 stores only the quote id), so it works statelessly.

Verified against mint.minibits.cash + live relays: receive ln returns a
real invoice, complete/resume poll a pending quote, mint-rec round-trips,
and every error path (insufficient_funds, no_mint, mint_quote_gone) is
clean. Happy-path mint/melt completions need a payable bolt11 (interop
harness, PR 9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 00:10:40 +00:00
Claude 8bb537438f feat(cli): amy cashu wallet/mint/balance on shared NIP-60/61 code
Add the offline Cashu command tier to amy, all driven by the shared
commons wallet code so amy exercises the same path as the Android app:

  amy cashu wallet create [--mint URL] [--mints a,b] [--privkey HEX] [--relay r1,r2]
  amy cashu wallet show
  amy cashu wallet export-key
  amy cashu wallet destroy
  amy cashu mint ping URL          (stateless)
  amy cashu mint info URL          (stateless)
  amy cashu balance [--mint URL]

- create/destroy reuse commons CashuWalletOps.publishWalletEvents /
  deleteWallet; show/balance reuse the CashuWalletReader projection over
  the local event store; mint ping/info hit quartz's MintHttpClient.
- Context gains cashuOps() (wired to the file NUT-13 counter store + a
  per-run seed cache) and cashuSnapshot(); DataDir gains cashu.json.
- Extraction D: CashuKeysetCounterStore contract in commons +
  FileCashuKeysetCounterStore (atomic ~/.amy/<account>/cashu.json).

PRs 4 of cli/plans/2026-05-28-cashu-cli.md (extractions A–D + offline
tier). receive/send/maintenance/mint-rec + interop harness still pending.
Verified end-to-end against mint.minibits.cash and live relays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 22:30:59 +00:00
Claude 3df9f831b8 feat(quartz): complete KindNames registry for every supported kind
Fill the KindNames registry from 145 to 280 entries so every event kind
quartz defines a class for has a canonical English label + NIP. Adds the
whole NIP-90 DVM request/response set, NIP-29 relay groups, NIP-60/61
Cashu wallet + nutzaps, NIP-43 relay members, WebRTC calls (NIP-AC),
marketplace (NIP-15), git PRs/state (NIP-34), CLINK, NIP-51 curation
sets, NIP-85 assertions, Marmot MLS events, and more.

For the handful of kind numbers shared by multiple classes, the registry
keeps one canonical entry (e.g. CashuToken over the deprecated nip61
TokenEvent, ExternalIdentities over GalleryList, ReleaseArtifactSet over
SoftwareRelease). Kind 1 stays "Notes" rather than the bounty value-add
helper that reuses it.

Also point `amy nip`'s Nostr fallback at quartz's canonical NipText kind
30817 (NipTextEvent) alongside the wiki and long-form kinds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 21:41:52 +00:00
Claude b6c065d421 feat(quartz,cli): add KindNames registry + amy kind
Introduce a canonical, i18n-free event-kind registry in quartz:
`com.vitorpamplona.quartz.kinds.KindNames` maps each of the 145 known
kinds to an English label + the defining NIP. The data is ported from
Amethyst's relay-view `kindDisplayName` mapping (the NIP derived from
each event class's package), so the "what is kind N" knowledge now lives
once in quartz instead of only in the Android UI.

`amy kind <N|NAME>` looks a kind up by number (label + NIP) or searches
labels by name — a thin wrapper over KindNames, dispatched statelessly
(no account/network).

i18n split: quartz holds the canonical English (quartz is intentionally
translation-free); localized front ends overlay their own strings and can
fall back to KindNames.nameFor() for kinds they don't translate. amy
prints the English directly.

Verified: kind 1/0/30023/1059/24133 labels+NIPs, name search ("podcast"
→ 4), unknown → known:false, text + JSON output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 21:00:54 +00:00
Claude 749a301469 feat(cli): cheap nak-parity wins — key nip49, nip lookup, blossom check/mirror
- `amy key encrypt|decrypt` (NIP-49): encrypt a secret key to ncryptsec1…
  and back. Bidirectionally interop-verified vs the real nak binary
  (amy-encrypt → nak-decrypt and nak-encrypt → amy-decrypt both match).
- `amy nip N` / `amy nip list`: look up a NIP — the nostr-protocol/nips
  git repo FIRST, then a Nostr fallback (NIP-50 search over wiki kind:30818
  + long-form kind:30023 on search-capable relays). Slug normalization
  handles 1→01 and hex-suffixed NIPs (7d→7D, 5a→5A); titles parsed from the
  setext headings NIP docs use.
- `amy blossom check|mirror`: HEAD-check blobs (exit 1 if any missing, like
  nak) and request BUD-04 mirroring of a blob from a source URL.

Also persists the full introspected nak comparison into ROADMAP. `kind`
stays deferred — it needs a kind→schema registry that doesn't exist in
quartz (would be data/logic that doesn't belong in cli).

Verified: key round-trip + nak cross-impl both ways; nip repo titles for
01/46/5A/7D + Nostr fallback on miss; blossom check 200/404 + exit codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:50:02 +00:00
Claude 2c37ae642c feat(nip46): handle auth_url challenges in the remote-signer client
Adds NIP-46 `auth_url` (web-authorization) support to quartz's remote
signer, so amy (and Amethyst) can use auth-requiring bunkers like
nsec.app / nsecbunker:

- Both BunkerResponse deserializers (kotlinx + the JVM/Android Jackson
  one actually used by OptimizedJsonMapper) now special-case
  `{result:"auth_url", error:<url>}` BEFORE the generic error branch,
  which previously flattened it to a plain error and dropped the marker.
- RemoteSignerManager gained an `onAuthUrl` callback and a wait loop: on
  an auth_url response it surfaces the URL once and keeps waiting for the
  real response under the same request id (UNLIMITED channel so both are
  buffered). newResponse peeks the pending entry (get, not remove) so the
  follow-up isn't dropped; the waiter still removes it in finally.
- NostrSignerRemote forwards `onAuthUrl`; amy's Context prints the URL to
  stderr and the pending command keeps waiting.

Tests: a deserializer test (auth_url keeps result+error) and a manager
test (auth_url surfaced via callback, then the real response resumes the
request) — both green; existing duplicate/late-response manager tests
still pass after the get-vs-remove change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:19:01 +00:00
Claude 8b7caa5b01 feat(cli): NIP-46 nostrconnect:// reverse flow (both sides)
Add the client-initiated NostrConnect flow to complete NIP-46 parity:

- `amy login --nostrconnect [--relay …] [--name N]` (client) mints a
  transport keypair, prints a nostrconnect:// offer, subscribes, and
  waits for the signer's connect ACK (a kind:24133 whose decrypted
  result echoes our secret). The ACK's author is the signer; it persists
  a bunker account acting as that key.
- `amy bunker connect nostrconnect://…` (signer) parses a client offer,
  sends the secret-echo ACK, then services that client's requests on the
  offer's relays. Shares the serve loop with `amy bunker`.

NostrConnect.kt holds the offer parse/build (+percent-decode) and the
client handshake. BunkerCommand grew a `connect` sub-mode and factored
the request loop into serve().

Verified end-to-end:
- amy client ⇄ real `nak bunker connect`: amy learns nak's key and signs;
  event authored by nak, signature valid.
- amy client ⇄ amy `bunker connect`: same, authored by the host key.

Only `auth_url` challenges remain unimplemented in the NIP-46 surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:04:03 +00:00
Claude 695a5b4b25 fix(cli): nak-compatible bunker — percent-encoded URIs + get_relays
Close the NIP-46 interop gap against the real nak binary:

- `Identity.fromBunkerUri` now URL-decodes the relay/secret params. nak
  emits `bunker://<pk>?relay=wss%3A%2F%2F…&secret=…`; without decoding,
  `amy login` of a nak bunker URI produced a broken relay and never
  connected. (This was the one real break — found by testing vs nak.)
- `amy bunker` now percent-encodes the relay/secret in the URI it prints,
  matching nak's output format.
- `amy bunker` implements `get_relays` (returns its relay set), so nak
  clients that probe it get a proper reply instead of an error.

Verified end-to-end with `go install`-built nak over relay.damus.io,
both directions:
- `amy login bunker://` ⇄ `nak bunker`: amy signs, event authored by
  nak's key, signature valid.
- `nak event --sec bunker://<amy>` ⇄ `amy bunker`: nak signs through
  amy, event authored by amy's key; amy logs connect→ok, sign_event→ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:52:37 +00:00
Claude 54a6b44324 feat(cli): NIP-46 bunker — remote signer server + bunker login
Add both halves of NIP-46 remote signing so two amy processes interop:

- `amy bunker [--relay …] [--secret S] [--timeout SECS]` runs a remote
  signer for the active local-key account: prints a bunker:// URI, then
  subscribes to kind:24133, decrypts each BunkerRequest, dispatches to
  ctx.signer (connect/get_public_key/sign_event/nip04/nip44/ping), and
  publishes the encrypted BunkerResponse. Long-running like `subscribe`.
- `amy login bunker://PUBKEY?relay=…&secret=…` creates a remote-signer
  account: mints a local transport keypair, records the connection, and
  acts as PUBKEY. Context builds a NostrSignerRemote (vs the local
  NostrSignerInternal) and runs openSubscription()+connect() in prepare();
  every signing/encryption call is delegated to the bunker.

Storage: IdentityFile gains a `bunker` block; the transport key is kept
in the existing SecretStore `secret` field. Identity/DataDir load+save
handle the remote-signer account type; `canSign` reflects "writeable via
bunker".

Thin assembly over quartz nip46RemoteSigner (NostrSignerRemote,
BunkerRequest*/BunkerResponse*, NostrConnectEvent). Verified end-to-end
over relay.damus.io: alice hosts a bunker, bob logs in and `amy event`
signs remotely — the note is authored by alice's key and verifies
(id_ok + signature_ok); bunker logs connect→ok, sign_event→ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:38:56 +00:00
Claude fb35c85de4 feat(cli): add nak-style sync (NIP-77 Negentropy) primitive
`amy sync --relay URL [filter] [--down] [--up]` reconciles the local
event store with a relay using NIP-77 Negentropy:

- negotiate the symmetric difference under the filter (drives quartz
  NegentropySession over a raw WebSocket, mirroring geode's interop
  driver),
- --down (default) downloads the ids the relay has and we lack via
  Context.drain,
- --up uploads the events we have and the relay lacks via
  Context.publish.

Filter flags match fetch/subscribe; an empty filter reconciles the
whole store. Verified against relay.damus.io: cold store -> need=60,
downloaded=60; immediate re-sync -> need=0 (idempotent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:09:18 +00:00
Claude 8e1059a4ab feat(cli): add nak-style blossom blob commands (upload/download/list/delete)
Sixth batch of nak parity — Blossom (NIP-B7 / BUD-01/02/04):

- `amy blossom upload --server URL FILE [--mime-type M]` — authed upload,
  prints the blob URL + sha256.
- `amy blossom download URL|HASH [--out FILE]` — public download (accepts
  a full URL or a HASH + --server).
- `amy blossom list --server URL [USER]` — list a user's blobs (authed).
- `amy blossom delete HASH --server URL` — delete a blob you own.

Reuses commons BlossomClient + BlossomAuth and quartz
BlossomAuthorizationEvent / sha256; list/delete use OkHttp directly with
the quartz-built kind:24242 auth header. Verified a full upload->download
sha256 round-trip against blossom.primal.net and an authed list.

This completes the Tier-1 nak-parity surface (decode/encode/verify/key/
event/publish/fetch/subscribe/count/encrypt/decrypt/gift/filter/relay
info/outbox/blossom); only the kind/nip reference lookups remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:46:27 +00:00
Claude 62d0b16740 feat(cli): add nak-style filter, outbox, relay info primitives
Fifth batch of nak parity:

- `amy filter [filter flags]` — stateless: assemble + print a NIP-01
  filter JSON from the same flags fetch/subscribe use (no query sent).
- `amy outbox USER` — show a user's NIP-65 read/write relays (outbox
  model), cache-first with a relay-drain fallback / --refresh.
- `amy relay info URL` — stateless NIP-11 info-document fetch over HTTP
  (Accept: application/nostr+json), parsed by quartz
  Nip11RelayInformation.

filter + relay info dispatch before account resolution (no ~/.amy
needed). Verified against relay.damus.io (NIP-11 doc) and fiatjaf's
kind:10002 (outbox read/write sets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:42:28 +00:00
Claude 0ec3fc69e8 feat(cli): add nak-style count, encrypt/decrypt, gift primitives
Fourth batch of nak parity:

- `amy count [filter]` — NIP-45 COUNT, per-relay match counts (reuses
  quartz INostrClient.count). Reports per-relay + max as `total`.
- `amy encrypt --to USER [TEXT]` / `amy decrypt --from USER [CIPHER]` —
  raw NIP-44 (default) or NIP-04 (--nip04) with the active account key.
- `amy gift wrap --to USER [EVENT]` / `amy gift unwrap [WRAP]` — NIP-59
  seal+wrap and unwrap+unseal (reuses SealedRumorEvent/GiftWrapEvent).

Text/ciphertext/JSON all read from arg or stdin. Verified with two
local accounts: NIP-44 + NIP-04 round-trips, gift wrap(1059)->unwrap
recovering the inner note + author, and count=60 against relay.damus.io.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:38:01 +00:00
Claude afc3c83baa feat(cli): add nak-style fetch + subscribe query primitives
Third batch of nak parity — the fetch-vs-subscribe split of nak's `req`:

- `amy fetch [filter] [--timeout SECS]` — one-shot query: open a
  subscription, collect until every relay sends EOSE (or timeout),
  dedupe by id, sort newest-first, cap at --limit (default 100), exit.
- `amy subscribe [filter] [--timeout SECS]` — live stream: print each
  matching event as it arrives (NDJSON under --json), until timeout or
  interrupt.

Shared filter flags (--kind/--author/--id/--tag/--since/--until/--limit
/--search) are assembled by RawEventSupport.buildFilter; --author/--id
accept npub/nevent/note/naddr or hex (local decode). fetch reuses
Context.drain; subscribe uses the client subscription directly and
verifies each event before printing. Verified against relay.damus.io
(kind:0 by author, kind:1 stream).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:34:24 +00:00
Claude b539609dfe feat(cli): add nak-style event + publish primitives
Second batch of nak parity:

- `amy event --kind N [--content …] [--tags JSON] [--created-at TS]`
  builds and signs an arbitrary event with the active account. Prints
  the signed event by default; `--publish` / `--relay` broadcasts it.
- `amy publish [EVENT-JSON] [--relay …]` broadcasts a pre-made, signed
  event (verified before broadcast; reads stdin when no arg).

Both reuse quartz EventTemplate/NostrSigner.sign and the existing
Context.publish path. New RawEventSupport holds the shared arg/stdin +
relay-target helpers for the raw-event verbs. Verified offline via an
event -> verify round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:30:47 +00:00
Claude b41160f1cb feat(cli): add nak-style stateless primitives (decode, encode, verify, key)
Add the first batch of nak parity commands to amy — the army-knife
primitives that operate purely on their arguments, with no account or
network. They dispatch before account resolution (like `use`), so they
run with zero `~/.amy/` state:

- `amy decode ENTITY`   NIP-19/21 entity -> JSON
- `amy encode <type> …` raw parts -> NIP-19 entity
- `amy verify [JSON]`   id-hash + signature check (reads stdin)
- `amy key generate|public`  mint a keypair / derive a pubkey

All four are thin wrappers over quartz (Nip19Parser, the NIP-19
entities, Event.verifyId/verifySignature, KeyPair) per the cli
thin-assembly rule. README + ROADMAP updated with a nak-parity matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:13:05 +00:00
Claude 6ef0c372b1 docs(cli): make USAGE.md the README; move contract material to DEVELOPMENT
USAGE.md was the better README — entry-point users want examples and
quick start, not the public-API contract. Flip them and refresh the
amy-expert skill so it matches the post-refactor reality.

cli/README.md (was USAGE.md):
- Install, quick start, seven worked examples, full command reference,
  output modes, multi-account workflows, agent recipes, troubleshooting.
- Cross-refs point at DEVELOPMENT.md for the contract / architecture
  and ROADMAP.md for what's coming.

cli/DEVELOPMENT.md absorbs the old README's architecture sections:
- New "Public contract" section at the top — the stable promises
  (text-default + --json contract, stderr for humans, exit codes,
  ~/.amy/ as the world).
- "Local event store" deep-dive with the cache-helper API.
- "Relay routing" rules table.
- "Full on-disk layout" tree with annotations.

cli/ROADMAP.md, cli/USAGE.md:
- ROADMAP cross-refs collapsed (no more USAGE.md row).
- USAGE.md deleted — content lives in README now.

.claude/skills/amy-expert refreshed end-to-end:
- SKILL.md description + Rules 2 and 4 rewritten for the dual-output
  contract (text default, --json opt-in) and the ~/.amy/ layout.
- "Where things live" listing matches the current source tree
  (Output.kt, Aliases.kt, UseCommand.kt, secrets/, all the new
  command files).
- "Common mistakes" lists the new traps: don't read user.home
  directly, don't add a global flag that collides with subcommand
  --name, don't use Json.writeLine (it's gone).
- references/command-template.md uses Output.emit / Output.error
  (Json.writeLine / Json.error helpers no longer exist).
- references/output-conventions.md rewritten around the dual-mode
  contract — same JSON shape rules, but framed as "this is what
  --json emits" rather than "this is stdout."
2026-04-25 16:35:10 +00:00
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
Claude b0698e0a66 test(cli): move tests/ out of tools/, separate marmot vs dm
`tools/` is for libraries (arti-build); shell-based interop harnesses
that drive the `amy` binary belong with the CLI module. New layout:

  cli/tests/
  ├── lib.sh                       # shared logging + result tracking
  ├── headless/helpers.sh          # shared amy_a / amy_json / assertions
  ├── marmot/                      # MLS group-messaging interop (vs whitenoise-rs)
  │   ├── marmot-interop.sh
  │   ├── marmot-interop-headless.sh
  │   ├── setup.sh
  │   ├── tests-create.sh
  │   ├── tests-manage.sh
  │   ├── tests-extras.sh
  │   └── patches/
  └── dm/                          # NIP-17 DM interop (amy ↔ amy)
      ├── dm-interop-headless.sh
      ├── setup.sh
      └── tests-dm.sh

Per-suite changes:
- Each suite owns its own setup.sh; previously the Marmot setup was at
  headless/setup.sh and the DM setup at headless/setup-dm.sh, which
  implied shared infra they don't actually share.
- `cli/tests/lib.sh` and `cli/tests/headless/helpers.sh` are the only
  shared bits across suites.
- The DM harness still reuses Marmot's `start_local_relay` /
  `stop_local_relay` (sourced via `../marmot/setup.sh`) — same relay
  binary, no duplication.
- All sourcing paths updated; REPO_ROOT now climbs three levels (was
  two when the harness lived under `tools/`); patches/ is now a sibling
  of marmot/setup.sh rather than under marmot/headless/.
- `.gitignore` updated to cover both suites' state dirs.

cli/ROADMAP.md and cli/tests/README.md updated to point at the new paths.
No behavioural change — every test runs the same code; only the file
layout and source paths moved.
2026-04-23 20:37:22 +00:00
Claude 112801ba82 test(cli): amy↔amy NIP-17 DM interop harness
Adds `tools/marmot-interop/dm-interop-headless.sh` — a zero-prompt
end-to-end harness that runs two independent `amy` processes against
a loopback nostr-rs-relay and verifies the NIP-17 DM surface.

Six scenarios:

  dm-01  text round-trip (kind:14) in both directions
  dm-02  `dm list` surfaces prior exchange with `type:text` discriminator
  dm-03  strict kind:10050 refuses sends to an inboxless recipient
         (surfaces the `no_dm_relays` error JSON)
  dm-04  `--allow-fallback` opts into NIP-65 read / bootstrap chain
         (asserts a `relay_source: "bootstrap"` or `"nip65_read"` row)
  dm-05  file message reference mode (kind:15) — send-file with
         --key / --nonce, verify recipient decodes url + key + nonce + mime
  dm-06  no-flag `dm list` advances `state.giftWrapSince`; second call
         returns an empty `messages` array

Shape matches the existing Marmot harness: reuses `lib.sh` for logging
+ result tracking, reuses `setup.sh` for the nostr-rs-relay lifecycle
(`start_local_relay` / `stop_local_relay`), adds a slim `setup-dm.sh`
preflight (amy + relay only, no whitenoise-rs / Marmot patches), and
defines tests in `tests-dm.sh`.

No new CI job yet — the relay build needs Rust + ~3 minutes on a
cold cache, same constraint as the Marmot harness.

Upload-mode (`dm send-file --file PATH --server URL`) is not scripted
here: it needs a local Blossom server, which is out of scope for this
pass. The shared upload classes are unit-tested on desktop at
`desktopApp/src/jvmTest/kotlin/.../service/upload/`.
2026-04-23 20:14:08 +00:00
Claude 15205c24b5 feat(cli): amy dm send|list|await (NIP-17)
Adds three verbs wired into the top-level dispatch:

- `amy dm send RECIPIENT TEXT` — builds a kind:14 ChatMessageEvent,
  runs it through NIP17Factory to seal and gift-wrap, then publishes
  each wrap to its recipient's DM inbox (kind:10050, fallback kind:10002
  read, final fallback bootstrap). Emits one JSON line with the inner
  kind:14 id, per-recipient wrap ids, and per-relay ACK status.

- `amy dm list [--peer NPUB] [--since TS] [--limit N] [--timeout SECS]`
  — drains gift wraps addressed to us from our inbox relays (or outbox /
  bootstrap fallbacks), unwraps+unseals each one, dedupes by inner id,
  and returns them oldest-first. With no flags it advances the
  giftWrapSince cursor in state.json (matches the Marmot syncIncoming
  convention); with --peer/--since it's a stateless query.

- `amy dm await --peer NPUB --match TEXT [--timeout SECS]` — polls the
  same drain on a 2s tick until a DM from NPUB contains TEXT. Timeout
  exits 124 like the other await verbs.

All three reuse Quartz entirely (NIP17Factory, GiftWrapEvent,
SealedRumorEvent, RecipientRelayFetcher); the only shared piece we
needed in commons — filterGiftWrapsToPubkey — was extracted in the
previous commit. No business logic leaks into cli/.

Plan: cli/plans/2026-04-23-nip17-dm.md.
2026-04-23 18:23:12 +00:00
Claude 9b9084ffaf docs(cli): split amy docs by responsibility + add amy-expert skill
Separate the single DEVELOPMENT.md into focused docs per audience:

- cli/README.md — trim agent/interop sub-sections; user-facing contract,
  commands, data-dir, troubleshooting only.
- cli/DEVELOPMENT.md — pared down to architecture, how-to-add-a-command,
  output conventions, testing, housekeeping.
- cli/ROADMAP.md (new) — north-star, parity matrix, ordered milestones,
  non-goals. The live checklist for CLI feature parity.
- cli/plans/2026-04-21-cli-distribution.md (new) — packaging strategy
  (Homebrew / winget / Scoop / deb / rpm / AUR / AppImage).
- commons/plans/2026-04-21-event-renderer.md (new) — cross-cutting
  renderer design owned by commons, consumed by cli + desktop + android.

Agentic routing:

- .claude/skills/amy-expert/SKILL.md (new) — routing triggers + the
  five hard rules (thin-layer, JSON contract, non-interactive,
  data-dir-is-world, extract-before-adding).
- references/command-template.md, extraction-recipe.md,
  output-conventions.md — bundled copy-paste references.

Root .claude/CLAUDE.md:

- Adds cli/ to the module list with its sharing rule.
- Registers amy-expert in the skills table.
- Documents per-module plans/ convention; freezes docs/plans/.

https://claude.ai/code/session_01BQ5ZHwa8BAgEQ9zeM4CKhW
2026-04-22 00:23:00 +00:00