/** * `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 { return withRestKey( { meshSlug: flags.mesh ?? null, purpose: "workspace-overview", capabilities: ["read"], }, async ({ secret }) => { const ws = await request({ 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; }, ); } interface WorkspaceTopic { topicId: string; name: string; description: string | null; visibility: string; createdAt: string; meshId: string; meshSlug: string; meshName: string; memberId: string; unread: number; lastMessageAt: string | null; } interface WorkspaceTopicsResponse { topics: WorkspaceTopic[]; totals: { topics: number; unread: number }; } export interface MeTopicsFlags extends MeFlags { unread?: boolean; } export async function runMeTopics(flags: MeTopicsFlags): Promise { return withRestKey( { meshSlug: flags.mesh ?? null, purpose: "workspace-topics", capabilities: ["read"], }, async ({ secret }) => { const ws = await request({ path: "/api/v1/me/topics", token: secret, }); const visible = flags.unread ? ws.topics.filter((t) => t.unread > 0) : ws.topics; if (flags.json) { console.log( JSON.stringify( { topics: visible, totals: ws.totals }, null, 2, ), ); return EXIT.SUCCESS; } render.section( `${clay("topics")} — ${ws.totals.topics} across all meshes ${dim( ws.totals.unread > 0 ? `· ${ws.totals.unread} unread` : "· all read", )}`, ); if (visible.length === 0) { process.stdout.write( dim( flags.unread ? " no unread topics\n" : " no topics — run `claudemesh topic create #general`\n", ), ); return EXIT.SUCCESS; } const slugWidth = Math.max(...visible.map((t) => t.meshSlug.length), 6); const nameWidth = Math.max(...visible.map((t) => t.name.length), 8); for (const t of visible) { const slug = dim(t.meshSlug.padEnd(slugWidth)); const name = cyan(t.name.padEnd(nameWidth)); const unread = t.unread > 0 ? yellow(`${t.unread} unread`.padStart(10)) : dim("·".padStart(10)); const last = t.lastMessageAt ? dim(formatRelativeTime(t.lastMessageAt)) : dim("never"); process.stdout.write(` ${slug} ${name} ${unread} ${last}\n`); } return EXIT.SUCCESS; }, ); } interface WorkspaceNotification { notificationId: string; messageId: string; topicId: string; topicName: string; meshId: string; meshSlug: string; meshName: string; senderName: string | null; snippet: string | null; ciphertext: string | null; bodyVersion: number; read: boolean; readAt: string | null; createdAt: string; } interface WorkspaceNotificationsResponse { notifications: WorkspaceNotification[]; totals: { unread: number; total: number }; } export interface MeNotificationsFlags extends MeFlags { all?: boolean; since?: string; } export async function runMeNotifications( flags: MeNotificationsFlags, ): Promise { return withRestKey( { meshSlug: flags.mesh ?? null, purpose: "workspace-notifications", capabilities: ["read"], }, async ({ secret }) => { const params = new URLSearchParams(); if (flags.all) params.set("include", "all"); if (flags.since) params.set("since", flags.since); const path = "/api/v1/me/notifications" + (params.toString() ? `?${params.toString()}` : ""); const ws = await request({ path, token: secret, }); if (flags.json) { console.log(JSON.stringify(ws, null, 2)); return EXIT.SUCCESS; } const headerLabel = flags.all ? "@-mentions (all)" : "@-mentions (unread)"; render.section( `${clay(headerLabel)} — ${ws.totals.total} ${dim( ws.totals.unread > 0 ? `· ${ws.totals.unread} unread` : "· nothing pending", )}`, ); if (ws.notifications.length === 0) { process.stdout.write( dim( flags.all ? " no @-mentions in window\n" : " inbox zero — nothing waiting\n", ), ); return EXIT.SUCCESS; } const slugWidth = Math.max( ...ws.notifications.map((n) => n.meshSlug.length), 6, ); for (const n of ws.notifications) { const slug = dim(n.meshSlug.padEnd(slugWidth)); const topic = cyan(`#${n.topicName}`); const sender = n.senderName ? `from ${n.senderName}` : "from ?"; const ago = formatRelativeTime(n.createdAt); const dot = n.read ? dim("·") : yellow("●"); const snippet = n.snippet ?? (n.ciphertext ? dim("[encrypted]") : dim("[empty]")); process.stdout.write( ` ${dot} ${slug} ${topic} ${dim(sender)} ${dim(ago)}\n` + ` ${snippet.length > 200 ? snippet.slice(0, 200) + "…" : snippet}\n`, ); } return EXIT.SUCCESS; }, ); } function formatRelativeTime(iso: string): string { const then = new Date(iso).getTime(); const now = Date.now(); const sec = Math.max(0, Math.floor((now - then) / 1000)); if (sec < 60) return `${sec}s ago`; if (sec < 3600) return `${Math.floor(sec / 60)}m ago`; if (sec < 86_400) return `${Math.floor(sec / 3600)}h ago`; if (sec < 86_400 * 30) return `${Math.floor(sec / 86_400)}d ago`; if (sec < 86_400 * 365) return `${Math.floor(sec / (86_400 * 30))}mo ago`; return `${Math.floor(sec / (86_400 * 365))}y ago`; }