feat(cli+docs): colorize --help output + workspace view spec
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.
This commit is contained in:
204
.artifacts/specs/2026-05-02-workspace-view.md
Normal file
204
.artifacts/specs/2026-05-02-workspace-view.md
Normal file
@@ -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 "<q>" # 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.
|
||||||
@@ -9,6 +9,7 @@ import { renderVersion } from "~/cli/output/version.js";
|
|||||||
import { isInviteUrl, normaliseInviteUrl } from "~/utils/url.js";
|
import { isInviteUrl, normaliseInviteUrl } from "~/utils/url.js";
|
||||||
import { classifyInvocation } from "~/cli/policy-classify.js";
|
import { classifyInvocation } from "~/cli/policy-classify.js";
|
||||||
import { gate, type ApprovalMode } from "~/services/policy/index.js";
|
import { gate, type ApprovalMode } from "~/services/policy/index.js";
|
||||||
|
import { bold, clay, cyan, dim, orange } from "~/ui/styles.js";
|
||||||
|
|
||||||
installSignalHandlers();
|
installSignalHandlers();
|
||||||
installErrorHandlers();
|
installErrorHandlers();
|
||||||
@@ -192,8 +193,59 @@ Flags
|
|||||||
-q, --quiet suppress non-essential output
|
-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 <verb> ...`) 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<void> {
|
async function main(): Promise<void> {
|
||||||
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); }
|
if (flags.version || flags.V) { console.log(renderVersion()); process.exit(EXIT.SUCCESS); }
|
||||||
|
|
||||||
// Policy gate — runs before any broker-touching command. Skipped for help,
|
// Policy gate — runs before any broker-touching command. Skipped for help,
|
||||||
|
|||||||
@@ -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
|
`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
|
to thread a reply without a follow-up query. *Shipped 2026-05-02 in
|
||||||
CLI v1.9.0.*
|
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** —
|
- **v0.3.2 — multi-session DM routing + broadcast self-loopback** —
|
||||||
fixes two production bugs: (1) replies via `claudemesh send
|
fixes two production bugs: (1) replies via `claudemesh send
|
||||||
<from_id>` rejected with "no connected peer" when the sender's
|
<from_id>` rejected with "no connected peer" when the sender's
|
||||||
|
|||||||
Reference in New Issue
Block a user