diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 795b026..2db7e96 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 1.31.6 (2026-05-04) — hex-prefix sends actually deliver now + +`claudemesh send <16-hex-prefix> "..."` would acknowledge with `sent +to (daemon)` but the recipient never received the message. +The broker's pre-flight matched `peer.pubkey === targetSpec` and the +drain query matched `target_spec = ` — both exact-equal +checks, so a 16-hex prefix queued successfully but no recipient drain +ever fetched the row. Sender saw "sent", recipient saw nothing. + +Fix: the CLI now resolves any hex prefix (4-63 chars, not full 64) to +the full pubkey via the daemon's peer list before submitting to the +broker. Three outcomes: + +- **Unique match:** prefix is canonicalized to the full 64-char + pubkey; the rest of the send pipeline is unchanged. +- **No match:** clear error `No peer matches hex prefix "X"` with the + list of online peers' display names. +- **Multiple matches:** clear error listing the candidates and a hint + to lengthen the prefix. + +The 16-hex prefix shown in `peer list` rows is now safe to copy-paste +into `claudemesh send` — what worked in the docs finally works in the +CLI. + ## 1.31.5 (2026-05-04) — JSON peer list lifts profile.role to top-level + skill guides LLMs to render it Two follow-ups after 1.31.4 made the human renderer show role/groups diff --git a/apps/cli/package.json b/apps/cli/package.json index d51f484..2629e20 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "1.31.5", + "version": "1.31.6", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli/src/commands/send.ts b/apps/cli/src/commands/send.ts index 4a98204..9dadb28 100644 --- a/apps/cli/src/commands/send.ts +++ b/apps/cli/src/commands/send.ts @@ -47,6 +47,59 @@ export async function runSend(flags: SendFlags, to: string, message: string): Pr flags.mesh ?? (config.meshes.length === 1 ? config.meshes[0]!.slug : null); + // 1.31.6: hex-prefix resolution. If `to` looks like hex but isn't a + // full 64-char pubkey, resolve it against the peer list and replace + // it with the matching full pubkey. The broker stores `targetSpec` + // verbatim and the drain query at apps/broker/src/broker.ts:2408 + // matches only on full pubkeys, so a 16-hex prefix would queue + // successfully but never fetch — sender saw "sent", recipient saw + // nothing. Resolving here makes the CLI's prefix UX work end-to-end + // and surfaces ambiguous / unmatched prefixes with a clear error + // instead of a silent drop. + if ( + !to.startsWith("@") && + !to.startsWith("#") && + to !== "*" && + /^[0-9a-f]{4,63}$/i.test(to) + ) { + try { + const { tryListPeersViaDaemon } = await import("~/services/bridge/daemon-route.js"); + const peers = (await tryListPeersViaDaemon()) ?? []; + const lower = to.toLowerCase(); + const matches = peers.filter((p) => { + const pk = (p as { pubkey?: string }).pubkey ?? ""; + const mpk = (p as { memberPubkey?: string }).memberPubkey ?? ""; + return pk.toLowerCase().startsWith(lower) || mpk.toLowerCase().startsWith(lower); + }); + if (matches.length === 0) { + render.err(`No peer matches hex prefix "${to}".`); + const names = peers + .map((p) => (p as { displayName?: string }).displayName) + .filter(Boolean) + .join(", "); + if (names) render.hint(`online: ${names}`); + process.exit(1); + } + if (matches.length > 1) { + const candidates = matches + .map((p) => { + const pk = (p as { pubkey?: string }).pubkey ?? ""; + const dn = (p as { displayName?: string }).displayName ?? "?"; + return `${dn} ${pk.slice(0, 16)}…`; + }) + .join(", "); + render.err(`Ambiguous hex prefix "${to}" — matches ${matches.length} peers.`); + render.hint(`candidates: ${candidates}`); + render.hint("Use a longer prefix or paste the full 64-char pubkey."); + process.exit(1); + } + to = (matches[0] as { pubkey?: string }).pubkey ?? to; + } catch { + // Daemon unreachable — fall through; cold path will try a name + // lookup and surface its own error if that also fails. + } + } + // Self-DM safety check: if target is a 64-char hex that matches the // caller's own member pubkey (or any of the caller's session/member // entries), refuse without --self. Catches the common pasted-from-