Files
claudemesh/apps/cli/src/commands/me.ts
Alejandro Gutiérrez f679b49b6c
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
feat(workspace): default-aggregation for task/state/memory
ships v0.5.0 phase 2.

api: three new aggregator endpoints for the per-mesh subsystems
that didn't have one yet.
- GET /v1/me/tasks — open + claimed by default; ?status=all
  surfaces completed (30d window). sorted open > claimed > done.
- GET /v1/me/state — every (key, value) row across the user's
  meshes, sorted by recency. ?key=foo filters to one key.
- GET /v1/me/memory?q=... — ilike on content + tags, no q
  returns the last 30 days. excludes forgotten rows.

cli (1.16.0): task list, state list, recall now route through
the matching aggregator when --mesh is omitted. --mesh foo
still scopes to one mesh (existing behavior preserved).

with this, every per-mesh read verb in the cli either has a
cross-mesh aggregator or doesn't need one. v0.5.0 substrate is
complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:17:41 +01:00

756 lines
21 KiB
TypeScript

/**
* `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 { readConfig } from "~/services/config/facade.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";
/**
* /v1/me/* endpoints resolve the caller's user from the apikey issuer
* regardless of which mesh issued the key — every mesh works. When the
* user didn't pass --mesh, silently pick the first joined mesh for
* apikey-mint instead of prompting; the endpoint sees the same user.
*/
function resolveMeshForMint(explicit: string | null | undefined): string | null {
if (explicit) return explicit;
const cfg = readConfig();
return cfg.meshes[0]?.slug ?? null;
}
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: resolveMeshForMint(flags.mesh),
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;
},
);
}
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<number> {
return withRestKey(
{
meshSlug: resolveMeshForMint(flags.mesh),
purpose: "workspace-topics",
capabilities: ["read"],
},
async ({ secret }) => {
const ws = await request<WorkspaceTopicsResponse>({
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<number> {
return withRestKey(
{
meshSlug: resolveMeshForMint(flags.mesh),
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<WorkspaceNotificationsResponse>({
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;
},
);
}
interface WorkspaceActivity {
messageId: string;
topicId: string;
topicName: string;
meshId: string;
meshSlug: string;
meshName: string;
senderName: string;
senderMemberId: string;
snippet: string | null;
ciphertext: string | null;
bodyVersion: number;
createdAt: string;
}
interface WorkspaceActivityResponse {
activity: WorkspaceActivity[];
totals: { events: number };
}
export interface MeActivityFlags extends MeFlags {
since?: string;
}
export async function runMeActivity(flags: MeActivityFlags): Promise<number> {
return withRestKey(
{
meshSlug: resolveMeshForMint(flags.mesh),
purpose: "workspace-activity",
capabilities: ["read"],
},
async ({ secret }) => {
const params = new URLSearchParams();
if (flags.since) params.set("since", flags.since);
const path =
"/api/v1/me/activity" +
(params.toString() ? `?${params.toString()}` : "");
const ws = await request<WorkspaceActivityResponse>({
path,
token: secret,
});
if (flags.json) {
console.log(JSON.stringify(ws, null, 2));
return EXIT.SUCCESS;
}
render.section(
`${clay("activity")}${ws.totals.events} ${dim(
flags.since ? `since ${flags.since}` : "in the last 24h",
)}`,
);
if (ws.activity.length === 0) {
process.stdout.write(dim(" quiet — no activity in window\n"));
return EXIT.SUCCESS;
}
const slugWidth = Math.max(
...ws.activity.map((a) => a.meshSlug.length),
6,
);
for (const a of ws.activity) {
const slug = dim(a.meshSlug.padEnd(slugWidth));
const topic = cyan(`#${a.topicName}`);
const sender = a.senderName ?? "?";
const ago = formatRelativeTime(a.createdAt);
const snippet =
a.snippet ?? (a.ciphertext ? dim("[encrypted]") : dim("[empty]"));
process.stdout.write(
` ${slug} ${topic} ${dim(sender + " ·")} ${dim(ago)}\n` +
` ${snippet.length > 200 ? snippet.slice(0, 200) + "…" : snippet}\n`,
);
}
return EXIT.SUCCESS;
},
);
}
interface WorkspaceSearchTopicHit {
id: string;
name: string;
description: string | null;
meshId: string;
meshSlug: string;
meshName: string;
}
interface WorkspaceSearchMessageHit {
messageId: string;
topicId: string;
topicName: string;
meshId: string;
meshSlug: string;
senderName: string;
snippet: string | null;
bodyVersion: number;
createdAt: string;
}
interface WorkspaceSearchResponse {
query: string;
topics: WorkspaceSearchTopicHit[];
messages: WorkspaceSearchMessageHit[];
totals: { topics: number; messages: number };
}
export interface MeSearchFlags extends MeFlags {
query: string;
}
export async function runMeSearch(flags: MeSearchFlags): Promise<number> {
if (!flags.query || flags.query.length < 2) {
process.stderr.write(
"Usage: claudemesh me search <query> (min 2 chars)\n",
);
return EXIT.INVALID_ARGS;
}
return withRestKey(
{
meshSlug: resolveMeshForMint(flags.mesh),
purpose: "workspace-search",
capabilities: ["read"],
},
async ({ secret }) => {
const params = new URLSearchParams({ q: flags.query });
const ws = await request<WorkspaceSearchResponse>({
path: `/api/v1/me/search?${params.toString()}`,
token: secret,
});
if (flags.json) {
console.log(JSON.stringify(ws, null, 2));
return EXIT.SUCCESS;
}
render.section(
`${clay("search")} — "${flags.query}" ${dim(
`${ws.totals.topics} topic${ws.totals.topics === 1 ? "" : "s"}, ` +
`${ws.totals.messages} message${ws.totals.messages === 1 ? "" : "s"}`,
)}`,
);
if (ws.topics.length === 0 && ws.messages.length === 0) {
process.stdout.write(dim(" no matches\n"));
return EXIT.SUCCESS;
}
if (ws.topics.length > 0) {
process.stdout.write(dim("\n topics\n"));
const slugWidth = Math.max(
...ws.topics.map((t) => t.meshSlug.length),
6,
);
for (const t of ws.topics) {
const slug = dim(t.meshSlug.padEnd(slugWidth));
const name = cyan(`#${t.name}`);
const desc = t.description ? dim(`${t.description}`) : "";
process.stdout.write(` ${slug} ${name}${desc}\n`);
}
}
if (ws.messages.length > 0) {
process.stdout.write(dim("\n messages\n"));
const slugWidth = Math.max(
...ws.messages.map((m) => m.meshSlug.length),
6,
);
for (const m of ws.messages) {
const slug = dim(m.meshSlug.padEnd(slugWidth));
const topic = cyan(`#${m.topicName}`);
const sender = m.senderName;
const ago = formatRelativeTime(m.createdAt);
const snippet =
m.snippet ??
(m.bodyVersion === 2 ? dim("[encrypted — open the topic to decrypt]") : dim("[empty]"));
const highlighted =
m.snippet
? highlightMatch(snippet, flags.query)
: snippet;
process.stdout.write(
` ${slug} ${topic} ${dim(sender + " ·")} ${dim(ago)}\n` +
` ${highlighted}\n`,
);
}
}
return EXIT.SUCCESS;
},
);
}
function highlightMatch(text: string, query: string): string {
if (!query) return text;
const idx = text.toLowerCase().indexOf(query.toLowerCase());
if (idx === -1) return text;
const before = text.slice(0, idx);
const match = text.slice(idx, idx + query.length);
const after = text.slice(idx + query.length);
return `${before}${yellow(match)}${after}`;
}
interface WorkspaceTask {
id: string;
meshId: string;
meshSlug: string;
title: string;
assignee: string | null;
claimedByName: string | null;
priority: string;
status: string;
tags: string[];
result: string | null;
createdByName: string | null;
createdAt: string;
claimedAt: string | null;
completedAt: string | null;
}
interface WorkspaceTasksResponse {
tasks: WorkspaceTask[];
totals: { open: number; claimed: number; completed: number };
}
export interface MeTasksFlags extends MeFlags {
status?: string;
}
export async function runMeTasks(flags: MeTasksFlags): Promise<number> {
return withRestKey(
{
meshSlug: resolveMeshForMint(flags.mesh),
purpose: "workspace-tasks",
capabilities: ["read"],
},
async ({ secret }) => {
const params = new URLSearchParams();
if (flags.status) params.set("status", flags.status);
const path =
"/api/v1/me/tasks" +
(params.toString() ? `?${params.toString()}` : "");
const ws = await request<WorkspaceTasksResponse>({
path,
token: secret,
});
if (flags.json) {
console.log(JSON.stringify(ws, null, 2));
return EXIT.SUCCESS;
}
render.section(
`${clay("tasks")}${dim(
`${ws.totals.open} open · ${ws.totals.claimed} in-flight · ${ws.totals.completed} done`,
)}`,
);
if (ws.tasks.length === 0) {
process.stdout.write(dim(" no tasks in window\n"));
return EXIT.SUCCESS;
}
const slugWidth = Math.max(...ws.tasks.map((t) => t.meshSlug.length), 6);
for (const t of ws.tasks) {
const slug = dim(t.meshSlug.padEnd(slugWidth));
const status =
t.status === "open"
? yellow("open ")
: t.status === "claimed"
? cyan("working ")
: green("done ");
const prio =
t.priority === "urgent"
? yellow("!")
: t.priority === "low"
? dim("·")
: " ";
const claimer = t.claimedByName ? dim(`${t.claimedByName}`) : "";
process.stdout.write(
` ${slug} ${prio} ${status} ${t.title}${claimer}\n`,
);
}
return EXIT.SUCCESS;
},
);
}
interface WorkspaceStateEntry {
meshId: string;
meshSlug: string;
key: string;
value: unknown;
updatedByName: string | null;
updatedAt: string;
}
interface WorkspaceStateResponse {
entries: WorkspaceStateEntry[];
totals: { entries: number; meshes: number };
}
export interface MeStateFlags extends MeFlags {
key?: string;
}
export async function runMeState(flags: MeStateFlags): Promise<number> {
return withRestKey(
{
meshSlug: resolveMeshForMint(flags.mesh),
purpose: "workspace-state",
capabilities: ["read"],
},
async ({ secret }) => {
const params = new URLSearchParams();
if (flags.key) params.set("key", flags.key);
const path =
"/api/v1/me/state" +
(params.toString() ? `?${params.toString()}` : "");
const ws = await request<WorkspaceStateResponse>({
path,
token: secret,
});
if (flags.json) {
console.log(JSON.stringify(ws, null, 2));
return EXIT.SUCCESS;
}
render.section(
`${clay("state")}${ws.totals.entries} entr${ws.totals.entries === 1 ? "y" : "ies"} ${dim(
`across ${ws.totals.meshes} mesh${ws.totals.meshes === 1 ? "" : "es"}`,
)}`,
);
if (ws.entries.length === 0) {
process.stdout.write(dim(" no state entries\n"));
return EXIT.SUCCESS;
}
const slugWidth = Math.max(...ws.entries.map((e) => e.meshSlug.length), 6);
const keyWidth = Math.max(...ws.entries.map((e) => e.key.length), 8);
for (const e of ws.entries) {
const slug = dim(e.meshSlug.padEnd(slugWidth));
const key = cyan(e.key.padEnd(keyWidth));
const valueStr =
typeof e.value === "string"
? e.value
: JSON.stringify(e.value);
const trimmed =
valueStr.length > 80 ? valueStr.slice(0, 80) + "…" : valueStr;
const ago = dim(formatRelativeTime(e.updatedAt));
process.stdout.write(` ${slug} ${key} ${trimmed} ${ago}\n`);
}
return EXIT.SUCCESS;
},
);
}
interface WorkspaceMemory {
id: string;
meshId: string;
meshSlug: string;
content: string;
tags: string[];
rememberedByName: string | null;
rememberedAt: string;
}
interface WorkspaceMemoryResponse {
query: string;
memories: WorkspaceMemory[];
totals: { entries: number };
}
export interface MeMemoryFlags extends MeFlags {
query?: string;
}
export async function runMeMemory(flags: MeMemoryFlags): Promise<number> {
return withRestKey(
{
meshSlug: resolveMeshForMint(flags.mesh),
purpose: "workspace-memory",
capabilities: ["read"],
},
async ({ secret }) => {
const params = new URLSearchParams();
if (flags.query) params.set("q", flags.query);
const path =
"/api/v1/me/memory" +
(params.toString() ? `?${params.toString()}` : "");
const ws = await request<WorkspaceMemoryResponse>({
path,
token: secret,
});
if (flags.json) {
console.log(JSON.stringify(ws, null, 2));
return EXIT.SUCCESS;
}
const headerLabel = flags.query
? `recall — "${flags.query}"`
: "recall — last 30 days";
render.section(
`${clay(headerLabel)} ${dim(`${ws.totals.entries} match${ws.totals.entries === 1 ? "" : "es"}`)}`,
);
if (ws.memories.length === 0) {
process.stdout.write(dim(" no memories\n"));
return EXIT.SUCCESS;
}
const slugWidth = Math.max(
...ws.memories.map((m) => m.meshSlug.length),
6,
);
for (const m of ws.memories) {
const slug = dim(m.meshSlug.padEnd(slugWidth));
const ago = dim(formatRelativeTime(m.rememberedAt));
const tags =
m.tags.length > 0
? " " + dim("[" + m.tags.join(", ") + "]")
: "";
const content =
m.content.length > 240 ? m.content.slice(0, 240) + "…" : m.content;
process.stdout.write(` ${slug} ${ago}${tags}\n ${content}\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`;
}