feat(cli): claudemesh me — cross-mesh workspace overview (v0.4.0)
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

This commit is contained in:
Alejandro Gutiérrez
2026-05-02 23:35:01 +01:00
parent ad70782171
commit 7de13cbb71
3 changed files with 132 additions and 1 deletions

View File

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

111
apps/cli/src/commands/me.ts Normal file
View File

@@ -0,0 +1,111 @@
/**
* `claudemesh me` — cross-mesh workspace overview for the caller's user.
*
* Calls GET /v1/me/workspace which aggregates over every mesh the
* authenticated user belongs to: peer count, online count, topic count,
* unread @-mention count per mesh + global totals.
*
* Auth: mints a temporary read-scoped REST apikey on whichever mesh
* the user has joined first (any mesh works — the endpoint resolves
* to the issuing user, not the apikey's mesh).
*
* v0.4.0 substrate. Future verbs (`me topics`, `me notifications`,
* `me activity`, `me search`) layer on top of similar aggregating
* endpoints once they ship.
*/
import { withRestKey } from "~/services/api/with-rest-key.js";
import { request } from "~/services/api/client.js";
import { render } from "~/ui/render.js";
import { bold, clay, cyan, dim, green, yellow } from "~/ui/styles.js";
import { EXIT } from "~/constants/exit-codes.js";
interface WorkspaceMesh {
meshId: string;
slug: string;
name: string;
memberId: string;
myRole: string;
joinedAt: string;
peers: number;
online: number;
topics: number;
unreadMentions: number;
}
interface WorkspaceResponse {
userId: string;
meshes: WorkspaceMesh[];
totals: {
meshes: number;
peers: number;
online: number;
topics: number;
unreadMentions: number;
};
}
export interface MeFlags {
mesh?: string;
json?: boolean;
}
export async function runMe(flags: MeFlags): Promise<number> {
return withRestKey(
{
meshSlug: flags.mesh ?? null,
purpose: "workspace-overview",
capabilities: ["read"],
},
async ({ secret }) => {
const ws = await request<WorkspaceResponse>({
path: "/api/v1/me/workspace",
token: secret,
});
if (flags.json) {
console.log(JSON.stringify(ws, null, 2));
return EXIT.SUCCESS;
}
render.section(
`${clay("workspace")}${bold(ws.userId.slice(0, 8))} ${dim(
`· ${ws.totals.meshes} mesh${ws.totals.meshes === 1 ? "" : "es"}`,
)}`,
);
const totalsLine = [
`${green(String(ws.totals.online))}/${ws.totals.peers} online`,
`${ws.totals.topics} topic${ws.totals.topics === 1 ? "" : "s"}`,
ws.totals.unreadMentions > 0
? yellow(`${ws.totals.unreadMentions} unread @you`)
: dim("0 unread @you"),
].join(dim(" · "));
process.stdout.write(" " + totalsLine + "\n\n");
if (ws.meshes.length === 0) {
process.stdout.write(
dim(" no meshes joined — run `claudemesh new` or accept an invite\n"),
);
return EXIT.SUCCESS;
}
const slugWidth = Math.max(...ws.meshes.map((m) => m.slug.length), 8);
for (const m of ws.meshes) {
const slug = cyan(m.slug.padEnd(slugWidth));
const peers = `${m.online}/${m.peers}`;
const role = dim(m.myRole);
const unread =
m.unreadMentions > 0
? " " + yellow(`${m.unreadMentions} @you`)
: "";
process.stdout.write(
` ${slug} ${peers.padStart(5)} online ${dim(
String(m.topics).padStart(2) + " topics",
)} ${role}${unread}\n`,
);
}
return EXIT.SUCCESS;
},
);
}

View File

@@ -123,6 +123,7 @@ Topic (conversation scope, v0.2.0)
claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2) [--reply-to <id>]
claudemesh send "#topic" "msg" send to a topic (WS path, v1 plaintext)
claudemesh me cross-mesh workspace overview (v0.4.0)
claudemesh member list mesh roster with online state [--online]
claudemesh notification list recent @-mentions of you [--since <ISO>]
@@ -672,6 +673,25 @@ async function main(): Promise<void> {
break;
}
// me — cross-mesh workspace overview (v0.4.0)
case "me": {
const sub = positionals[0];
const f = {
mesh: flags.mesh as string,
json: !!flags.json,
};
if (!sub || sub === "workspace" || sub === "overview") {
const { runMe } = await import("~/commands/me.js");
process.exit(await runMe(f));
} else {
console.error(
"Usage: claudemesh me (cross-mesh overview; future: me topics, me notifications, me activity)",
);
process.exit(EXIT.INVALID_ARGS);
}
break;
}
// member — mesh roster with online state (v1.7.0)
case "member": case "members": {
const sub = positionals[0] ?? "list";