Skip to content

feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages - #7259

Merged
wpfleger96 merged 9 commits into
mainfrom
duncan/agent-gif-emoji
Sep 3, 2026
Merged

feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages#7259
wpfleger96 merged 9 commits into
mainfrom
duncan/agent-gif-emoji

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Sep 2, 2026

Copy link
Copy Markdown
Member

What

Two new agent-facing capabilities in buzz-cli:

1. buzz gifs command group (agent KLIPY picker path)

Agents can now search and share GIFs via the relay's authenticated KLIPY proxy without holding a provider credential.

buzz gifs search                         # trending GIFs
buzz gifs search --query "celebration"   # search GIFs
buzz gifs share --slug <slug>            # report selection to provider Recents

Output is a JSON array of GIF objects. Paste the cdn_url field directly into buzz messages send --content — sending a GIF is a plain message containing the CDN URL, no special send-path handling.

Implementation details:

  • Gates on NIP-11 supported_extensions containing buzz-gif and gif.provider == "klipy"
  • Uses relay-relative paths from the NIP-11 gif descriptor — no hardcoded paths; safe-path validation mirrors desktop/src/features/gifs/api.ts
  • New post_json_authed helper in BuzzClient handles NIP-98-signed JSON POSTs and 204 No Content responses
  • customer_id derived as SHA-256(secret_key_bytes || '\0' || relay_url_bytes)[..16] → 32 hex chars: stable, relay-scoped, not computable from public data, no storage needed
  • locale defaults to $LANG (stripped of encoding suffix) or en_US

2. NIP-30 custom emoji tags on outgoing messages

buzz messages send now automatically attaches ["emoji", shortcode, url] tags for any :shortcode: patterns in the content that resolve in the workspace palette — identical to the desktop composer behavior.

buzz messages send --channel <uuid> --content "hello :wave: everyone :tada:"
# → event carries ["emoji", "wave", "..."] and ["emoji", "tada", "..."] tags

Implementation details:

  • Hand-rolled single-pass scanner (no new dependency) implementing :([a-z0-9_-]+): case-insensitively with canonical lowercase output — mirrors desktop/src/shared/lib/customEmojiTags.ts exactly
  • Zero extra relay round-trips when content contains no : character; one query when candidates exist but none match
  • Palette fetch reuses the existing union_custom_emoji logic from commands/emoji.rs
  • build_message in buzz-sdk gains a new emoji_tags: &[Vec<String>] parameter (additive — all existing callers pass &[]); NIP-30 tag attachment lives in the SDK alongside imeta tags
  • MCP send path (buzz-acp) continues to pass &[] and is not affected; the MCP gap is noted in a comment

Files changed

Crate File Change
buzz-cli src/commands/gifs.rs New — search + share handlers, NIP-11 gating, tests
buzz-cli src/commands/mod.rs pub mod gifs
buzz-cli src/lib.rs Gifs(GifsCmd) variant, dispatch arm, inventory test update
buzz-cli src/client.rs post_json_authed helper
buzz-cli src/commands/emoji.rs scan_shortcodes + resolve_emoji_tags_for_content + tests
buzz-cli src/commands/messages.rs Emoji scan + tag injection in cmd_send_message + seam tests
buzz-cli README.md buzz gifs section + emoji-in-messages note
buzz-sdk src/builders.rs build_message gains emoji_tags param + tests
buzz-acp src/pool.rs Update build_message call site (&[])
buzz-acp src/setup_mode.rs Update build_message call site (&[])
countdown-bot src/main.rs Update build_message call site (&[])

Relates to: https://buzz.block.builderlab.xyz — buzz-team channel thread on agent GIF/emoji support

Add two agent-facing capabilities to buzz-cli:

1. buzz gifs search / gifs share — lets agents search KLIPY GIFs via the
   relay's NIP-98-authenticated proxy, gate on the relay's NIP-11 buzz-gif
   extension descriptor, and report selections. Sending a GIF is a plain
   message containing the cdn_url; no send-path changes required. Adds
   post_json_authed helper to BuzzClient for NIP-98-authed JSON POSTs.

2. NIP-30 custom emoji tags on outgoing messages — buzz messages send now
   scans final content for :shortcode: sequences (hand-rolled scanner,
   no new dep, mirrors desktop customEmojiTags.ts exactly) and attaches
   ["emoji", shortcode, url] tags from the workspace palette. The palette
   fetch is skipped when content contains no colon sequences. Extends
   build_message with an emoji_tags parameter (additive; all existing call
   sites pass &[]).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…s arg

Add missing `emoji_tags` (&[]) 7th argument to the SDK build_message
call in examples/countdown-bot/src/main.rs, fixing E0061 compile error.
This was the one remaining call site that wasn't updated when
build_message's signature gained the additive emoji_tags parameter.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Four IMPORTANT findings from Thufir's pass-1 review, all in gifs.rs:

1. cdn_url normalization: cmd_search now emits typed [{cdn_url, slug, title,
   width, height, preview_url?}] records via normalize_gif_response(), which
   ports normalizeKlipyGifs() from desktop/src/features/gifs/api.ts. Asset
   fallback order (md>hd>sm>xs for original; sm.webp>sm.gif>xs.webp>xs.gif>
   md.webp for preview) mirrors the desktop exactly. Malformed response
   envelopes (missing data.data array) now return a clear error instead of
   silently emitting [].

2. safe_relay_path validation: resolve_gif_descriptor() now validates both
   search and share paths with safe_relay_path() before either reaches
   post_json_authed(). Ports the desktop safeRelayPath contract exactly
   (api.ts:64-74): leading /, not //, no backslash, percent, query, fragment,
   or dot/dot-dot segments. Adversarial corpus from api.test.mjs is bound as
   Rust tests against the production validator.

3. customer_id anonymity: replaced SHA-256(pubkey_hex) with a domain-separated
   derivation from secret key material: SHA-256(secret_bytes || NUL || relay_url),
   truncated to 32 hex chars. Relay-scoped (different relay -> different ID),
   not computable from public data, stateless. Tests verify relay-scoping,
   key-scoping, and that the result differs from the old pubkey-hash approach.

4. Test teeth: new tests drive resolve_gif_descriptor gate logic through a
   pure typed parse_descriptor() extractor; normalize_gif_response() is tested
   directly for all normalization semantics (fallback order, type filtering,
   missing-slug skip, malformed-envelope error, empty-array ok). Real HTTP
   integration tests via axum fake server verify that cmd_search and cmd_share
   hit the relay-advertised paths with NIP-98 Authorization headers and the
   expected JSON request bodies; a gating test drives cmd_search against a
   fake relay missing buzz-gif and confirms a clear error.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
(a) Missing send-path palette seam test: add 7 tokio integration tests to
    emoji.rs that drive the production resolve_emoji_tags_for_content()
    through a real BuzzClient against an axum fake /query server.  Tests
    cover: known shortcode -> correct tag, unknown shortcode filtered out,
    dedup of repeated shortcode, first-appearance order, case-insensitive
    match -> canonical lowercase tag, no-colon content -> zero palette
    queries (short-circuit), and unknown-only content -> exactly one palette
    query with empty tag result.

(b) Descriptor test helper re-introduced non-falsifiable pattern: extract the
    synchronous NIP-11 parsing step from resolve_gif_descriptor() into a new
    pub(crate) fn parse_gif_descriptor_info(), which resolve_gif_descriptor()
    now calls after fetching the document.  Descriptor gate tests are updated
    to call the production function directly; the test-local parse_descriptor
    helper is removed entirely.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
- gifs.rs: fake NIP-11 now advertises non-default paths /x/search-alt
  and /x/share-alt, proving production reads relay-advertised paths
  rather than using hardcoded defaults

- gifs.rs: add test_client_with_tag() + search_forwards_x_auth_tag_header
  asserting the x-auth-tag header equals the exact tag JSON; add
  search_nip98_token_has_correct_u_method_and_payload_hash decoding the
  NIP-98 base64 token and asserting u, method, and payload tags

- gifs.rs: search_output_contains_cdn_url replaced by
  search_entries_returns_top_level_cdn_url, which calls the production
  search_entries() helper and asserts entries[0].cdn_url is set to the
  expected normalized URL; raw-passthrough regression now fails the test

- messages.rs: add cmd_send_message_attaches_emoji_tags_for_known_shortcodes
  and cmd_send_message_skips_palette_query_when_no_colon_in_content driving
  cmd_send_message against a fake /query + /events relay, asserting the
  submitted raw event carries emoji tags for known shortcodes and zero
  palette queries for no-colon content; removing the resolver call or
  passing &[] at :718 causes both tests to fail

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Forum sends (kind 45001/45003) previously resolved the workspace
palette before kind selection, paying the relay query and then
discarding the result because the forum SDK builders do not accept
emoji_tags.  Move the resolution into the None | Some(9) match arm
so it only runs for the kind that actually uses it.

The kind-9 happy and short-circuit paths are unchanged; the existing
cmd_send_message seam tests cover both.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three needless-borrow / manual_contains / needless_splitn warnings in
gifs.rs that the pre-push hook missed (hook scopes clippy to the
desktop-tauri manifest only):

- splitn(2, '.') → split('.') (only .next() consumed)
- .iter().any(|&e| e == REQUIRED_EXTENSION) → .contains(&REQUIRED_EXTENSION)
- hex::encode(&sk) → hex::encode(sk) in test (needless borrow for generic arg)

Behavior unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Changes requested: one P2

Reviewed head ef2adb4dc3e6be0daa07a5d31556ed2da0cf6c33 against base 0dbd036f5bff33e7ade75e7639f3218d424a6e73.

P2: Normalize and deduplicate each palette event before automatic emoji lookup

The new resolver at crates/buzz-cli/src/commands/emoji.rs:341–345 builds a case-sensitive map from the existing raw union, but the scanner lowercases every candidate. A current member kind-30030 set containing ["emoji","WAVE","https://example.com/wave.png"] is accepted by the relay and resolves on Desktop/Mobile. Sending :wave: through this new CLI path instead emits no NIP-30 tag. Recipients relying on the message tags receive literal text rather than the emoji.

The same seam can pin the wrong image: within one event, tags for wave ordered https://example.com/z.png, then https://example.com/a.png resolve to z in Desktop/Mobile (first normalized entry wins), but a in CLI (the cross-event lexical tie-break is also applied within the event).

These are valid stored inputs, not merely malformed fake responses: ingest.rs:145–158 validates the shortcode through the SDK normalizer but discards the normalized result and does not reject duplicates. Ingest passes the original signed event to replacement; storage serializes the original tags. The raw CLI parser predates this PR; its new use to pin outgoing message tags is the introduced failure.

Smallest fix / exit criteria: match customEmojiFromTags before applying the cross-event union: normalize shortcode keys, ignore missing/empty URLs, and keep the first normalized shortcode within each event. Add production-resolver regressions with uppercase palette keys, normalized duplicates and empty URLs. Keep the existing newest-set / equal-timestamp-smallest-URL rule across distinct events.

Scope and validation

Source-only review; no checkout, build, tests, or PR-code execution. The examples above are source-derived reproductions, not runtime results. Independent GIF and emoji lanes returned before this integrated review.

Reviewed CLI GIF capability discovery, authenticated request/response and relay authority boundaries, customer-ID derivation, emoji send/tag integration, and all exact-head Rust build_message references. Existing SDK callers supply the new argument; Desktop uses a separate builder. GIF sharing remains an ordinary CDN-URL message, with provider credentials staying at the relay. No additional blocker established in those paths.

Forum, ACP notices and CLI edits remain unchanged in capability; legacy tag-less edits preserve original emoji tags in the Desktop overlay. Broader pagination and unrelated hardening are not additional exit criteria.

Comment thread crates/buzz-cli/src/commands/emoji.rs
Comment thread crates/buzz-cli/src/commands/messages.rs Outdated
P2: emoji_tags_of now normalizes shortcodes to lowercase (relay stores
original case but scan_shortcodes always lowercases, so an uppercase
stored key like "WAVE" would never resolve), skips entries with empty
or missing URLs, and keeps only the first occurrence of each normalized
shortcode within one event. Mirrors desktop customEmojiFromTags.

P3: The ? on resolve_emoji_tags_for_content inside cmd_send_message is
replaced with a match that logs a stderr warning and falls back to an
empty emoji-tag list. Palette enrichment is decorative; a fetch failure
must not abort delivery of an otherwise-valid message.

Tests added:
- emoji_tags_of_normalizes_uppercase_shortcode_to_lowercase
- emoji_tags_of_skips_empty_url
- emoji_tags_of_first_occurrence_wins_within_event
- cmd_send_message_succeeds_when_palette_query_errors

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The previous fix applied only to_lowercase() to palette shortcodes.
The relay validates shortcodes via normalize_custom_emoji_shortcode but
stores the original signed tag, so a relay-valid key like "  :WAVE:  "
(whitespace + colons + uppercase) would still fail resolution against
scan_shortcodes output, which always emits lowercase undecorated strings.

Replace the bare to_lowercase() with buzz_sdk::normalize_custom_emoji_shortcode:
trim whitespace/colons, validate charset/length, lowercase. Entries that
fail normalization (malformed tags not caught at ingest) are skipped.

Regression test: resolve_tags_non_canonical_palette_key_resolves drives
production resolve_emoji_tags_for_content through a fake palette whose
entry carries the relay-valid key "  :WAVE:  " and asserts that content
containing 👋 resolves to the canonical emoji tag.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 merged commit 47d068e into main Sep 3, 2026
79 of 80 checks passed
@wpfleger96
wpfleger96 deleted the duncan/agent-gif-emoji branch September 3, 2026 00:03
wpfleger96 pushed a commit that referenced this pull request Sep 3, 2026
…-enforcement

* origin/main:
  docs(nip-fi): document Git smart-HTTP credential exemption (#7268)
  feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)
  feat(desktop): add persistent Bestie experience (#7223)
  fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188)

Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
brow added a commit that referenced this pull request Sep 3, 2026
…eway-origin

* origin/main:
  fix(desktop): derive agent availability from relay presence (#7127)
  fix(desktop): preserve spacing after multi-word mentions (#7128)
  docs(nip-fi): document Git smart-HTTP credential exemption (#7268)
  feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)
  feat(desktop): add persistent Bestie experience (#7223)
  fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188)
  docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#7254)
  fix(composer): align wrapped inline chip fragments (#7242)
  Add operation-aware database pool acquisition metrics (#7195)

Signed-off-by: Tom Brow <tomb@block.xyz>
wpfleger96 pushed a commit that referenced this pull request Sep 3, 2026
* origin/main:
  🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270)
  Show status and huddle indicators beside names (#7112)
  Add mobile voice notes (#7121)
  perf(desktop): publish mention sends before waking agents (#7154)
  fix(desktop): unify owned-agent cloud provenance markers (#7129)
  fix(desktop): derive agent availability from relay presence (#7127)
  fix(desktop): preserve spacing after multi-word mentions (#7128)
  docs(nip-fi): document Git smart-HTTP credential exemption (#7268)
  feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This was referenced Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants