diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 2db7e96..5635875 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -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:` 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 "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 diff --git a/apps/cli/package.json b/apps/cli/package.json index 2629e20..4240143 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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", diff --git a/apps/cli/src/cli/argv.ts b/apps/cli/src/cli/argv.ts index 10084ec..d985aa9 100644 --- a/apps/cli/src/cli/argv.ts +++ b/apps/cli/src/cli/argv.ts @@ -2,6 +2,35 @@ import { defineCommand, runMain } from "citty"; export interface ParsedArgs { command: string; positionals: string[]; flags: Record; } +/** + * 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 "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 = {}; @@ -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 { diff --git a/apps/cli/src/commands/launch.ts b/apps/cli/src/commands/launch.ts index 8a48775..a975897 100644 --- a/apps/cli/src/commands/launch.ts +++ b/apps/cli/src/commands/launch.ts @@ -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 { + 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 }>({ + 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 --- diff --git a/apps/cli/src/commands/peers.ts b/apps/cli/src/commands/peers.ts index 142df38..f1a7428 100644 --- a/apps/cli/src/commands/peers.ts +++ b/apps/cli/src/commands/peers.ts @@ -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 { 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 { 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 { 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 { 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 { 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 diff --git a/apps/cli/src/commands/send.ts b/apps/cli/src/commands/send.ts index 9dadb28..9e51492 100644 --- a/apps/cli/src/commands/send.ts +++ b/apps/cli/src/commands/send.ts @@ -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 diff --git a/apps/cli/src/commands/whoami.ts b/apps/cli/src/commands/whoami.ts index a4f0ceb..edc1671 100644 --- a/apps/cli/src/commands/whoami.ts +++ b/apps/cli/src/commands/whoami.ts @@ -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 { 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 ` 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})`)}`], diff --git a/apps/cli/src/entrypoints/cli.ts b/apps/cli/src/entrypoints/cli.ts index e193129..9aaa664 100644 --- a/apps/cli/src/entrypoints/cli.ts +++ b/apps/cli/src/entrypoints/cli.ts @@ -93,6 +93,10 @@ Peer (resource form, recommended) Message (resource form) claudemesh message send send a message (alias: send) + flags: [--priority now|next|low] [--mesh ] + [--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 delivery status (alias: msg-status) @@ -388,7 +392,7 @@ async function main(): Promise { 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 { 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 { 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 "); process.exit(EXIT.INVALID_ARGS); } diff --git a/apps/cli/src/services/session/resolve.ts b/apps/cli/src/services/session/resolve.ts index b8ed38d..322d46c 100644 --- a/apps/cli/src/services/session/resolve.ts +++ b/apps/cli/src/services/session/resolve.ts @@ -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;