diff --git a/cli/README.md b/cli/README.md index 2b4c55eb7e..bfef61f701 100644 --- a/cli/README.md +++ b/cli/README.md @@ -217,6 +217,7 @@ Army-knife verbs that operate purely on their arguments. They never touch | `amy key decrypt NCRYPTSEC --password X` | NIP-49 decrypt back to nsec/hex/npub. | | `amy filter [filter flags]` | Assemble and print a NIP-01 filter JSON from the same flags `fetch`/`subscribe` use — no query is sent. | | `amy nip N` / `amy nip list` | Look up a NIP — the `nostr-protocol/nips` repo first, then a Nostr wiki/long-form fallback. `list` fetches the index. | +| `amy kind N` / `amy kind NAME` | Look up an event kind's label + defining NIP (number), or search labels by name. Backed by quartz's `KindNames` registry. | | `amy relay info URL` | Fetch and print a relay's NIP-11 information document. | ### Remote signing (NIP-46 bunker) diff --git a/cli/ROADMAP.md b/cli/ROADMAP.md index 3c0308f249..f5f0ea52c1 100644 --- a/cli/ROADMAP.md +++ b/cli/ROADMAP.md @@ -94,7 +94,7 @@ vs streaming `subscribe`). Stateless verbs run with no account or network. | `filter` | `amy filter` | ✅ | stateless — assemble + print a filter JSON. | | `blossom` | `amy blossom` | ✅ | upload/download/list/delete/check/mirror (reuses commons `BlossomClient`). | | `nip` | `amy nip` | ✅ | repo-first lookup + Nostr wiki/long-form fallback; `nip list`. | -| `kind` | `amy kind` | 🆕 | needs a kind→schema registry (nak embeds one); none in quartz today. | +| `kind` | `amy kind` | ✅ | quartz `KindNames` registry (kind → English label + NIP), ported from the Android relay view; number lookup + name search. | | `sync` | `amy sync` | ✅ | NIP-77 Negentropy reconcile with the local store (down/up/both). | | `git` | `amy git` | ✅ in part | NIP-34 repo announce/list/show/issue. clone/push (packfile transport) out of scope. | | `podcast` | `amy podcast` | ✅ | NIP-F4 show metadata (10154) + episode publish (54) + list. | @@ -105,15 +105,15 @@ vs streaming `subscribe`). Stateless verbs run with no account or network. nak has 34 functional commands. Coverage: -- **Full / equivalent (18):** `event`, `req`(→`fetch`+`subscribe`), `filter`, +- **Full / equivalent (19):** `event`, `req`(→`fetch`+`subscribe`), `filter`, `count`, `decode`, `encode`, `verify`, `relay`, `bunker`(+nostrconnect+auth_url), `encrypt`, `decrypt`, `gift`, `publish`, `sync`, `profile`, `podcast`, `nip`, - `blossom`. Protocol-sensitive ones (`bunker`, `sync`, `key` NIP-49, + `kind`, `blossom`. Protocol-sensitive ones (`bunker`, `sync`, `key` NIP-49, `encode`/`decode`) are interop-verified against the real `nak` binary. - **Partial / adapted (4):** `key` (no `expand`/`combine`/`validate`/`default`), `git` (NIP-34 events only — no packfile transport), `outbox` (shows NIP-65 vs nak's hints DB), `fetch` (filter-based, not nip19-hint resolution). -- **Missing (12):** `kind` (needs a kind-schema registry), `admin` (NIP-86), +- **Missing (11):** `admin` (NIP-86), `serve` (geode is a standalone relay), `dekey` (NIP-4E), `wallet` (NIP-60 Cashu), `mcp`, `curl` (NIP-98), `fs` (FUSE), `group`/`nip29` (NIP-29 — amy has MLS/Marmot instead), `spell` (MuSig2/FROST), `validate` (RoK schema). @@ -125,8 +125,7 @@ a large surface nak lacks: Marmot/MLS, NIP-17 DMs, zaps, CLINK offer/debit, NIP-02 follow, NIP-50 search, napplets, profile edit, store management, account management. -**Cheap remaining wins:** `kind` (needs a registry), the `key` -`expand`/`combine`/`validate`/`default` sub-verbs. +**Cheap remaining wins:** the `key` `expand`/`combine`/`validate`/`default` sub-verbs. --- diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt index b800811eb0..5cda741f43 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.cli.commands.GroupCommands import com.vitorpamplona.amethyst.cli.commands.InitCommands import com.vitorpamplona.amethyst.cli.commands.KeyCommands import com.vitorpamplona.amethyst.cli.commands.KeyPackageCommands +import com.vitorpamplona.amethyst.cli.commands.KindCommand import com.vitorpamplona.amethyst.cli.commands.LoginCommand import com.vitorpamplona.amethyst.cli.commands.MarmotResetCommand import com.vitorpamplona.amethyst.cli.commands.MessageCommands @@ -172,6 +173,7 @@ private suspend fun dispatch(argv: Array): Int { "key" -> return KeyCommands.dispatch(tail) "filter" -> return FilterCommand.run(tail) "nip" -> return NipCommand.run(tail) + "kind" -> return KindCommand.run(tail) } // `relay info URL` is a stateless NIP-11 fetch — no account needed. The @@ -350,6 +352,7 @@ private fun printUsage() { | [--id …] [--tag …] … same flags fetch/subscribe use (no query sent) | nip N show a NIP (repo first, then a Nostr wiki/long-form fallback) | nip list fetch the NIP index (README) from the repo + | kind N|NAME look up an event kind's label + NIP (number, or search by name) | |Identity: | init [--nsec NSEC] create or import a bare identity (no defaults published) diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/KindCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/KindCommand.kt new file mode 100644 index 0000000000..e2654428d6 --- /dev/null +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/KindCommand.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.cli.commands + +import com.vitorpamplona.amethyst.cli.Args +import com.vitorpamplona.amethyst.cli.Output +import com.vitorpamplona.quartz.kinds.KindNames + +/** + * `amy kind ` — look up a Nostr event kind (nak's `kind`). Local, + * accountless. A number prints that kind's label + defining NIP; a text query + * searches labels. + * + * kind 1 -> {"kind":1,"name":"Short Text Note","nip":"10",...} + * kind "text note" -> {"query":…,"matches":[…]} + * + * Thin assembly only: the canonical kind registry lives in quartz + * (`KindNames`) — the same data the Android relay view localizes on top of. + */ +object KindCommand { + fun run(rest: Array): Int { + val args = Args(rest) + val arg = args.positional(0, "kind-number-or-name").trim() + val n = arg.toIntOrNull() + if (n != null) { + val info = KindNames.infoFor(n) + Output.emit( + mapOf( + "kind" to n, + "name" to info?.name, + "nip" to info?.nip, + "known" to (info != null), + ), + ) + return 0 + } + val matches = KindNames.search(arg) + Output.emit( + mapOf( + "query" to arg, + "count" to matches.size, + "matches" to matches.map { (kind, info) -> mapOf("kind" to kind, "name" to info.name, "nip" to info.nip) }, + ), + ) + return 0 + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt new file mode 100644 index 0000000000..f09a5fe5fc --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.kinds + +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent +import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent +import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent +import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.experimental.nns.NNSEvent +import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent +import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent +import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent +import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent +import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent +import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent +import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent +import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent +import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent +import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent +import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent +import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent +import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent +import com.vitorpamplona.quartz.nip64Chess.draw.LiveChessDrawOfferEvent +import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent +import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent +import com.vitorpamplona.quartz.nip64Chess.jester.JesterEvent +import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent +import com.vitorpamplona.quartz.nip71Video.VideoShortEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent +import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent +import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.NIP90UserDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.NIP90UserDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent +import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent +import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent +import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent +import com.vitorpamplona.quartz.nipF4Podcasts.authored.AuthoredPodcastsEvent +import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent +import com.vitorpamplona.quartz.nipF4Podcasts.favorites.FavoritePodcastsListEvent +import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent + +/** + * Human-readable label and defining NIP for a Nostr event kind. + */ +data class KindName( + val name: String, + val nip: String?, +) + +/** + * Canonical, **i18n-free** registry mapping event-kind numbers to an English + * label and the NIP that defines them. This is the single source of truth for + * "what is kind N" across the project: + * + * - headless consumers (the `amy` CLI) print [nameFor] directly; + * - localized front ends (the Android app) overlay their own translated + * strings on top and fall back to [nameFor] for kinds they don't translate. + * + * Keep this list as the place new kinds are registered; translations are a + * platform concern layered on top, never a fork of this data. + */ +object KindNames { + val names: Map = + mapOf( + AcceptedBadgeSetEvent.KIND to KindName("Accepted Badge Set", "58"), + AdvertisedRelayListEvent.KIND to KindName("Outbox Relays", "65"), + AppDefinitionEvent.KIND to KindName("Apps", "89"), + AppRecommendationEvent.KIND to KindName("App Recommendations", "89"), + AppSpecificDataEvent.KIND to KindName("User Settings", "78"), + AudioHeaderEvent.KIND to KindName("Audio Header", null), + AudioTrackEvent.KIND to KindName("Audio Track", null), + MusicTrackEvent.KIND to KindName("Music Track", null), + MusicPlaylistEvent.KIND to KindName("Music Playlist", null), + PodcastEpisodeEvent.KIND to KindName("Podcast Episode", "F4"), + PodcastMetadataEvent.KIND to KindName("Podcast Show", "F4"), + AuthoredPodcastsEvent.KIND to KindName("Authored Podcasts", "F4"), + FavoritePodcastsListEvent.KIND to KindName("Favorite Podcasts", "F4"), + AttestationEvent.KIND to KindName("Attestation", null), + AttestationRequestEvent.KIND to KindName("Attestation Request", null), + AttestorRecommendationEvent.KIND to KindName("Attestor Recommendation", null), + AttestorProficiencyEvent.KIND to KindName("Attestor Proficiency", null), + BadgeAwardEvent.KIND to KindName("Badge Awards", "58"), + BadgeDefinitionEvent.KIND to KindName("Badge Definitions", "58"), + BlockedRelayListEvent.KIND to KindName("Blocked Relays", "51"), + BlossomServersEvent.KIND to KindName("Blossom Servers", "B7"), + NestsServersEvent.KIND to KindName("Nests Servers", "53"), + BlossomAuthorizationEvent.KIND to KindName("Blossom Auth", "B7"), + BroadcastRelayListEvent.KIND to KindName("Broadcast Relays", "51"), + BookmarkListEvent.KIND to KindName("Bookmark List", "51"), + OldBookmarkListEvent.KIND to KindName("Old Bookmark List", "51"), + CalendarDateSlotEvent.KIND to KindName("Day Appointment", "52"), + CalendarEvent.KIND to KindName("Calendar", "52"), + CalendarTimeSlotEvent.KIND to KindName("Appointment", "52"), + CalendarRSVPEvent.KIND to KindName("Appt RSVP", "52"), + ChessGameEvent.KIND to KindName("Chess Games", "64"), + JesterEvent.KIND to KindName("Chess Auth", "64"), + RelayFeedsListEvent.KIND to KindName("Favorite Relays", "51"), + LiveChessGameChallengeEvent.KIND to KindName("Chess Challenges", "64"), + LiveChessGameAcceptEvent.KIND to KindName("Chess Game Accept", "64"), + LiveChessMoveEvent.KIND to KindName("Chess Move", "64"), + LiveChessGameEndEvent.KIND to KindName("Chess Game End", "64"), + LiveChessDrawOfferEvent.KIND to KindName("Chess Draw Offer", "64"), + ChannelCreateEvent.KIND to KindName("Channel Definition", "28"), + ChannelHideMessageEvent.KIND to KindName("Channel Hide Msg", "28"), + ChannelListEvent.KIND to KindName("Channel List", "28"), + ChannelMessageEvent.KIND to KindName("Channel Message", "28"), + ChannelMetadataEvent.KIND to KindName("Channel Metadata", "28"), + ChannelMuteUserEvent.KIND to KindName("Channel Mute User", "28"), + ChatMessageEncryptedFileHeaderEvent.KIND to KindName("DM File", "17"), + ChatMessageEvent.KIND to KindName("DM Message", "17"), + ChatMessageRelayListEvent.KIND to KindName("DM Relays", "17"), + ClassifiedsEvent.KIND to KindName("Classifieds", "99"), + CommentEvent.KIND to KindName("Comments", "22"), + CommunityDefinitionEvent.KIND to KindName("Community Def", "72"), + CommunityListEvent.KIND to KindName("Community List", "72"), + CommunityPostApprovalEvent.KIND to KindName("Community Post", "72"), + ContactListEvent.KIND to KindName("Follow List", "02"), + DeletionEvent.KIND to KindName("Deletions", "09"), + DraftWrapEvent.KIND to KindName("Drafts", "37"), + EmojiPackEvent.KIND to KindName("Emoji Packs", "30"), + EmojiPackSelectionEvent.KIND to KindName("Emoji Pack List", "30"), + EphemeralChatEvent.KIND to KindName("Ephemeral Chat", null), + EphemeralChatListEvent.KIND to KindName("Ephemeral Chatrooms", null), + FileHeaderEvent.KIND to KindName("File Headers", "94"), + ProfileGalleryEntryEvent.KIND to KindName("Profile Gallery", null), + FileServersEvent.KIND to KindName("File Servers", "96"), + FileStorageEvent.KIND to KindName("Blob Data", null), + FileStorageHeaderEvent.KIND to KindName("Blob Headers", null), + FhirResourceEvent.KIND to KindName("Medical Data", null), + FollowListEvent.KIND to KindName("Follow Packs", "51"), + GenericRepostEvent.KIND to KindName("Reposts (16)", "18"), + GeohashListEvent.KIND to KindName("Geohash Follows", "51"), + GiftWrapEvent.KIND to KindName("GiftWraps", "59"), + EphemeralGiftWrapEvent.KIND to KindName("GiftWraps", "59"), + GitIssueEvent.KIND to KindName("Git Issue", "34"), + GitPatchEvent.KIND to KindName("Git Patch", "34"), + GitRepositoryEvent.KIND to KindName("Git Repo", "34"), + GitReplyEvent.KIND to KindName("Git Reply", "34"), + GoalEvent.KIND to KindName("Zap Goals", "75"), + HashtagListEvent.KIND to KindName("Hashtag Follows", "51"), + HighlightEvent.KIND to KindName("Highlights", "84"), + HTTPAuthorizationEvent.KIND to KindName("Http Auth", "98"), + IndexerRelayListEvent.KIND to KindName("Index Relay List", "51"), + InteractiveStoryPrologueEvent.KIND to KindName("Adventure Prologue", null), + InteractiveStorySceneEvent.KIND to KindName("Adventure Scene", null), + InteractiveStoryReadingStateEvent.KIND to KindName("Adventure Reading", null), + LabeledBookmarkListEvent.KIND to KindName("Named Bookmarks", "51"), + LiveActivitiesChatMessageEvent.KIND to KindName("Live Chats", "53"), + LiveActivitiesEvent.KIND to KindName("Live Streams", "53"), + LnZapEvent.KIND to KindName("Zaps", "57"), + LnZapPaymentRequestEvent.KIND to KindName("NWC Request", "47"), + LnZapPaymentResponseEvent.KIND to KindName("NWC Response", "47"), + LnZapPrivateEvent.KIND to KindName("Private Zaps", "57"), + LnZapRequestEvent.KIND to KindName("Zap Req", "57"), + LongTextNoteEvent.KIND to KindName("Blogs", "23"), + MeetingRoomEvent.KIND to KindName("Meeting Room", "53"), + MeetingRoomPresenceEvent.KIND to KindName("Room Presence", "53"), + MeetingSpaceEvent.KIND to KindName("Meeting Space", "53"), + MetadataEvent.KIND to KindName("Profile", "01"), + MuteListEvent.KIND to KindName("Mute List", "51"), + NNSEvent.KIND to KindName("NNS", null), + NipTextEvent.KIND to KindName("NIP", null), + NostrConnectEvent.KIND to KindName("Nostr Connect", "46"), + NIP90StatusEvent.KIND to KindName("DVM Status", "90"), + NIP90ContentDiscoveryRequestEvent.KIND to KindName("DVM Content Req", "90"), + NIP90ContentDiscoveryResponseEvent.KIND to KindName("DVM Content Resp", "90"), + NIP90UserDiscoveryRequestEvent.KIND to KindName("DVM User Req", "90"), + NIP90UserDiscoveryResponseEvent.KIND to KindName("DVM User Resp", "90"), + OtsEvent.KIND to KindName("OTS", "03"), + PaymentTargetsEvent.KIND to KindName("PayTo", null), + PeopleListEvent.KIND to KindName("People Lists", "51"), + ProfileBadgesEvent.KIND to KindName("Profile Badges", "58"), + PictureEvent.KIND to KindName("Pictures", "68"), + WorkoutRecordEvent.KIND to KindName("Workouts", null), + PinListEvent.KIND to KindName("Pins", "51"), + ZapPollEvent.KIND to KindName("Zap Poll", null), + PollEvent.KIND to KindName("Poll", "88"), + PollResponseEvent.KIND to KindName("Poll Response", "88"), + PrivateDmEvent.KIND to KindName("NIP-04 DMs", "04"), + PrivateOutboxRelayListEvent.KIND to KindName("Private Relays", "37"), + ProxyRelayListEvent.KIND to KindName("Proxy Relays", "51"), + PublicMessageEvent.KIND to KindName("Public Message", "A4"), + ReactionEvent.KIND to KindName("Reactions", "25"), + ContactCardEvent.KIND to KindName("Contact Card", "85"), + RelayAuthEvent.KIND to KindName("Relay Auth", "42"), + RelayDiscoveryEvent.KIND to KindName("Relay Discovery", "66"), + RelayMonitorEvent.KIND to KindName("Relay Monitor Announcement", "66"), + RelaySetEvent.KIND to KindName("Relay Set", "51"), + ReportEvent.KIND to KindName("Reports", "56"), + RepostEvent.KIND to KindName("Reposts", "18"), + RequestToVanishEvent.KIND to KindName("User Delete", "62"), + SealedRumorEvent.KIND to KindName("Seals", "59"), + SearchRelayListEvent.KIND to KindName("Search Relays", "50"), + StatusEvent.KIND to KindName("User Status", "38"), + TextNoteEvent.KIND to KindName("Notes", "10"), + TextNoteModificationEvent.KIND to KindName("Edits", null), + TorrentEvent.KIND to KindName("Torrents", "35"), + TorrentCommentEvent.KIND to KindName("Torrent Comments", "35"), + TrustedRelayListEvent.KIND to KindName("Trusted Relays", "51"), + TrustProviderListEvent.KIND to KindName("Trusted Providers", "85"), + VideoHorizontalEvent.KIND to KindName("Video (Repl)", "71"), + VideoVerticalEvent.KIND to KindName("Shorts (Repl)", "71"), + VideoNormalEvent.KIND to KindName("Video", "71"), + VideoShortEvent.KIND to KindName("Shorts", "71"), + VoiceEvent.KIND to KindName("Voice Msg", "A0"), + VoiceReplyEvent.KIND to KindName("Voice Reply", "A0"), + WakeUpEvent.KIND to KindName("WakeUp", null), + WebBookmarkEvent.KIND to KindName("Web Bookmark", "B0"), + WikiNoteEvent.KIND to KindName("Wiki", "54"), + ) + + fun infoFor(kind: Int): KindName? = names[kind] + + fun nameFor(kind: Int): String? = names[kind]?.name + + fun nipFor(kind: Int): String? = names[kind]?.nip + + /** Case-insensitive substring search by label; returns matching (kind, info) sorted by kind. */ + fun search(query: String): List> { + val q = query.trim().lowercase() + if (q.isEmpty()) return emptyList() + return names.entries + .filter { + it.value.name + .lowercase() + .contains(q) + }.map { it.key to it.value } + .sortedBy { it.first } + } +}