From 5ceb311d74135777b298b0d744e7ee8e95844b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sun, 3 May 2026 04:56:33 +0100 Subject: [PATCH] feat(cli): default-aggregation for topic list + notification list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ships v0.5.0 phase 1. omitting --mesh on these read verbs now routes through /v1/me/topics and /v1/me/notifications instead of prompting the user to pick a mesh. behavior preserved for explicit --mesh foo. implementation: resolveMeshForMint helper in commands/me.ts silently picks the first joined mesh for apikey-mint when flags.mesh is null. /v1/me/* endpoints resolve the user from the apikey issuer regardless of which mesh issued the key, so mint location is irrelevant — only the user identity matters. help text updated to reflect the new default. phase 2 (task list, state list, memory recall) needs /v1/me/* aggregator endpoints first; deferred. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/cli/package.json | 2 +- apps/cli/src/commands/me.ts | 23 ++++++++++++++++++----- apps/cli/src/entrypoints/cli.ts | 30 ++++++++++++++++++++++++++---- docs/roadmap.md | 18 ++++++++++++------ 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 18f9250..62a530f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "1.14.0", + "version": "1.15.0", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli/src/commands/me.ts b/apps/cli/src/commands/me.ts index a54c2b7..fc58232 100644 --- a/apps/cli/src/commands/me.ts +++ b/apps/cli/src/commands/me.ts @@ -16,10 +16,23 @@ 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; @@ -53,7 +66,7 @@ export interface MeFlags { export async function runMe(flags: MeFlags): Promise { return withRestKey( { - meshSlug: flags.mesh ?? null, + meshSlug: resolveMeshForMint(flags.mesh), purpose: "workspace-overview", capabilities: ["read"], }, @@ -136,7 +149,7 @@ export interface MeTopicsFlags extends MeFlags { export async function runMeTopics(flags: MeTopicsFlags): Promise { return withRestKey( { - meshSlug: flags.mesh ?? null, + meshSlug: resolveMeshForMint(flags.mesh), purpose: "workspace-topics", capabilities: ["read"], }, @@ -232,7 +245,7 @@ export async function runMeNotifications( ): Promise { return withRestKey( { - meshSlug: flags.mesh ?? null, + meshSlug: resolveMeshForMint(flags.mesh), purpose: "workspace-notifications", capabilities: ["read"], }, @@ -321,7 +334,7 @@ export interface MeActivityFlags extends MeFlags { export async function runMeActivity(flags: MeActivityFlags): Promise { return withRestKey( { - meshSlug: flags.mesh ?? null, + meshSlug: resolveMeshForMint(flags.mesh), purpose: "workspace-activity", capabilities: ["read"], }, @@ -416,7 +429,7 @@ export async function runMeSearch(flags: MeSearchFlags): Promise { return withRestKey( { - meshSlug: flags.mesh ?? null, + meshSlug: resolveMeshForMint(flags.mesh), purpose: "workspace-search", capabilities: ["read"], }, diff --git a/apps/cli/src/entrypoints/cli.ts b/apps/cli/src/entrypoints/cli.ts index 72af506..b7f4858 100644 --- a/apps/cli/src/entrypoints/cli.ts +++ b/apps/cli/src/entrypoints/cli.ts @@ -114,7 +114,7 @@ Bridge (forward a topic between two meshes, v0.2.0) Topic (conversation scope, v0.2.0) claudemesh topic create create a topic [--description --visibility] - claudemesh topic list list topics in the mesh + claudemesh topic list list topics across all meshes (or --mesh ) claudemesh topic join subscribe (via name or id) claudemesh topic leave unsubscribe claudemesh topic members list topic subscribers @@ -129,7 +129,7 @@ Topic (conversation scope, v0.2.0) claudemesh me activity cross-mesh recent messages [--since=ISO] claudemesh me search cross-mesh search (topics + messages) claudemesh member list mesh roster with online state [--online] - claudemesh notification list recent @-mentions of you [--since ] + claudemesh notification list @-mentions across all meshes (or --mesh ) Schedule (resource form) claudemesh schedule msg one-shot or recurring (alias: remind) @@ -628,7 +628,17 @@ async function main(): Promise { }; const arg = positionals[1] ?? ""; if (sub === "create") { const { runTopicCreate } = await import("~/commands/topic.js"); process.exit(await runTopicCreate(arg, f)); } - else if (sub === "list") { const { runTopicList } = await import("~/commands/topic.js"); process.exit(await runTopicList(f)); } + else if (sub === "list") { + // v0.5.0: aggregate across every joined mesh when --mesh is omitted. + // The per-mesh runTopicList prompts for a mesh; me topics doesn't + // need one and surfaces last-activity + unread per topic. + if (!f.mesh) { + const { runMeTopics } = await import("~/commands/me.js"); + process.exit(await runMeTopics({ mesh: undefined, json: f.json, unread: !!flags.unread })); + } + const { runTopicList } = await import("~/commands/topic.js"); + process.exit(await runTopicList(f)); + } else if (sub === "join") { const { runTopicJoin } = await import("~/commands/topic.js"); process.exit(await runTopicJoin(arg, f)); } else if (sub === "leave") { const { runTopicLeave } = await import("~/commands/topic.js"); process.exit(await runTopicLeave(arg, f)); } else if (sub === "members") { const { runTopicMembers } = await import("~/commands/topic.js"); process.exit(await runTopicMembers(arg, f)); } @@ -668,10 +678,22 @@ async function main(): Promise { since: flags.since as string, }; if (sub === "list") { + // v0.5.0: aggregate across every joined mesh when --mesh is omitted. + if (!f.mesh) { + const { runMeNotifications } = await import("~/commands/me.js"); + process.exit( + await runMeNotifications({ + mesh: undefined, + json: f.json, + all: !!flags.all, + since: f.since, + }), + ); + } const { runNotificationList } = await import("~/commands/notification.js"); process.exit(await runNotificationList(f)); } else { - console.error("Usage: claudemesh notification list [--since ]"); + console.error("Usage: claudemesh notification list [--mesh ] [--since ] [--all]"); process.exit(EXIT.INVALID_ARGS); } break; diff --git a/docs/roadmap.md b/docs/roadmap.md index 589685c..e38e7e3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -284,12 +284,18 @@ level, or wire claudemesh to messaging surfaces beyond Claude Code. highlighting + 30-day scan note. *Shipped 2026-05-03 in CLI v1.14.0.* v0.4.0 substrate is complete — every aggregating read verb now has CLI + web parity. -- **v0.5.0 — default-aggregation rule** for existing per-mesh - read verbs (`task list`, `state list`, `memory recall`, - `notification list`, `topic list`) — when no `--mesh` is - passed, route through the `/v1/me/*` aggregator instead of - prompting for a mesh. Backward-compatible: `--mesh foo` still - scopes to one mesh. +- **v0.5.0 phase 1 — default-aggregation for `topic list` + + `notification list`** — when no `--mesh` is passed these verbs + now route through `/v1/me/topics` and `/v1/me/notifications` + instead of prompting. `--mesh foo` keeps the per-mesh + behavior. *Shipped 2026-05-03 in CLI v1.15.0.* +- **v0.5.0 phase 2+ — default-aggregation for `task list`, + `state list`, `memory recall`** — needs `/v1/me/tasks`, + `/v1/me/state`, `/v1/me/memory` aggregator endpoints first. + Each subsystem's per-mesh keying scheme decides whether + aggregation is straight union (state) or needs ranking + (memory recall — vector similarity across meshes is non- + trivial). - **v0.3.2 — multi-session DM routing + broadcast self-loopback** — fixes two production bugs: (1) replies via `claudemesh send ` rejected with "no connected peer" when the sender's