feat(cli): 1.32.0 — multi-session UX bundle (self-identity, --self fan-out, broker welcome)
Some checks failed
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Nine UX bugs surfaced from a real two-session interconnect smoke
test, shipped together.

Self-identity is visible
- peer list now shows the caller as (this session), sorted to top.
  Daemon path resolves session pubkey via /v1/sessions/me so
  isThisSession is set correctly warm.
- whoami shows session pubkey, session id, mesh, role, groups, cwd,
  pid when run inside a launched session.

Sibling-session disambiguation
- peer list rows carry sid:<short> tag so visually-identical rows
  can be told apart at a glance.

Daemon hidden by default
- claudemesh-daemon presence rows hidden from peer list by default.
  --all opts back in. Header shows N daemon hidden when applicable.

--self flag works end-to-end
- Argv parser was greedy: --self ate the next arg as its value.
  BOOLEAN_FLAGS set in cli/argv.ts now lists known no-value switches.
- message send subcommand now passes self through (only legacy send
  was wired before).
- Help text lists --self.

Member-pubkey fan-out
- Sending to your own member pubkey with --self now resolves to every
  connected sibling session and sends one message per recipient.
  Required because the broker drain matches target_spec only against
  full session pubkeys; member-pubkey sends queued but never drained.

Broker welcome at launch
- After the launch banner, one line confirms WS state, peer count,
  and unread inbox count. Best-effort — falls back gracefully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-04 17:02:28 +01:00
parent 25586d298f
commit 7460d34335
9 changed files with 353 additions and 23 deletions

View File

@@ -1,5 +1,75 @@
# Changelog
## 1.32.0 (2026-05-04) — multi-session UX bundle
Nine UX bugs surfaced from a real two-session interconnect smoke test
shipped together as a single release.
### Self-identity is now visible
- **`peer list` includes the calling session as a row**, marked
`(this session)`, sorted to the top. The daemon path now resolves the
caller's session pubkey via `/v1/sessions/me` so `isThisSession`
is set correctly even when running warm. (Previously the row was
present but indistinguishable, and the daemon path always set
`isThisSession=false`.)
- **`whoami` shows in-session identity** when run inside a launched
session: session pubkey (truncated + full), session id, mesh, role,
groups, cwd, pid. Previously whoami only reported web sign-in state.
### Sibling-session disambiguation
- **`peer list` rows now carry a `sid:<short>` tag** so two
visually-identical rows (same name, same cwd) can be told apart at
a glance.
- **JSON output already had `sessionId`**; the human renderer
surfaces a short prefix.
### Daemon presence hidden by default
- `claudemesh-daemon` rows used to clutter `peer list` and confused
users into thinking the daemon counted as a peer. They're now hidden
in the human renderer; `--all` opts back in for debugging. The header
line shows `(N peers, M daemon hidden — use --all)` when applicable.
JSON output is unchanged.
### `--self` flag works end-to-end
- **Argv parser bug fixed.** `--self` was being parsed greedily — every
`--flag` consumed the next non-`-` arg as its value, so
`claudemesh send --self <pubkey> "msg"` ate the pubkey as the value
of `--self` and left zero positionals. A `BOOLEAN_FLAGS` set in
`cli/argv.ts` now lists known no-value switches (`self`, `json`,
`all`, `quiet`, `yes`, `strict`, `force`, `dry-run`, etc.).
`--flag=value` form also recognized for explicit overrides.
- **`message send` subcommand now passes `self`** through to `runSend`
(only the legacy `send` form had been wired).
- **Help text updated** to list `--self` (and `--priority`, `--mesh`,
`--json`) under `claudemesh message send`.
### Member-pubkey fan-out
- **Sending to your own member pubkey with `--self` now fans out** to
every connected sibling session of your member. Previously the broker
drain query at `apps/broker/src/broker.ts:2408` matched
`target_spec` only against full session pubkeys, so member-pubkey
sends queued successfully but no recipient drain ever fetched. The
CLI now resolves the member pubkey to all sibling session pubkeys
via the peer list and sends one message per recipient. Output reports
`fanned out to N sibling sessions` with per-recipient ack/error.
### Broker welcome at launch
- After the launch banner, a single line confirms WS connectivity:
```
● broker connected · 6 peers online · 0 unread
```
Hits `/v1/health` for broker WS state, `peer list` (daemon-cached)
for peer count, and `/v1/inbox` for unread. All best-effort — falls
back gracefully if any call fails so launch never blocks on it.
## 1.31.6 (2026-05-04) — hex-prefix sends actually deliver now
`claudemesh send <16-hex-prefix> "..."` would acknowledge with `sent

View File

@@ -1,6 +1,6 @@
{
"name": "claudemesh-cli",
"version": "1.31.6",
"version": "1.32.0",
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
"keywords": [
"claude-code",

View File

@@ -2,6 +2,35 @@ import { defineCommand, runMain } from "citty";
export interface ParsedArgs { command: string; positionals: string[]; flags: Record<string, string | boolean | undefined>; }
/**
* Flags that NEVER take a value. The parser's default behavior is greedy
* (any `--flag` consumes the next non-`-` arg as its value), which is
* fine for `--mesh foo` and `--priority now` but breaks for booleans:
* `claudemesh send --self <pubkey> "msg"` was eating the pubkey as the
* value of --self, leaving zero positionals and triggering Usage errors.
*
* Adding to this set: any new boolean / no-arg switch.
*/
const BOOLEAN_FLAGS = new Set([
"self",
"json", // also accepts --json=a,b,c form below
"all",
"yes", "y",
"help", "h",
"version", "v",
"quiet",
"strict",
"continue",
"no-daemon",
"no-color",
"debug",
"allow-ci-persistent",
"force",
"dry-run",
"verbose",
"skip-service",
]);
export function parseArgv(argv: string[]): ParsedArgs {
const args = argv.slice(2);
const flags: Record<string, string | boolean | undefined> = {};
@@ -10,14 +39,26 @@ export function parseArgv(argv: string[]): ParsedArgs {
for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
// --flag=value (always parsed as a value, regardless of boolean set)
if (arg.startsWith("--") && arg.includes("=")) {
const eq = arg.indexOf("=");
const key = arg.slice(2, eq);
flags[key] = arg.slice(eq + 1);
continue;
}
if (arg.startsWith("--")) {
const key = arg.slice(2);
// Known boolean → never consume the next token as a value.
if (BOOLEAN_FLAGS.has(key)) { flags[key] = true; continue; }
const next = args[i + 1];
if (next && !next.startsWith("-")) { flags[key] = next; i++; } else flags[key] = true;
if (next !== undefined && !next.startsWith("-")) { flags[key] = next; i++; }
else flags[key] = true;
} else if (arg.startsWith("-") && arg.length === 2) {
const key = arg.slice(1);
if (BOOLEAN_FLAGS.has(key)) { flags[key] = true; continue; }
const next = args[i + 1];
if (next && !next.startsWith("-")) { flags[key] = next; i++; } else flags[key] = true;
if (next !== undefined && !next.startsWith("-")) { flags[key] = next; i++; }
else flags[key] = true;
} else if (!command) {
command = arg;
} else {

View File

@@ -349,6 +349,64 @@ async function runLaunchWizard(opts: {
return { mesh, role, groups, messageMode, skipPermissions };
}
/**
* 1.32.0 — broker welcome line printed right after the launch banner.
* Polls the daemon's /v1/health (per-mesh broker WS state) and tries
* to fetch the inbox + peer count via daemon-route helpers. Best-effort:
* if any call fails the welcome simply prints what it knows and moves
* on — never blocks the launch path.
*/
async function printBrokerWelcome(meshSlug: string): Promise<void> {
const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[22m` : s);
const yellow = (s: string): string => (useColor ? `\x1b[33m${s}\x1b[22m` : s);
// Probe daemon health for broker WS state.
let brokerState = "unknown";
try {
const { ipc } = await import("~/daemon/ipc/client.js");
const res = await ipc<{ ok?: boolean; brokers?: Record<string, string> }>({
path: "/v1/health",
timeoutMs: 1_500,
});
if (res.status === 200 && res.body?.brokers) {
brokerState = res.body.brokers[meshSlug] ?? "unknown";
}
} catch { /* daemon unreachable — not fatal */ }
// Peer count (best-effort).
let peerCount = -1;
try {
const { tryListPeersViaDaemon } = await import("~/services/bridge/daemon-route.js");
const peers = (await tryListPeersViaDaemon()) ?? [];
peerCount = peers.filter((p) =>
(p as { channel?: string }).channel !== "claudemesh-daemon",
).length;
} catch { /* skip peer count */ }
// Unread inbox count (best-effort).
let unread = -1;
try {
const { ipc } = await import("~/daemon/ipc/client.js");
const res = await ipc<{ messages?: unknown[] }>({
path: "/v1/inbox",
timeoutMs: 1_500,
});
if (res.status === 200 && Array.isArray(res.body?.messages)) {
unread = res.body.messages.length;
}
} catch { /* skip unread */ }
const dot = brokerState === "open" ? green("●") : yellow("●");
const parts: string[] = [];
parts.push(`broker ${brokerState === "open" ? "connected" : brokerState}`);
if (peerCount >= 0) parts.push(`${peerCount} peer${peerCount === 1 ? "" : "s"} online`);
if (unread >= 0) parts.push(`${unread} unread`);
console.log(`${dot} ${parts.join(dim(" · "))}`);
console.log("");
}
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[], messageMode: "push" | "inbox" | "off"): void {
const useColor =
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
@@ -752,6 +810,10 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
// 5. Print summary banner (wizard already handled all interactive config).
if (!args.quiet) {
printBanner(displayName, mesh.slug, role, parsedGroups, messageMode);
// 1.32.0+: broker welcome — confirm the per-session WS is actually
// attached and surface peer count + unread inbox so the user lands
// in claude code with a clear state instead of silent assumptions.
await printBrokerWelcome(mesh.slug);
}
// --- Install native MCP entries for deployed mesh services ---

View File

@@ -21,6 +21,12 @@ export interface PeersFlags {
mesh?: string;
/** `true`/`undefined` = full record; comma-separated string = field projection. */
json?: boolean | string;
/** When false (default), hide claudemesh-daemon presence rows from the
* human renderer — they're infrastructure, not interactive peers, and
* confused users into thinking the daemon counted as a "peer". The
* JSON output still includes them so scripts that need a full inventory
* can opt in via --all (or just consume JSON). */
all?: boolean;
}
interface PeerRecord {
@@ -29,6 +35,10 @@ interface PeerRecord {
* this with a peer, they're talking to the same person across all
* their open sessions. */
memberPubkey?: string;
/** Per-launch session identifier (uuid). Used by the renderer to
* disambiguate sibling sessions of the same member that otherwise
* look identical (same name, same cwd). */
sessionId?: string;
displayName: string;
status?: string;
summary?: string;
@@ -82,6 +92,20 @@ async function listPeersForMesh(slug: string): Promise<PeerRecord[]> {
const joined = config.meshes.find((m) => m.slug === slug);
const selfMemberPubkey = joined?.pubkey ?? null;
// Resolve our own session pubkey via the daemon's /v1/sessions/me when
// we're inside a launched session. Without this, isThisSession can't
// be set on the daemon path (only on the cold path where a fresh WS
// creates the keypair), and the renderer can't tell the user which
// row in `peer list` is them.
let selfSessionPubkey: string | null = null;
try {
const { getSessionInfo } = await import("~/services/session/resolve.js");
const sess = await getSessionInfo();
if (sess && sess.mesh === slug && sess.presence?.sessionPubkey) {
selfSessionPubkey = sess.presence.sessionPubkey;
}
} catch { /* not in a launched session; isThisSession stays false */ }
// Daemon path — preferred when running. Same routing pattern as send.ts:
// ~1 ms IPC round-trip; broker WS already warm in the daemon. The
// lifecycle helper inside tryListPeersViaDaemon auto-spawns the
@@ -91,7 +115,7 @@ async function listPeersForMesh(slug: string): Promise<PeerRecord[]> {
const { tryListPeersViaDaemon } = await import("~/services/bridge/daemon-route.js");
const dr = await tryListPeersViaDaemon();
if (dr !== null) {
return dr.map((p) => annotateSelf(p as PeerRecord, selfMemberPubkey, null));
return dr.map((p) => annotateSelf(p as PeerRecord, selfMemberPubkey, selfSessionPubkey));
}
} catch { /* daemon route helper not available; fall through */ }
@@ -184,14 +208,36 @@ export async function runPeers(flags: PeersFlags): Promise<void> {
continue;
}
render.section(`peers on ${slug} (${peers.length})`);
// Hide claudemesh-daemon rows by default — they're infrastructure
// (the daemon's own member-keyed presence), not interactive peers,
// and they confused users into thinking the daemon counted as a
// separate peer. --all opts back in for debugging.
const visible = flags.all
? peers
: peers.filter((p) => p.channel !== "claudemesh-daemon");
if (peers.length === 0) {
// Sort: this-session first, then your-other-sessions, then real
// peers. Within each group, idle/working ahead of dnd. Inside the
// groups, leave broker order. The point is: when you run peer
// list, the row that's YOU is row 1.
const sorted = visible.slice().sort((a, b) => {
const score = (p: PeerRecord) =>
p.isThisSession ? 0 : p.isSelf ? 1 : 2;
return score(a) - score(b);
});
const hiddenDaemons = peers.length - visible.length;
const header = hiddenDaemons > 0
? `peers on ${slug} (${sorted.length}, ${hiddenDaemons} daemon hidden — use --all)`
: `peers on ${slug} (${sorted.length})`;
render.section(header);
if (sorted.length === 0) {
render.info(dim(" (no peers connected)"));
continue;
}
for (const p of peers) {
for (const p of sorted) {
const statusDot = p.status === "working" ? yellow("●") : green("●");
const name = bold(p.displayName);
const meta: string[] = [];
@@ -201,6 +247,12 @@ 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)}`);
// Short sessionId tag — appears for sibling sessions of the same
// member that would otherwise be visually identical (same name,
// same cwd, only the truncated pubkey on the right differs).
const sidTag = p.sessionId
? dim(` · sid:${p.sessionId.slice(0, 8)}`)
: "";
const selfTag = p.isThisSession
? dim(" ") + yellow("(this session)")
: p.isSelf
@@ -224,7 +276,7 @@ export async function runPeers(flags: PeersFlags): Promise<void> {
const tagsStr = inlineTags.length ? " [" + inlineTags.join(", ") + "]" : "";
render.info(
`${statusDot} ${name}${selfTag}${tagsStr}${metaStr}${pubkeyTag}${summary}`,
`${statusDot} ${name}${selfTag}${tagsStr}${metaStr}${pubkeyTag}${sidTag}${summary}`,
);
// Second line: cwd + an explicit role/groups footer when both

View File

@@ -101,12 +101,17 @@ export async function runSend(flags: SendFlags, to: string, message: string): Pr
}
// 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-
// peer-list-not-realizing-it-was-mine footgun.
if (!flags.self && meshSlug) {
// caller's own member pubkey, refuse without --self. Catches the
// common pasted-from-peer-list-not-realizing-it-was-mine footgun.
// With --self, member-pubkey targeting fans out to every connected
// sibling session of your member (the broker's drain only matches
// exact session pubkeys, so we resolve here in the CLI).
if (meshSlug) {
const joined = config.meshes.find((m) => m.slug === meshSlug);
if (joined && /^[0-9a-f]{64}$/i.test(to) && to.toLowerCase() === joined.pubkey.toLowerCase()) {
const isOwnMemberKey =
joined && /^[0-9a-f]{64}$/i.test(to) && to.toLowerCase() === joined.pubkey.toLowerCase();
if (isOwnMemberKey && !flags.self) {
render.err(
`Target "${to.slice(0, 16)}…" is your own member pubkey on mesh "${meshSlug}".`,
);
@@ -115,6 +120,68 @@ export async function runSend(flags: SendFlags, to: string, message: string): Pr
);
process.exit(1);
}
if (isOwnMemberKey && flags.self) {
// Member-pubkey fan-out: resolve to every connected sibling
// session pubkey and send one message per recipient. Required
// because the broker's drain query at apps/broker/src/broker.ts
// matches target_spec only against full session pubkeys —
// sending to a member pubkey would queue successfully but no
// drain would fetch.
try {
const { tryListPeersViaDaemon } = await import("~/services/bridge/daemon-route.js");
const { getSessionInfo } = await import("~/services/session/resolve.js");
const peers = (await tryListPeersViaDaemon()) ?? [];
const session = await getSessionInfo();
const ownSessionPk = session?.presence?.sessionPubkey?.toLowerCase();
const siblings = peers.filter((p) => {
const r = p as { memberPubkey?: string; pubkey?: string; channel?: string };
if (!r.pubkey) return false;
if (ownSessionPk && r.pubkey.toLowerCase() === ownSessionPk) return false;
if (r.channel === "claudemesh-daemon") return false;
return r.memberPubkey?.toLowerCase() === to.toLowerCase();
});
if (siblings.length === 0) {
render.err(`--self fan-out: no other sibling sessions of your member online.`);
process.exit(1);
}
const results: Array<{ pubkey: string; ok: boolean; messageId?: string; error?: string }> = [];
for (const peer of siblings) {
const pk = (peer as { pubkey: string }).pubkey;
const dr = await trySendViaDaemon({ to: pk, message, priority, expectedMesh: meshSlug ?? undefined });
if (dr === null) {
results.push({ pubkey: pk, ok: false, error: "daemon path unavailable" });
continue;
}
if (dr.ok) {
results.push({
pubkey: pk,
ok: true,
...(dr.messageId ? { messageId: dr.messageId } : {}),
});
} else {
results.push({ pubkey: pk, ok: false, error: dr.error });
}
}
const okCount = results.filter((r) => r.ok).length;
if (flags.json) {
console.log(JSON.stringify({ ok: okCount > 0, fanout: results, via: "daemon" }));
} else if (okCount === results.length) {
render.ok(`fanned out to ${okCount} sibling session${okCount === 1 ? "" : "s"} (daemon)`);
for (const r of results) render.info(dim(`${r.pubkey.slice(0, 16)}${r.messageId ? dim(r.messageId.slice(0, 8)) : ""}`));
} else {
render.warn(`fanned out: ${okCount}/${results.length} delivered`);
for (const r of results) {
const tag = r.ok ? "✔" : "✘";
render.info(` ${tag} ${r.pubkey.slice(0, 16)}${r.error ? dim(`${r.error}`) : ""}`);
}
}
return;
} catch (e) {
render.err(`--self fan-out failed: ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
}
}
}
// Daemon path — preferred when a long-lived daemon is local. UDS at

View File

@@ -1,25 +1,51 @@
import { whoAmI } from "~/services/auth/facade.js";
import { getSessionInfo } from "~/services/session/resolve.js";
import { render } from "~/ui/render.js";
import { bold, clay, dim } from "~/ui/styles.js";
import { bold, clay, dim, yellow } from "~/ui/styles.js";
import { EXIT } from "~/constants/exit-codes.js";
export async function whoami(opts: { json?: boolean }): Promise<number> {
const result = await whoAmI();
// 1.32.0+: surface the calling session's identity when whoami is run
// from inside a `claudemesh launch`-spawned shell. Previously the
// command only reported web sign-in + local mesh memberships, and a
// launched session had to dig env vars + parse config.json to figure
// out its own session pubkey.
const session = await getSessionInfo();
if (opts.json) {
console.log(JSON.stringify({ schema_version: "1.0", ...result }, null, 2));
return result.signed_in || result.local ? EXIT.SUCCESS : EXIT.AUTH_FAILED;
console.log(JSON.stringify({ schema_version: "1.0", ...result, session }, null, 2));
return result.signed_in || result.local || session ? EXIT.SUCCESS : EXIT.AUTH_FAILED;
}
// Show whatever we have. Both the web session and the local mesh
// config are independent surfaces of identity; suppress sections that
// are empty.
if (!result.signed_in && !result.local) {
// Show whatever we have. Web session, local mesh config, and the
// launched-session identity are three independent surfaces.
if (!result.signed_in && !result.local && !session) {
render.err("Not signed in", "Run `claudemesh login` to sign in or `claudemesh <invite>` to join.");
return EXIT.AUTH_FAILED;
}
render.section("whoami");
if (session) {
const sessionPk = session.presence?.sessionPubkey;
const groups = (session.groups ?? []).join(", ") || dim("(none)");
render.kv([
["this session", `${yellow(session.displayName)} on ${bold(session.mesh)}`],
["session id", dim(session.sessionId)],
...(sessionPk
? [["session pubkey", dim(`${sessionPk.slice(0, 16)}… (full: ${sessionPk})`)] as [string, string]]
: []),
...(session.role
? [["role", session.role] as [string, string]]
: []),
["groups", groups],
...(session.cwd ? [["cwd", dim(session.cwd)] as [string, string]] : []),
["pid", String(session.pid)],
]);
render.blank();
}
if (result.signed_in) {
render.kv([
["user", `${bold(result.user!.display_name)} ${dim(`(${result.user!.email})`)}`],

View File

@@ -93,6 +93,10 @@ Peer (resource form, recommended)
Message (resource form)
claudemesh message send <to> <m> send a message (alias: send)
flags: [--priority now|next|low] [--mesh <slug>]
[--self] (allow targeting your own member/session pubkey;
fans out to every sibling session of your member)
[--json] (machine-readable result)
claudemesh message inbox drain pending (alias: inbox)
claudemesh message status <id> delivery status (alias: msg-status)
@@ -388,7 +392,7 @@ async function main(): Promise<void> {
case "bans": { const { runBans } = await import("~/commands/ban.js"); process.exit(await runBans({ mesh: flags.mesh as string, json: !!flags.json })); break; }
// Messaging
case "peers": { const { runPeers } = await import("~/commands/peers.js"); await runPeers({ mesh: flags.mesh as string, json: flags.json as boolean | string | undefined }); break; }
case "peers": { const { runPeers } = await import("~/commands/peers.js"); await runPeers({ mesh: flags.mesh as string, json: flags.json as boolean | string | undefined, all: !!flags.all }); break; }
case "send": { const { runSend } = await import("~/commands/send.js"); await runSend({ mesh: flags.mesh as string, priority: flags.priority as string, json: !!flags.json, self: !!flags.self }, positionals[0] ?? "", positionals.slice(1).join(" ")); break; }
case "inbox": { const { runInbox } = await import("~/commands/inbox.js"); await runInbox({ json: !!flags.json }); break; }
case "state": {
@@ -510,7 +514,7 @@ async function main(): Promise<void> {
case "peer": {
const sub = positionals[0];
const f = { mesh: flags.mesh as string, json: flags.json as boolean | string | undefined };
const f = { mesh: flags.mesh as string, json: flags.json as boolean | string | undefined, all: !!flags.all };
const id = positionals[1] ?? "";
if (sub === "list") { const { runPeers } = await import("~/commands/peers.js"); await runPeers(f); }
else if (sub === "kick") { const { runKick } = await import("~/commands/kick.js"); process.exit(await runKick(id, { mesh: flags.mesh as string, stale: flags.stale as string, all: !!flags.all })); }
@@ -525,7 +529,7 @@ async function main(): Promise<void> {
case "message": {
const sub = positionals[0];
if (sub === "send") { const { runSend } = await import("~/commands/send.js"); await runSend({ mesh: flags.mesh as string, priority: flags.priority as string, json: !!flags.json }, positionals[1] ?? "", positionals.slice(2).join(" ")); }
if (sub === "send") { const { runSend } = await import("~/commands/send.js"); await runSend({ mesh: flags.mesh as string, priority: flags.priority as string, json: !!flags.json, self: !!flags.self }, positionals[1] ?? "", positionals.slice(2).join(" ")); }
else if (sub === "inbox") { const { runInbox } = await import("~/commands/inbox.js"); await runInbox({ json: !!flags.json }); }
else if (sub === "status") { const { runMsgStatus } = await import("~/commands/broker-actions.js"); process.exit(await runMsgStatus(positionals[1], { mesh: flags.mesh as string, json: !!flags.json })); }
else { console.error("Usage: claudemesh message <send|inbox|status>"); process.exit(EXIT.INVALID_ARGS); }

View File

@@ -28,6 +28,14 @@ export interface ResolvedSession {
cwd?: string;
role?: string;
groups?: string[];
/** 1.32.0+: per-launch presence material lifted from the daemon's
* registry so callers (peer list, whoami) can identify themselves
* in the broker's peer list without re-handshaking a fresh WS. */
presence?: {
sessionPubkey: string;
sessionSecretKey: string;
parentAttestation?: unknown;
};
}
let cached: ResolvedSession | null | undefined = undefined;