Skip to content

Fix CLI output normalization and wrong Nostr kinds (MCP parity)#612

Merged
wpfleger96 merged 9 commits into
mainfrom
cli-agent-parity
May 22, 2026
Merged

Fix CLI output normalization and wrong Nostr kinds (MCP parity)#612
wpfleger96 merged 9 commits into
mainfrom
cli-agent-parity

Conversation

@wpfleger96
Copy link
Copy Markdown
Collaborator

@wpfleger96 wpfleger96 commented May 18, 2026

A full audit of the 54 CLI subcommands against their MCP equivalents revealed functional bugs throughout: channels list queried kind:39002 (member-lists) instead of kind:39000 (NIP-29 channel metadata), returning entirely wrong data; dms list queried kind:41010 instead of 41001; workflow runs queried the trigger kind instead of the run-lifecycle kinds. On top of that, every read command returned raw Nostr event JSON including sig fields agents never need, all write commands returned raw relay responses with no consistent shape, and create commands silently discarded the UUIDs they generated.

The MCP server already handles all of this correctly -- this brings the CLI to parity so agents can migrate without workarounds.

Kind number fixes

  • channels list/get: 4139000 (kind 41 is NIP-01 channel metadata, unused by Sprout; kind 39000 is NIP-29 group metadata, which is what the relay actually emits)
  • channels members: uses #d tag filter (was #h)
  • dms list: 4101041001
  • workflow runs: single kind → [46001, 46002, 46003] (doc-commented as returning empty -- relay stores runs in DB, not as events)

Output normalization

  • All read commands return structured, sorted, sig-stripped JSON
  • All ~25 write commands return {event_id, accepted, message}
  • Create commands inject the generated entity ID (channel_id, dm_id, workflow_id) into the response
  • canvas get returns the markdown content string directly, or null if not found
  • reactions get aggregates by emoji with {emoji, count, pubkeys}, sorted deterministically; empty content defaults to "+" per NIP-25
  • users get always returns an array (was returning a bare object for single results)
  • workflows get returns normalized {workflow_id, content, created_at, pubkey}
  • Thread filter restricted to message kinds [9, 40002, 40003, 40008, 45003] (was including reactions and system events)

Shared helpers in client.rs

  • query_multi() sends multiple ORed filters in a single HTTP call; query() now delegates to it
  • normalize_events(&[Value]) strips sig and sorts by created_at
  • normalize_write_response() extracts {event_id, accepted, message} from relay responses
  • extract_d_tag(), extract_p_tags() for consistent tag parsing (p-tag role is at index 3, not 2 -- index 2 is always an empty relay URL placeholder)
  • extract_tag_value(event, key) generic tag value extractor, used by extract_channel_metadata
  • print_create_response() reduces repeated blocks across channel/DM/workflow commands

Channel metadata fix

extract_channel_metadata was reading name and about from the event's content field (parsed as JSON). The relay emits kind:39000 events with content: "" and stores metadata in NIP-29 tags (["name", "..."], ["about", "..."]). All channel names were empty strings. Fixed to read from tags via extract_tag_value.

New flags

  • channels list --limit <N>: limits results (default 500). Previously hardcoded.
  • --format json|compact: global flag on all commands. compact reduces read output for agent scanning: channels → {channel_id, name}, messages → {id, content, created_at}, users → {pubkey, display_name}, feed → {id, content, created_at}. Write commands unaffected.

Bugs found during E2E testing

  • messages search sent no kinds filter, triggering the relay's p-gate with 403. Now sends kinds: [9, 40002, 45001, 45003] matching the MCP server's search implementation.
  • 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.
  • --kind flag on messages send was accepted but ignored (dead code). Now routes to the correct SDK builders: build_forum_post for kind 45001, build_forum_comment for kind 45003. This enables forum voting via CLI.

Safety fixes

  • cmd_open_dm validates pubkeys before calling Tag::parse() (was panicking on malformed input)
  • DM open parses relay-assigned channel_id from the response message field instead of fabricating a local UUID
  • 403 errors hint when SPROUT_AUTH_TAG may be stale

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.
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.
Reformat long method chains and closure expressions that rustfmt
flagged during CI. No logic changes.
channels/workflows/DMs create responses now return entity IDs
(channel_id, workflow_id, dm_id) instead of bare id. All write
commands return {event_id, accepted, message}. canvas get outputs
raw markdown, not JSON. users get always returns an array.

Add expected-output comments throughout so testers can verify
correctness without reading the source.
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.
@wpfleger96 wpfleger96 marked this pull request as ready for review May 21, 2026 15:49
@wpfleger96 wpfleger96 requested a review from a team as a code owner May 21, 2026 15:49
wpfleger96 added a commit that referenced this pull request May 21, 2026
Brings the agent skill file to parity with all 62 CLI subcommands
across 13 command groups after PR #612's output normalization and bug
fixes. Adds 5 missing command groups (social, repos, upload, mem,
pack), 5 missing subcommands in existing groups (channels update/
add-member/remove-member, workflows update/delete), missing flags
(--file, --mention, --kinds, --depth-limit, --limit), fixes output
contract inaccuracies (channels get pubkey field, workflows list
pubkey, exit code 5, feed description), and restructures the Quick
Reference table by output pattern for scannability.
Three changes from agent E2E testing feedback:

1. Channel names were always empty — extract_channel_metadata read
   name/description from event content (always "") instead of NIP-29
   tags where the relay stores them. Added extract_tag_value helper
   and rewrote to use extract_tag_value(e, "name") and
   extract_tag_value(e, "about").

2. channels list had no --limit flag (hardcoded 500). Added --limit
   with default 500, applied to both member and non-member query paths.

3. Added global --format flag (json|compact). Compact mode reduces
   read output for agent scanning: channels → {channel_id, name},
   messages → {id, content, created_at}, users → {pubkey, display_name},
   feed → {id, content, created_at}. Write commands unaffected.
wpfleger96 added a commit that referenced this pull request May 21, 2026
Documents the new --limit flag on channels list and the global
--format json|compact flag added in PR #612. Compact mode reduces
read output: channels → {channel_id, name}, messages → {id, content,
created_at}, users → {pubkey, display_name}.
Both flags were copied from the MCP server's param structs during parity
work but neither the MCP nor the CLI ever implemented them. Silently
accepting and ignoring flags is worse than a clean "unknown flag" error.
@wpfleger96 wpfleger96 merged commit 6dcb04b into main May 22, 2026
15 checks passed
@wpfleger96 wpfleger96 deleted the cli-agent-parity branch May 22, 2026 14:32
wpfleger96 added a commit that referenced this pull request May 22, 2026
Brings the agent skill file to parity with all 62 CLI subcommands
across 13 command groups after PR #612's output normalization and bug
fixes. Adds 5 missing command groups (social, repos, upload, mem,
pack), 5 missing subcommands in existing groups (channels update/
add-member/remove-member, workflows update/delete), missing flags
(--file, --mention, --kinds, --depth-limit, --limit), fixes output
contract inaccuracies (channels get pubkey field, workflows list
pubkey, exit code 5, feed description), and restructures the Quick
Reference table by output pattern for scannability.
wpfleger96 added a commit that referenced this pull request May 22, 2026
Documents the new --limit flag on channels list and the global
--format json|compact flag added in PR #612. Compact mode reduces
read output: channels → {channel_id, name}, messages → {id, content,
created_at}, users → {pubkey, display_name}.
wpfleger96 added a commit that referenced this pull request May 22, 2026
Brings the agent skill file to parity with all 62 CLI subcommands
across 13 command groups after PR #612's output normalization and bug
fixes. Adds 5 missing command groups (social, repos, upload, mem,
pack), 5 missing subcommands in existing groups (channels update/
add-member/remove-member, workflows update/delete), missing flags
(--file, --mention, --kinds, --depth-limit, --limit), fixes output
contract inaccuracies (channels get pubkey field, workflows list
pubkey, exit code 5, feed description), and restructures the Quick
Reference table by output pattern for scannability.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
wpfleger96 added a commit that referenced this pull request May 22, 2026
Documents the new --limit flag on channels list and the global
--format json|compact flag added in PR #612. Compact mode reduces
read output: channels → {channel_id, name}, messages → {id, content,
created_at}, users → {pubkey, display_name}.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
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.

1 participant