Expand description
Bot Interface — manifests + slash commands (Phase 1 of the bot-UI layer).
Transport-agnostic by design: everything here is CONTENT-level (structured tags on ordinary chat rumors, plus one plain replaceable discovery event), so the same commands work in NIP-17 DMs and Concord v1/v2 channels — the envelope is whatever the conversation already uses.
Two pieces:
- Manifest (
BotManifest, kindKIND_BOT_MANIFEST): a replaceable event signed by the bot’s key describing every command with typed args. Clients fetch it by pubkey to render a/picker with argument hints and validate input before anything hits the wire. - Invocation (
parse_command_text): a command is a NORMAL chat message whosecontentIS the invocation (“/price btc”) — no extra structure on the wire. The manifest’s ordering rules (required args first, a greedy String only in trailing position) make the text deterministically parseable, so the bot recovers exact typed arguments from content alone, and EVERY existing client is already a fully capable command sender.
The kind number is provisional pending upstream registry coordination.
Structs§
- ArgSpec
- One declared argument of a command.
- BotManifest
- The bot’s full published interface. Unknown fields are ignored on read
(forward compatibility);
vgates breaking schema changes. - Chat
BotCommands - One bot’s published commands, shaped for a client’s
/picker. The client resolves the bot’s display name/avatar from its own profile cache. - Chat
Commands Snapshot - What the composer’s
/picker renders, instantly answerable from local state.fresh: falsemeans a background refetch was spawned — achat_commands_updatedevent follows with the converged list. - Command
Spec - One command a bot answers.
- Parsed
Command - A command invocation recovered from a message’s content. Values are raw
strings in manifest order; type them via
typed_args.
Enums§
Constants§
- DISCOVERY_
RELAYS - Public relays that index replaceable events network-wide — the reliable discovery path for bot manifests. Read side: always queried ALONGSIDE a chat’s own relays, so a manifest resolves even when a community relay is unreachable or drops stranger events (Ditto does). Write side: the SDK publishes every bot’s manifest here for the same reason.
- KIND_
BOT_ MANIFEST - Replaceable bot-interface manifest (outside any wrap): one authoritative command catalog per bot pubkey, the same shape as a profile or relay list.
- MAX_
ARGS - MAX_
ARG_ VALUE_ LEN - A single argument value on the wire (tag or text) is clamped before typing.
- MAX_
BOT_ TAGS - Recipient tags honored per message (routing metadata must stay cheap).
- MAX_
CHOICES - MAX_
COMMANDS - Bounds (validated on BOTH build and parse — a foreign manifest is untrusted input and must never cost unbounded memory or render work).
- MAX_
DESCRIPTION_ LEN - MAX_
MANIFEST_ BYTES - MAX_
NAME_ LEN - TAG_BOT
- Optional recipient tag a picker client attaches to an invocation:
["bot", <bot pubkey hex>]. Addressing is the ONE piece of a command not derivable from content — two bots can share a command name — so it rides a tag while the invocation stays plain text. Semantics: tagged → only the named bot(s) execute (others skip even on a manifest match); untagged → broadcast, any matching bot may answer (the legacy-client path — bots never REQUIRE the tag). Deliberately NOTp: chat rumors already carrypfor DM recipients and reply parents, and a skip-unless-me rule keyed onpwould silently swallow a command sent as a reply to a human. The tag is routing, not authority — bots authorize by SENDER, never by tag.
Functions§
- addressed_
bots - Extract the addressed bots from a rumor’s tags as npubs (deduped, capped, invalid values skipped). Empty = untagged/broadcast.
- assemble_
from_ store - Assemble picker entries from the persistent manifest store for a set of bot pubkeys (hex, pre-sorted order preserved). Bots with no stored manifest are absent; a stored row that no longer parses/validates is skipped.
- bot_tag
- Build the recipient tag a picker attaches:
["bot", <hex>]. - command_
text - The canonical content for an invocation a picker client builds — exactly
what a human would have typed (“/name value…”). Values containing spaces
or quotes are quoted with
\"escapes so the text re-parses to the same arguments. - commands_
fresh truewhile the last completed refresh for this chat is within TTL AND covered exactlybot_hexes.- fetch_
manifest - Fetch + validate a bot’s manifest by pubkey from the given relays. Returns
the newest valid one, or
Nonewhen the bot has published no interface. - fetch_
manifests - Batch-fetch validated manifests for a set of authors over specific relays —
the picker’s ONE REQ (all bots of a room in a single query). Transport-
generic so community relays are queried directly and tests run offline.
Per author: the NEWEST event wins, then must validate (a bot that breaks
its own manifest has no usable interface — parity with
fetch_manifest); authors the relay volunteers beyond the asked set are dropped, as is anything failing signature verification. Returns each manifest with its event timestamp (the store’s newest-wins key). - parse_
command_ text - Parse “/name arg arg…” content against the manifest’s arg order — THE
invocation parse. Returns
Nonewhen the text isn’t a known command (it is then ordinary chat). - publish_
manifest - Publish
manifestas the signed replaceable event over the given relays (targeted send — the caller decides the reach: login relays, communities, indexers). Returns how many relays accepted it. - spawn_
commands_ refresh - Background half of the picker flow: ONE REQ for every bot’s manifest (5s unification window), persist newer editions, mark the chat fresh, and tell the UI to swap in the converged list. Deduped per chat; session-guarded before every write.
- typed_
args - Type-check a parsed invocation against the manifest: every provided value parses as its declared type, choices are members, required args are present. Unknown arg names are DROPPED (a newer client may know newer args; the bot’s manifest is authoritative for what it consumes).