Skip to content

feat: add --events flag for machine-readable NDJSON lifecycle events on stderr#85

Open
nextbysam wants to merge 5 commits intosteipete:mainfrom
nextbysam:feat/events-flag
Open

feat: add --events flag for machine-readable NDJSON lifecycle events on stderr#85
nextbysam wants to merge 5 commits intosteipete:mainfrom
nextbysam:feat/events-flag

Conversation

@nextbysam
Copy link
Copy Markdown

Problem

wacli has --json for structured stdout output, but the auth/sync lifecycle — the most important part for programmatic consumers — only emits human-readable strings to stderr:

Starting authentication…
Connected.
Processing history sync (42 conversations)...
Synced 75 messages...
Idle for 30s, exiting.

Anyone building an application on top of wacli is forced to regex-parse English sentences to track what's happening. This is fragile: one wording change upstream silently breaks the consumer, and useful structured data (the Progress float from whatsmeow, conversation counts) gets thrown away in the string formatting.

This is a blind spot in an otherwise well-designed scripting interface — --json clearly signals intent for programmatic use, and issue #83 (JSON-RPC daemon) and issue #65 (Unix socket IPC) reinforce that vision.

Solution

Add a --events persistent flag that emits newline-delimited JSON to stderr during auth and sync, one event per line:

{"event":"auth_starting","ts":1709042400000}
{"event":"qr_code","data":{"code":"2@abc123..."},"ts":1709042400001}
{"event":"connected","ts":1709042400002}
{"event":"history_sync","data":{"conversations":42},"ts":1709042400003}
{"event":"progress","data":{"messages_synced":75},"ts":1709042400004}
{"event":"idle_exit","data":{"idle_duration":"30s","messages_synced":150},"ts":1709042400005}

Without --events, behavior is byte-for-byte identical to before. This is purely additive.

Events emitted

Event When Data fields
auth_starting wacli auth begins
qr_code QR ready to scan code (raw QR string, not terminal art)
connected whatsmeow events.Connected
disconnected whatsmeow events.Disconnected
history_sync whatsmeow events.HistorySync conversations (count)
progress every 25 messages stored messages_synced (count)
reconnecting backoff reconnect starts
stopping graceful shutdown (SIGINT/SIGTERM) messages_synced
idle_exit idle timer fires idle_duration, messages_synced

The qr_code event is particularly useful: instead of detecting Unicode block characters in a stream, consumers get the raw QR payload string and can render it however they want (web canvas, mobile SDK, PNG file).

Changes

  • internal/out/events.goEventWriter: thread-safe NDJSON emitter, no-op when disabled
  • cmd/wacli/root.go--events persistent flag wired to App.events
  • internal/app/app.goEvents *out.EventWriter in Options; App.Events() accessor
  • internal/app/sync.go — every fmt.Fprintf(os.Stderr, ...) dual-pathed: JSON event or existing text
  • cmd/wacli/auth.goauth_starting and qr_code events
  • internal/out/events_test.go — unit tests for EventWriter
  • internal/app/sync_events_test.go — integration tests using the existing fakeWA harness

Usage

# Machine-readable (new)
wacli auth --json --events --idle-exit 5s 2>events.ndjson

# Human-readable (unchanged)
wacli auth --json --idle-exit 5s

Combine with jq for easy scripting:

wacli sync --events 2>&1 | grep '^{' | jq 'select(.event == "progress") | .data.messages_synced'

🤖 Generated with Claude Code

nextbysam and others added 5 commits February 26, 2026 23:34
Add internal/out/EventWriter — a thread-safe NDJSON event emitter that
writes structured {"event","data","ts"} lines to a writer. When disabled
(default), Emit() is a no-op.

Wire it through: --events persistent flag on root → rootFlags.asEvents →
app.Options.Events → App.events field. Exposed via App.Events() accessor.

No behavioral change yet — all stderr output still uses fmt.Fprintf.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace every fmt.Fprintf(os.Stderr, ...) in Sync() with a dual-path:
when --events is set, emit a typed JSON event; otherwise print the
existing human-readable string unchanged.

Events covered: connected, disconnected, history_sync (with conversation
count), progress (with messages_synced count), reconnecting, stopping,
idle_exit (with idle_duration and messages_synced).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the two stderr prints in auth.go with event-aware paths:
- "Starting authentication…" → {"event":"auth_starting"}
- QR terminal art → {"event":"qr_code","data":{"code":"<raw-string>"}}

The raw QR payload lets consumers render their own QR (web canvas,
mobile SDK, image file) instead of being stuck with ASCII block art.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- out/events_test.go: verify NDJSON output, disabled no-op, nil data omitted
- app/sync_events_test.go: use fakeWA to drive Sync() with events enabled,
  assert connected/history_sync/progress/stopping events are emitted,
  assert idle_exit fires on bootstrap mode, assert no output when disabled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When --events is enabled, each incoming WhatsApp message now emits a
new_message NDJSON event to stderr with the full message payload (id,
chat, sender, text, media info, reactions, replies). This enables
programmatic consumers to react to individual messages in real-time
rather than only tracking aggregate sync progress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
achhabra2 added a commit to achhabra2/wacli that referenced this pull request Apr 8, 2026
Update whatsmeow to v0.0.0-20260327181659-02ec817e7cf4 to fix
WhatsApp 405 client-outdated error (issue steipete#106).

Also fix scoping of pm variable in sync event handler after
merging PR steipete#80 (revoke/edit) with PR steipete#85 (events).
@steipete steipete requested a review from dinakars777 as a code owner April 21, 2026 03:54
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