Three bugs found during E2E testing against a local relay:
1. `messages search` sent no `kinds` filter, triggering the relay's
p-gate (403). Now sends `kinds: [9, 40002, 45001, 45003]` matching
the MCP server's search implementation.
2. `channels list --visibility open` always returned `[]` because the
filter checked for `["visibility", "open"]` tags, but the relay
emits `["public"]`/`["private"]` per NIP-29. Rewritten to match
the relay's actual tag convention.
3. `--kind` flag on `messages send` was dead code (`#[allow(dead_code)]`).
Now routes to `build_forum_post` (kind 45001) and `build_forum_comment`
(kind 45003) SDK builders, enabling forum voting via CLI.
TESTING.md updated with relay behavior notes for channels leave
(last-owner guard), workflows trigger (DB indexing delay), workflows
update (missing --channel flag), and corrected expected error messages.
Crossfire review (Codex + Gemini) surfaced 15 issues the Claude agents
missed. Critical: p-tag role read from wrong index (empty relay_url at
index 2 vs actual role at index 3), cmd_open_dm panics on malformed
pubkey, users get returns inconsistent types, channels query wrong Nostr
kind (41 vs 39000). Also: DM open now parses relay-assigned channel_id,
canvas not-found returns null, thread filter restricts to message kinds,
query() delegates to query_multi(), normalize_events takes &[Value],
extract_channel_metadata and print_create_response DRY helpers,
workflows get normalized, empty reaction content defaults to "+",
reactions sorted deterministically.
Comprehensive audit revealed the CLI queried wrong Nostr kinds (channels
list used kind:39002 instead of 41, dms list used 41010 instead of 41001,
workflow runs used 46020 instead of [46001,46002,46003]), used wrong tag
filters (#h instead of #d), discarded generated IDs from create commands,
and returned raw Nostr events with sig fields instead of structured data.
Adds query_multi() for compound queries, normalize_write_response() for
consistent {event_id, accepted, message} write output, normalize_events()
for stripping sig and reshaping reads, and extract_d_tag/extract_p_tags
helpers. All 25+ write handlers and 10+ read handlers now produce
structured output matching the MCP server.