From 82ee89d0dc786e820ed272a7b8b3518ad0000ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sat, 2 May 2026 22:28:46 +0100 Subject: [PATCH] feat(cli+docs): colorize --help output + workspace view spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Help text was a wall of monochrome ASCII. Now section headers print bold-clay, the program title is brand-orange, each verb's syntax is tinted cyan, and `(alias: ...)` parentheticals are dimmed so they read as secondary metadata. The styles helper already gates on TTY + NO_COLOR, so non-interactive output stays unchanged. Adds .artifacts/specs/2026-05-02-workspace-view.md — the v0.4.0 spec for a per-user virtual workspace that aggregates reads across all joined meshes while keeping writes mesh-scoped. Roadmap entry added under v0.3.0. --- .artifacts/specs/2026-05-02-workspace-view.md | 204 ++++++++++++++++++ apps/cli/src/entrypoints/cli.ts | 54 ++++- docs/roadmap.md | 10 + 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 .artifacts/specs/2026-05-02-workspace-view.md diff --git a/.artifacts/specs/2026-05-02-workspace-view.md b/.artifacts/specs/2026-05-02-workspace-view.md new file mode 100644 index 0000000..ba733c1 --- /dev/null +++ b/.artifacts/specs/2026-05-02-workspace-view.md @@ -0,0 +1,204 @@ +# Workspace view — per-user superset over joined meshes + +**Status:** spec / not started +**Target:** v0.4.0 +**Author:** Alejandro +**Date:** 2026-05-02 + +## Why + +Users routinely belong to multiple meshes — work, personal, side +projects, ECIJA + flexicar + openclaw + prueba1 in our own dogfood. +Today's CLI is mesh-scoped: every read or write either auto-picks the +default mesh or forces an interactive picker. Common questions like +*"who's online across all my meshes?"* or *"any new @-mentions +anywhere?"* require N round-trips, one per mesh. + +A few verbs already aggregate implicitly (`peer list`, `inbox`, +`list`), but the surface is patchy and inconsistent. + +We want the equivalent of "all my Slacks in one sidebar" — without +breaking the per-mesh trust model that v0.3.0 was built around. + +## What it is NOT + +- **Not a literal universal mesh.** A single global mesh everyone + joins collapses the trust boundary, blows up broadcast fan-out + (O(users²)), and turns into spam. See the universal-mesh discussion + rejected in this same session. +- **Not federation.** Federation is the broker-side equivalent + (already roadmapped under v0.3.0). Workspace is purely client-side. +- **Not identity stitching for *other* peers.** `Mou@openclaw` and + `Mou@flexicar-2` may or may not be the same human. Don't auto-merge. + Stitching MY identities is fine — local config knows. + +## What it IS + +A virtual layer that aggregates reads across the meshes the user has +joined, while keeping writes mesh-scoped. Pure projection over +existing per-mesh tables. Zero broker changes. Zero protocol changes. + +``` + ┌──────────────────────────────┐ + │ workspace │ + │ (per-user view, client) │ + └─┬────────┬────────┬─────────┬┘ + │ │ │ │ + ┌─────▼──┐ ┌───▼──┐ ┌───▼──┐ ┌────▼──┐ + │ mesh A │ │ B │ │ C │ │ ... │ + └────────┘ └──────┘ └──────┘ └───────┘ + (each remains its own crypto + trust domain) +``` + +## Surface + +### New verbs (all read-only, all aggregating) + +```bash +claudemesh me # overview: meshes, online peers, unread, tasks +claudemesh me topics # all subscribed topics, namespaced +claudemesh me notifications # cross-mesh @-mentions feed +claudemesh me activity # cross-mesh recent send/recv/topic-post +claudemesh me search "" # full-text across memory + topics + tasks +``` + +`claudemesh me` (no subcommand) prints a one-screen dashboard: + +``` + workspace — agutmou (4 meshes · 23 peers visible · 2 unread @you) + + meshes + openclaw 7 peers · 3 topics · last activity 2m + flexicar-2 5 peers · 1 topic · last activity 18m + prueba1 4 peers · idle + ECIJA 7 peers · 2 topics · 1 @you · last activity 4h + + unread @-mentions + ECIJA · #incident-2026-05-02 · 1 from coronel-abos + openclaw · #deploys · 1 from claudemesh-2 + + pending tasks (3) + ECIJA ship-F4-cliente high claimed by you + ... +``` + +### Default-aggregation rule for existing verbs + +When `--mesh` is omitted on a *read-only* verb, aggregate. When +`--mesh` is omitted on a *write* verb, fall back to current behavior +(default mesh or interactive picker). Already-aggregating verbs keep +working unchanged. + +| Verb | Today | After workspace | +|---|---|---| +| `peer list` | aggregates ✅ | unchanged | +| `inbox` | aggregates ✅ | unchanged | +| `list` | aggregates ✅ (lists meshes) | unchanged | +| `notification list` | mesh-scoped | aggregates by default | +| `topic list` | mesh-scoped | aggregates with namespacing | +| `task list` | mesh-scoped | aggregates by default | +| `state list` | mesh-scoped | aggregates by default | +| `memory recall` | mesh-scoped | aggregates by default | +| `info` / `stats` / `ping` | mesh-scoped | unchanged (per-mesh diagnostics) | +| `send`, `topic post`, `state set`, `remember`, ... | mesh-scoped | unchanged (writes pick a mesh) | + +### Rendering rules for aggregated views + +1. **Topic namespacing.** `#deploys` exists in two meshes — they're + different rooms. Render as `openclaw/#deploys`. Inside a + mesh-scoped command, keep the bare `#deploys` shorthand. +2. **Peer name collisions.** `Mou@openclaw` notation when the same + display name resolves in more than one mesh. Single resolution = + bare name. +3. **Time-grouped activity.** `me activity` sorts globally by ts + descending; mesh tag is shown as a dim suffix. +4. **Unread roll-up.** `me notifications` is a per-row + `[mesh][topic][snippet]` list, newest first. + +## API surface (REST) + +Mirror the read aggregations server-side so the dashboard + future +mobile/web UIs share the same endpoints. + +``` +GET /v1/me # workspace overview +GET /v1/me/meshes # joined meshes + summary stats +GET /v1/me/topics # all subscribed topics, all meshes +GET /v1/me/notifications # cross-mesh @-mentions +GET /v1/me/activity # unified activity feed +GET /v1/me/peers # already implicit; formalize +GET /v1/me/search?q=... # full-text across tables +``` + +Auth: needs a *user-scoped* api key (one issued per user, sees all +their meshes), which we don't have today — current keys are mesh- +scoped. Two options: + +- **(a) Per-user key.** New token type `cm_u_...` issued by the + dashboard, scopes to all meshes the issuing user belongs to. Cheaper + to build; harder to reason about because the blast radius is + larger if leaked. +- **(b) Multi-mesh aggregation.** Accept N mesh-scoped keys + concurrently; CLI auto-mints them via the existing `withRestKey` + pattern, one per joined mesh. No new key type. More round-trips on + cold start, but rotation/revocation stays simple. + +**Recommendation: (b).** Reuses today's auth model, doesn't widen the +blast radius, and the ephemeral keys we already mint per-command keep +the surface area minimal. The CLI orchestrates the fan-out client- +side. + +## Storage + +Pure projection at first. The cross-mesh queries are SELECT joins +over `mesh_member`, `mesh_topic`, `mesh_topic_member`, +`mesh_notification`, `mesh_topic_message`, `mesh_task`, `presence`. + +If `me` queries become hot (likely once dashboards land), add a +materialized `user_workspace_view` refreshed on writes. Don't +optimize early. + +## Effort + +| Component | Effort | +|---|---| +| CLI verbs (`me`, `me topics`, etc.) | 1.5 days | +| Default-aggregation rule across existing verbs | 0.5 day | +| REST endpoints `/v1/me/*` | 1 day | +| Multi-mesh apikey orchestration in `withRestKey` | 0.5 day | +| Tests + docs | 0.5 day | +| **Total** | **~4 days** | + +## Open questions + +1. **`me` as namespace vs. flag.** Could be `claudemesh --workspace + topics` instead of `claudemesh me topics`. The verb form is + shorter and reads better; sticking with it. +2. **Notification ordering.** All notifications globally interleaved + by ts, or per-mesh sections? Default to **interleaved** with mesh + tag prefix; users can `--by-mesh` to group. +3. **Search relevance.** Cross-mesh full-text search is easy when each + mesh has its own pg full-text index. Cross-mesh ranking is the + harder problem (IDF varies). Punt to v0.4.1 — start with simple + tied-rank merge. +4. **Web dashboard.** Should the web dashboard's main view become a + workspace view by default? Yes, but that's downstream of this + spec — once `/v1/me/*` exists, the web rewrite is the obvious + next step. + +## Out of scope (v0.4.0) + +- Federation / cross-broker workspace. +- Identity stitching for non-self peers. +- Cross-mesh search ranking sophistication. +- Cross-mesh write fan-out (`me broadcast` is intentionally NOT a + verb — too easy to misuse). +- Mobile/web parity beyond the REST endpoints. + +## Why we ship this + +Because "I want one Slack-like sidebar for all my claudemesh meshes" +is the highest-frequency UX gap users hit, and the answer is two +days of plumbing on top of what already exists. Federation is the +right answer for cross-organization reach; workspace is the right +answer for *one user, many meshes*. Both compose. diff --git a/apps/cli/src/entrypoints/cli.ts b/apps/cli/src/entrypoints/cli.ts index 6dc4d9c..02c63a3 100644 --- a/apps/cli/src/entrypoints/cli.ts +++ b/apps/cli/src/entrypoints/cli.ts @@ -9,6 +9,7 @@ import { renderVersion } from "~/cli/output/version.js"; import { isInviteUrl, normaliseInviteUrl } from "~/utils/url.js"; import { classifyInvocation } from "~/cli/policy-classify.js"; import { gate, type ApprovalMode } from "~/services/policy/index.js"; +import { bold, clay, cyan, dim, orange } from "~/ui/styles.js"; installSignalHandlers(); installErrorHandlers(); @@ -192,8 +193,59 @@ Flags -q, --quiet suppress non-essential output `; +/** + * Apply color treatment to the HELP block for terminal readability. + * + * Strategy is line-based and intentionally conservative: + * - Section header lines (the title-case categories like `Mesh`, + * `Topic`, `Auth`, `USAGE`) get bold + accent. + * - Each verb row (` claudemesh ...`) gets the command tinted + * cyan up to the second whitespace gap (separating the syntax from + * the description), and any trailing `(alias: ...)` parenthetical + * dimmed so it reads as secondary metadata. + * - The header (program name + version) gets the brand orange. + * + * Falls through to plain output when stdout is not a TTY or NO_COLOR + * is set — the underlying style helpers already gate on that. + */ +function colorizeHelp(raw: string): string { + const lines = raw.split("\n"); + const SECTION_HEADER_RE = /^([A-Z][A-Za-z0-9 /+-]*?)(\s*\(.*\))?$/; + const VERB_ROW_RE = /^(\s{2})(claudemesh[^\s]*(?:\s+[^\s]+)*?)(\s{2,})(.*)$/; + const ALIAS_RE = /(\(alias[^)]*\))/g; + const out: string[] = []; + for (const line of lines) { + if (line.startsWith("claudemesh —")) { + out.push(orange(line)); + continue; + } + if (line.trim() === "") { + out.push(line); + continue; + } + // Section header: a line with no leading spaces that isn't a verb. + if (!line.startsWith(" ") && SECTION_HEADER_RE.test(line)) { + const m = line.match(SECTION_HEADER_RE)!; + const head = bold(clay(m[1]!)); + const meta = m[2] ? dim(m[2]) : ""; + out.push(head + meta); + continue; + } + // Verb row: tint the syntax, dim the alias parenthetical. + const verbMatch = line.match(VERB_ROW_RE); + if (verbMatch) { + const [, indent, syntax, gap, rest] = verbMatch; + const dimmedRest = rest!.replace(ALIAS_RE, (m) => dim(m)); + out.push(`${indent}${cyan(syntax!)}${gap}${dimmedRest}`); + continue; + } + out.push(line); + } + return out.join("\n"); +} + async function main(): Promise { - if (flags.help || flags.h) { console.log(HELP); process.exit(EXIT.SUCCESS); } + if (flags.help || flags.h) { console.log(colorizeHelp(HELP)); process.exit(EXIT.SUCCESS); } if (flags.version || flags.V) { console.log(renderVersion()); process.exit(EXIT.SUCCESS); } // Policy gate — runs before any broker-touching command. Skipped for help, diff --git a/docs/roadmap.md b/docs/roadmap.md index c53755d..6536134 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -234,6 +234,16 @@ level, or wire claudemesh to messaging surfaces beyond Claude Code. `message_id`, `reply_to_id` so the recipient has everything needed to thread a reply without a follow-up query. *Shipped 2026-05-02 in CLI v1.9.0.* +- **v0.4.0 — workspace view (per-user superset)** — virtual layer + that aggregates reads across the meshes a user has joined while + keeping writes mesh-scoped. New verbs: `claudemesh me`, `me topics`, + `me notifications`, `me activity`, `me search`. Default-aggregation + rule for existing read verbs (`notification list`, `topic list`, + `task list`, `state list`, `memory recall`) when no `--mesh` is + passed. Mirror REST surface at `/v1/me/*`. Pure client-side + projection — zero broker / protocol changes; per-mesh trust + boundaries preserved. Spec at + `.artifacts/specs/2026-05-02-workspace-view.md`. - **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