feat(cli): 1.32.0 — multi-session UX bundle (self-identity, --self fan-out, broker welcome)
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:
@@ -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 {
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})`)}`],
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user