From bfc62b9a72c6a65f7e2c3e95555d7bbec747daee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:51:12 +0100 Subject: [PATCH] fix(cli): display system push messages without decryption System messages (watch_triggered, mcp_deployed, peer_joined, etc.) have senderPubkey='system' with empty ciphertext. The push handler now formats them as readable plaintext instead of failing to decrypt. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/cli/package.json | 2 +- apps/cli/src/ws/client.ts | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index e9c951e..0249276 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "0.8.7", + "version": "0.8.8", "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.", "keywords": [ "claude-code", diff --git a/apps/cli/src/ws/client.ts b/apps/cli/src/ws/client.ts index bc35539..a86bbe5 100644 --- a/apps/cli/src/ws/client.ts +++ b/apps/cli/src/ws/client.ts @@ -1612,11 +1612,37 @@ export class BrokerClient { // Decrypt asynchronously, then enqueue. Ordering within the // buffer is preserved by awaiting before push. void (async (): Promise => { - const kind: InboundPush["kind"] = senderPubkey - ? "direct" - : "unknown"; + // System messages (peer_joined, watch_triggered, mcp_deployed, etc.) + // have senderPubkey="system" with empty nonce/ciphertext — skip decryption. + const isSystem = msg.subtype === "system" || senderPubkey === "system"; + const kind: InboundPush["kind"] = isSystem + ? "broadcast" + : senderPubkey + ? "direct" + : "unknown"; let plaintext: string | null = null; - if (senderPubkey && nonce && ciphertext) { + if (isSystem) { + // Format system event as readable plaintext + const event = msg.event ? String(msg.event) : "system"; + const data = msg.eventData as Record | undefined; + if (event === "watch_triggered" && data) { + plaintext = `[WATCH] ${data.label ?? data.url}: ${data.oldValue} → ${data.newValue}`; + } else if (event === "mcp_deployed" && data) { + plaintext = `[SERVICE] "${data.name}" deployed (${data.tool_count} tools) by ${data.deployed_by}`; + } else if (event === "mcp_undeployed" && data) { + plaintext = `[SERVICE] "${data.name}" undeployed by ${data.by}`; + } else if (event === "mcp_scope_changed" && data) { + plaintext = `[SERVICE] "${data.name}" scope changed to ${JSON.stringify(data.scope)} by ${data.by}`; + } else if (event === "peer_joined" && data) { + plaintext = `[MESH] ${data.displayName ?? "peer"} joined`; + } else if (event === "peer_left" && data) { + plaintext = `[MESH] ${data.displayName ?? "peer"} left`; + } else if (data) { + plaintext = `[${event}] ${JSON.stringify(data)}`; + } else { + plaintext = `[${event}]`; + } + } else if (senderPubkey && nonce && ciphertext) { plaintext = await decryptDirect( { nonce, ciphertext }, senderPubkey,