diff --git a/apps/cli/README.md b/apps/cli/README.md index 6892e5d..49f4339 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -2,7 +2,7 @@ Peer mesh for Claude Code sessions. Connect multiple Claude Code instances into a shared mesh with real-time messaging, shared state, memory, file sharing, vector store, scheduled jobs, and more — all driven from the `claudemesh` CLI. The MCP server is a tool-less push-pipe that delivers inbound peer messages to Claude as `` interrupts; everything else lives behind CLI verbs that Claude learns from the auto-installed `claudemesh` skill. -> **What's new in 1.6.0:** topics (channel pub/sub), API keys for human/REST clients, and bridge peers that forward a topic between two meshes. New verbs: `claudemesh topic`, `claudemesh apikey`, `claudemesh bridge`. The broker now exposes a REST surface at `/api/v1/*` (messages, topics, peers, history) for non-WebSocket clients. +> **What's new in 1.6.0:** topics (channel pub/sub), API keys for human/REST clients, and bridge peers that forward a topic between two meshes. New verbs: `claudemesh topic`, `claudemesh apikey`, `claudemesh bridge`. A REST surface at `https://claudemesh.com/api/v1/*` (messages, topics, peers, history) accepts `Authorization: Bearer cm_...` keys, so any HTTPS client can participate without WebSocket + ed25519 plumbing. **Note**: REST lives on the web host (`claudemesh.com`), not the broker host (`ic.claudemesh.com`) — the broker only speaks WebSocket. > > **Migration note (1.5.0):** the previous 79 MCP tools (`send_message`, `list_peers`, `remember`, …) are removed. Use the matching CLI verbs (`claudemesh send`, `claudemesh peers`, `claudemesh remember`). Run `claudemesh install` and the bundled skill teaches Claude the full surface. diff --git a/apps/cli/package.json b/apps/cli/package.json index 4ed8bad..d4afd49 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "1.6.0", + "version": "1.6.1", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli/src/commands/whoami.ts b/apps/cli/src/commands/whoami.ts index 7cb8ffa..a4f0ceb 100644 --- a/apps/cli/src/commands/whoami.ts +++ b/apps/cli/src/commands/whoami.ts @@ -1,6 +1,6 @@ import { whoAmI } from "~/services/auth/facade.js"; import { render } from "~/ui/render.js"; -import { bold, dim } from "~/ui/styles.js"; +import { bold, clay, dim } from "~/ui/styles.js"; import { EXIT } from "~/constants/exit-codes.js"; export async function whoami(opts: { json?: boolean }): Promise { @@ -8,22 +8,40 @@ export async function whoami(opts: { json?: boolean }): Promise { if (opts.json) { console.log(JSON.stringify({ schema_version: "1.0", ...result }, null, 2)); - return EXIT.SUCCESS; + return result.signed_in || result.local ? EXIT.SUCCESS : EXIT.AUTH_FAILED; } - if (!result.signed_in) { - render.err("Not signed in", "Run `claudemesh login` to sign in."); + // 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) { + render.err("Not signed in", "Run `claudemesh login` to sign in or `claudemesh ` to join."); return EXIT.AUTH_FAILED; } render.section("whoami"); - render.kv([ - ["user", `${bold(result.user!.display_name)} ${dim(`(${result.user!.email})`)}`], - ["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`], - ...(result.meshes - ? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`] as [string, string]] - : []), - ]); + if (result.signed_in) { + render.kv([ + ["user", `${bold(result.user!.display_name)} ${dim(`(${result.user!.email})`)}`], + ["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`], + ...(result.meshes + ? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`] as [string, string]] + : []), + ]); + } else { + render.kv([ + ["web", dim("not signed in · run `claudemesh login` for account features")], + ]); + } + if (result.local) { + render.blank(); + render.kv([ + ["local", `${result.local.meshes.length} mesh${result.local.meshes.length === 1 ? "" : "es"} · ${dim(result.local.config_path)}`], + ]); + for (const m of result.local.meshes) { + console.log(` ${clay("●")} ${bold(m.slug)} ${dim(`member ${m.member_id.slice(0, 8)}… pk ${m.pubkey_prefix}…`)}`); + } + } render.blank(); return EXIT.SUCCESS; diff --git a/apps/cli/src/services/auth/client.ts b/apps/cli/src/services/auth/client.ts index 366ab4a..0bedd75 100644 --- a/apps/cli/src/services/auth/client.ts +++ b/apps/cli/src/services/auth/client.ts @@ -1,5 +1,7 @@ import { my } from "~/services/api/facade.js"; import { ApiError } from "~/services/api/facade.js"; +import { readConfig } from "~/services/config/facade.js"; +import { PATHS } from "~/constants/paths.js"; import { getStoredToken, clearToken } from "./token-store.js"; import { NotSignedIn } from "./errors.js"; import type { WhoAmIResult } from "./schemas.js"; @@ -10,9 +12,27 @@ function requireToken(): string { return auth.session_token; } +/** Snapshot the local mesh-config view for whoami. Always populated when + * config.json has any mesh entries — independent of web session state. */ +function localView(): WhoAmIResult["local"] { + const cfg = readConfig(); + if (cfg.meshes.length === 0) return undefined; + return { + config_path: PATHS.CONFIG_FILE, + meshes: cfg.meshes.map((m) => ({ + slug: m.slug, + mesh_id: m.meshId, + member_id: m.memberId, + pubkey_prefix: m.pubkey.slice(0, 12), + })), + }; +} + export async function whoAmI(): Promise { const auth = getStoredToken(); - if (!auth) return { signed_in: false }; + const local = localView(); + + if (!auth) return { signed_in: false, local }; try { const profile = await my.getProfile(auth.session_token); @@ -23,11 +43,12 @@ export async function whoAmI(): Promise { user: profile, token_source: auth.token_source, meshes: { owned, guest: meshes.length - owned }, + local, }; } catch (err) { if (err instanceof ApiError && err.isUnauthorized) { clearToken(); - return { signed_in: false }; + return { signed_in: false, local }; } throw err; } diff --git a/apps/cli/src/services/auth/schemas.ts b/apps/cli/src/services/auth/schemas.ts index b3d34cb..76e0653 100644 --- a/apps/cli/src/services/auth/schemas.ts +++ b/apps/cli/src/services/auth/schemas.ts @@ -18,4 +18,15 @@ export interface WhoAmIResult { }; token_source?: string; meshes?: { owned: number; guest: number }; + /** + * Local mesh memberships from ~/.claudemesh/config.json. Always present + * when the config has any mesh entries, regardless of whether a web + * session is also signed in. Lets `claudemesh whoami` show useful + * identity info for users who joined via invite without ever signing + * in to claudemesh.com. + */ + local?: { + config_path: string; + meshes: Array<{ slug: string; mesh_id: string; member_id: string; pubkey_prefix: string }>; + }; }