chore(release): claudemesh-cli@1.6.1
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Patch release on top of 1.6.0:

- Revoke-by-id-prefix bug fix (broker.revokeApiKey now returns
  structured status; CLI surfaces not_found / not_unique). Pasting
  the 8-char prefix from `apikey list` output now works as users
  expect, instead of silently no-op'ing with a misleading "✔
  revoked" message. Already deployed to broker.
- whoami falls back to local mesh-config view when no web session
  is signed in. Users who joined via invite (and never ran
  `claudemesh login`) now see their member ids and pubkey prefixes
  per mesh, instead of a "Not signed in" dead end.
- README updated: REST surface lives at claudemesh.com/api/v1/*
  (web app), NOT ic.claudemesh.com/api/v1/* (broker). Surfaced
  during CLI-only smoke test against prod when curl on the broker
  host returned 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-02 18:50:22 +01:00
parent 0f32529370
commit d7cef45640
5 changed files with 65 additions and 15 deletions

View File

@@ -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 `<channel>` interrupts; everything else lives behind CLI verbs that Claude learns from the auto-installed `claudemesh` skill. 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 `<channel>` 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. > **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.

View File

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

View File

@@ -1,6 +1,6 @@
import { whoAmI } from "~/services/auth/facade.js"; import { whoAmI } from "~/services/auth/facade.js";
import { render } from "~/ui/render.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"; import { EXIT } from "~/constants/exit-codes.js";
export async function whoami(opts: { json?: boolean }): Promise<number> { export async function whoami(opts: { json?: boolean }): Promise<number> {
@@ -8,22 +8,40 @@ export async function whoami(opts: { json?: boolean }): Promise<number> {
if (opts.json) { if (opts.json) {
console.log(JSON.stringify({ schema_version: "1.0", ...result }, null, 2)); 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) { // Show whatever we have. Both the web session and the local mesh
render.err("Not signed in", "Run `claudemesh login` to sign in."); // 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 <invite>` to join.");
return EXIT.AUTH_FAILED; return EXIT.AUTH_FAILED;
} }
render.section("whoami"); render.section("whoami");
render.kv([ if (result.signed_in) {
["user", `${bold(result.user!.display_name)} ${dim(`(${result.user!.email})`)}`], render.kv([
["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`], ["user", `${bold(result.user!.display_name)} ${dim(`(${result.user!.email})`)}`],
...(result.meshes ["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`],
? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`] as [string, string]] ...(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(); render.blank();
return EXIT.SUCCESS; return EXIT.SUCCESS;

View File

@@ -1,5 +1,7 @@
import { my } from "~/services/api/facade.js"; import { my } from "~/services/api/facade.js";
import { ApiError } 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 { getStoredToken, clearToken } from "./token-store.js";
import { NotSignedIn } from "./errors.js"; import { NotSignedIn } from "./errors.js";
import type { WhoAmIResult } from "./schemas.js"; import type { WhoAmIResult } from "./schemas.js";
@@ -10,9 +12,27 @@ function requireToken(): string {
return auth.session_token; 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<WhoAmIResult> { export async function whoAmI(): Promise<WhoAmIResult> {
const auth = getStoredToken(); const auth = getStoredToken();
if (!auth) return { signed_in: false }; const local = localView();
if (!auth) return { signed_in: false, local };
try { try {
const profile = await my.getProfile(auth.session_token); const profile = await my.getProfile(auth.session_token);
@@ -23,11 +43,12 @@ export async function whoAmI(): Promise<WhoAmIResult> {
user: profile, user: profile,
token_source: auth.token_source, token_source: auth.token_source,
meshes: { owned, guest: meshes.length - owned }, meshes: { owned, guest: meshes.length - owned },
local,
}; };
} catch (err) { } catch (err) {
if (err instanceof ApiError && err.isUnauthorized) { if (err instanceof ApiError && err.isUnauthorized) {
clearToken(); clearToken();
return { signed_in: false }; return { signed_in: false, local };
} }
throw err; throw err;
} }

View File

@@ -18,4 +18,15 @@ export interface WhoAmIResult {
}; };
token_source?: string; token_source?: string;
meshes?: { owned: number; guest: number }; 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 }>;
};
} }