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.
This commit is contained in:
Claude
2026-04-25 03:25:11 +00:00
parent d460584d81
commit bf1c4c23cb
2 changed files with 27 additions and 4 deletions
+2
View File
@@ -169,6 +169,8 @@ Run `amy --help` for the canonical list. As of today:
| `create [--name NAME]` | Provision a full account + publish the nine Amethyst bootstrap events. |
| `login KEY [--password X] [--private]` | Import `nsec` / `ncryptsec` / BIP-39 mnemonic / `npub` / `nprofile` / hex / NIP-05. Read-only when no secret material is supplied. |
| `whoami` | Print the identity stored in `--data-dir`. |
| `profile show [PUBKEY] [--refresh] [--timeout SECS]` | Print kind:0 metadata. Default reads from the local store (cache-first); `--refresh` forces a relay drain. PUBKEY accepts `npub` / `nprofile` / hex / `name@domain.tld`; omit for self. |
| `profile edit --name X [--display-name X] …` | Build + publish a new kind:0 starting from the current cached metadata (or fetched if missing). |
| `relay add URL [--type T]` | `T = nip65 \| inbox \| key_package \| all`. |
| `relay list` | Dump configured relays by bucket. |
| `relay publish-lists` | Publish kind:10002 (NIP-65) + kind:10050 (DM inbox) + kind:10051 (KeyPackage relay list). |
@@ -58,6 +58,7 @@ object ProfileCommands {
rest: Array<String>,
): Int {
val args = Args(rest)
val refresh = args.bool("refresh")
val timeoutSecs = args.longFlag("timeout", 8L)
val ctx = Context.open(dataDir)
try {
@@ -66,14 +67,33 @@ object ProfileCommands {
args.positionalOrNull(0)?.let { ctx.requireUserHex(it) }
?: ctx.identity.pubKeyHex
val isSelf = pubKey == ctx.identity.pubKeyHex
val relays = relaysForReadingProfile(ctx, isSelf)
val event = fetchLatestMetadata(ctx, pubKey, relays, timeoutSecs * 1000)
// Cache-first: serve from the local store unless --refresh is
// set. The store is the source of truth (see cli/README.md);
// every event drained from a relay has already been persisted
// here, so an offline read just works after the first sync.
val cached = if (!refresh) ctx.profileOf(pubKey) else null
val event: MetadataEvent?
val source: String
val queried: List<NormalizedRelayUrl>
if (cached != null) {
event = cached
source = "cache"
queried = emptyList()
} else {
val relays = relaysForReadingProfile(ctx, isSelf)
event = fetchLatestMetadata(ctx, pubKey, relays, timeoutSecs * 1000)
source = "relays"
queried = relays.toList()
}
if (event == null) {
Json.writeLine(
mapOf(
"pubkey" to pubKey,
"found" to false,
"queried_relays" to relays.map { it.url },
"source" to source,
"queried_relays" to queried.map { it.url },
),
)
return 0
@@ -88,10 +108,11 @@ object ProfileCommands {
mapOf(
"pubkey" to pubKey,
"found" to true,
"source" to source,
"event_id" to event.id,
"created_at" to event.createdAt,
"metadata" to (metadata ?: emptyMap<String, Any?>()),
"queried_relays" to relays.map { it.url },
"queried_relays" to queried.map { it.url },
),
)
return 0