#19398: feat(signal): support native signal-cli JSON-RPC WebSocket
channel: signal
size: M
Cluster:
Cross-Platform Fixes
## Summary
- Add auto-detection of signal-cli API mode (SSE vs JSON-RPC WebSocket) at startup
- When `/api/v1/events` (SSE, used by bbernhard/signal-cli-rest-api) is unavailable, transparently fall back to the native signal-cli JSON-RPC WebSocket at `/api/v1/rpc`
- JSON-RPC `receive` notifications are normalised into the existing `SignalSseEvent` shape, so the event handler works unchanged
## Motivation
The signal channel currently requires the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) Docker wrapper, which adds an SSE endpoint (`/api/v1/events`) on top of signal-cli. The native signal-cli daemon (`signal-cli daemon --http`) doesn't expose SSE — it uses JSON-RPC over HTTP and WebSocket.
This change eliminates the dependency on the third-party wrapper by supporting signal-cli's native protocol directly. The existing SSE path is preserved for users running the bbernhard wrapper.
## Changes
| File | What |
|------|------|
| `src/signal/client.ts` | Add `detectSignalApiMode()` and `streamSignalJsonRpc()` |
| `src/signal/sse-reconnect.ts` | Add `runSignalReceiveLoop()` (auto-detect) and `runSignalJsonRpcLoop()` |
| `src/signal/monitor.ts` | Switch from `runSignalSseLoop` → `runSignalReceiveLoop` |
| `src/signal/client.jsonrpc.test.ts` | New tests for detection and WebSocket streaming |
| `src/signal/monitor.tool-result.test-harness.ts` | Add new exports to client mock |
## Test plan
- [x] All 108 existing signal tests pass (12 test files)
- [x] 7 new tests cover `detectSignalApiMode` and `streamSignalJsonRpc`
- [ ] Manual: verify SSE mode still works with bbernhard wrapper
- [ ] Manual: verify JSON-RPC WebSocket mode works with native signal-cli daemon
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds auto-detection of signal-cli API mode at startup, transparently falling back from the bbernhard SSE wrapper (`/api/v1/events`) to the native signal-cli JSON-RPC WebSocket (`/api/v1/rpc`) when SSE is unavailable. The implementation is well-structured: detection is a one-shot probe at startup, the JSON-RPC `receive` notifications are normalised to the existing `SignalSseEvent` shape so the event handler is untouched, and the reconnect loop mirrors the SSE loop pattern. The `ws` dependency and `@types/ws` are already in `package.json`.
Key observations:
- The `String(raw)` used to parse WebSocket messages should be `rawDataToString(raw)` from `src/infra/ws.ts`, consistent with every other WebSocket handler in the codebase (e.g. `src/gateway/client.ts`). `String()` fails silently on `ArrayBuffer` and `Buffer[]` variants of `WebSocket.RawData`.
- `detectSignalApiMode` intentionally uses its own 3 s `AbortController` and does not accept the external abort signal; this is acceptable at startup but means a shutdown during detection will pause up to 3 s before the loop exits.
- The `account` parameter is correctly omitted from `runSignalJsonRpcLoop` since the native signal-cli WebSocket is not filtered by account URL param (the daemon is typically run per-account).
- Existing 108 signal tests remain unaffected: the test harness mocks `detectSignalApiMode` to return `"sse"`, preserving the old SSE-only code path in unit tests.
<h3>Confidence Score: 4/5</h3>
- Safe to merge with the rawDataToString style fix applied; no logic or security issues found.
- The core logic is correct — URL transformation, abort signal lifecycle, timeout cleanup, and payload normalisation are all sound. The only actionable finding is using String(raw) instead of the codebase's rawDataToString utility for WebSocket message parsing, which is a robustness issue rather than a present bug (signal-cli sends text frames which arrive as Buffer, where String() works correctly). Test coverage is good for the new code paths.
- src/signal/client.ts — the String(raw) → rawDataToString(raw) change in streamSignalJsonRpc's message handler.
<sub>Last reviewed commit: d91da57</sub>
<!-- greptile_other_comments_section -->
<sub>(5/5) You can turn off certain types of comments like style [here](https://app.greptile.com/review/github)!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#12984: fix(signal): fall back to JSON-RPC for health check on signal-cli 0...
by omair445 · 2026-02-10
77.6%
#10709: fix(signal): guard JSON.parse against malformed RPC responses
by Yida-Dev · 2026-02-06
73.1%
#8271: feat(signal): Add full quoted message context support
by ProofOfReach · 2026-02-03
72.0%
#4878: fix: string/type handling and API fixes (#4537, #4380, #4373, #4547...
by lailoo · 2026-01-30
70.8%
#16085: Signal: add REST API support for containerized deployments
by Hua688 · 2026-02-14
70.7%
#23775: fix(build): stabilize Windows script execution and Telegram monitor...
by ly85206559 · 2026-02-22
70.1%
#18511: fix(signal): signal:-prefixed phone numbers fail target resolution ...
by yinghaosang · 2026-02-16
69.9%
#23724: fix(security): sanitize RPC error messages in signal and imessage c...
by kevinWangSheng · 2026-02-22
69.7%
#8767: fix(signal): validate cliPath before spawning signal-cli daemon
by yubrew · 2026-02-04
69.5%
#19358: test(signal): add E.164 format acceptance for signal target detection
by saurav470 · 2026-02-17
69.4%