feat(cli): claudemesh me — cross-mesh workspace overview (v0.4.0)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "1.9.5",
|
"version": "1.10.0",
|
||||||
"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",
|
||||||
|
|||||||
111
apps/cli/src/commands/me.ts
Normal file
111
apps/cli/src/commands/me.ts
Normal 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;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -123,6 +123,7 @@ Topic (conversation scope, v0.2.0)
|
|||||||
claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
|
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 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 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 member list mesh roster with online state [--online]
|
||||||
claudemesh notification list recent @-mentions of you [--since <ISO>]
|
claudemesh notification list recent @-mentions of you [--since <ISO>]
|
||||||
|
|
||||||
@@ -672,6 +673,25 @@ async function main(): Promise<void> {
|
|||||||
break;
|
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)
|
// member — mesh roster with online state (v1.7.0)
|
||||||
case "member": case "members": {
|
case "member": case "members": {
|
||||||
const sub = positionals[0] ?? "list";
|
const sub = positionals[0] ?? "list";
|
||||||
|
|||||||
Reference in New Issue
Block a user