Commit Graph
76 Commits
Author SHA1 Message Date
Claude 57f95cf154 feat(cli): add amy bolt12 — decode, verify, offers, and two-step send
Adds a BOLT12 zap (NIP-XX) command group over a new shared commons
Bolt12ZapActions (assembly-only, mirrors ZapActions):

  bolt12 decode LNO1|LNP1        decode an offer or payer proof
  bolt12 verify EVENT-ID         validate a kind:9736 in the local store
  bolt12 offer get/set           read/publish a kind:10058 offer list
  bolt12 intent … / zap …        two-step out-of-band send (amy has no NWC
                                  rail): intent prints the payer_note; zap
                                  wraps the signed intent + settled proof
                                  into a validated kind:9736 and publishes

Keeps cli a thin assembly layer — all logic is quartz's Bolt12ZapBuilder/
Validator/codecs via commons Bolt12ZapActions. Adds Bolt12ZapActionsTest;
updates README + ROADMAP. Interop harness and NWC-fetched proofs remain TODO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 00:22:42 +00:00
Claude bb39fb29fc feat(buzz): invite-link redemption + live-interop fixes (amy)
Validated against the production relay wss://amethyst.communities.buzz.xyz
by joining and running a full DM round-trip. Adds the join primitive and
fixes the interop gaps that live testing surfaced.

- quartz: BuzzInviteLink — parse `https://<host>/invite/<token>` (relay-signed
  base64url payload → community/role/expiry). A Buzz invite is NOT a NIP-29
  code; it is redeemed over HTTP against the tenant host. Unit-tested with a
  real token; rejects the Concord `/invite/<naddr>#…` shape (no collision).
- cli: `amy buzz join <invite-url>` — the real 3-step claim: GET /api/join-policy,
  POST /api/invites/accept-policy, then NIP-98-signed POST /api/invites/claim.
  Proven live (status: joined, role: member).
- cli: Context.publish now authenticates-then-retries on an `auth-required`
  relay (warm the connection with a pendingOnAuthRequired REQ, then re-publish)
  — the write path had no NIP-42 handling, so every Buzz write was rejected.
- cli: Buzz reads (dm list / read / console / personas) use the auth-aware
  drain (pendingOnAuthRequired).
- cli: `dm open` surfaces the relay's synchronous OK `response:{channel_id}` —
  the authoritative DM channel id (the relay assigns it; it is not polled).
- cli: `dm list` rewritten to the relay's actual discovery — kind-44100
  member-added notifications (#p=me) filtered to the kind-40099 `dm_created`
  channels. The deployed relay does NOT emit kind-41001.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-22 14:56:37 +00:00
Claude c79e7d361f feat(buzz): wire Buzz direct messages end-to-end (app + amy)
A Buzz DM is a relay-authoritative NIP-29 group whose h/id is a
relay-generated UUID, so its timeline reuses the whole relay-group chat
stack unchanged. This adds the missing discovery + product layer:

- commons: BuzzDmRegistry — process-wide registry fed by LocalCache from
  the relay-signed DmCreatedEvent (41001) and per-viewer DmVisibilityEvent
  (30622); tracks conversations (channel id -> participants/relay) and the
  viewer's hidden set. Unit-tested.
- LocalCache: record 41001/30622 into the registry on consume (was
  store-only).
- Account: openBuzzDm (41010), hideBuzzDm (41012), addBuzzDmMember (41011).
  The relay assigns the channel UUID and confirms via 41001 — we never
  mint it.
- Android: BuzzDmListViewModel (two-phase fetch: discover 41001/30622 #p=me,
  then fetch each DM's 39000-39003 roster so the shared composer's member
  gate passes), BuzzDmListScreen (inbox), BuzzNewDmScreen (publish 41010,
  await the 41001, jump into the shared RelayGroupChatScreen). Reached from
  a Direct Messages card on the Workspaces tab.
- CLI: amy buzz dm list/open/hide/add-member, mirroring buzz-cli.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-22 14:09:45 +00:00
Claude 12aa9fe954 feat(cli): first-class 'amy buzz' commands for block/buzz workspaces
Thin assembly over quartz + commons (no new protocol in cli/):
- buzz post RELAY GID <text>  — publish a kind-40002 stream message (h-scoped)
- buzz read RELAY GID         — drain the recent human-visible timeline (9/40002/40099)
- buzz attest AGENT           — sign a NIP-OA OwnerAttestation offline, print the auth tag
- buzz console [--relays]     — drain kind-44200 turn metrics (#p=me), NIP-44-decrypt,
                                and aggregate via the shared commons AgentFleetAggregator
- buzz personas [--relays]    — list my kind-30175 personas (newest per slug)

Join/leave/create reuse 'amy relaygroup' (Buzz workspaces are NIP-29 groups). Wired
into Main dispatch + usage; README command table + ROADMAP updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-22 05:01:25 +00:00
Claude d3e208322c feat(cli): add amy git label (NIP-32) and amy git apply (patch → working tree)
Close two more ngit/nak parity gaps:

- `git label TARGET LABEL[,LABEL]` — attach NIP-32 kind:1985 labels to an
  issue/patch/PR (the `ngit pr label` / `issue label` surface), over quartz's
  existing `LabelEvent`. Namespace defaults to `ugc`; `--namespace` overrides.
- `git apply PATCH_ID` — fetch a kind:1617 patch and apply it to the local
  working tree via `git am` (the `nak git patch apply` / `ngit pr apply`
  surface); `--check` dry-runs `git apply --check`, `--print` emits the patch.
  Shells out to `git` like `git init`, since it operates on the local checkout.

Verified end-to-end: a patch published to a relay, fetched, and `git am`'d as a
real commit into a scratch repo; labels land as kind 1985. The harness gains 5
assertions (label + a full publish→apply round-trip), now 33 offline.

Remaining out-of-scope items are documented: git-packfile push (needs a git
write layer quartz lacks) and NIP-34 cover notes (kind 1624, no quartz builder).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
2026-07-21 03:37:58 +00:00
Claude b06184ac49 feat(cli): add amy git init — bootstrap a repo from the local git checkout
Match `ngit init` / `nak git init`: read the local git repository and publish a
NIP-34 repository announcement, deriving the fields instead of making the user
type them. Shells out to `git` to determine the name (top-level dir), clone URL
(origin remote, ssh→https normalized), earliest-unique-commit (root commit),
and — for the accompanying kind:30618 state — the branch/tag tips and HEAD.
Publishes the 30617 announcement and (unless `--no-state`) the 30618 state in
one shot. Every derived value is overridable with a flag; outside a git repo
the derivation is skipped and `--name`/`--clone` are supplied manually.

This is the one `amy git` verb that shells out to `git`, since it is inherently
about the local working tree — exactly like the tools it mirrors.

Verified against the amethyst checkout itself (derives name=amethyst, the origin
clone URL, the root commit as EUC, and a 30618 with the live branches + HEAD).
The harness gains 4 assertions driving `git init` against its own checkout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
2026-07-21 03:37:58 +00:00
Claude 45d33c016e feat(cli): add amy git browse|cat|log — read git objects over smart-HTTP
Give `amy git` the git-object read side of `nak git download` / a shallow
clone. `browse` lists a repo's tree, `cat` prints (or `--out` writes) a file at
a ref, and `log` shows recent commit history — all over the git smart-HTTP v2
protocol via quartz's `GitHttpClient` (the same shallow-clone path the Android
repo browser uses). REPO may be a NIP-34 coordinate/naddr (whose announcement
supplies the clone URL) or a raw http(s) clone URL; `--clone` and `--ref`
override the URL and branch/tag.

Read-only: pushing git objects back to clone/GRASP servers stays out of scope.

Verified live against a public repo (octocat/Hello-World) — browse/cat/log all
return correct trees, blobs, and history. The harness gains a `--live` block
(28 assertions with `--live`, 24 in the default offline run) exercising these
against `$LIVE_REPO`, skipped by default since it needs a reachable git host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
2026-07-21 03:37:57 +00:00
Claude 9922eef9f7 feat(cli): add amy git grasp list|set (NIP-34 GRASP server list, kind 10317)
Declare/read a user's preferred GRASP (Git-over-Nostr hosting) servers in
preference order — the NIP-65-style list `ngit`/`nak git` consult to decide
where PR tip branches (`refs/nostr/<pr-id>`) get pushed. `set` publishes a
kind:10317 to the outbox; `list` reads it back cache-first (anonymous-capable).
Thin assembly over quartz `UserGraspListEvent`. The git push itself stays out
of scope, as with the rest of the packfile transport.

Extends the git NIP-34 harness with a grasp round-trip (24 assertions) and
updates the README/ROADMAP/help tables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
2026-07-21 03:37:57 +00:00
Claude ad66e6c224 feat(cli): full NIP-34 git collaboration parity for amy git
Extend `amy git` from repo announce/list/show/issue to the complete
pure-Nostr surface of `ngit` and `nak git`, so every NIP-34 collaboration
flow is scriptable without a GUI.

New sub-verbs (all thin assembly over quartz's `nip34Git` builders):

- `git state`   — kind:30618 repository state (branch/tag tips + HEAD)
- `git patch`   — kind:1617 patch from `git format-patch` (--file or stdin),
                  with --root/--root-revision, --commit, --parent-commit,
                  and --in-reply-to for revision chains
- `git pr` / `git pr-update` — kind:1618 pull request + kind:1619 tip update
- `git comment` — NIP-22 kind:1111 reply on an issue/patch/PR/repo (the
                  modern replacement for the deprecated kind:1622 git reply)
- `git open|applied|close|draft` — kind:1630/1631/1632/1633 status events
                  (aliases `merged`/`resolved` for applied); applied carries
                  --merge-commit / --commit / --patch
- `git issues|patches|prs` — list a repo's items with status derived from the
                  newest authoritative (owner/maintainer/author) status event,
                  with --open/--applied/--closed/--draft/--status filters
- `git thread`  — one item plus its status timeline and comments

Shared parsing/fetch/routing glue lives in `GitSupport`; the existing
announce/list/show/issue verbs now reuse it. The git *packfile* transport
(clone/fetch/push of real objects to clone/GRASP servers) stays out of
scope — it needs a git plumbing layer, not an event builder — and is
documented as such.

Adds `cli/tests/git/git-nip34-headless.sh` (21 assertions, drives the whole
flow against `amy serve` and checks the status-deriving reads) and updates
the README/ROADMAP command tables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
2026-07-21 03:37:57 +00:00
Claude 4efb2cca98 fix(quartz,cli): audit fixes — bounded drains, no event loss, honest verdicts, false-reject traps
Adversarial audit of the PR's own changes (8 finder angles, verified
before fixing). Quartz core:

- fetchAll-family drains get a wall-clock ceiling (maxTotalMs, default
  10x the idle window, delay()-watchdog: cancellable and virtual-time
  testable). The pure idle window was unbounded when a relay trickled
  events forever — sandboxed napplet queries, set -e fetches, and
  marmot await stuck inside one drain. Streaming relays still finish.
- The suspending onEvent hook no longer runs inside a cancellable
  timeout scope (an expiring window could cancel verifyAndStore
  mid-write and silently drop a received event); the timeout is armed
  only when the channels are dry (no per-message timeout-job churn).
- fetchAll is a projection over fetchAllWithHooks: fixes its
  unsynchronized events/seenIds mutation from concurrent socket
  threads and deletes the duplicate loop + per-event activity channel.
- publishAndConfirmDetailed regains its only-responders contract
  (synthetic no-response entries no longer render as 'relay rejected
  your message' in app callers); results built by pure associateWith;
  shared failure-reason constants + PublishResult.isTransportFailure.
- NIP-65 mutations: split read+write r-tags for the same URL now merge
  to BOTH instead of last-wins dropping a facet (+ test).
- TcpProber's 128-thread pool drains after 60s idle.

CLI:

- publishGuard: all-transport failure exits 124 as timeout; rejected/1
  is reserved for an actual OK-false answer.
- --help anywhere in argv is hoisted centrally; 'amy notes post "x"
  --help' prints usage instead of publishing.
- rejectUnknown false-reject traps fixed: geochat --no-fetch behind an
  early return, and 13 elvis-alias short-circuit sites read eagerly.
- Aliases load once per Context and only match name-shaped inputs (no
  shadowing a real npub/NIP-05/hex); stderr color requires a
  positively-known terminal (TERM sniff polluted captured logs).
- Relay-CSV strictness unified on RawEventSupport.relayFlag (post,
  graperank publish/followers/register no longer silently drop
  malformed URLs); Args.timeoutMs(+OrNull) replaces 27 hand-rolled
  conversions, all strict; offer/debit --timeout > 3600 rejected with
  a 'looks like milliseconds' hint; NPub.create idiom; stale jq .id in
  the marmot reactions harness; printUsage drift (offer pay --with,
  profile --clink-offer, search --kind).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
2026-07-19 00:17:43 +00:00
Claude 962d1706dc fix(quartz): fetchAll-family timeouts are idle windows, not absolute deadlines
The fetchAll/fetchAllWithHooks accessories wrapped their whole
collection loop in one withTimeoutOrNull, so a relay actively
streaming a large backlog was cropped mid-delivery the moment the
absolute deadline hit — even though the loop already has proper
terminal conditions (per-relay EOSE / CLOSED / cannot-connect) and the
timeout's only real job is stall detection.

timeoutMs now measures the delta since the LAST message: every event
or terminal signal resets the window (fetchAll gains a conflated
activity ping so event progress is visible to its wait loop), and only
a full window of silence ends the fetch early. fetchFirst/count keep
absolute waits (single-response — idle and absolute coincide), and
subscribe's duration timeout stays absolute by design (a live stream
has no terminal state).

Since the pages/pool helpers delegate to fetchAll, pagination inherits
the semantics. This also changes app-side callers of these accessories
— in their favor: the timeout only ever fired on slow relays, exactly
when cropping loses data.

New commonTest suite pins the behavior: a relay emitting every 200ms
under a 300ms window streams to completion (10/10 events); a stall
ends one window after the last message, not after the start; EOSE
still returns immediately. CLI docs reworded (--timeout = idle window).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
2026-07-18 23:39:25 +00:00
Claude 620fc465df feat(cli): publish results carry each relay's rejection reason; converge output shapes
With no external consumers yet, converge the --json surface to its
ideal shape in one pass:

- quartz gains publishAndCollectResults: the NIP-01 OK message, connect
  errors, and silent timeouts now survive as PublishResult(accepted,
  message) per relay instead of dying in a debug log. The existing
  boolean APIs delegate unchanged. Silent relays are reported as
  'no response within timeout' rather than omitted.
- Context.publish returns the rich map; the new
  RawEventSupport.ackFields(ack) is the one canonical projection every
  publisher emits: published_to (urls) + rejected_by as
  [{relay, reason}] — 'why didn't it post' now answers itself, in
  partial failures and in the rejected error alike.
- author/pubkey rule enforced module-wide: 'author' is the key that
  signed an event (feed/search/dm/message list items), 'pubkey' an
  identity being described; profile show and outbox add the bech32
  npub beside the hex when the user is the primary subject.
- Event-list items converge on event_id/author/created_at/content
  (dm, feed, search, marmot message, geochat, concord).
- Byte counts standardize on *_bytes keys (blossom/nsite size ->
  size_bytes); the text renderer drops the fragile bare-'size'
  heuristic and colors stderr progress independently of a piped
  stdout.
- Error details are sentences everywhere (not bare gids); dead Result
  class removed from the quartz publish accessory.

Docs updated (DEVELOPMENT output conventions, README rejected example).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
2026-07-18 23:30:06 +00:00
Claude 82f041b1f5 docs(cli): reconcile README/ROADMAP/DEVELOPMENT/tests with reality
The docs had drifted badly behind the code: geochat was documented
nowhere, concord/zap/search/podcast20/nsite-publish were README-
invisible, the ROADMAP matrix contradicted its own nak table on six
shipped features, and DEVELOPMENT described the legacy FS event store
as the default when SQLite is.

- README: sections for search, zap (incl. --with auto-pay), podcast20,
  nsite/napplet (all four sub-verbs), concord (13 verbs), geochat, and
  a 'Which chat system?' comparison table; output section rewritten for
  the new contract (exit-code derivation, rejected, unknown-flag
  errors, -- terminator, per-command --help); layout diagram fixed for
  the SQLite default + operator/ + concord.json; the bunker nak-interop
  claim reworded honestly; RECIPES.md linked.
- ROADMAP: stale new-item rows flipped (follow, outbox, Blossom,
  bunker, search; zap partial), rows added for relaygroup/geochat/
  concord/nsite/napplet/podcast20/CLINK/fof, orphaned thread note
  fixed, test-suite section updated.
- DEVELOPMENT: canonical error-code list pinned, exit-code rule
  documented, no-prompts carve-outs, refreshed architecture tree +
  command template (USAGE/route(help=)/rejectUnknown/publishGuard),
  SQLite store section, testing table covers the new JVM suites.
- tests/README: all ten suite dirs listed, JVM contract suite noted,
  mis-spliced marmot row repaired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
2026-07-18 22:59:37 +00:00
Claude 5f65405b08 feat(graperank): add reverse follower crawl (amy graperank followers)
The outbox model can't find an observer's followers — you don't know a
follower exists until you've seen their kind:3, so you can't route to their
outbox first. FollowerCrawler casts a wide net instead: it asks as many relays
as possible for kind:3 lists that #p-tag the observer, paging each relay past
its per-REQ cap via fetchAllPagesFromPool, verifies with ParallelEventVerifier,
keeps only lists that genuinely tag the observer, dedups by id, and
group-commits to the store.

Each follower's list is a full contact list, so persisting it also enriches the
graph a later `graperank score` builds — every follower becomes a FOLLOW edge
into the observer.

CLI: `amy graperank followers [OBSERVER]` assembles "all possible relays" from
the reachability-cache live set + every kind:10002/30166 relay in the store +
the index/aggregator relays, skipping proven-dead relays. Runs anonymously (no
signing) when given an explicit observer. Tunable via
--relay/--page-limit/--timeout/--relay-concurrency/--insert-batch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4
2026-07-17 19:34:06 +00:00
Claude c8ff1bb18f feat(graperank): persist follower count and hop distance on trust cards
Each kind:30382 GrapeRank card now carries two more public tags alongside
`rank`:

- `followers` — the number of the target's followers whose own score clears a
  threshold (`--followers-threshold`, default 0.02), mirroring Brainstorm's
  trusted-follower cutoff.
- `hops` — the shortest follow-graph distance from the observer (1 = a direct
  follow), matching the `hops` field on Brainstorm's ScoreCard.

New `HopsTag` (the `followers`/`FollowerCountTag` already existed) is wired
through the ContactCardEvent tag accessors/builders. TrustGraph gains
`hopsFrom` (a follow-only BFS over the compact int-CSR) and
`trustedFollowerCounts`; the out-CSR now packs the relation code so a forward
walk can filter FOLLOW edges. The publisher's `reconcileLocal` takes a richer
`ScoredCard` and diffs the full (rank, followers, hops) triple, so a card
re-signs when any of them moves and older cards migrate onto the new tags once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4
2026-07-17 19:34:06 +00:00
Claude f628783186 refactor(cli): rename amy wot -> amy fof (follows-of-follows)
`wot` claimed the whole web-of-trust concept for what is actually a cheap
single-hop metric — the count of your follows who also follow X. The real
computed web of trust is `graperank`. Renamed the command (and WotCommand ->
FofCommand) to `fof`, reframing its KDoc/usage away from "trust," and kept
`wot` as a deprecation alias that warns and points at both `fof` and
`graperank`.

Also documents the command for the first time: `amy wot` had no --help block
and no README row. Added both (help block + three README rows for
get/list/sync), so the follows-of-follows score and its relationship to
graperank are now discoverable. JSON output shape is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
2026-07-14 22:43:44 +00:00
Claude 1a8a4012a6 docs(cli): audit graperank docs against code; simplify the --help block
Audited every graperank verb/flag in the code against the three doc
surfaces and fixed the drift:

- Main.kt `--help`: rewrote the GrapeRank block to match the terse house
  style (one line per command, key flags on continuation lines) — it had
  grown to ~68 lines of prose. Now ~33 lines, reordered into pipeline
  order (crawl -> score -> publish, then rank/status/refresh, then the
  provider/operator discovery group), with the per-verb timeout-semantics
  prose dropped (it lives in the KDoc). Verbs and primary flags verified
  against the dispatch and each function's arg reads.
- README: added the missing `graperank crawl` and `graperank score` rows
  (stage 1 and 2 of the pipeline — crawl had no table row at all, score
  was only mentioned inline) and labelled the three pipeline stages.

No stale references remain (no `graperank sync`, no removed publish/
bench flags, `operator providers`/`update` only as documented aliases).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
2026-07-14 21:44:26 +00:00
Claude 883365d6a5 feat(cli): graperank status verb, flag consistency, deprecate the sync alias
- New `graperank status`: read-only local inventory with no network, no
  signing, and no side effects — WoT record counts in the store (the "do I
  need to crawl again?" answer), reachability-cache size + newest-record
  age, operator/service-key state, and persisted card + retraction counts
  per observer. Reads the reachability cache through a throwaway signer so
  a fresh machine's status never lazily creates the operator master.
- Flag consistency: --relay-concurrency and --concurrency are now accepted
  interchangeably on `graperank refresh`, `graperank publish`, and
  `relay probe` (each keeps its documented spelling as canonical), and the
  help text states what --timeout means per verb (per-REQ drain 10s for
  crawl/score, idle watchdog 30s for refresh/publish, per wave 15s for
  probe, drain 8s for rank/register/providers).
- `graperank sync` now prints a deprecation warning before running crawl —
  the name points at the wrong concept since the negentropy record refresh
  became `graperank refresh` — and is removed from the docs; removal comes
  in a later release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
2026-07-14 21:18:36 +00:00
Claude 8b822029e2 feat(cli): graperank refresh rename, unregister verb, operator keys rename
- `graperank update` -> `graperank refresh`: the verb says what it does
  (refresh the WoT record kinds from each author's outbox) and stops
  colliding with the sync/crawl naming tangle. `update` stays as an alias.
- New `graperank unregister PROVIDER [--service KIND:TAG] [--relay URL]`:
  the missing inverse of `register` — removes matching entries (public +
  private) from the account's kind:10040 and re-publishes it; without
  narrowing flags every entry for that provider key is dropped.
- `graperank operator providers` -> `operator keys`: it lists the
  observer -> service-key map, and the old name collided with
  `graperank providers` (the kind:10040 read). `providers` stays as an
  alias; the JSON key `providers` becomes `keys` in both the listing and
  `operator status` (breaking, matches the rename).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
2026-07-14 20:52:12 +00:00
Claude a815614247 refactor(cli): move the relay census to amy relay probe
The census probes the whole known relay universe and feeds the shared NIP-66
reachability cache (kind:30166) that every reachability-aware command reads —
it was never graperank-specific, so it moves from GrapeRankCommand to
RelayCommands as `amy relay probe`. `amy graperank probe` stays as an alias,
and flags, JSON output, and behaviour are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
2026-07-14 20:42:17 +00:00
Claude 58e018c877 feat(cli): graperank scores always persist locally; add publish + rank verbs
Every `amy graperank` / `graperank score` run now reconciles its result into
the local event store as NIP-85 kind:30382 cards signed by the per-observer
service key (cutoff --min-rank, default 2): changed ranks are re-signed,
unchanged ones skipped (no event-id churn), dropped targets retracted with a
kind:5 the store applies on insert. The store is the source of truth, so the
score of a run is durable and reusable instead of ephemeral stdout.

New verbs complete the crawl -> score -> publish pipeline:
- `graperank publish [OBSERVER] [--relay URL[,URL...]]` — transport only:
  one NIP-77 up-only reconcile per operator relay over the provider key's
  kind:30382 + kind:5, converging the relay to the local set (deletions
  propagate, lost cards restored, full-set publish fallback for relays
  without negentropy); also refreshes the observer's kind:10040 pointer.
- `graperank rank USER [--provider P] [--refresh]` — the consumer side:
  newest card per provider from the local store, with a relay drain on miss.

GrapeRankPublisher (quartz) is reworked accordingly: reconcileAndPublish is
replaced by reconcileLocal (sign+insert+retract against the store) and
syncToRelays (NegentropyStoreSync up-only + blastPublish fallback), with a
commonTest covering upsert/skip/retract and re-carding a retracted target.

BREAKING (--json / flags): `--publish`, `--publish-limit`, `--publish-relay`
and `--bench-sign` are removed from the score command. Its JSON drops
published/publish_rejected/deleted/delete_rejected/skipped_unchanged/
publish_truncated/published_kind/published_to/publish_error/observer_10040/
bench_signed/bench_sign_ms and gains provider_pubkey/min_rank/cards_total/
cards_signed/cards_unchanged/cards_retracted/cards_ms. Publishing moved to
the new `graperank publish` verb.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
2026-07-14 20:29:09 +00:00
Claude ac98036a8e perf(pow): mine NIP-13 proof of work on all cores and drop per-hash allocations
The miner enumerates the nonce space deterministically (the random base is
overwritten before the first hash), so naively racing N copies of the search
duplicates the exact same candidate sequence N times. PoWMiner.mine() now
races workers over disjoint slices instead: each worker's nonce carries a
distinct fixed prefix while only the bytes after it are enumerated, making the
aggregate hash rate scale with cores (~3.8x on a 4-core box).

The hot loop also switches from sha256() to sha256Into() with a reused
32-byte buffer, so hashing no longer allocates per attempt.

amy wiring:
- `pow mine` and `post --pow` mine on all cores by default; `pow mine
  --threads N` overrides.
- `pow bench` measures the all-cores rate (what mining now uses, also the
  basis for expected_seconds) alongside a new hashes_per_second_single_core.
- PoWEstimator benchmarks with sha256Into to match the miner, and gains a
  workers overload that prices in cross-core contention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhaF5scnAvhP9wr9R7bTWM
2026-07-11 04:04:28 +00:00
Claude 81dc4449bd feat(cli): NIP-13 for amy — post --pow and the pow verb group
- `amy notes post TEXT --pow BITS [--pow-timeout SECS]` mines the note
  pre-signature via quartz's PoWMiner (blocking — the CLI process is the
  job), exits 124 on timeout with nothing published, and adds additive
  --json keys: pow, pow_target, pow_millis.
- `amy pow check EVENT-JSON|-` reports actual_bits, committed_target,
  has_commitment and effective_pow (capped at the commitment per
  NIP-13's anti-lucky-spam rule) plus id+sig validity.
- `amy pow mine --target N [--pubkey HEX] [--timeout SECS] TEMPLATE|-`
  mines an unsigned template for any pubkey — NIP-13's delegated PoW:
  ids don't commit to signatures, so a headless box can mine for a
  phone and hand the template back for signing.
- `amy pow bench` prints the machine's hash rate and expected seconds
  at 16/20/24/28 bits (commons PoWEstimator).
- cli/tests/pow/pow-headless.sh: relay-free harness covering bench,
  mine (commitment shape + 124 timeout), and the delegated round trip
  (mine → sign via `amy event` → pow check ≥ target). 6/6 passing.

No logic added to cli/ — thin assembly over quartz nip13Pow + commons
PoWEstimator per the CLI architecture rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-10 22:47:05 +00:00
Claude 2eb7ff2ef2 Merge remote-tracking branch 'origin/main' into claude/armada-nip29-integration-lwqard
# Conflicts:
#	cli/tests/.gitignore
2026-07-09 21:48:04 +00:00
Claude 633903b5c1 fix: relay-group audit — timeline, roster, membership, list-safety
Fixes found in a full audit of the NIP-29 relay-groups feature across
quartz/commons/amethyst/cli.

Correctness (app):
- Own group messages never appeared in the timeline until an app restart:
  the optimistic send is consumed with a null relay, so attachToRelayGroup
  bailed on the relay==null guard, and the host relay's echo (new==false)
  was skipped by the "only attach when newly consumed" gate. Attach now runs
  on every arrival, gated on the note being loaded, and the null-relay case
  attaches to the already-open channel(s) for that group id. Also avoids the
  wrong-relay phantom by only fabricating a channel from real provenance.
- Roster subscription was frozen after an in-place join/leave (state keyed
  on the stable account, never re-derived); it now invalidates on every
  liveRelayGroupList change, so a fresh join's 39002 admission is fetched.
- membershipOf demoted a 39001 admin with an empty/unknown role to MEMBER,
  hiding moderation; presence in the admins list now means at least MODERATOR.
- Members roster showed permanent truncated-hex names (one-shot
  getUserIfExists cached null); uses checkGetOrCreateUser so UsernameDisplay
  fills in when kind:0 arrives.

Protocol / data:
- GroupTag had no value equality → joined-group Sets never deduped and the
  StateFlow re-emitted on every identical re-arrival. Equality is now the
  (id, relay) pair, excluding the cosmetic name.
- create/edit emitted non-canonical ["public"]/["open"] status tags; NIP-29
  flags are presence-only, so only private/closed are emitted when set.
- Metadata/member/admin supersede guards use <= so an equal-createdAt
  duplicate isn't reprocessed (first-arrival wins); updatedMetadataAt is now
  private-set. Relay-group channels are now included in the prune loops.

CLI:
- join/leave/create updated the kind:10009 list from a network-only drain;
  a slow/empty fetch could publish a fresh list containing ONLY the new
  group, wiping the rest. Now reads the local store (source of truth) too.
- edit re-asserted both visibility axes from flag presence, so --closed on a
  private group leaked it public. It now reads current 39000 and merges,
  with --public/--open counter-flags; only the specified axis changes.
- create now tracks the new group in kind:10009 (parity with join/Android).

UI polish:
- Invite dialog no longer mints a 9009 for open groups and won't copy a code
  it never displayed. Browse "popular" list normalizes URLs before filtering.

Tests: GroupTag identity, unknown-role-admin-moderates, equal-createdAt
no-resupersede added; all quartz+commons NIP-29 suites and the amy
relaygroup harness pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-08 02:27:18 +00:00
Claude d85bac1f55 feat(cli): amy relaygroup — NIP-29 relay groups in the CLI
Adds a first-class `amy relaygroup` verb group so the CLI can drive the
same NIP-29 relay groups as the app. Thin assembly over quartz builders +
Context.publish/drain — no protocol logic in cli/.

Verbs:
- list / browse RELAY / info RELAY GID — reads (joined kind:10009 list with
  private-item decryption; a relay's 39000-39003 directory; one group's
  metadata + roster).
- create / join / leave / message — lifecycle. join/leave also maintain the
  caller's kind:10009 list (add/remove private item) so `list` reflects them,
  mirroring the app's follow/unfollow.
- edit / invite / put-user / remove-user — moderation (9002/9009/9000/9001).

All writes pin to the group's single host relay. Output follows amy's
text/--json contract with snake_case keys.

Verified end-to-end against an embedded relay (amy serve) with a new
self-contained harness, cli/tests/relaygroup/relaygroup-headless.sh:
create/message/join/list/browse all pass (5/5). browse/info return empty
against geode since it doesn't sign 39000-39003 — a relay capability, not a
client issue. README command table + ROADMAP updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-08 01:41:15 +00:00
Claude 24c1ef3100 Merge remote-tracking branch 'origin/main' into claude/graperank-wot-cli-qreg2a
# Conflicts:
#	cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/StoreCommands.kt
2026-07-07 23:18:50 +00:00
Vitor PamplonaandGitHub 325eb3ccfe Merge pull request #3492 from vitorpamplona/claude/amethyst-status-command-yf5be6
Add `amy status` command for cross-account disk overview
2026-07-07 19:05:26 -04:00
Claude 439b85a8e1 feat(cli): let read-only verbs run without an account
amy gated every non-primitive verb behind a chosen account: `DataDir.resolve`
threw when `~/.amy/` had no unambiguous account, and every networked command
called `Context.open`, which requires an identity — even though queries only
read relays and the shared store and never sign. `store` maintenance and the
local `offer`/`debit info` decoders were caught by the same gate despite
touching no account state.

Reads now work anonymously; only signing needs an account:

- `DataDir.resolveOptional` hands back an accountless dir (`hasAccount = false`)
  pointing only at the shared event store when there is no unambiguous account,
  instead of throwing.
- `Context.openOrAnonymous` uses the resolved account when present, else an
  ephemeral key-less `Identity.anonymous()` — can read, can't sign. Marmot
  stores are now lazy and run-state isn't persisted for anonymous runs, so an
  accountless read leaves `~/.amy/shared/` clean.
- `Context.open` (signing path) re-asserts the requirement with the "which
  account?" hint, so ambiguous/no-account signing verbs still exit 2.
- Main resolves optionally for every verb except the identity-lifecycle ones
  (`init`/`create`/`login`/`logoff`/`whoami`), which still need a concrete
  account. `offer info` / `debit info` join the stateless primitive block.
- Read subverbs (fetch, subscribe, count, publish, outbox, search, sync,
  store, profile/git/podcast/podcast20 reads, nsite/napplet fetch·serve·list,
  blossom download·check) switch to `openOrAnonymous`.

No `--json` shapes change. Docs updated (help text, README, DEVELOPMENT).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRaoqGod5LUeSwq4GHRSNF
2026-07-07 22:58:22 +00:00
Claude 85d6f5a162 feat(cli): add amy status overview command
A cross-account, read-only snapshot of everything amy holds under
`~/.amy/`, built for the returning user: which accounts exist, which
one is pinned as current, each signer type (local keychain/ncryptsec/
plaintext, NIP-46 bunker, or read-only) and whether it can still sign,
the per-account local footprint (aliases, Marmot groups, published
KeyPackage bundle, Cashu wallet, sync cursors), and the shared event
store's size.

Like `use`, it dispatches before account resolution so it works with
zero, one, or many accounts. Strictly metadata-only: it never unlocks a
private key (no keychain prompt / NIP-49 passphrase) and never touches
the network.

Factors the on-disk event-store walk into a shared `StoreStats` helper
reused by both `status` and `store stat`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3GJb11JkvopP61ETWAVyy
2026-07-07 22:32:06 +00:00
Claude c8c4111e0c feat(cli): add amy logoff to clear an account's local data
Adds `amy logoff [--yes] [--keep-events]`, the CLI counterpart to logging
out: it removes everything an account left on the machine.

  - the identity file and any backend-held secret (keychain / ncryptsec /
    plaintext), via DataDir.deleteIdentity
  - the rest of the per-account directory ~/.amy/<account>/ (run-state
    cursors, aliases, cashu counters, all Marmot/MLS state)
  - the ~/.amy/current pin, when it points at this account
  - the account's events in the SHARED ~/.amy/shared/events-store/

The event store is shared across accounts, so logoff does not wipe it
wholesale — it deletes only the events that involve this account: those it
authored plus those addressed to it via a #p tag (gift wraps, nutzaps,
reactions, mentions). Other accounts' cached events are left untouched.
`--keep-events` skips the shared-cache purge entirely.

The public key is read straight from identity.json (never unlocking the
private key), so logoff needs no passphrase and pops no keychain prompt.
Destructive and irreversible, so it follows the `marmot reset` precedent:
`--yes` is required to execute; without it the command prints a dry run of
what would be deleted and exits 2.

Thin-assembly only — event deletion is quartz's FsEventStore.delete; this
just resolves the account, counts, and wires the filesystem teardown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PH3rqz5KaA7CYFPAtxgoz1
2026-07-07 22:11:52 +00:00
Claude be6ff5456d docs(cli): document graperank operator keys + publish reconciliation
README + amy usage: add the `graperank operator [status|relay|providers]`
sub-verb, update the `graperank --publish` description to the per-observer
service-key model (sign with a derived key, publish to the operator relay,
reconcile new/changed/skip/retract, cutoff rank>=2, NIP-09-drop retracted
reports), and add a 'Publishing GrapeRank scores' section explaining the
operator master, deterministic per-observer key derivation, and the kind:10040
discovery wiring.
2026-07-07 14:39:32 +00:00
Claude 4ec3da1e86 feat(cli): exhaustive graperank crawl — no user cap, check every outbox
Replace the depth-limited, user-capped BFS with a completeness loop that runs
until every discovered user's kind:10002 outbox has been checked and their
latest kind:3/10000/1984 pulled from it:

- Delete the --max-users cap entirely.
- Crawl round by round until the pending set (discovered minus done) is empty.
  A user is "done" once we download its contact list, or after --max-attempts
  (default 3) failed tries of its outbox — so an unreachable outbox can't stall
  the crawl, and it still terminates on a finite graph.
- --max-rounds replaces --max-depth as an (unbounded by default) safety backstop.
- Track and report the pool of relays actually contacted (relays_contacted),
  the "running relays" we connect to as more outboxes are discovered.

JSON: `depth_reached` -> `crawl_rounds`, add `relays_contacted`. Per-round and
final crawl-summary progress on stderr. Local regression: scores unchanged
(rank 26); the crawl retries contact-list-less users then terminates cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
2026-07-06 18:23:10 +00:00
Claude 1b78dfec57 feat(cli): add NIP-85 provider discovery to graperank (kind:10040)
Publishing kind:30382 rank cards is only half of NIP-85 — clients also need
the kind:10040 TrustProviderListEvent to discover which key provides which
assertion, and where. Add the discovery layer as two sub-verbs:

- `amy graperank register [PROVIDER]` — append a ServiceProviderTag
  (default `30382:rank`, self, first outbox relay) to the account's kind:10040,
  fetching the freshest list first so existing providers are preserved.
  Idempotent, supports `--service KIND:TAG`, `--relay`, and `--private`.
- `amy graperank providers [USER]` — list a user's declared providers
  (cache-first; own private entries are decrypted and included).

Bare `amy graperank [OBSERVER]` still computes scores; the dispatcher only
peels off the `register` / `providers` words. All built on quartz's existing
`TrustProviderListEvent` / `ServiceProviderTag` / `ProviderTypes`.

Verified against a local geode relay: register creates the 10040 and is
idempotent on re-run; providers lists both a public 30382:rank entry and a
private 30382:followers entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
2026-07-06 16:14:48 +00:00
Claude 7b7c553331 refactor(cli): drop graperank --target and the signal-toggle flags
- Remove `--target USER`: the command already emits the full ranking, and a
  single-user lookup is a trivial slice of it.
- Remove `--no-mutes` / `--no-reports` and the include* parameters on
  TrustGraphBuilder.build. GrapeRank is defined over follows, mutes and
  reports together; scoring with a signal disabled isn't a meaningful WoT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
2026-07-06 15:11:27 +00:00
Claude e6291fa912 feat(cli): add GrapeRank web-of-trust calculator (amy graperank)
Bring the GrapeRank algorithm into Amethyst as a WoT service calculator
on the CLI.

commons/wot (protocol-agnostic, CLI-safe, reusable by the apps):
- TrustGraph / TrustEdge / TrustRelation — pubkey-keyed graph model.
- GrapeRank — single-observer scoring engine, a faithful port of the
  reference v3 TargetedBFS variant using a worklist that reaches the same
  fixed point a full sweep would while only touching reachable users.
- TrustGraphBuilder — pure kind:3 / kind:10000 / kind:1984 events -> graph
  (latest-replaceable-per-author, dedup, self-edge drop).
- Unit tests: hand-computed values plus an adversarial full-sweep
  cross-check over 50 random graphs.

cli: `amy graperank [OBSERVER]` crawls the follow/mute/report graph via the
outbox model (locate each user's kind:10002 write relays, then fetch their
lists from their own relays, with a broad event-finder fallback) until no
new users appear, scores it, and prints a ranked list (text / --json).
--target queries one user, --offline scores from the local store, and
--publish writes NIP-85 kind:30382 ContactCard assertions
(rank = round(score*100)) per user at or above --min-rank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
2026-07-06 15:11:27 +00:00
Claude 5aae4368da refactor(cli): noun-first amy relay, outbox/inbox NIP-65 facets
Restructure `amy relay` from verb-first `relay add URL --type T` to noun-first
`relay <noun> <verb>`, matching amy's `marmot group …` / `cashu mint …`
convention. The relay-list type is now a required path segment (no implicit
default), and a bare noun lists that bucket.

NIP-65 (kind:10002) is fronted by two facet-nouns, `outbox` (write) and `inbox`
(read), replacing the `--marker` flag. They edit the single 10002 event and
apply the spec's merge rules:
- outbox add R on a read-only R  → both
- inbox  add R on a write-only R → both
- outbox remove R on a both-R    → read  (stays in inbox)
- inbox  remove R on a both-R    → write (stays in outbox)
- dropping the last facet removes R entirely
`relay nip65` shows the combined view; `nip65 remove`/`clear` edit the whole
event.

Other buckets are noun+verb: `relay dm|key-package|search|private|blocked|
trusted|proxy|indexer|broadcast|feeds <add|remove|set|clear|list>`. `set` needs
≥1 URL; `clear` empties. `relay add|remove URL` (no noun) stays as the
transport fan-out (nip65 both + dm + key-package).

BREAKING (cli --json/args): removes `relay add/remove/set --type T` and
`--marker`; `relay list` overview now keys nip65 as `outbox`/`inbox`/`nip65`
and the DM bucket as `dm` (was `inbox`). In-repo harnesses updated
(cache/dm/marmot setup drop `--type all`; cache T5 asserts `.dm`).

Verified end-to-end: merge semantics, encrypted NIP-51 round-trips, facet
set/clear, fan-out, aliases, and error paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjHzNewJ2sfBGCcSwe35Mc
2026-07-06 15:02:28 +00:00
Claude c7bde868e1 feat(cli): require --clear to empty a relay bucket
`relay set --type T` with no URLs is now rejected (bad_args, exit 2) instead
of silently wiping the list — a bare empty `set` is almost always a shell
variable that expanded to nothing. Emptying a bucket is explicit: pass
`--clear` (mutually exclusive with URLs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjHzNewJ2sfBGCcSwe35Mc
2026-07-06 14:30:08 +00:00
Claude 0319809b8c feat(cli): full relay-settings parity for amy relay
Expand `amy relay` from the 3 transport lists (nip65/inbox/key_package) to
every relay-list bucket Amethyst's relay-settings screen manages, and add
remove/set verbs alongside add/list.

Buckets (kind): nip65 (10002, read/write markers), inbox/dm (10050),
key_package (10051), search (10007), private (10013), blocked (10006),
trusted (10089), proxy (10087), indexer (10086), broadcast (10088),
feeds/favorites (10012). The private NIP-51 lists are signed NIP-44-encrypted
via the quartz event factories, exactly like the app. Local relays (device
pref, no event) and named relay sets (30002) are intentionally out of scope.

New/changed commands:
- `relay add URL --type T [--marker read|write|both]` — `--marker` sets the
  nip65 role; `all` still means nip65+inbox+key_package.
- `relay remove URL --type T` — new.
- `relay set --type T [URL…] [--marker …]` — new; replace a whole bucket
  (no URLs clears it).
- `relay list [--type T]` — lists every bucket, or one.
- `relay publish-lists` — now broadcasts every configured list.

Thin-assembly only: buckets are a small registry over the existing quartz
`create`/`relays` factories; adds one generic `Context.latestReplaceable`
helper. `--json` is additive — legacy keys (`nip65`/`inbox`/`key_package`,
`nip65_event_id`/…) are unchanged, so the existing test harnesses keep passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjHzNewJ2sfBGCcSwe35Mc
2026-07-06 13:59:47 +00:00
Vitor PamplonaandClaude Opus 4.8 112ef0536a fix(cli): --server reuses NamecoinSettings.parseServerString (keeps pinned trust store)
`amy namecoin resolve --server` hand-rolled its own ElectrumX server-string
parser that constructed `ElectrumxServer(host, port, useSsl)` and left
`usePinnedTrustStore` at its `false` default. The Namecoin ElectrumX servers
use self-signed certs, so a TLS connection with the default system trust
manager fails the handshake — meaning `--server electrumx.testls.space:50002`
could not connect even though that exact host resolves fine via the default
list. It also duplicated logic already in `commons`, violating the cli
thin-assembly-layer rule.

Delegate each comma-separated entry to the shared
`NamecoinSettings.parseServerString` (the same parser the Android/Desktop
Settings use), so the CLI inherits both the `host:port[:tcp]` syntax and
`usePinnedTrustStore = true`. The README claim that it "reuses the same …
pinned trust store as the apps" is now actually true for `--server` overrides.

Also: reject a non-integer `--timeout` as bad_args instead of silently
falling back to the default, and document exit code 2 + the `host:port[:tcp]`
syntax accurately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:27:32 -04:00
mstrofnoneandVitor Pamplona 305d6bc733 feat(cli): amy namecoin resolve + servers verbs
Add Namecoin NIP-05 resolution to the amy CLI as a stateless verb
group, matching the Android and Desktop apps' resolution surface.

  amy namecoin resolve IDENT [--server URL[,URL]] [--timeout SECS]
  amy namecoin servers

IDENT accepts the same shapes the apps accept: raw `d/` / `id/`
names, bare `.bit` domains, and `alice@example.bit` NIP-05-style
local-parts. Output is the resolved Nostr pubkey + relay list (+ the
resolved Namecoin name + matched local-part) as machine-readable
JSON (with `--json`) or human-readable text.

The verb is stateless — no account, no `~/.amy/`, no relays — so it
dispatches alongside `decode`/`encode`/`verify`/`nip`/`kind` before
account resolution and the secret store.

Zero new logic in cli/: the implementation is a thin command-file
wrapper around quartz's existing `NamecoinNameResolver` +
`ElectrumXClient` + the canonical `DEFAULT_ELECTRUMX_SERVERS` set
the apps already ship with, including the pinned trust store for
the self-signed Namecoin ElectrumX ecosystem.

amy is headless so no UI piece is wired in. The `--server` flag
accepts `host`, `host:port`, `tcp://`, `tls://`, `ssl://` per entry
(defaults to TLS on 50002); empty / malformed entries fail with
`bad_args` rather than silently using the default set, so a fat-
fingered override can't go unnoticed.

Outcomes from `NamecoinResolveOutcome` map to amy error codes:
  Success           -> emit JSON, exit 0
  NameNotFound      -> error not_found
  NoNostrField      -> error no_nostr_field
  MalformedRecord   -> error malformed_record (+ namecoin_name extra)
  ServersUnreachable-> error servers_unreachable
  InvalidIdentifier -> error invalid_identifier
  Timeout           -> error timeout

Smoke-tested end-to-end on macOS arm64 against the live ElectrumX
fleet:

  $ amy --json namecoin resolve d/testls
  {"identifier":"d/testls","namecoin_name":"d/testls",
   "local_part":"_","pubkey":"460c25e6…","relays":[]}

  $ amy namecoin servers
  count:   6
  servers:
    - host: electrumx.testls.space
      port: 50002
      tls:  yes
    …

No new runtime deps. The "no Compose UI in the amy image" CI
assertion still passes — `NamecoinNameResolver` + `ElectrumXClient`
are pure JVM (kotlinx.coroutines + kotlinx.serialization, both
already on the CLI classpath via :quartz).

Tests: the resolver, ElectrumX client, identifier parser, and the
default server set already have JVM tests under
`quartz/src/jvmTest/.../namecoin/` — no new core code in this PR,
so the existing coverage applies. CLI verbs are exercised via the
shell harnesses in `cli/tests/`; a Namecoin harness fits the same
pattern but isn't included here.

Parity matrix in `cli/ROADMAP.md` flags `name_history` and the
Namecoin Core JSON-RPC backend as pending separate PRs — both
already exist on Android and Desktop but aren't on upstream main
yet (open PRs against this repo carry them).
2026-06-26 18:23:13 -04:00
Claude 7a3c35c645 docs: document cashu, admin, serve, fetch code mode, key validate
Update the amy docs to reflect the new command surface:

- cli/README.md — add the Cashu (NIP-60/61), Relay management (NIP-86
  admin), and Run-a-relay (serve) sections; document fetch's nip19/nip05
  code mode and key validate.
- cli/DEVELOPMENT.md — add cashu.json to the on-disk layout and pin the
  command-family --json contracts (cashu keys/error codes + pointer to the
  cashu plan, admin {relay,method,result}, serve startup object).
- .claude/skills/amy-expert/SKILL.md — extend the "where things live" tree
  with AdminCommand/ServeCommand/cashu/, the commons/cashu + relayManagement
  shared modules, and the allowed :geode dependency.
- .claude/CLAUDE.md — note cli may depend on :geode (for serve), never on
  :amethyst/:desktopApp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 02:14:11 +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 69d03bacca feat(cli): add nak-style git (NIP-34) + podcast (NIP-F4) commands
git (repository metadata + collaboration events; clone/push packfile
transport is out of scope):
- git announce  publish a kind:30617 repository announcement
- git list      list a user's repo announcements
- git show      print one announcement (naddr or kind:pubkey:id), cache-first
- git issue     publish a kind:1621 issue against a repo (fetches the repo
                announcement to build the EventHintBundle)

podcast (NIP-F4):
- podcast metadata  publish kind:10154 show metadata (replaceable)
- podcast publish   publish a kind:54 episode (--audio URL[,URL])
- podcast list      list a user's metadata + episodes

Thin assembly over quartz GitRepositoryEvent / GitIssueEvent /
PodcastMetadataEvent / PodcastEpisodeEvent. Verified offline: announce->
show cache round-trip (name/clone/hashtags), issue kind:1621 against the
repo, podcast metadata kind:10154 + episode kind:54.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:13:21 +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