feat(cli): peer list self-marking + send self-DM guard
closes the "DM looped back to my own inbox" footgun.
what was happening: peer list returns one row per presence,
including the caller's own session AND its sibling sessions.
the cli filtered out the exact-session row but left siblings
unlabeled — copying their pubkey from peer list silently
targeted your own sibling, and the message arrived in "your
own inbox" because the sender was you.
fix is two-part.
(1) peer list — tag rows whose memberPubkey matches the
caller's stable JoinedMesh.pubkey:
● displayName (this session) — the exact session running
the cli call
● displayName (your other session) — sibling session of
your own member
visually identical otherwise; just the marker.
(2) claudemesh send — refuse a target that exactly matches the
caller's own member pubkey on the mesh, with a hint pointing
at --self for the rare intentional sibling-DM case.
both changes additive — existing scripts that pass display
names or other peers' pubkeys behave identically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,10 @@ export interface PeersFlags {
|
||||
|
||||
interface PeerRecord {
|
||||
pubkey: string;
|
||||
/** Stable member pubkey (independent of session). When sender shares
|
||||
* this with a peer, they're talking to the same person across all
|
||||
* their open sessions. */
|
||||
memberPubkey?: string;
|
||||
displayName: string;
|
||||
status?: string;
|
||||
summary?: string;
|
||||
@@ -34,6 +38,13 @@ interface PeerRecord {
|
||||
channel?: string;
|
||||
model?: string;
|
||||
cwd?: string;
|
||||
/** True when this peer is one of the caller's own member's sessions.
|
||||
* Set in the cli (not the broker) by comparing memberPubkey against
|
||||
* the caller's stable JoinedMesh.pubkey. */
|
||||
isSelf?: boolean;
|
||||
/** When isSelf is true, true if this is the exact session running
|
||||
* the command (vs a sibling session of the same member). */
|
||||
isThisSession?: boolean;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -52,21 +63,53 @@ function projectFields(record: PeerRecord, fields: string[]): Record<string, unk
|
||||
}
|
||||
|
||||
async function listPeersForMesh(slug: string): Promise<PeerRecord[]> {
|
||||
const config = readConfig();
|
||||
const joined = config.meshes.find((m) => m.slug === slug);
|
||||
const selfMemberPubkey = joined?.pubkey ?? null;
|
||||
|
||||
// Try warm path first.
|
||||
const bridged = await tryBridge(slug, "peers");
|
||||
if (bridged && bridged.ok) {
|
||||
return bridged.result as PeerRecord[];
|
||||
const peers = bridged.result as PeerRecord[];
|
||||
return peers.map((p) => annotateSelf(p, selfMemberPubkey, null));
|
||||
}
|
||||
// Cold path — open our own WS.
|
||||
let result: PeerRecord[] = [];
|
||||
await withMesh({ meshSlug: slug }, async (client) => {
|
||||
const all = await client.listPeers();
|
||||
const selfPubkey = client.getSessionPubkey();
|
||||
result = (selfPubkey ? all.filter((p) => p.pubkey !== selfPubkey) : all) as unknown as PeerRecord[];
|
||||
const all = (await client.listPeers()) as unknown as PeerRecord[];
|
||||
const selfSessionPubkey = client.getSessionPubkey();
|
||||
result = all.map((p) =>
|
||||
annotateSelf(p, selfMemberPubkey, selfSessionPubkey),
|
||||
);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag each peer record with `isSelf` / `isThisSession` so the renderer
|
||||
* (and downstream code that picks targets, e.g. `claudemesh send`) can
|
||||
* tell sender's own sessions from real peers. The broker has always
|
||||
* surfaced a sender's siblings as separate rows because they're separate
|
||||
* presence rows; the cli just hadn't been making that visible.
|
||||
*/
|
||||
function annotateSelf(
|
||||
peer: PeerRecord,
|
||||
selfMemberPubkey: string | null,
|
||||
selfSessionPubkey: string | null,
|
||||
): PeerRecord {
|
||||
const isSelf = !!(
|
||||
selfMemberPubkey &&
|
||||
peer.memberPubkey &&
|
||||
peer.memberPubkey === selfMemberPubkey
|
||||
);
|
||||
const isThisSession = !!(
|
||||
isSelf &&
|
||||
selfSessionPubkey &&
|
||||
peer.pubkey === selfSessionPubkey
|
||||
);
|
||||
return { ...peer, isSelf, isThisSession };
|
||||
}
|
||||
|
||||
export async function runPeers(flags: PeersFlags): Promise<void> {
|
||||
const config = readConfig();
|
||||
const slugs = flags.mesh ? [flags.mesh] : config.meshes.map((m) => m.slug);
|
||||
@@ -122,7 +165,14 @@ export async function runPeers(flags: PeersFlags): Promise<void> {
|
||||
const metaStr = meta.length ? dim(` (${meta.join(", ")})`) : "";
|
||||
const summary = p.summary ? dim(` — ${p.summary}`) : "";
|
||||
const pubkeyTag = dim(` · ${p.pubkey.slice(0, 16)}…`);
|
||||
render.info(`${statusDot} ${name}${groups}${metaStr}${pubkeyTag}${summary}`);
|
||||
const selfTag = p.isThisSession
|
||||
? dim(" ") + yellow("(this session)")
|
||||
: p.isSelf
|
||||
? dim(" ") + yellow("(your other session)")
|
||||
: "";
|
||||
render.info(
|
||||
`${statusDot} ${name}${selfTag}${groups}${metaStr}${pubkeyTag}${summary}`,
|
||||
);
|
||||
if (p.cwd) render.info(dim(` cwd: ${p.cwd}`));
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user