Commit Graph

17 Commits

Author SHA1 Message Date
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 e17ef42e54 fix(cli): rename global --name to --account to free --name for subcommands
The marmot harness surfaced the bug on first run: amy stripped
`marmot group create --name "Interop-02"` thinking the global
account selector ran into the group's display-name flag. Result:
amy resolved the "Interop-02" account, found no identity.json, and
errored — no group ever got created.

Renames the global account-selector flag to `--account`. The per-
subcommand `--name` flags (`marmot group create --name "Demo"`,
`profile edit --name "Alice"`, `marmot await group --name X`) are
untouched — they're free of the global parser now that it doesn't
claim the same name.

Sweep:
- `Main.kt`: GlobalFlag.NAME → ACCOUNT, long "--account".
- `Config.kt`: DataDir.resolve param renamed nameFlag → accountFlag;
  every error message points the user at --account.
- `UseCommand.kt`: error hint says `amy --account NAME init`.
- All test wrappers + direct $AMY_BIN calls under cli/tests/ swap
  the global `--name X` for `--account X` (subcommand --name kept
  exactly where it appeared).
- README + DEVELOPMENT updated.
2026-04-25 15:26:17 +00:00
Claude 99586fbe7e feat(cli): drop --data-dir; tests isolate via $HOME override
There is no installed base to protect, so the self-contained
`--data-dir P` escape hatch goes away entirely. amy now has exactly
one layout — the production multi-account one — and tests exercise
that same code path. Drops one global flag, one DataDir construction
mode, and the "tests use a different code path than users" footgun.

Layout (unchanged from the previous commits — just the only mode now):

    ~/.amy/
    ├── current                      # `amy use NAME` marker
    ├── shared/
    │   └── events-store/            # FsEventStore, one per machine
    ├── alice/
    │   ├── identity.json
    │   ├── state.json
    │   ├── aliases.json             # {"alice": "<own npub>"} after init
    │   └── marmot/
    └── bob/ ...

`DataDir.DEFAULT_ROOT` reads `$HOME` directly (falling back to
`user.home`) because JDK 21 resolves `user.home` via getpwuid and
ignores `$HOME` — which would have broken the standard CLI test
isolation pattern of `HOME=/tmp/foo amy …` (the same convention
git, gpg, npm follow).

Test sweep:

- `cli/tests/headless/helpers.sh`, `cli/tests/dm/setup.sh`,
  `cli/tests/cache/cache-headless.sh` wrappers all switch to
  `HOME=$STATE_DIR amy --name X …`.
- `ensure_identity_for` drops its `dir` parameter; the function and
  every harness call site go through `--name` only.
- `A_DIR`/`B_DIR`/`D_DIR` get repointed at `$STATE_DIR/.amy/X`. The
  one consumer (`cache-headless.sh`'s T6 `relays.json` check) still
  works since it's just a path probe.
- `cli/tests/dm/tests-dm.sh` ghost identities get their own
  short-lived `$HOME` so they don't pollute the main test root.

Cache-test T4 inverts: pre-shared-store, "B has not seen A → first
profile show is a relay miss, second is a cache hit" tested
per-account caching. With one shared events-store, B's first lookup
of A is already a cache hit because A wrote kind:0 there during
bootstrap. T4 now asserts that — drops the stale "second lookup hits
cache" half. T7 (no-identity maintenance verbs) gets a fresh fake
`$HOME` plus a throwaway `--name` so the empty store has no inherited
events.

Docs (README + DEVELOPMENT) rewritten to match — quick-start now uses
`amy --name alice create`, the on-disk-layout section shows the new
tree, and the global-flags table replaces `--data-dir` with `--name`
plus the new `amy use` verb.
2026-04-25 15:07:31 +00:00
Claude 03eb7be509 feat(cli): default amy stdout to human-readable text; --json opts in
amy's default stdout is now a YAML-ish render of the underlying result
map; the previous single-line JSON contract moves behind a global
`--json` flag. Errors mirror the same rule (`error: <code>: <detail>`
on stderr by default, JSON `{"error":...,"detail":...}` under --json).
Exit codes (0/1/2/124) and the --json shape itself are unchanged —
only the default presentation flips.

- Replaces Json.writeLine / Json.error with mode-aware
  Output.emit / Output.error. The same Jackson mapper is reused for
  on-disk JSON via Output.mapper.
- Adds `--json` to the global flag set in Main.kt; honoured even when
  argument parsing fails so error JSON keeps shape under --json.
- Updates the test harness wrappers (amy_a / amy_b / amy_d in
  cli/tests/{headless,dm,cache}/) and the few direct $AMY_BIN call
  sites whose stdout is consumed via $() / 2>&1 — they now pass
  --json so the existing jq pipelines keep working.
- Rewrites the README "Output contract" and DEVELOPMENT design
  principles to describe the new default, and clarifies that only
  --json is the public API; the text shape is allowed to drift.
2026-04-25 12:23:37 +00:00
Claude 37a4f89178 feat(cli): drop relays.json — kind:10002/10050/10051 in the store ARE the config
The local relay configuration was redundant once the event store
became the source of truth: every account already publishes signed
kind:10002 (NIP-65), kind:10050 (DM inbox), and kind:10051
(KeyPackage relays) events, and Context now reads each set straight
out of the local store.

Removed:
  - data class RelayConfig (nip65/inbox/keyPackage buckets)
  - DataDir.relaysFile / loadRelays() / saveRelays()
  - Context.relays parameter — Context.open() no longer reads JSON
  - CreateCommand.defaultRelayConfig() helper
  - CreateCommand's saveRelays() call (redundant: ctx.publish persists
    the bootstrap events into the store anyway)

Context now serves the four relay accessors from the store with sane
fallbacks:
  outboxRelays()      = relaysOf(self)?.writeRelaysNorm() ?: DefaultNIP65RelaySet
  inboxRelays()       = dmInboxOf(self)?.relays() ?: DefaultDMRelayList.toSet()
  keyPackageRelays()  = keyPackageRelaysOf(self)?.relays() ?: outboxRelays()
  anyRelays()         = union of the three
  bootstrapRelays()   = anyRelays() ∪ DefaultNIP65RelaySet ∪ DefaultDMRelayList

RelayCommands rewritten:
  - `relay add URL --type T` — read existing relay-list event from the
    store, append URL, build + sign + ingest a new event of the
    matching kind. The replaceable slot mechanism replaces the old
    winner atomically.
  - `relay list` — dump URLs from the latest event in each bucket.
  - `relay publish-lists` — broadcast whichever events are present in
    the store; errors out cleanly when none exist (suggests `relay add`
    or `create`).

No migration: existing data dirs that still have `relays.json` will
ignore it. Run `amy relay add` or `amy create` to repopulate. Per
discussion this is a clean break, not a backwards-compat shim.

README + DEVELOPMENT updated to reflect the new on-disk layout (no
more `relays.json`; events-store/ is the relay configuration).
2026-04-25 03:45:12 +00:00
Claude b2bf874c15 feat(cli): cache-first reads across feed, dm, marmot commands
Audit found four commands draining replaceable relay-list events
(kind:10050, 10051, 10002) on every invocation, plus one fetching
the user's own kind:3 contact list. All five are now cache-first.

New Context helpers:
  - dmInboxOf(pk)         → kind:10050  (ChatMessageRelayListEvent)
  - keyPackageRelaysOf(pk)→ kind:10051  (KeyPackageRelayListEvent)
  - cachedRelayListsOf(pk)→ assembles a RecipientRelayFetcher.Lists
                             from the local store; returns null if
                             nothing is cached so callers can fall
                             back with `?:`.

Wired into:
  - FeedCommand.resolveFollowing — replaces a kind:3 drain with
    ctx.contactsOf(self).
  - DmCommands.resolveDmRelays — `amy dm send` / `dm list`.
  - KeyPackageCommands.check.
  - AwaitCommands (key-package / member / admin polling loops).
  - GroupAddMemberCommand (one cache hit per invitee).

All five sites now go: try cache → if miss, run the existing
RecipientRelayFetcher / drain. The drain itself populates the cache
via verifyAndStore, so subsequent runs are local-only. Replaceable
slot shortcut means each cached read is one stat + one readString,
not a directory walk.

README "Local event store" section gains a list of which commands
are cache-first today.
2026-04-25 03:34:00 +00:00
Claude bf1c4c23cb feat(cli): amy profile show is now cache-first
The first command to consume the local event store as a read source.
Default behaviour: read from the FsEventStore via Context.profileOf().
Pass --refresh to skip the cache and drain from relays (which then
re-populates the store as a side effect, like every other drain).

Output JSON gains a "source" field so scripts/agents can tell whether
the result came from disk or the network:

  source = "cache"  → served from <data-dir>/events-store/, queried_relays = []
  source = "relays" → fresh relay drain, queried_relays = list

Cache hit goes through the planner's slot shortcut from the previous
commit: one stat + one read on replaceable/0/<pubkey>.json, no
directory scan.

profile show + profile edit now have entries in the verb table in
cli/README.md.
2026-04-25 03:25:11 +00:00
Claude 22a418bea2 feat(cli): make FsEventStore the source of truth for relay events
Every event Amy observes — drained from a relay subscription,
unwrapped from a NIP-59 gift wrap, or generated locally for publish —
now flows through Context.verifyAndStore: NIP-01 id + signature check
via Event.verify(), then store.insert(). Bad events are dropped with
a stderr log and never reach command code; persistence failures are
logged but do not propagate, so a broken store never breaks a relay
subscription.

- Context.drain() now persists every received event before surfacing
  it to the caller. Existing callers (FeedCommand, DmCommands,
  ProfileCommands, Marmot sync) get caching for free.
- Context.publish() persists outbound events too, so the local store
  reflects what Amy has done even when every relay rejects.
- Three cache-first read helpers expose the most common lookups
  without hitting relays: profileOf(pubKey) → kind:0,
  relaysOf(pubKey) → kind:10002, contactsOf(pubKey) → kind:3.
- Class doc on Context spells out the contract; README.md gets a new
  "Local event store — the source of truth" section pointing at the
  on-disk layout and the design plans.
2026-04-25 01:40:05 +00:00
Claude 8cbe8c67fe refactor(cli): rename default data-dir to ./amy and nest MLS state under marmot/
Two on-disk layout changes that only affect freshly-created data-dirs (no
migration shim since `amethyst-cli-data` was never tagged or released):

- Default `--data-dir` is now `./amy` instead of `./amethyst-cli-data` —
  shorter, matches the binary name, and still overridable via the flag or
  `$AMETHYST_CLI_DATA`.
- MLS-only files (`groups/` and `keypackages.bundle`) move under a new
  `marmot/` subdirectory so the top-level root stays tidy as more
  non-Marmot state lands (notes, profile caches, etc). Existing
  top-level files (`identity.json`, `relays.json`, `state.json`) are
  unchanged.

README tree diagram and DEVELOPMENT.md test table updated to match.
2026-04-24 19:15:58 +00:00
Claude bd88f68dcd docs: point README / DEVELOPMENT / amy-expert at cli/tests/
After the harness move from tools/marmot-interop/ to cli/tests/,
three doc locations were still describing the old layout:

- cli/DEVELOPMENT.md § Testing — the "round-trip" row pointed at a
  non-existent `cli/src/test/resources/scripts/`, and the "interop
  with other clients" row claimed it was out of scope. Both now name
  `cli/tests/marmot/` and `cli/tests/dm/` with what each covers, and
  the interop-script template points at the concrete test files
  rather than re-inventing a smaller example.

- cli/README.md — the "For an interop-test script template" pointer
  now links to `cli/tests/README.md` alongside the DEVELOPMENT.md
  section.

- .claude/skills/amy-expert/SKILL.md — the "Where things live" tree
  omitted the new `tests/` subtree entirely. Added it with a
  one-line description per suite. Also extended the wire-up checklist
  with a step 7: add a harness case when a new verb changes observable
  wire behaviour.

No content changes elsewhere; just path corrections.
2026-04-23 21:36:52 +00:00
Claude c614400b23 feat(cli): amy dm send-file --file PATH (encrypt + upload + publish)
`dm send-file` now has two modes:

- **Upload mode** — `dm send-file RECIPIENT --file PATH --server URL`:
  generate a fresh AES-GCM cipher, encrypt the file, upload the
  ciphertext to the Blossom server, then publish a kind:15 referencing
  the returned URL. Auto-detected metadata (encrypted hash + size,
  original sha256, mime type, image dimensions, blurhash) is folded
  into the kind:15 tags. The response surfaces the encryption key and
  nonce so the same encrypted blob can be re-shared without
  re-uploading.

- **Reference mode** — the previous shape still works:
  `dm send-file RECIPIENT URL --key HEX --nonce HEX [...]`. Useful
  when the upload happened elsewhere (Android client, third-party
  tool) or for replaying a previous upload.

Mode is selected by the presence of `--file`. Both paths share the
same wrap-and-publish pipeline (`publishWraps`) and respect the strict
NIP-17 §6 relay rule: kind:1059 only reaches the recipient's kind:10050
unless `--allow-fallback` is passed.

Implementation entirely reuses the upload helpers just promoted to
`commons/jvmMain/.../service/upload/`, so no new business logic lives
in `cli/`. The Blossom auth event (kind:24242) is built and signed via
`BlossomAuth.createUploadAuth(... ctx.signer ...)`.
2026-04-23 19:51:06 +00:00
Claude 305ce20b52 feat(cli): NIP-17 file DMs (kind:15) on receive + send, strict 10050
Receive:
- decryptDms now accepts both ChatMessageEvent (kind:14) and
  ChatMessageEncryptedFileHeaderEvent (kind:15). The result is a sealed
  DecryptedDm hierarchy (TextDm / FileDm) with a `type: "text"|"file"`
  discriminator in JSON. File messages emit url, mime_type,
  encryption_algorithm, decryption_key, decryption_nonce, hash,
  original_hash, size, dim, blurhash. `dm await --match X` searches the
  text body for kind:14 and the URL for kind:15.

Send:
- New `dm send-file RECIPIENT URL --key HEX --nonce HEX [...]` verb.
  Builds a kind:15 ChatMessageEncryptedFileHeaderEvent via the existing
  ChatMessageEncryptedFileHeaderEvent.build + AESGCM(key, nonce), then
  publishes through NIP17Factory.createEncryptedFileNIP17. The file is
  expected to already be uploaded; carrying the upload itself in `amy`
  is the next change.

Spec compliance (NIP-17 §6, "if no kind:10050, shouldn't try"):
- resolveDmRelays now returns kind:10050 only by default. If the
  recipient has none, send/send-file exit with `no_dm_relays` and a
  message recommending `--allow-fallback`. The fallback chain (kind:10002
  read marker → bootstrap) is preserved behind that opt-in flag for
  interop tests and brand-new accounts.

Each recipient row in the send response now includes `relay_source`
("kind_10050" / "nip65_read" / "bootstrap") so callers can tell where
the wraps actually went.

Note: Quartz already enforces the NIP-17 §7 step-3 impersonation guard
(seal pubkey is forced over the rumor's claimed pubkey inside
`Rumor.mergeWith`), so no extra check is needed in the CLI.
2026-04-23 19:34:52 +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 0657f6e1c7 fix(cli): apply Marmot relay-routing rules across every marmot command
The first pass only fixed `group add`. Auditing the rest of the CLI
against MIP-00..03 turned up three more spots where `amy` either
queried the wrong relays or silently skipped a required advertisement:

1. `marmot key-package check <npub>` used `anyRelays()` — i.e. the
   inviter's configured relays — so checking for a KeyPackage on a
   user who advertises `kind:10051` somewhere we don't know about
   always returned `not_found`. Now runs RecipientRelayFetcher against
   bootstrap seeds and fetches from the union of (target kind:10051,
   target kind:10002 write, bootstrap). Emits `found_on` so callers
   can see which relay served the hit.

2. `await key-package <npub>` had the same bug inside the poll loop.
   Resolved once up front, then the loop fetches from the target's
   advertised relays every tick. Throws an `AwaitTimeout` early if no
   relays can be discovered at all, instead of silently polling void.

3. `relay publish-lists` published kind:10002 + kind:10050 but never
   kind:10051. Per MIP-00 the KeyPackage Relay List is how other
   Marmot clients discover where our KPs live — without it the
   `key_package` bucket on disk is invisible to anyone else. Now also
   publishes kind:10051; falls back to the NIP-65 set if the bucket
   is empty so we never advertise an empty list. (`amy create`
   already publishes it via AccountBootstrapEvents.)

Docs: cli/README adds a "Relay routing" section that lists the exact
relay set used for publish vs fetch of every Marmot event kind, plus
the bootstrap-pool definition, so agents + interop-test authors can
reason about cross-user reachability without reading the code.
2026-04-22 22:15:10 +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
Claude 35a84ffaf9 docs(cli): add user README and development roadmap for amy
Add cli/README.md covering the JSON-output contract, install via
installDist, today's command surface (identity / relays / marmot),
and use from agents + interop tests. Add cli/DEVELOPMENT.md with the
feature-parity matrix vs Amethyst, the extract-from-Android recipe
(amethyst/ -> commons/ -> cli/), command template, EventRenderer
plan shared with Desktop, testing strategy focused on what quartz
and commons don't already cover, distribution matrix across macOS /
Windows / Linux / Nix / Zapstore, and an ordered 10-step roadmap.

https://claude.ai/code/session_01BQ5ZHwa8BAgEQ9zeM4CKhW
2026-04-21 23:26:06 +00:00