refactor: rename cli-v2 → cli, archive legacy cli, plus broker-side grants + auto-migrate
- apps/cli/ is now the canonical CLI (was apps/cli-v2/). - apps/cli/ legacy v0 archived as branch 'legacy-cli-archive' and tag 'cli-v0-legacy-final' before deletion; git history preserves it too. - .github/workflows/release-cli.yml paths updated. - pnpm-lock.yaml regenerated. Broker-side peer-grant enforcement (spec: 2026-04-15-per-peer-capabilities): - 0020_peer-grants.sql adds peer_grants jsonb + GIN index on mesh.member. - handleSend in broker fetches recipient grant maps once per send, drops messages silently when sender lacks the required capability. - POST /cli/mesh/:slug/grants to update from CLI; broker_messages_dropped_by_grant_total metric. - CLI grant/revoke/block now mirror to broker via syncToBroker. Auto-migrate on broker startup: - apps/broker/src/migrate.ts runs drizzle migrate with pg_advisory_lock before the HTTP server binds. Exits non-zero on failure so Coolify healthcheck fails closed. - Dockerfile copies packages/db/migrations into /app/migrations. - postgres 3.4.5 added as direct broker dep. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,243 @@
|
||||
# CLI Wizard Architecture Refactor
|
||||
|
||||
**Status:** backlog
|
||||
**Created:** 2026-04-10
|
||||
**Source:** Reverse-engineered from `@posthog/wizard` (npm cache), applied to `apps/cli/src/commands/launch.ts`
|
||||
|
||||
## Why
|
||||
|
||||
Launch wizard has three compounding problems:
|
||||
|
||||
1. **Imperative branching** — `launch.ts` checks account → mesh → name → role → exec in hardcoded order. Adding a screen requires touching existing code. Hard to reason about `--resume`, `--non-interactive`, and skip conditions.
|
||||
2. **Terminal bleed-through on handoff** — wizard→`claude` exec corrupts Ink's TUI state (garbled word wraps, tool labels overwritten, spinner fragments fused to paths). Root cause is spread across multiple exit paths instead of one choke point.
|
||||
3. **Inconsistent visual design** — ad-hoc colors per file, no central palette, no shared icon set, no shared layout primitives. Every screen reinvents status rows, centering, and spacing.
|
||||
|
||||
PostHog's wizard solves all three with one architectural pattern: **declarative flow pipelines + session-as-store + shared visual primitives**. This artifact captures the plan to port that pattern.
|
||||
|
||||
## What PostHog does (the reference)
|
||||
|
||||
### Flow pipeline (`flows.ts` + `router.ts`)
|
||||
|
||||
Each wizard flow is an array of screen entries:
|
||||
|
||||
```ts
|
||||
export const FLOWS = {
|
||||
[Flow.Wizard]: [
|
||||
{ screen: Screen.Intro, isComplete: s => s.setupConfirmed },
|
||||
{ screen: Screen.HealthCheck, isComplete: s => s.readinessResult !== null },
|
||||
{ screen: Screen.Setup, show: needsSetup, isComplete: s => !needsSetup(s) },
|
||||
{ screen: Screen.Auth, isComplete: s => s.credentials !== null },
|
||||
{ screen: Screen.Run, isComplete: s => s.runPhase === RunPhase.Completed },
|
||||
{ screen: Screen.Outro, isComplete: s => s.outroDismissed },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
The router walks the array, skips entries where `show(s) === false` or `isComplete(s) === true`, and returns the first remaining entry. Zero switch statements. Zero hardcoded transitions. Adding a screen = appending an object.
|
||||
|
||||
### Overlay stack
|
||||
|
||||
Separate from the linear flow cursor. Interrupts (port conflict, auth expired, managed settings) are pushed onto `overlays[]` from anywhere and popped when dismissed. Active screen = top of overlay stack OR flow cursor. Flows never need to know about interrupts.
|
||||
|
||||
### Session as single source of truth
|
||||
|
||||
One `WizardStore` holds all session state. Screens subscribe via React 18 `useSyncExternalStore`. Completion predicates read session; imperative code writes session; the router re-resolves on every change.
|
||||
|
||||
### Visual primitives
|
||||
|
||||
- `styles.ts` — 6-color palette (`Colors`), 9-icon set (`Icons`), alignment enums (`HAlign`, `VAlign`)
|
||||
- `CardLayout` — semantic centering wrapper used by every screen
|
||||
- `PickerMenu` — the only selection primitive, used for every choice
|
||||
- `screen-registry.ts` — maps `Screen` enum → React component
|
||||
- Brand mark: three colored `█` blocks next to the wizard name on every screen header
|
||||
|
||||
## What claudemesh should do
|
||||
|
||||
### Target file layout
|
||||
|
||||
```
|
||||
apps/cli/src/
|
||||
├── commands/
|
||||
│ └── launch.ts # thin entrypoint: parse flags → start TUI
|
||||
└── ui/
|
||||
├── styles.ts # palette, icons, alignment enums
|
||||
├── store.ts # LaunchStore (session + subscribe)
|
||||
├── router.ts # flow cursor + overlay stack
|
||||
├── flows.ts # FLOWS = { Launch: [...], Join: [...] }
|
||||
├── screen-registry.ts # Screen enum → component
|
||||
├── primitives/
|
||||
│ ├── CardLayout.tsx
|
||||
│ ├── PickerMenu.tsx
|
||||
│ ├── StatusRows.tsx # new: "Directory ✓ /claudemesh" pattern
|
||||
│ ├── BrandMark.tsx # new: 3 colored squares + label
|
||||
│ └── LoadingBox.tsx
|
||||
└── screens/
|
||||
├── WelcomeScreen.tsx
|
||||
├── AccountScreen.tsx
|
||||
├── MeshPickerScreen.tsx
|
||||
├── NameRoleScreen.tsx
|
||||
├── ConfirmScreen.tsx
|
||||
└── HandoffScreen.tsx # last screen; its unmount triggers exec claude
|
||||
```
|
||||
|
||||
### Flow definition
|
||||
|
||||
```ts
|
||||
export const FLOWS = {
|
||||
[Flow.Launch]: [
|
||||
{ screen: Screen.Welcome, isComplete: s => s.welcomed },
|
||||
{ screen: Screen.Account, show: s => !s.hasAccount, isComplete: s => s.hasAccount },
|
||||
{ screen: Screen.MeshPicker, show: s => s.meshes.length > 1, isComplete: s => s.meshSlug !== null },
|
||||
{ screen: Screen.NameRole, isComplete: s => s.displayName !== null && s.role !== null },
|
||||
{ screen: Screen.Confirm, isComplete: s => s.confirmed },
|
||||
{ screen: Screen.Handoff, isComplete: () => false }, // terminal screen
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### `--resume` works for free
|
||||
|
||||
`--resume <id>` populates the session from saved state; every satisfied predicate auto-skips. The wizard renders only the screens that still need input. No special `--resume` branches in screen code.
|
||||
|
||||
### `--non-interactive` works for free
|
||||
|
||||
Non-interactive mode: walk the flow, for each incomplete entry check if its required session fields can be sourced from CLI flags. If yes, populate and continue. If no, **fail fast with a clear message** naming the missing flag. Never silently guess defaults.
|
||||
|
||||
```
|
||||
$ claudemesh launch --non-interactive --name Alexis
|
||||
✗ Missing --mesh (required in non-interactive mode when >1 mesh joined)
|
||||
Available meshes: alexis-mou, dev, staging
|
||||
```
|
||||
|
||||
### Overlay interrupts claudemesh needs
|
||||
|
||||
- `BrokerDisconnect` — WS dropped mid-wizard, retry countdown
|
||||
- `InviteInvalid` — paste invite screen rejected token
|
||||
- `MeshNotFound` — `--mesh foo` passed but not joined
|
||||
- `RateLimit` — broker rate limited the CLI, backoff timer
|
||||
- `UpdateAvailable` — newer CLI version on npm, non-blocking banner
|
||||
|
||||
### Terminal handoff choke point
|
||||
|
||||
The last flow entry (`Screen.Handoff`) renders a brief "Launching Claude Code…" card, then:
|
||||
|
||||
```ts
|
||||
// apps/cli/src/ui/screens/HandoffScreen.tsx (on mount)
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await inkApp.unmount();
|
||||
await inkApp.waitUntilExit();
|
||||
resetTerminal(); // single choke point for ANSI teardown
|
||||
await flushStdout();
|
||||
execa('claude', claudeArgs, { stdio: 'inherit' });
|
||||
})();
|
||||
}, []);
|
||||
```
|
||||
|
||||
`resetTerminal()` lives in `apps/cli/src/ui/terminal.ts`:
|
||||
|
||||
```ts
|
||||
export function resetTerminal() {
|
||||
process.stdout.write(
|
||||
'\x1b[0m' + // reset SGR
|
||||
'\x1b[?25h' + // show cursor
|
||||
'\x1b[?1049l' + // exit alt-screen
|
||||
'\x1b[?1000l' + // disable mouse tracking
|
||||
'\x1b[?1002l' +
|
||||
'\x1b[?1003l' +
|
||||
'\x1b[?1006l' +
|
||||
'\x1b[?2004l' + // disable bracketed paste
|
||||
'\x1b[2J' + // clear screen
|
||||
'\x1b[H' // cursor home
|
||||
);
|
||||
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
||||
}
|
||||
```
|
||||
|
||||
PostHog only does SGR reset + clear + home on unmount — they don't hand off to another full-screen app, so that's enough for them. Claudemesh needs the full mode-reset because Claude Code takes over the TTY.
|
||||
|
||||
### Visual design system
|
||||
|
||||
`apps/cli/src/ui/styles.ts`:
|
||||
|
||||
```ts
|
||||
export const Colors = {
|
||||
primary: 'cyan',
|
||||
accent: '#7C3AED', // claudemesh purple
|
||||
title: '#4C1D95',
|
||||
success: 'green',
|
||||
error: 'red',
|
||||
warning: 'yellow',
|
||||
muted: 'gray',
|
||||
} as const;
|
||||
|
||||
export const Icons = {
|
||||
check: '✔',
|
||||
cross: '✘',
|
||||
warning: '⚠',
|
||||
arrow: '▶',
|
||||
smallArrow: '▸',
|
||||
bullet: '•',
|
||||
diamond: '◆',
|
||||
square: '█',
|
||||
} as const;
|
||||
|
||||
export enum HAlign { Left = 'flex-start', Center = 'center', Right = 'flex-end' }
|
||||
export enum VAlign { Top = 'flex-start', Center = 'center', Bottom = 'flex-end' }
|
||||
```
|
||||
|
||||
Every screen imports from here. No inline color strings allowed.
|
||||
|
||||
### Status rows pattern
|
||||
|
||||
Replaces the current plain-text banner:
|
||||
|
||||
```
|
||||
██ claudemesh launch
|
||||
|
||||
Directory ✔ /claudemesh
|
||||
Account ✔ agutierrez@mineryreport.com
|
||||
Mesh ✔ alexis-mou (9 peers online)
|
||||
Name ✔ Alexis
|
||||
Role ▸ (pick one)
|
||||
|
||||
▸ Continue
|
||||
Change mesh
|
||||
Cancel
|
||||
```
|
||||
|
||||
## Implementation order
|
||||
|
||||
| # | Impact | Effort | Scope |
|
||||
|---|---|---|---|
|
||||
| 1 | High | S | `ui/styles.ts` — palette + icons + alignment enums; migrate existing screens |
|
||||
| 2 | High | S | `ui/primitives/StatusRows.tsx` + `BrandMark.tsx` |
|
||||
| 3 | High | M | `ui/store.ts` + `ui/router.ts` + `ui/flows.ts` (flow pipeline core) |
|
||||
| 4 | High | M | Refactor `launch.ts` to render through router; port existing screens |
|
||||
| 5 | High | S | `HandoffScreen` + `resetTerminal()` choke point — fixes TUI bleed bug |
|
||||
| 6 | High | S | Preselect "Continue" on every confirmation screen (one-keypress happy path) |
|
||||
| 7 | Med | M | Overlay stack + first two overlays (`BrokerDisconnect`, `InviteInvalid`) |
|
||||
| 8 | Med | M | `--non-interactive` mode using flow walker + fail-fast flag check |
|
||||
| 9 | Med | S | Per-mesh/per-role `preRunNotice` extension point |
|
||||
| 10| Low | L | `DissolveTransition` / `ContentSequencer` polish primitives |
|
||||
|
||||
Steps 1–5 are the atomic unit of value: they fix the bleed-through bug, establish the visual system, and unblock everything else. Should ship as one PR.
|
||||
Steps 6–9 can each ship independently.
|
||||
Step 10 is polish — defer until after v0.2.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Ink version**: current CLI uses Ink 4.x? PostHog is on Ink 5 with `useSyncExternalStore`. Check `apps/cli/package.json` before porting the store pattern — Ink 4 needs a different subscription approach.
|
||||
- **React version**: `useSyncExternalStore` is React 18+. Confirm.
|
||||
- **Flow granularity**: should `Join` (paste invite) be a separate flow from `Launch`, or an overlay inside `Launch`? PostHog-style: separate flow triggered from the welcome screen. Simpler.
|
||||
- **Resume semantics**: does `--resume <id>` resume the *Claude* session only, or also restore the wizard's last mesh/name/role choice? If the latter, need a `~/.claudemesh/sessions/<id>.json` alongside Claude's own session file.
|
||||
|
||||
## References
|
||||
|
||||
- PostHog wizard source: `~/.npm/_npx/b48b11b34a0cada0/node_modules/@posthog/wizard/dist/src/ui/tui/`
|
||||
- `start-tui.js` — Ink bootstrap + cleanup
|
||||
- `router.js` — flow cursor + overlay stack
|
||||
- `flows.js` — declarative pipeline definition
|
||||
- `styles.js` — palette + icons
|
||||
- `screens/IntroScreen.js` — reference for status rows + picker
|
||||
- `primitives/CardLayout.js` — semantic centering
|
||||
820
.artifacts/backlog/2026-04-11-v1-feature-inventory.md
Normal file
@@ -0,0 +1,820 @@
|
||||
# claudemesh v1 — Feature Inventory
|
||||
|
||||
**Status:** backlog reference
|
||||
**Created:** 2026-04-11
|
||||
**Purpose:** Exhaustive audit of what v1 ships today. **Every row in this document must still work after v2 lands.** v2 is a refactor + CLI user flows, NOT a functional rewrite; this inventory is the regression checklist.
|
||||
|
||||
**Source of truth**:
|
||||
- `apps/cli/src/` — 22 files, ~12 k LOC (v0.10.5)
|
||||
- `apps/broker/src/` — 23 files, ~11 k LOC
|
||||
- `packages/db/src/schema/mesh.ts` — 1,019 lines, 23 tables
|
||||
|
||||
---
|
||||
|
||||
## 0. Summary counts
|
||||
|
||||
| Surface | v1 count |
|
||||
|---|---|
|
||||
| CLI commands (subcommands in `index.ts`) | 23 |
|
||||
| MCP tools (handlers in `mcp/server.ts`) | 79 |
|
||||
| Broker WS message types (dispatched in `index.ts`) | 85 |
|
||||
| Broker HTTP endpoints | 18 |
|
||||
| Postgres tables in `mesh` schema | 23 |
|
||||
| External backend services the broker manages | 5 (Postgres, Neo4j, Qdrant, MinIO, Docker) |
|
||||
| Lines of source (CLI + broker, excluding tests) | ~23,450 |
|
||||
|
||||
---
|
||||
|
||||
## 1. CLI commands
|
||||
|
||||
All dispatched from `apps/cli/src/index.ts`. v1 ships 23 public subcommands plus the bare-command welcome wizard.
|
||||
|
||||
| Command | File | Purpose | Flags / args |
|
||||
|---|---|---|---|
|
||||
| `claudemesh` (bare) | `commands/welcome.ts` | Interactive welcome wizard. Entry point for new users. | (none) |
|
||||
| `launch` | `commands/launch.ts` (775 lines, biggest) | Spawn a Claude Code session with mesh connectivity + MCP tools | `--name`, `--role`, `--groups`, `--mesh`, `--join`, `--message-mode`, `--system-prompt`, `-y/--yes`, `-r/--resume`, `-c/--continue`, `--quiet`, + passthrough to `claude` after `--` |
|
||||
| `create` | `commands/create.ts` | Create a new mesh from a template | `--template`, `--list-templates` |
|
||||
| `install` | `commands/install.ts` (538 lines) | Register MCP server + status hooks with Claude Code (`~/.claude.json`, `~/.claude/settings.json`) | `--no-hooks` |
|
||||
| `uninstall` | `commands/install.ts` | Remove MCP server + hooks from Claude Code config | (none) |
|
||||
| `join` | `commands/join.ts` (193 lines) | Join a mesh via invite URL or token | positional `<url>` |
|
||||
| `list` | `commands/list.ts` | Show joined meshes, slugs, local identities | (none) |
|
||||
| `leave` | `commands/leave.ts` | Leave a joined mesh + remove its local keypair | positional `<slug>` |
|
||||
| `peers` | `commands/peers.ts` | List online peers with status, summary, groups | `--mesh`, `--json` |
|
||||
| `send` | `commands/send.ts` | Send a message to a peer, group, or all peers | positional `<to> <message>`, `--mesh`, `--priority` |
|
||||
| `inbox` | `commands/inbox.ts` | Drain pending inbound messages | `--mesh`, `--json`, `--wait` |
|
||||
| `state` | `commands/state.ts` | Get / set / list shared KV state in the mesh | positional `<action> <key> [value]`, `--mesh`, `--json` |
|
||||
| `info` | `commands/info.ts` | Mesh overview: slug, broker, peer count, state keys | `--mesh`, `--json` |
|
||||
| `remember` | `commands/memory.ts` | Store a persistent memory visible to all peers | positional `<content>`, `--mesh`, `--tags`, `--json` |
|
||||
| `recall` | `commands/memory.ts` | Full-text search of mesh memories | positional `<query>`, `--mesh`, `--json` |
|
||||
| `remind` | `commands/remind.ts` (142 lines) | Schedule a delayed message. Also: `remind list`, `remind cancel <id>` | positional `<message>`, `--in`, `--at`, `--cron`, `--to`, `--mesh`, `--json` |
|
||||
| `sync` | `commands/sync.ts` | Sync meshes from the user's claudemesh.com dashboard account | `--force` |
|
||||
| `profile` | `commands/profile.ts` | View or edit member profile (self or another member if admin) | `--mesh`, `--role-tag`, `--groups`, `--message-mode`, `--name`, `--member`, `--json` |
|
||||
| `status` | `commands/status.ts` | Check broker connectivity for each joined mesh | (none) |
|
||||
| `doctor` | `commands/doctor.ts` (212 lines) | Diagnose install, config, keypairs, PATH | 7 checks: Node >= 20, claude binary, MCP registered, hooks registered, config parses, file perms, keypairs valid |
|
||||
| `mcp` | `mcp/server.ts` (2139 lines) | Start MCP server on stdio (internal — invoked by Claude Code) | (none) |
|
||||
| `seed-test-mesh` | `commands/seed-test-mesh.ts` | Dev-only: inject a mesh into local config without invite flow | `<slug>`, `<broker_url>` |
|
||||
| `hook` | `commands/hook.ts` | Internal: handle Claude Code hook events (status updates from session lifecycle) | stdin JSON from Claude Code |
|
||||
| `connect telegram` | `commands/connect-telegram.ts` | Link a Telegram bot to a mesh | inline token prompts, calls broker `/tg/token` |
|
||||
| `disconnect telegram` | `commands/disconnect-telegram.ts` | Unlink Telegram bot | (none) |
|
||||
|
||||
### Flag-first invocation rewrite
|
||||
|
||||
`apps/cli/src/index.ts` lines 339–355 implement a **friction reducer**: if the user types `claudemesh --resume xxx` or any flag-first invocation, the argv is rewritten to `claudemesh launch --resume xxx` before citty parses it. This lets users skip typing `launch` for common flag-only forms.
|
||||
|
||||
**Must preserve in v2.** Users may depend on this. Applies to `--resume`, `--continue`, `-y`, `--mesh`, `--name`, etc.
|
||||
|
||||
---
|
||||
|
||||
## 2. MCP tools (79 total)
|
||||
|
||||
Defined in `apps/cli/src/mcp/tools.ts` with schemas, implemented in `apps/cli/src/mcp/server.ts` with per-tool case handlers. Each MCP tool is a RPC that the CLI's MCP server handles locally or forwards to the broker via WS.
|
||||
|
||||
Grouped by domain family. Every tool listed here has a working handler in v1.
|
||||
|
||||
### 2.1 Messaging (4)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `send_message` | Send encrypted message to peer, group, or broadcast. Supports priorities: `now` (immediate), `next` (default), `low`. Broker queues if recipient offline. |
|
||||
| `list_peers` | List connected peers in the mesh with `presenceId`, `displayName`, `status`, `summary`, `groups`, `roleTag`. |
|
||||
| `message_status` | Query delivery state of a sent message by `messageId`. |
|
||||
| `check_messages` | Drain pending inbox messages (push mode). |
|
||||
|
||||
### 2.2 Profile + identity (4)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `set_summary` | Set the current peer's work summary (visible to others). |
|
||||
| `set_status` | Set status: `idle`, `working`, `dnd`. Priority-ranked by source (`hook` > `manual` > `jsonl`). |
|
||||
| `set_visible` | Toggle visibility. Hidden peers skip `list_peers` and broadcasts but still receive direct messages. |
|
||||
| `set_profile` | Update display name, role tag, groups, avatar, title, bio, capabilities. |
|
||||
|
||||
### 2.3 Groups (2)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `join_group` | Join a `@group` with optional role (`lead`, `member`, or free-form). |
|
||||
| `leave_group` | Leave a `@group`. |
|
||||
|
||||
### 2.4 State KV (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `set_state` | Set a key-value pair in the mesh's shared state. Broadcasts `state_change` push to all peers. |
|
||||
| `get_state` | Read a value by key. |
|
||||
| `list_state` | List all state keys with values, authors, timestamps. |
|
||||
|
||||
### 2.5 Memory (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `remember` | Store a text memory with optional tags. Persists across sessions. |
|
||||
| `recall` | Full-text search memories by query, ranked results. |
|
||||
| `forget` | Delete a memory by ID. |
|
||||
|
||||
### 2.6 Files (8)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `share_file` | Upload a file to MinIO. Supports `to: <peer>` for E2E encryption (symmetric key wrapped with peer pubkey), or mesh-wide sharing. Supports `persistent` vs `ephemeral` storage. |
|
||||
| `get_file` | Download a file by `fileId`. Returns a presigned MinIO URL. |
|
||||
| `list_files` | List files in the mesh by `scope`, `tags`, author. |
|
||||
| `file_status` | Query status of a file: who downloaded, when. |
|
||||
| `delete_file` | Delete a file (owner only). |
|
||||
| `grant_file_access` | Add another peer as a recipient of an already-encrypted file (re-wraps symmetric key). |
|
||||
| `read_peer_file` | Read a file from another peer's working directory (requires peer online + sharing). |
|
||||
| `list_peer_files` | List files in a peer's shared directory (tree of names, not contents). |
|
||||
|
||||
### 2.7 Vectors (Qdrant) (4)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `vector_store` | Store embedding with metadata in a named collection. |
|
||||
| `vector_search` | Nearest-neighbor search in a collection with `limit`. |
|
||||
| `vector_delete` | Delete a vector by ID. |
|
||||
| `list_collections` | List collections in the mesh's Qdrant namespace. |
|
||||
|
||||
### 2.8 Graph (Neo4j) (2)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `graph_query` | Read-only Cypher MATCH query on the per-mesh Neo4j database. |
|
||||
| `graph_execute` | Write Cypher (CREATE/MERGE/DELETE). |
|
||||
|
||||
### 2.9 Shared SQL (Postgres) (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `mesh_query` | SELECT-only query on the per-mesh Postgres schema. |
|
||||
| `mesh_execute` | DDL + DML (CREATE TABLE, INSERT, UPDATE, DELETE). |
|
||||
| `mesh_schema` | List tables + columns in the mesh's schema. |
|
||||
|
||||
### 2.10 Streams (4)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `create_stream` | Create a named stream for live data pub-sub. |
|
||||
| `publish` | Push data to a stream. Subscribers receive in real-time. |
|
||||
| `subscribe` | Subscribe to a stream. Events arrive as channel notifications. |
|
||||
| `list_streams` | List active streams. |
|
||||
|
||||
### 2.11 Contexts (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `share_context` | Share session understanding with the mesh (summary + files_read + key_findings + tags). |
|
||||
| `get_context` | Search contexts by query (file path, topic, etc.). |
|
||||
| `list_contexts` | Show what peers currently know about the codebase. |
|
||||
|
||||
### 2.12 Tasks (4)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `create_task` | Create a work item (title, assignee, priority, tags). |
|
||||
| `claim_task` | Claim an unclaimed task. |
|
||||
| `complete_task` | Mark done with optional result summary. |
|
||||
| `list_tasks` | Filter by status and/or assignee. |
|
||||
|
||||
### 2.13 Scheduling (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `schedule_reminder` | One-shot (`deliver_at`, `in_seconds`) or recurring (`cron`). Delivered to self or `to`. Persists across broker restarts. |
|
||||
| `list_scheduled` | List pending scheduled messages. |
|
||||
| `cancel_scheduled` | Cancel by ID. |
|
||||
|
||||
### 2.14 Mesh metadata — read (4)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `mesh_info` | Overview: peers, groups, state, memory, files, tasks, streams, tables. |
|
||||
| `mesh_stats` | Resource usage per peer: messages in/out, tool calls, uptime, errors. |
|
||||
| `mesh_clock` | Simulation clock status: speed, tick count, simulated time. |
|
||||
| `ping_mesh` | Test messages through the full pipeline, measure round-trip per priority. Diagnoses push delivery issues. |
|
||||
|
||||
### 2.15 Mesh clock — write (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `mesh_set_clock` | Set simulation clock speed (1–100x). Peers receive heartbeat ticks at the simulated rate. |
|
||||
| `mesh_pause_clock` | Pause simulation clock. |
|
||||
| `mesh_resume_clock` | Resume paused clock. |
|
||||
|
||||
### 2.16 Skills (5)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `share_skill` | Publish a reusable skill (name + description + instructions + tags + when_to_use + allowed_tools + model + context + agent + user_invocable + argument_hint). Exposed as MCP prompts and `skill://` resources. |
|
||||
| `get_skill` | Load a skill's full instructions by name. |
|
||||
| `list_skills` | Browse available skills, optionally filter by keyword. |
|
||||
| `remove_skill` | Remove a shared skill. |
|
||||
| `mesh_skill_deploy` | Deploy a multi-file skill bundle from zip or git repo. |
|
||||
|
||||
### 2.17 MCP registry tier 1 — peer-hosted (4)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `mesh_mcp_register` | Register a peer's local MCP server with the mesh (server_name, description, tools schema, persistent flag). Other peers can invoke via `mesh_tool_call`. |
|
||||
| `mesh_mcp_list` | List MCP servers in the mesh with their tools + hosting peer. |
|
||||
| `mesh_tool_call` | Call a tool on a mesh-registered MCP server. Routes: caller → broker → hosting peer → execute → result back. 30s timeout. |
|
||||
| `mesh_mcp_remove` | Unregister a peer-hosted MCP server. |
|
||||
|
||||
### 2.18 MCP registry tier 2 — broker-deployed (7)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `mesh_mcp_deploy` | Deploy an MCP server from zip (via `file_id`), git URL, or npx package. Runs on broker VPS in Docker sandbox. Scope: `peer` (default), `mesh`, or `{group/groups/role/peers}`. Runtime: node / python / bun. Memory, network_allow, env with `$vault:` references. |
|
||||
| `mesh_mcp_undeploy` | Stop and remove a managed MCP server. |
|
||||
| `mesh_mcp_update` | Pull latest + restart a git-sourced server. |
|
||||
| `mesh_mcp_logs` | Tail recent logs from a managed server. |
|
||||
| `mesh_mcp_scope` | Get or set visibility scope. |
|
||||
| `mesh_mcp_schema` | Inspect tool schemas for a deployed server. |
|
||||
| `mesh_mcp_catalog` | List all deployed services with status, scope, tool count. |
|
||||
|
||||
### 2.19 Vault (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `vault_set` | Store encrypted credential. `type: env` (string, injected as env var via `$vault:<key>`) or `type: file` (file written to `mount_path` in container). |
|
||||
| `vault_list` | List vault entries (keys + metadata only, no values). |
|
||||
| `vault_delete` | Remove a credential. |
|
||||
|
||||
### 2.20 URL watch (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `mesh_watch` | Watch a URL for changes. Modes: `hash` (SHA-256 body), `json` (jsonpath extract), `status` (HTTP code). Polling `interval` (min 5s). `notify_on: change \| match:<val> \| not_match:<val>`. Custom headers. |
|
||||
| `mesh_unwatch` | Stop watching by `watch_id`. |
|
||||
| `mesh_watches` | List active watches. |
|
||||
|
||||
### 2.21 Webhooks (3)
|
||||
|
||||
| Tool | v1 behavior |
|
||||
|---|---|
|
||||
| `create_webhook` | Create an inbound webhook. Returns a URL external services (GitHub, CI/CD, monitoring) can POST to. Payload becomes a mesh message to all peers. |
|
||||
| `list_webhooks` | List active webhooks. |
|
||||
| `delete_webhook` | Deactivate by name. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Broker WS protocol
|
||||
|
||||
`apps/broker/src/index.ts` dispatches 85 message types over a single WebSocket endpoint (`WS_PATH`). Each WS message is a client-initiated RPC; most of the 79 MCP tools above map 1:1 to a WS message. Some additional WS messages exist for connection lifecycle + internal routing.
|
||||
|
||||
### 3.1 Connection lifecycle (3)
|
||||
|
||||
- `hello` — client authentication. Ed25519 signature over `{meshId, memberId, pubkey, timestamp}`. Broker verifies, creates presence row, replies with `hello_ack`.
|
||||
- `hello_ack` — server → client, confirms authentication + sends restored peer state.
|
||||
- `get_clock` — get current simulation clock state.
|
||||
|
||||
### 3.2 Messaging (4 WS ops)
|
||||
|
||||
- `send` — send a message. Envelope contains sender, recipient (peer/group/*), priority, nonce, ciphertext.
|
||||
- `peer_dir_request` / `peer_dir_response` — peer-to-peer directory request (read_peer_file under the hood).
|
||||
- `peer_file_request` / `peer_file_response` — peer-to-peer file read.
|
||||
|
||||
### 3.3 Profile + presence (5)
|
||||
|
||||
- `set_status`, `set_summary`, `set_visible`, `set_profile`, `set_stats`
|
||||
|
||||
### 3.4 Groups (2)
|
||||
|
||||
- `join_group`, `leave_group`
|
||||
|
||||
### 3.5 State KV (3)
|
||||
|
||||
- `set_state`, `get_state`, `list_state`
|
||||
|
||||
### 3.6 Memory (3)
|
||||
|
||||
- `remember`, `recall`, `forget`
|
||||
|
||||
### 3.7 Files (5)
|
||||
|
||||
- `get_file`, `list_files`, `file_status`, `grant_file_access`, `delete_file`
|
||||
|
||||
### 3.8 Vectors (3)
|
||||
|
||||
- `vector_store`, `vector_search`, `vector_delete`, `list_collections`
|
||||
|
||||
### 3.9 Graph (2)
|
||||
|
||||
- `graph_query`, `graph_execute`
|
||||
|
||||
### 3.10 Shared SQL (3)
|
||||
|
||||
- `mesh_query`, `mesh_execute`, `mesh_schema`
|
||||
|
||||
### 3.11 Streams (4)
|
||||
|
||||
- `create_stream`, `publish`, `subscribe`, `unsubscribe`, `list_streams`
|
||||
|
||||
### 3.12 Contexts (3)
|
||||
|
||||
- `share_context`, `get_context`, `list_contexts`
|
||||
|
||||
### 3.13 Tasks (4)
|
||||
|
||||
- `create_task`, `claim_task`, `complete_task`, `list_tasks`
|
||||
|
||||
### 3.14 Scheduling (3)
|
||||
|
||||
- `schedule`, `list_scheduled`, `cancel_scheduled`
|
||||
|
||||
### 3.15 Mesh metadata (3)
|
||||
|
||||
- `mesh_info`, `peers_list` (from `list_peers`), `message_status`
|
||||
|
||||
### 3.16 Simulation clock (4)
|
||||
|
||||
- `set_clock`, `pause_clock`, `resume_clock`, `get_clock`
|
||||
|
||||
### 3.17 Skills (4)
|
||||
|
||||
- `share_skill`, `get_skill`, `list_skills`, `remove_skill`, `skill_deploy`
|
||||
|
||||
### 3.18 MCP registry (11)
|
||||
|
||||
- `mcp_register`, `mcp_unregister`, `mcp_list`, `mcp_call`, `mcp_call_response` (peer → peer relay)
|
||||
- `mcp_deploy`, `mcp_undeploy`, `mcp_update`, `mcp_logs`, `mcp_scope`, `mcp_schema`, `mcp_catalog`
|
||||
|
||||
### 3.19 Vault (4)
|
||||
|
||||
- `vault_set`, `vault_get`, `vault_list`, `vault_delete`
|
||||
|
||||
### 3.20 URL watch (3)
|
||||
|
||||
- `watch`, `unwatch`, `watch_list`
|
||||
|
||||
### 3.21 Webhooks (3)
|
||||
|
||||
- `create_webhook`, `list_webhooks`, `delete_webhook`
|
||||
|
||||
### 3.22 Audit (2)
|
||||
|
||||
- `audit_query`, `audit_verify`
|
||||
|
||||
---
|
||||
|
||||
## 4. Broker HTTP endpoints
|
||||
|
||||
The broker serves both WS (`/ws`) and HTTP on the same port. HTTP endpoints are listed here by (method, path) with purpose.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `GET` | `/health` | Health check: liveness probe |
|
||||
| `GET` | `/metrics` | Prometheus metrics endpoint |
|
||||
| `POST` | `/hook/set-status` | Receive hook status updates from CLI `hook` command (Claude Code session lifecycle) |
|
||||
| `POST` | `/join` | Accept v1 invite join (legacy) |
|
||||
| `POST` | `/invites/:code/claim` | v2 invite claim (public, unauthenticated) |
|
||||
| `POST` | `/upload` | Upload a file (returns fileId, used by `share_file`) |
|
||||
| `GET` | `/download/:id` | Download a file (returns content or presigned URL) |
|
||||
| `POST` | `/cli-sync` | CLI sync endpoint — fetches user's meshes from `claudemesh.com` dashboard via JWT, returns mesh list |
|
||||
| `POST` | `/tg/token` | Register a Telegram bot token for a mesh (connects via `connect telegram` CLI command) |
|
||||
| `PATCH` | `/mesh/:id/member/:memberId` | Update a member's profile (admin or self) |
|
||||
| `GET` | `/mesh/:id/members` | List mesh members |
|
||||
| `PATCH` | `/mesh/:id/settings` | Update mesh-level settings (owner/admin) |
|
||||
| `POST` | `/hook/:meshId/:webhookId` | Inbound webhook — external systems POST here to publish a mesh message |
|
||||
| `GET` | `/test/clock` | Dev-only: simulation clock state |
|
||||
| `GET` | `/test/flip` | Dev-only: test flip endpoint |
|
||||
| `GET` | `/test/html` | Dev-only: test HTML endpoint |
|
||||
| `WS` | `/ws` | WebSocket connection for mesh peers (all WS ops above) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Database schema — `mesh` Postgres schema
|
||||
|
||||
23 tables in the `mesh` schema (managed via Drizzle). Defined in `packages/db/src/schema/mesh.ts`.
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `mesh.mesh` | Mesh identity. slug, name, ownerId, createdAt, settings. |
|
||||
| `mesh.member` | Per-mesh member record. Stable, durable. pubkey, displayName, role, groups, joinedAt. |
|
||||
| `mesh.invite` | Invite codes + metadata. |
|
||||
| `mesh.pending_invite` | v2 invite handshake state (pending claim). |
|
||||
| `mesh.audit_log` | Audit events per mesh. |
|
||||
| `mesh.presence` | Ephemeral WS session — one row per active connection. Status, statusSource, statusUpdatedAt. |
|
||||
| `mesh.message_queue` | Queued messages pending push delivery (priority ordered). |
|
||||
| `mesh.pending_status` | In-flight status updates (10s TTL). |
|
||||
| `mesh.state` (meshState) | Shared KV state per mesh. |
|
||||
| `mesh.memory` (meshMemory) | Shared memories with full-text search. |
|
||||
| `mesh.file` (meshFile) | File metadata (uploader, size, sha256, persistence, storage location). |
|
||||
| `mesh.file_access` (meshFileAccess) | Per-recipient ACL on files. |
|
||||
| `mesh.file_key` (meshFileKey) | Per-recipient wrapped symmetric keys for E2E encryption. |
|
||||
| `mesh.context` (meshContext) | Shared context entries. |
|
||||
| `mesh.task` (meshTask) | Tasks with lifecycle (open, claimed, completed, cancelled). |
|
||||
| `mesh.stream` (meshStream) | Stream metadata. |
|
||||
| `mesh.skill` (meshSkill) | Skill registrations (name, content, frontmatter, tags). |
|
||||
| `mesh.webhook` (meshWebhook) | Inbound webhook registrations. |
|
||||
| `mesh.service` (meshService) | Deployed MCP server state (container ID, scope, env, runtime, memory, logs). |
|
||||
| `mesh.vault_entry` (meshVaultEntry) | Encrypted vault entries per (mesh, peer, key). |
|
||||
| `mesh.scheduled_message` | Scheduled / recurring reminders (cron + one-shot). |
|
||||
| `mesh.peer_state` | Per-peer state (groups, role, profile, message mode preference). |
|
||||
| `mesh.telegram_bridge` | Telegram bot registration per mesh. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Broker backend services
|
||||
|
||||
Five external services the broker manages at runtime. All currently work in v1 and ship in the default Docker Compose deployment.
|
||||
|
||||
| Service | Purpose | File | Per-mesh model |
|
||||
|---|---|---|---|
|
||||
| **Postgres** (Drizzle) | Primary data store for mesh schema. Also used for `mesh_execute` / `mesh_query` / `mesh_schema` shared-SQL tools via per-mesh schemas. | `db.ts` | Schema-per-mesh for shared SQL tools |
|
||||
| **Neo4j** | Graph queries (`graph_query`, `graph_execute`). | `neo4j-client.ts` | Database-per-mesh (Enterprise) or labeled-node fallback (Community) |
|
||||
| **Qdrant** | Vector embeddings + nearest-neighbor search. | `qdrant.ts` | Collection naming: `mesh_<meshId>_<collection>`, 1536-dim default, cosine distance |
|
||||
| **MinIO** | File storage for `share_file` / `get_file`. | `minio.ts` | Bucket-per-mesh: `mesh-<meshId>`. Persistent + ephemeral key paths. |
|
||||
| **Docker** | Runs deployed MCP servers in sandboxed containers. | `index.ts` (deploy handler) | Container-per-deployment. Read-only root, dropped caps, memory limits, network_allow. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Broker core subsystems
|
||||
|
||||
### 7.1 Status engine (`broker.ts`, 2066 lines)
|
||||
|
||||
**Battle-tested status model** ported from `claude-intercom`. Rules:
|
||||
|
||||
- Status sources are ranked: `hook` (3) > `manual` (2) > `jsonl` (1)
|
||||
- On status update:
|
||||
- If status **changed** → bump everything, record new source
|
||||
- If status **unchanged**, incoming source ≥ recorded → upgrade
|
||||
- If status **unchanged**, incoming source < recorded:
|
||||
- Recorded source still fresh → keep it (bump timestamp only)
|
||||
- Recorded source stale → downgrade to honest attribution
|
||||
- `HOOK_FRESHNESS_MS` window (default 60s) for "fresh" classification
|
||||
- `WORKING_TTL_MS` after which `working` status reverts to `idle`
|
||||
- `PENDING_TTL_MS = 10_000` for pending status cleanup
|
||||
- `TTL_SWEEP_INTERVAL_MS = 15_000` for periodic cleanup
|
||||
|
||||
**Must preserve** — this is the correctness engine for `set_status`, `list_peers`, and Claude Code's status line.
|
||||
|
||||
### 7.2 Message queue + priority delivery
|
||||
|
||||
- Messages are stored in `mesh.message_queue` with priority (`now`, `next`, `low`)
|
||||
- `now` messages bypass busy-gate and are pushed immediately
|
||||
- `next` messages wait for idle peer
|
||||
- `low` messages are pull-only (delivered when peer explicitly drains via `check_messages`)
|
||||
- Queue is drained via `drainForMember(meshId, memberId)` on WS message arrival or manual `check_messages`
|
||||
- Duplicate delivery prevention via `messageId` UUID tracking
|
||||
|
||||
### 7.3 Scheduled message delivery (`index.ts` in-memory + DB persistence)
|
||||
|
||||
- One-shot: `deliver_at` (timestamp) or `in_seconds`
|
||||
- Recurring: standard 5-field cron expression
|
||||
- Persists to `mesh.scheduled_message` table — survives broker restart
|
||||
- On broker start, pending schedules are re-registered
|
||||
- Delivery is via the normal `send_message` pipeline with `subtype: reminder`
|
||||
|
||||
### 7.4 URL watch subsystem (`index.ts`)
|
||||
|
||||
- Poller runs in-process (worker per watch)
|
||||
- Modes: `hash` (SHA-256 of body), `json` (extract jsonpath value), `status` (HTTP status)
|
||||
- `notify_on: change | match:<val> | not_match:<val>`
|
||||
- Persists to DB so watches survive broker restart
|
||||
- Min interval 5s, max 24h
|
||||
|
||||
### 7.5 Telegram bridge (`telegram-bridge.ts`, 1711 lines)
|
||||
|
||||
**Substantial subsystem.** Provides Telegram Bot API integration:
|
||||
|
||||
- Bot token registration per mesh via `POST /tg/token`
|
||||
- Long-polling or webhook mode
|
||||
- `tg:<username>` peer identity registration in the mesh's member table
|
||||
- Inbound Telegram messages → mesh `send_message` events with `subtype: telegram`
|
||||
- Outbound `send_message(to: "tg:<name>")` → Telegram Bot API call
|
||||
- Chat-to-mesh mapping (Telegram chat_id ↔ mesh peer)
|
||||
- User discovery (`connectChat`)
|
||||
- Bridge row persistence in `mesh.telegram_bridge`
|
||||
|
||||
**This is ~18% of the broker's total source**. v2 must either:
|
||||
1. Port the logic into a standalone MCP connector (`apps/mcp-telegram/`), or
|
||||
2. Keep this file in the broker and wire it into the v2 architecture unchanged (my recommendation per the previous conversation — bundled into the broker image)
|
||||
|
||||
Either way, **every behavior documented here must still work after v2 lands**.
|
||||
|
||||
### 7.6 Auth + crypto (`crypto.ts`, `broker-crypto.ts`, `jwt.ts`)
|
||||
|
||||
- **Hello signatures**: Ed25519 signed tuple of `(meshId, memberId, pubkey, timestamp)`. Verified on every WS connection. Replay protection via timestamp window.
|
||||
- **Invite verification**: canonical invite payload (`canonicalInvite`) signed by mesh owner, Ed25519 verified on claim
|
||||
- **JWT**: for `/cli-sync` endpoint — the CLI obtains a JWT from `claudemesh.com` via browser flow, passes it to the broker, broker verifies and returns the user's mesh list
|
||||
- **File envelopes**: client-side AES-GCM + per-recipient key wrapping (file_key table)
|
||||
|
||||
### 7.7 Rate limiting (`rate-limit.ts`)
|
||||
|
||||
- Per-peer rate limits on expensive operations
|
||||
- Currently in-process (not Redis-backed)
|
||||
- Enforces limits on `send`, `vector_store`, `mesh_execute`, `mesh_mcp_deploy`, etc.
|
||||
|
||||
### 7.8 Metrics (`metrics.ts`)
|
||||
|
||||
Prometheus metrics exposed at `/metrics`:
|
||||
- Request counts by op type
|
||||
- Latencies p50/p99
|
||||
- Connection counts per mesh
|
||||
- Message delivery counts by priority
|
||||
- Error rates
|
||||
|
||||
### 7.9 Audit log (`audit.ts`)
|
||||
|
||||
- Every mutation is audited to `mesh.audit_log`
|
||||
- Tamper-evidence via hash chaining
|
||||
- Accessible via `audit_query` and `audit_verify` WS ops
|
||||
|
||||
### 7.10 Member API (`member-api.ts`, 284 lines)
|
||||
|
||||
Exports:
|
||||
- `updateMemberProfile()` — used by `PATCH /mesh/:id/member/:memberId`
|
||||
- `listMeshMembers()` — used by `GET /mesh/:id/members`
|
||||
- `updateMeshSettings()` — used by `PATCH /mesh/:id/settings`
|
||||
|
||||
### 7.11 CLI sync (`cli-sync.ts`, 133 lines)
|
||||
|
||||
Exports `handleCliSync()` for `POST /cli-sync`. This is **already the "CLI sync meshes from dashboard" feature** — v2 will reuse this endpoint for its mesh-list refresh logic.
|
||||
|
||||
### 7.12 Webhook subsystem (`webhooks.ts`, 97 lines)
|
||||
|
||||
Handles `POST /hook/:meshId/:webhookId` inbound. Signature verification (HMAC), payload normalization, mesh message emission.
|
||||
|
||||
---
|
||||
|
||||
## 8. CLI core subsystems
|
||||
|
||||
### 8.1 WS client (`ws/client.ts`, 2191 lines)
|
||||
|
||||
**The biggest CLI file.** Implements the full WS protocol with:
|
||||
- Connection management, reconnect with exponential backoff
|
||||
- Message queue for offline buffering
|
||||
- Request/response correlation via `_reqId`
|
||||
- Ed25519 hello signature generation
|
||||
- Crypto envelope wrapping for `send_message` payloads
|
||||
- Push notification delivery (messages, state changes, system events)
|
||||
- Per-mesh connection pooling (one WS per mesh)
|
||||
|
||||
### 8.2 MCP server (`mcp/server.ts`, 2139 lines)
|
||||
|
||||
Second biggest CLI file. Implements:
|
||||
- MCP stdio transport (registered with Claude Code via `install.ts`)
|
||||
- Tool registry from `mcp/tools.ts`
|
||||
- Dispatch to 79 handlers (one per tool)
|
||||
- WS client pooling (one connection per mesh)
|
||||
- Crypto primitives for memory/state encryption
|
||||
- Inline file-read helpers for `read_peer_file`
|
||||
- Channel notification forwarding from broker → Claude Code via MCP elicitation
|
||||
|
||||
### 8.3 Crypto (`crypto/*.ts`)
|
||||
|
||||
- `keypair.ts` — Ed25519 keypair generation + persistence (`~/.claudemesh/keys/<mesh>.key`)
|
||||
- `envelope.ts` — NaCl `crypto_box` envelope wrapping
|
||||
- `file-crypto.ts` — AES-GCM file encryption + per-recipient key wrapping
|
||||
- `hello-sig.ts` — Hello signature generation/verification
|
||||
|
||||
### 8.4 Auth + invite (`auth/*.ts`, `invite/*.ts`, `lib/invite-v2.ts`)
|
||||
|
||||
- `callback-listener.ts` — local HTTP server that catches browser OAuth callback (for `sync` command)
|
||||
- `open-browser.ts` — cross-platform browser launcher
|
||||
- `pairing-code.ts` — pairing code display
|
||||
- `sync-with-broker.ts` — JWT-based sync from dashboard
|
||||
- `invite/parse.ts` — parse v1 invite URLs
|
||||
- `invite/enroll.ts` — enroll into a mesh from an invite
|
||||
- `lib/invite-v2.ts` — v2 invite format (short-code + signed payload)
|
||||
|
||||
### 8.5 State + config (`state/config.ts`)
|
||||
|
||||
- `~/.claudemesh/config.json` read/write (mesh list, keypairs, profile defaults)
|
||||
- 0600 permission enforcement
|
||||
- Schema validation
|
||||
|
||||
### 8.6 TUI primitives (`tui/*.ts`)
|
||||
|
||||
- `colors.ts` — hard-coded ANSI colors
|
||||
- `index.ts` — input helpers
|
||||
- `screen.ts` — raw-mode screen control
|
||||
- `spinner.ts` — simple spinner
|
||||
|
||||
### 8.7 Templates (`templates/index.ts`)
|
||||
|
||||
- `dev-team`, `research`, `ops-incident`, `simulation`, `personal`
|
||||
- Each template seeds initial state + preset groups
|
||||
|
||||
### 8.8 Tests
|
||||
|
||||
- `__tests__/crypto-roundtrip.test.ts` — crypto round-trip verification
|
||||
- `__tests__/invite-parse.test.ts` — invite URL parsing
|
||||
- No integration tests against a real broker
|
||||
|
||||
---
|
||||
|
||||
## 9. Infrastructure + deployment
|
||||
|
||||
### 9.1 Broker runtime (`env.ts`)
|
||||
|
||||
Environment variables the broker expects:
|
||||
- `DATABASE_URL` — Postgres connection
|
||||
- `NEO4J_URL`, `NEO4J_USER`, `NEO4J_PASSWORD`
|
||||
- `QDRANT_URL`
|
||||
- `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_USE_SSL`
|
||||
- `STATUS_TTL_SECONDS` — working status timeout
|
||||
- `HOOK_FRESH_WINDOW_SECONDS` — hook source freshness window
|
||||
- `TELEGRAM_BOT_TOKEN` — for bridge
|
||||
- `DASHBOARD_JWT_SECRET` — for `/cli-sync` verification
|
||||
- `PORT` (default 8787)
|
||||
- Various feature flags
|
||||
|
||||
### 9.2 CLI runtime
|
||||
|
||||
- Node >= 20 required (checked in `doctor`)
|
||||
- `claude` binary must be on PATH
|
||||
- `~/.claudemesh/` directory with config + keys
|
||||
- `~/.claude.json` MCP server registration
|
||||
- `~/.claude/settings.json` status hooks registration
|
||||
|
||||
### 9.3 Deployment (Coolify/Docker Compose)
|
||||
|
||||
- Broker deployed via Coolify + Gitea CI on OVHcloud VPS (`ic.claudemesh.com`)
|
||||
- WS endpoint: `wss://ic.claudemesh.com/ws`
|
||||
- HTTP endpoint: `https://ic.claudemesh.com`
|
||||
- Postgres, Neo4j, Qdrant, MinIO run as siblings in Docker Compose
|
||||
- Deployed MCP sandboxes use the host Docker daemon via socket mount
|
||||
|
||||
---
|
||||
|
||||
## 10. Features not in the tool/WS surface (behavioral)
|
||||
|
||||
These are v1 behaviors that exist but aren't enumerated as tools. Each must still work after v2.
|
||||
|
||||
| Feature | Location | Notes |
|
||||
|---|---|---|
|
||||
| Flag-first `claudemesh --resume xxx` routing | `cli/src/index.ts` §339 | Rewrites argv to `launch --resume xxx` |
|
||||
| Bare `claudemesh` → welcome wizard | `cli/src/index.ts` §334 | Runs `runWelcome()` |
|
||||
| Status hook auto-registration | `commands/install.ts` | Writes to `~/.claude/settings.json` |
|
||||
| Claude Code session hook handling | `commands/hook.ts` | Receives stdin JSON, posts to `/hook/set-status` |
|
||||
| Per-mesh keypair directory | `crypto/keypair.ts` | `~/.claudemesh/keys/<mesh>.key` with 0600 perms |
|
||||
| E2E file encryption with re-wrapping | `crypto/file-crypto.ts` + `mesh_file_key` table | `grant_file_access` re-wraps symmetric key for new recipient |
|
||||
| Priority message delivery | `broker.ts` | `now` bypasses busy-gate, `next` waits for idle, `low` is pull-only |
|
||||
| Hook > manual > jsonl status priority | `broker.ts` | Documented in §7.1 |
|
||||
| Simulation clock for test time | `index.ts` (broker) | Peers receive heartbeat ticks at simulated rate |
|
||||
| Audit log hash chaining | `audit.ts` | Tamper-evident — tools call `audit_verify` to check |
|
||||
| Dashboard-CLI sync | `auth/sync-with-broker.ts` + `cli-sync.ts` | Browser JWT flow, fetches mesh list from dashboard |
|
||||
| Telegram chat ↔ mesh peer mapping | `telegram-bridge.ts` | Bidirectional routing via `tg:<username>` |
|
||||
| Inbound webhook payload normalization | `webhooks.ts` | External systems POST, becomes a mesh message |
|
||||
| Rate limiting per peer per operation | `rate-limit.ts` | In-memory token buckets |
|
||||
| Prometheus metrics | `metrics.ts` | `/metrics` endpoint |
|
||||
|
||||
---
|
||||
|
||||
## 11. Test coverage (v1)
|
||||
|
||||
| Test | File | Notes |
|
||||
|---|---|---|
|
||||
| Crypto round-trip | `apps/cli/src/__tests__/crypto-roundtrip.test.ts` | Encrypt → decrypt verification |
|
||||
| Invite URL parsing | `apps/cli/src/__tests__/invite-parse.test.ts` | v1 and v2 formats |
|
||||
| Broker tests | `apps/broker/tests/*.test.ts` | broker.test.ts, invite-signature.test.ts, invite-v2.test.ts, hello-signature.test.ts, rate-limit.test.ts, encoding.test.ts, dup-delivery.test.ts, metrics.test.ts, logging.test.ts, integration/health.test.ts |
|
||||
|
||||
**v1 test coverage is minimal for the CLI side.** 2 unit test files for 12k LOC.
|
||||
|
||||
Broker has ~10 test files. They cover crypto primitives, invite flow, hello signatures, rate limiting, metrics — but **not** the 85 WS message handlers comprehensively.
|
||||
|
||||
---
|
||||
|
||||
## 12. The "must preserve" list (high-priority regression checks)
|
||||
|
||||
If v2 breaks any of these, it's a user-facing regression:
|
||||
|
||||
### 12.1 First-run experience
|
||||
- [ ] `claudemesh` bare command → welcome wizard
|
||||
- [ ] `claudemesh install` registers MCP server + status hooks in Claude Code config
|
||||
- [ ] `claudemesh join <url>` enrolls into a mesh from a v1 OR v2 invite URL
|
||||
- [ ] `claudemesh launch` starts Claude Code with mesh connectivity
|
||||
|
||||
### 12.2 Session lifecycle
|
||||
- [ ] Status hooks fire correctly on Claude Code session start/stop/pause
|
||||
- [ ] `set_status` honors priority (hook > manual > jsonl)
|
||||
- [ ] `list_peers` shows live status with freshness gating
|
||||
- [ ] Status TTL sweeper runs every 15s
|
||||
|
||||
### 12.3 Messaging
|
||||
- [ ] `send_message(to: peer, priority: "now")` delivers immediately
|
||||
- [ ] `send_message(to: peer, priority: "next")` waits for idle
|
||||
- [ ] `send_message(to: "@group")` broadcasts to group members
|
||||
- [ ] `send_message(to: "*")` broadcasts to all mesh peers
|
||||
- [ ] Offline recipients receive queued messages on reconnect
|
||||
- [ ] Duplicate delivery is prevented by `messageId` tracking
|
||||
|
||||
### 12.4 Cryptographic integrity
|
||||
- [ ] Ed25519 keypair generation + persistence with 0600 perms
|
||||
- [ ] Hello signature verification rejects replay within timestamp window
|
||||
- [ ] `send_message` envelopes are E2E encrypted (NaCl crypto_box)
|
||||
- [ ] File uploads are AES-GCM encrypted with per-recipient key wrapping
|
||||
- [ ] `grant_file_access` re-wraps symmetric key for a new recipient
|
||||
|
||||
### 12.5 All 79 MCP tools
|
||||
- [ ] Every tool in §2 dispatches correctly through the CLI's MCP server
|
||||
- [ ] Every tool delegates to the broker WS protocol or local handler as appropriate
|
||||
- [ ] No tool returns "not implemented" or throws an unexpected error
|
||||
|
||||
### 12.6 Broker backends
|
||||
- [ ] `mesh_query` / `mesh_execute` / `mesh_schema` work against per-mesh Postgres schema
|
||||
- [ ] `graph_query` / `graph_execute` work against per-mesh Neo4j database
|
||||
- [ ] `vector_store` / `vector_search` work against per-mesh Qdrant collection
|
||||
- [ ] `share_file` / `get_file` work through per-mesh MinIO bucket
|
||||
- [ ] `mesh_mcp_deploy` spawns a Docker container with correct scope + env + network_allow
|
||||
- [ ] `vault_set` + `$vault:<key>` env injection works end-to-end for deployed MCPs
|
||||
|
||||
### 12.7 Scheduled + URL watch
|
||||
- [ ] `schedule_reminder` with `cron` survives broker restart (persisted in DB)
|
||||
- [ ] `mesh_watch` polls at the specified interval and notifies on change
|
||||
- [ ] Watch state persists across broker restart
|
||||
|
||||
### 12.8 Telegram bridge
|
||||
- [ ] `connect telegram` registers bot token via `POST /tg/token`
|
||||
- [ ] Bot token is stored in `mesh.telegram_bridge`
|
||||
- [ ] Inbound Telegram messages are routed as mesh messages
|
||||
- [ ] `send_message(to: "tg:<username>")` routes via Telegram Bot API
|
||||
- [ ] `disconnect telegram` tears down the bridge cleanly
|
||||
|
||||
### 12.9 Dashboard sync
|
||||
- [ ] `claudemesh sync` browser flow completes and fetches mesh list
|
||||
- [ ] `POST /cli-sync` with valid JWT returns user's dashboard meshes
|
||||
|
||||
### 12.10 Webhooks
|
||||
- [ ] `create_webhook` returns a POST URL
|
||||
- [ ] External POST to webhook URL becomes a mesh message
|
||||
- [ ] HMAC signature validation rejects unsigned requests
|
||||
- [ ] `list_webhooks` + `delete_webhook` work
|
||||
|
||||
### 12.11 Doctor checks
|
||||
- [ ] Node >= 20 check
|
||||
- [ ] `claude` binary on PATH
|
||||
- [ ] MCP server registered in `~/.claude.json`
|
||||
- [ ] Status hooks registered in `~/.claude/settings.json`
|
||||
- [ ] `~/.claudemesh/config.json` exists + parses + 0600 perms
|
||||
- [ ] Mesh keypairs valid
|
||||
|
||||
---
|
||||
|
||||
## 13. What v2 is adding (net new)
|
||||
|
||||
Not part of the regression list, but tracked here so we don't lose sight of the forward-looking scope.
|
||||
|
||||
### 13.1 New CLI features (from user's stated v2 intent)
|
||||
|
||||
- [ ] `claudemesh login` — device-code OAuth against claudemesh.com's Better Auth backend
|
||||
- [ ] `claudemesh register` — create a new account from the CLI (via browser handoff)
|
||||
- [ ] `claudemesh new` — create a mesh from the CLI against `POST /api/my/meshes` (not via templates in the CLI — via dashboard API)
|
||||
- [ ] `claudemesh invite` — generate an invite from the CLI via `POST /api/my/meshes/:slug/invites`
|
||||
- [ ] `claudemesh whoami` — show current identity + token source
|
||||
- [ ] `claudemesh logout` — revoke server-side session + clear local credentials
|
||||
|
||||
### 13.2 Architecture improvements (from user's v2 intent)
|
||||
|
||||
- [ ] Feature-folder `services/` layer with strict facade boundaries
|
||||
- [ ] ESLint + dependency-cruiser boundary enforcement
|
||||
- [ ] `cli/` vs `ui/` separation (non-Ink I/O vs Ink rendering)
|
||||
- [ ] `entrypoints/` folder with cli + mcp entries
|
||||
- [ ] Typed error classes per service with `toDomainError` helper
|
||||
- [ ] Coverage threshold enforcement in CI
|
||||
|
||||
### 13.3 Not in v1.0.0 scope (defer to v1.1+)
|
||||
|
||||
Everything from the Composer 2 review rounds that isn't Pass 1:
|
||||
|
||||
- Local-first SQLite source of truth (Lamport, sync daemon, publish transaction)
|
||||
- Broker security hardening (role-per-mesh Postgres, Docker egress proxy, SSRF policy)
|
||||
- ICU MessageFormat + per-locale budgets
|
||||
- Accessibility token-signal matrix
|
||||
- Tiered MCP catalog + audit process
|
||||
- session_kind enum
|
||||
- NFC peer_id normalization
|
||||
- Write queue state machine
|
||||
|
||||
These stay in the `.artifacts/specs/` as reference documents. They describe a good destination. They are NOT v1.0.0 requirements.
|
||||
|
||||
---
|
||||
|
||||
## 14. Known v1 technical debt / gaps (worth noting)
|
||||
|
||||
These aren't features — they're places where v1 is weaker than it could be. Document here so v2 doesn't blindly port the weaknesses.
|
||||
|
||||
- **CLI auth is missing** — v1 has no `login` / `logout` command. All account-level operations require the web dashboard. This is what v2 is adding.
|
||||
- **Imperative command branching** — `commands/launch.ts` is 775 lines with nested flag handling. Cleaner in v2's flow pipeline.
|
||||
- **Minimal CLI test coverage** — 2 test files for 12k LOC. v2 should have colocated tests per service.
|
||||
- **Rate limiting is in-memory only** — doesn't survive broker restart; not Redis-backed.
|
||||
- **No CLI-side caching** — every `list_peers` / `mesh_info` call hits the broker. v2's local-first layer (Pass 2) addresses this.
|
||||
- **Telegram bridge is a large monolithic file** (1711 lines) — legitimate complexity, but v2 may want to modularize if it touches it.
|
||||
- **v1 wizard bleed-through** — `launch` → `claude` handoff leaves ANSI state dirty. v2's `resetTerminal()` choke point fixes this.
|
||||
|
||||
None of these are regressions if v2 keeps them as-is. v2 should **not** prioritize fixing them — fix them when they become a problem, not speculatively.
|
||||
|
||||
---
|
||||
|
||||
## 15. Reading this inventory
|
||||
|
||||
**If you're implementing v2 Phase 1** (foundation layers): every tool in §2, every WS op in §3, every HTTP endpoint in §4, every DB table in §5 must have a place in the v2 folder structure. No new semantics, no improved algorithms — just move the working code.
|
||||
|
||||
**If you're reviewing a v2 PR**: check it against §12 ("must preserve" list). If the PR changes the behavior of anything in that list, it's a regression and needs explicit sign-off.
|
||||
|
||||
**If you're writing v2 docs**: reference this document. Every feature here is user-visible and documented in v1's README / slash-command help / tool descriptions. v2 docs should mention every feature from §2 as preserved.
|
||||
|
||||
---
|
||||
|
||||
**End of inventory.**
|
||||
1068
.artifacts/backlog/2026-04-11-v2-parity-test-plan.md
Normal file
BIN
.artifacts/hero-animation/clawd-apple-zoom.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
.artifacts/hero-animation/clawd-zoom-v2.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
.artifacts/hero-animation/clawd-zoom.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
.artifacts/hero-animation/fcc-preview-v1.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
.artifacts/hero-animation/fcc-preview-v2.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
.artifacts/hero-animation/fcc-preview-v3.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
.artifacts/hero-animation/features-section.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
.artifacts/hero-animation/features-with-skills.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
.artifacts/hero-animation/frame-01-alone.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
.artifacts/hero-animation/hero-with-mesh-v1.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
.artifacts/hero-animation/landing-cover.png
Normal file
|
After Width: | Height: | Size: 475 KiB |
BIN
.artifacts/hero-animation/landing-live.png
Normal file
|
After Width: | Height: | Size: 462 KiB |
BIN
.artifacts/hero-animation/mesh-constellation-v1.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
.artifacts/hero-animation/mesh-constellation-v2.png
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
.artifacts/hero-animation/mesh-constellation-v3.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
.artifacts/hero-animation/mesh-hero-apple-clawd.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
.artifacts/hero-animation/mesh-hero-clip.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
.artifacts/hero-animation/mesh-hero-full.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
.artifacts/hero-animation/mesh-hero-v1.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
.artifacts/hero-animation/mesh-icon-big.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
.artifacts/hero-animation/mesh-no-overlap.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
.artifacts/hero-animation/mesh-peers-equal.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
.artifacts/hero-animation/mesh-trail-5700.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
.artifacts/hero-animation/mesh-trail-inflight.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
.artifacts/hero-animation/mesh-trail-top.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
.artifacts/hero-animation/mesh-trail-v1.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
.artifacts/hero-animation/mesh-trail-v2.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
.artifacts/hero-animation/mesh-triangle.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
.artifacts/hero-animation/mesh-zoom-mid.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
.artifacts/hero-animation/prompt-box-early.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
.artifacts/hero-animation/prompt-input-live.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
.artifacts/hero-animation/reference.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
.artifacts/hero-animation/responsive-1200.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
.artifacts/hero-animation/responsive-1700.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
.artifacts/hero-animation/responsive-800.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
.artifacts/hero-animation/session-mid-2.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
.artifacts/hero-animation/session-mid-3.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
.artifacts/hero-animation/session-mid.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.artifacts/hero-animation/where-mesh-fits-v2.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
.artifacts/hero-animation/where-mesh-fits.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
29
.artifacts/prompts/claudemesh-prompts.rtf
Normal file
@@ -0,0 +1,29 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2867
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
{\*\expandedcolortbl;;}
|
||||
\margl1440\margr1440\vieww11180\viewh8060\viewkind0
|
||||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||
|
||||
\f0\fs24 \cf0 Mesh templates for predefined roles, groups\'85\
|
||||
Mesh blockchain, can it be a good addition? For what?\
|
||||
Mesh webhooks, external web sockets, restful apis to be connected to the mesh (mcp)\
|
||||
Mesh skills available for all ai? Like a mesh catalog of skills for sessions to get and use them?\
|
||||
Inicial private mesh by default for every new user\
|
||||
Mesh dashboard for situational awareness of mesh, to illustrate the peers connected, their activity, status, mesh structure\
|
||||
Mesh of meshes? bridge?\
|
||||
Mesh Connectors: slack, telegram, they can appear as peers? Or sth different?\
|
||||
Connect humans to the mesh? Peer info to know about if human, type of channel (telegram or whatever) or llm model if ai?\
|
||||
How to connect others than just claude code? The problem will be the push system I suppose\
|
||||
\
|
||||
Add path (pwd) where each session is being executed for them to understand how to reference files if same computer? Maybe only visible for peers on same computer?\
|
||||
What if a peer on connection can make available all the project files, folders and subfolders? Direct access? So other ai can read files if needed from connected projects?\
|
||||
Can we have peer stats for example about context consumption?\
|
||||
Mesh notifications about new peers, new connectors, new resources? Broadcast?\
|
||||
Allow group or role changes dynamically not only on mesh connection?\
|
||||
Dynamic mcp that can be connected or disconnected on realtime without resetting the claude code sessions?\
|
||||
Mesh templates on creation, with a predefined structure that it can be changed as well by mesh admin role? Or any? Or what idea?\
|
||||
What if reminders can be just cron so ai knows exactly how to configure crons for the mesh? So broker can handle the cron creation? What about mesh heartbeats to keep ai alive?\
|
||||
Sandbox for code execution, python, node, chromium, etc so any peer can connect to resources, and resources being scalable on real time if a new peer needs a sandbox?\
|
||||
\
|
||||
}
|
||||
593
.artifacts/specs/2026-04-10-cli-auth-device-code-pat.md
Normal file
@@ -0,0 +1,593 @@
|
||||
# CLI Auth — Device Code Flow + Personal Access Tokens
|
||||
|
||||
**Status:** spec
|
||||
**Created:** 2026-04-10
|
||||
**Owner:** CLI-Dev (implementation), Orchestrator (spec)
|
||||
**Target version:** v0.11.0
|
||||
**Related:** `2026-04-10-anthropic-vision-meshes-invites.md`, `2026-04-10-cli-wizard-architecture-refactor.md`
|
||||
|
||||
## Goal
|
||||
|
||||
The CLI is a first-class client. From a fresh terminal, with zero prior browser interaction, a user can:
|
||||
|
||||
```
|
||||
claudemesh login # device-code OAuth, browser handshake
|
||||
claudemesh create "Platform team" # creates real mesh via /api/my/meshes
|
||||
claudemesh invite --email alice@x.com # generates invite, sends email
|
||||
claudemesh launch --mesh platform-team -y # spawns Claude Code in the mesh
|
||||
```
|
||||
|
||||
For CI / scripting / non-interactive contexts, PAT works too:
|
||||
|
||||
```
|
||||
claudemesh login --token cm_pat_abc123
|
||||
claudemesh create "CI test mesh" --json | jq .id
|
||||
```
|
||||
|
||||
This is the auth substrate that unblocks the "Anthropic vision" — every other dashboard-only feature (meshes, invites, members, billing) becomes CLI-accessible after this lands.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- SSO / SAML / enterprise IdP integration (later, post-1.0)
|
||||
- Refresh tokens with rotation (long-lived API keys are sufficient for v1)
|
||||
- Multi-account switching (one logged-in identity per `~/.claudemesh/auth.json`)
|
||||
- Device fleet management UI (single "revoke" button per token is enough for v1)
|
||||
|
||||
## Auth model overview
|
||||
|
||||
Two coexisting credential types, both backed by **Better Auth's `apiKey` plugin**:
|
||||
|
||||
| Type | Created via | Lifetime | Use case | Storage |
|
||||
|---|---|---|---|---|
|
||||
| **Device-code session token** | `claudemesh login` (OAuth-style browser handshake) | 90 days, auto-renew on use | Interactive humans on their workstation | `~/.claudemesh/auth.json` |
|
||||
| **Personal access token (PAT)** | Dashboard → Settings → CLI tokens → Generate | User-chosen (30d / 90d / 1y / never), explicit revocation | CI, scripts, automation, server-side cron | Anywhere the user puts it; CLI reads from `--token` flag, env var, or `auth.json` |
|
||||
|
||||
Both flow through the same `Authorization: Bearer cm_<type>_<random>` header. The API doesn't care which one it gets — it just validates against the `api_key` table.
|
||||
|
||||
**Token format:**
|
||||
- `cm_session_<32-byte base32>` — device-code sessions
|
||||
- `cm_pat_<32-byte base32>` — personal access tokens
|
||||
|
||||
The `cm_` prefix lets us scan for leaked tokens with regex (e.g. GitHub secret scanning, internal scripts). The middle segment (`session` / `pat`) is for human readability in token lists, not for security.
|
||||
|
||||
## User flows
|
||||
|
||||
### 1. First-time login (interactive happy path)
|
||||
|
||||
```
|
||||
$ claudemesh login
|
||||
|
||||
██ claudemesh login
|
||||
|
||||
Opening browser for authentication…
|
||||
|
||||
If your browser didn't open, visit:
|
||||
https://claudemesh.com/cli-auth?code=ABCD-EFGH
|
||||
|
||||
Enter this code:
|
||||
ABCD-EFGH
|
||||
|
||||
Waiting for confirmation… ⠋
|
||||
```
|
||||
|
||||
In the browser:
|
||||
1. User lands on `/cli-auth?code=ABCD-EFGH`
|
||||
2. If not signed in, Better Auth login screen appears, then redirects back
|
||||
3. User sees a confirmation card:
|
||||
```
|
||||
Link this CLI session?
|
||||
Code: ABCD-EFGH
|
||||
Device: Alejandro's MacBook Pro · darwin · arm64
|
||||
Expires in 9:47
|
||||
[Approve] [Deny]
|
||||
```
|
||||
4. User clicks Approve
|
||||
|
||||
CLI polls every 1.5s, sees `approved`, receives token, writes `~/.claudemesh/auth.json` with `0600`, prints:
|
||||
|
||||
```
|
||||
✔ Authenticated as Alejandro Gutiérrez
|
||||
✔ Token saved to ~/.claudemesh/auth.json
|
||||
✔ Synced 3 meshes: alexis-mou, dev, claudefarm
|
||||
|
||||
Run claudemesh --help to get started.
|
||||
```
|
||||
|
||||
### 2. First-time login (PAT, non-interactive)
|
||||
|
||||
```
|
||||
$ claudemesh login --token cm_pat_abc123def456...
|
||||
✔ Authenticated as Alejandro Gutiérrez (via PAT "ci-deploy")
|
||||
✔ Token saved to ~/.claudemesh/auth.json
|
||||
```
|
||||
|
||||
Or one-shot, no save:
|
||||
|
||||
```
|
||||
$ CLAUDEMESH_TOKEN=cm_pat_abc123 claudemesh create "test"
|
||||
```
|
||||
|
||||
### 3. Already logged in, runs a command
|
||||
|
||||
```
|
||||
$ claudemesh create "Platform team"
|
||||
✔ Created mesh platform-team (id: q5RI89Fl…)
|
||||
✔ Joined locally
|
||||
▸ Invite peers: claudemesh invite --mesh platform-team
|
||||
```
|
||||
|
||||
No auth prompt — token in `auth.json` is used silently.
|
||||
|
||||
### 4. Token expired or revoked
|
||||
|
||||
```
|
||||
$ claudemesh peers
|
||||
✘ Authentication failed (token expired or revoked)
|
||||
|
||||
Run claudemesh login to re-authenticate.
|
||||
```
|
||||
|
||||
Exit code `2`. The `auth.json` is **not** auto-deleted (user might be debugging) but the next `claudemesh login` overwrites it cleanly.
|
||||
|
||||
### 5. Wizard launch flow with auth integration
|
||||
|
||||
When `claudemesh` (bare, no auth) is run:
|
||||
|
||||
```
|
||||
██ claudemesh
|
||||
|
||||
▸ Sign in (opens browser)
|
||||
Paste a personal access token
|
||||
Join a mesh via invite URL
|
||||
Exit
|
||||
```
|
||||
|
||||
After auth completes, the wizard transitions naturally into the launch flow (mesh picker → name → role → confirm → handoff). One uninterrupted experience from "fresh install" to "Claude Code in a mesh."
|
||||
|
||||
### 6. CI / non-interactive
|
||||
|
||||
```
|
||||
# .github/workflows/test.yml
|
||||
- run: |
|
||||
claudemesh login --token ${{ secrets.CLAUDEMESH_PAT }}
|
||||
claudemesh create "CI run $GITHUB_RUN_ID" --json > mesh.json
|
||||
```
|
||||
|
||||
Or zero-state:
|
||||
|
||||
```
|
||||
- env:
|
||||
CLAUDEMESH_TOKEN: ${{ secrets.CLAUDEMESH_PAT }}
|
||||
run: claudemesh create "CI run $GITHUB_RUN_ID" --json
|
||||
```
|
||||
|
||||
Token resolution order: `--token` flag > `CLAUDEMESH_TOKEN` env var > `~/.claudemesh/auth.json`.
|
||||
|
||||
### 7. Logout
|
||||
|
||||
```
|
||||
$ claudemesh logout
|
||||
✔ Token revoked on server
|
||||
✔ Removed ~/.claudemesh/auth.json
|
||||
```
|
||||
|
||||
`logout` calls `DELETE /api/my/cli/sessions/current` to revoke server-side, then unlinks the local file. Best-effort: if the server call fails, still delete locally and warn.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend — Better Auth `apiKey` plugin
|
||||
|
||||
Better Auth ships an `apiKey` plugin that handles:
|
||||
- Token generation (cryptographically random)
|
||||
- Hashed storage (only the hash hits the DB; raw token never persisted)
|
||||
- Verification middleware (validates `Authorization: Bearer …`)
|
||||
- Per-token metadata (name, scopes, expiry, last-used)
|
||||
- Per-token revocation
|
||||
|
||||
We use it for both PAT and device-code sessions. Device-code sessions just have a marker in metadata distinguishing them from user-generated PATs.
|
||||
|
||||
**Wire-up:** `apps/web/src/lib/auth/index.ts` (or wherever Better Auth is initialized) adds:
|
||||
|
||||
```ts
|
||||
import { apiKey } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
// …existing config
|
||||
plugins: [
|
||||
// …
|
||||
apiKey({
|
||||
enableMetadata: true,
|
||||
apiKeyHeaders: ["x-api-key", "authorization"],
|
||||
defaultPrefix: "cm_",
|
||||
rateLimit: { enabled: true, timeWindow: 60_000, maxRequests: 100 },
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Backend — device-code table
|
||||
|
||||
The `apiKey` plugin doesn't ship device-code flow out of the box. We add a small table + 4 endpoints on top.
|
||||
|
||||
```sql
|
||||
-- packages/db/migrations/0020_cli-device-code.sql
|
||||
CREATE TABLE cli_device_code (
|
||||
device_code text PRIMARY KEY, -- opaque random, sent to CLI
|
||||
user_code text UNIQUE NOT NULL, -- short human code: "ABCD-EFGH"
|
||||
user_id text REFERENCES "user"(id), -- nullable until approved
|
||||
api_key_id text REFERENCES api_key(id), -- the issued token, set on approve
|
||||
device_name text NOT NULL, -- "Alejandro's MacBook Pro"
|
||||
device_os text NOT NULL, -- "darwin"
|
||||
device_arch text NOT NULL, -- "arm64"
|
||||
ip_address text, -- for audit
|
||||
user_agent text,
|
||||
status text NOT NULL DEFAULT 'pending', -- 'pending' | 'approved' | 'denied' | 'expired'
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL, -- created_at + 10 min
|
||||
approved_at timestamptz
|
||||
);
|
||||
|
||||
CREATE INDEX cli_device_code_user_code_idx ON cli_device_code(user_code);
|
||||
CREATE INDEX cli_device_code_status_expires_idx ON cli_device_code(status, expires_at);
|
||||
```
|
||||
|
||||
A scheduled job (or lazy cleanup on insert) deletes rows where `status='expired'` AND `expires_at < now() - interval '7 days'`.
|
||||
|
||||
### Backend — endpoints
|
||||
|
||||
All under `apps/web/src/app/api/auth/cli/` (or wherever you keep public auth routes — these need to be **unauthed** since the CLI has no token yet).
|
||||
|
||||
| Method | Path | Auth | Purpose |
|
||||
|---|---|---|---|
|
||||
| `POST` | `/api/auth/cli/device-code` | none | CLI requests a new device code. Body: `{ device_name, device_os, device_arch }`. Returns `{ device_code, user_code, expires_at, verification_url }`. |
|
||||
| `GET` | `/api/auth/cli/device-code/:device_code` | none | CLI polls for status. Returns `{ status: 'pending'|'approved'|'denied'|'expired', token?: string, user?: { id, name, email } }`. Token only present when status=approved, and only **once** (subsequent polls return approved without token). |
|
||||
| `POST` | `/api/auth/cli/device-code/:user_code/approve` | session | Browser confirms. Creates an `api_key` row with metadata `{ kind: 'session', device_name, device_code }`, sets `cli_device_code.api_key_id`, status=approved. |
|
||||
| `POST` | `/api/auth/cli/device-code/:user_code/deny` | session | Browser denies. Sets status=denied. |
|
||||
|
||||
Authed endpoints (under `/api/my/cli/`):
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/my/cli/sessions` | List active CLI sessions for the user (devices, last seen, created). |
|
||||
| `DELETE` | `/api/my/cli/sessions/:id` | Revoke a specific session. |
|
||||
| `POST` | `/api/my/cli/tokens` | Create a PAT. Body: `{ name, expires_in_days?, scopes? }`. Returns the raw token **once**. |
|
||||
| `GET` | `/api/my/cli/tokens` | List PATs (no raw values, just metadata). |
|
||||
| `DELETE` | `/api/my/cli/tokens/:id` | Revoke a PAT. |
|
||||
|
||||
### Backend — middleware
|
||||
|
||||
Existing `enforceAuth` (in `packages/api/src/utils/`) currently reads cookies. Extend it to also accept `Authorization: Bearer cm_…`:
|
||||
|
||||
```ts
|
||||
export async function enforceAuth(ctx) {
|
||||
const bearer = ctx.req.headers.get("authorization")?.replace(/^Bearer /, "");
|
||||
if (bearer?.startsWith("cm_")) {
|
||||
const result = await auth.api.verifyApiKey({ key: bearer });
|
||||
if (result.valid) {
|
||||
// record last_used_at, increment usage counter
|
||||
return { user: result.user, via: "apiKey", apiKey: result.apiKey };
|
||||
}
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid token" });
|
||||
}
|
||||
// …existing cookie-based auth
|
||||
}
|
||||
```
|
||||
|
||||
The `apiKey` plugin handles `last_used_at` updates automatically.
|
||||
|
||||
### Backend — web route
|
||||
|
||||
`apps/web/src/app/[locale]/cli-auth/page.tsx`:
|
||||
|
||||
- Reads `?code=ABCD-EFGH` from query string
|
||||
- If no session, redirects to `/login?next=/cli-auth?code=ABCD-EFGH`
|
||||
- If session, fetches device code metadata via server component, renders confirmation card
|
||||
- Approve button → `POST /api/auth/cli/device-code/:user_code/approve`
|
||||
- Deny button → `POST /api/auth/cli/device-code/:user_code/deny`
|
||||
- After approve, shows: "✓ CLI authenticated. Return to your terminal."
|
||||
|
||||
Mobile-friendly. Confirmation card shows device fingerprint so the user can verify they're approving the right session.
|
||||
|
||||
### Backend — dashboard PAT UI
|
||||
|
||||
`apps/web/src/app/[locale]/dashboard/settings/cli-tokens/page.tsx`:
|
||||
|
||||
- List of existing PATs (name, created, last used, expires)
|
||||
- "Generate new token" button → modal with name + expiry picker
|
||||
- After creation, show raw token once with copy button + warning ("This token will not be shown again")
|
||||
- Per-row revoke button
|
||||
|
||||
Reuses existing dashboard layout. Should be ~150 lines including the modal.
|
||||
|
||||
### CLI — file layout
|
||||
|
||||
```
|
||||
apps/cli/src/
|
||||
├── commands/
|
||||
│ ├── login.ts # NEW
|
||||
│ ├── logout.ts # NEW
|
||||
│ ├── whoami.ts # NEW
|
||||
│ ├── create.ts # rewrite to call API
|
||||
│ ├── invite.ts # NEW
|
||||
│ ├── sync.ts # rewrite to call API
|
||||
│ └── …existing
|
||||
└── lib/
|
||||
├── auth-store.ts # NEW: read/write ~/.claudemesh/auth.json
|
||||
├── api-client.ts # NEW: typed fetch wrapper
|
||||
├── device-info.ts # NEW: collect hostname, os, arch for device-code request
|
||||
└── …existing
|
||||
```
|
||||
|
||||
### CLI — `auth-store.ts`
|
||||
|
||||
```ts
|
||||
// ~/.claudemesh/auth.json
|
||||
type AuthFile = {
|
||||
version: 1;
|
||||
token: string; // cm_session_… or cm_pat_…
|
||||
user: { id: string; name: string; email: string };
|
||||
created_at: string; // ISO
|
||||
source: "device-code" | "pat" | "env";
|
||||
};
|
||||
```
|
||||
|
||||
Read priority: `--token` flag > `CLAUDEMESH_TOKEN` env > `auth.json`.
|
||||
Write only on `login` success. File mode `0600`. Parent dir `0700`.
|
||||
On read, if file mode is too permissive, log a warning and continue.
|
||||
|
||||
### CLI — `api-client.ts`
|
||||
|
||||
Thin wrapper over `fetch`:
|
||||
|
||||
```ts
|
||||
export class ClaudemeshApi {
|
||||
constructor(private opts: { baseUrl: string; token: string }) {}
|
||||
|
||||
async createMesh(input: { name: string; slug?: string }) { … }
|
||||
async listMeshes() { … }
|
||||
async createInvite(input: { meshId: string; email?: string; role?: string }) { … }
|
||||
async listSessions() { … }
|
||||
async revokeSession(id: string) { … }
|
||||
async whoami() { … }
|
||||
}
|
||||
```
|
||||
|
||||
Type definitions live in `packages/api/src/contracts/cli.ts` (new file) — generated from the existing tRPC routers as plain types so the CLI doesn't need to import the whole tRPC client.
|
||||
|
||||
Base URL from `CLAUDEMESH_API_URL` env var, defaults to `https://claudemesh.com`. Allows local dev against `http://localhost:3000`.
|
||||
|
||||
### CLI — device-code login flow
|
||||
|
||||
```ts
|
||||
// commands/login.ts
|
||||
async function deviceCodeLogin() {
|
||||
const device = collectDeviceInfo();
|
||||
const { device_code, user_code, expires_at, verification_url } =
|
||||
await api.requestDeviceCode(device);
|
||||
|
||||
console.log(` Opening ${verification_url}…`);
|
||||
console.log(` Code: ${user_code}`);
|
||||
|
||||
await openBrowser(`${verification_url}?code=${user_code}`);
|
||||
|
||||
const spinner = ora("Waiting for confirmation").start();
|
||||
const deadline = new Date(expires_at).getTime();
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(1500);
|
||||
const result = await api.pollDeviceCode(device_code);
|
||||
if (result.status === "approved") {
|
||||
spinner.succeed("Authenticated");
|
||||
await authStore.write({ token: result.token, user: result.user, source: "device-code" });
|
||||
await syncMeshes();
|
||||
return;
|
||||
}
|
||||
if (result.status === "denied") {
|
||||
spinner.fail("Denied in browser");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
spinner.fail("Timed out");
|
||||
process.exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
Polls every 1.5s. Server returns `{ slow_down: true }` if polled too fast (rate limit at 1/sec).
|
||||
|
||||
## Security
|
||||
|
||||
1. **Tokens are hashed at rest** (Better Auth `apiKey` plugin handles this with bcrypt or argon2).
|
||||
2. **Raw tokens shown to user once.** PATs in dashboard, device-code tokens via `claudemesh login` output. Never logged, never re-displayable.
|
||||
3. **`auth.json` is `0600`.** CLI refuses to write if parent dir can't be made `0700`. Warns on read if mode is wider.
|
||||
4. **Token prefix `cm_` enables secret scanning.** Document the regex `cm_(session|pat)_[a-z0-9]{32,}` in security docs so GitHub secret scanning, GitGuardian, etc. can detect leaks.
|
||||
5. **`/api/auth/cli/device-code/:device_code` polling is rate-limited** to 1 req/sec per IP per device_code. Returns `429` with `slow_down: true` body.
|
||||
6. **Device codes expire in 10 minutes.** Approved-but-unclaimed tokens stay valid (the polling endpoint still returns the token for 60 seconds after approval, then the device_code row is GC'd).
|
||||
7. **Audit logging.** Every device-code approval, PAT creation, and PAT revocation emits an audit event (`auth.cli.session.created`, `auth.cli.pat.created`, etc.). Stored in existing audit log if there is one, otherwise new `audit_log` table.
|
||||
8. **Session invalidation on password change.** When a user changes their password via Better Auth, all `cli_session` `api_key` rows for that user are revoked. PATs are NOT auto-revoked (they're explicitly user-managed).
|
||||
9. **Token revocation is immediate.** `auth.api.verifyApiKey` checks DB on every request — no in-memory cache.
|
||||
10. **No CSRF concern** for device-code endpoints — the unauthed ones don't act on user state, the authed ones use Better Auth's existing CSRF protection.
|
||||
|
||||
## Wizard UX integration
|
||||
|
||||
The current welcome wizard already has:
|
||||
```
|
||||
▸ Create account (new to claudemesh)
|
||||
Sign in (existing account)
|
||||
Paste an invite URL
|
||||
Exit
|
||||
```
|
||||
|
||||
After this spec lands, the welcome screen becomes:
|
||||
```
|
||||
██ claudemesh
|
||||
|
||||
▸ Sign in ← device-code OAuth
|
||||
Paste an access token ← PAT path
|
||||
Join via invite URL ← unchanged
|
||||
Create account ← opens /register, then back to login
|
||||
Exit
|
||||
```
|
||||
|
||||
"Sign in" becomes the headline option. The current "Create account" still opens browser to `/register` but flows back through the device-code handshake instead of a custom callback.
|
||||
|
||||
Once authenticated, the wizard transitions to:
|
||||
```
|
||||
██ claudemesh launch
|
||||
|
||||
Account ✔ Alejandro Gutiérrez
|
||||
Mesh ▸ (pick one — 3 available)
|
||||
Name ✔ Alexis (from --name)
|
||||
Role ▸ (pick one)
|
||||
|
||||
▸ Continue
|
||||
Cancel
|
||||
```
|
||||
|
||||
Status rows show what's filled and what's left. Mesh picker fetches from `GET /api/my/meshes` via the freshly minted token.
|
||||
|
||||
This integrates cleanly with the wizard architecture refactor in `2026-04-10-cli-wizard-architecture-refactor.md`: auth becomes one screen in the launch flow with `isComplete: s => s.user !== null`. On a fresh machine the auth screen runs; on a returning machine it's auto-skipped.
|
||||
|
||||
## Error handling
|
||||
|
||||
| Scenario | Behavior |
|
||||
|---|---|
|
||||
| Browser doesn't open | Print URL prominently, keep polling |
|
||||
| Network down during poll | Retry with exponential backoff (1.5s → 3s → 6s, max 30s) |
|
||||
| Device code expires | Print "Login timed out, run `claudemesh login` to retry", exit 1 |
|
||||
| Token rejected by API | Print "Authentication failed", suggest `claudemesh login`, exit 2 |
|
||||
| `auth.json` corrupted | Print "Auth file corrupted, run `claudemesh login`", exit 2 |
|
||||
| `auth.json` permissions wrong | Warn, fix to `0600`, continue |
|
||||
| PAT pasted to `--token` is malformed | Print "Invalid token format (expected `cm_pat_…`)", exit 1 |
|
||||
| PAT pasted to `--token` is valid format but unknown | API returns 401, print "Token rejected", exit 2 |
|
||||
| Two CLI instances poll simultaneously | Both get the same approved status; first to read gets the token, second gets `{ status: 'approved', token: null }` (already_claimed). Document this. |
|
||||
| User clicks Approve in browser, then closes tab | CLI's poll catches it, login succeeds. The browser tab closure is irrelevant. |
|
||||
| User completes login on machine A, then runs `claudemesh login` on machine B with same account | Both sessions coexist as separate `api_key` rows. `claudemesh whoami --sessions` shows both. |
|
||||
|
||||
## Implementation phases
|
||||
|
||||
Each phase ships independently and is independently testable.
|
||||
|
||||
### Phase 1 — Backend foundation (4–6 hours)
|
||||
|
||||
- [ ] Wire Better Auth `apiKey` plugin in `apps/web/src/lib/auth/`
|
||||
- [ ] Migration `0020_cli-device-code.sql`
|
||||
- [ ] Drizzle schema for `cli_device_code` in `packages/db/src/schema/auth.ts`
|
||||
- [ ] Endpoints: `POST /api/auth/cli/device-code`, `GET /api/auth/cli/device-code/:device_code`, `POST /api/auth/cli/device-code/:user_code/approve`, `POST /api/auth/cli/device-code/:user_code/deny`
|
||||
- [ ] Extend `enforceAuth` middleware to accept `Authorization: Bearer cm_…`
|
||||
- [ ] Endpoints: `POST /api/my/cli/tokens`, `GET /api/my/cli/tokens`, `DELETE /api/my/cli/tokens/:id`, `GET /api/my/cli/sessions`, `DELETE /api/my/cli/sessions/:id`
|
||||
- [ ] Unit tests for token verification and device-code state machine
|
||||
|
||||
### Phase 2 — Web routes (3–4 hours)
|
||||
|
||||
- [ ] `/cli-auth?code=...` page (server component + approve/deny client component)
|
||||
- [ ] `/dashboard/settings/cli-tokens` page (list + create modal + revoke)
|
||||
- [ ] Translations for both pages (en, es)
|
||||
- [ ] E2E test: full device-code happy path with Playwright
|
||||
|
||||
### Phase 3 — CLI auth core (4–5 hours)
|
||||
|
||||
- [ ] `lib/device-info.ts` — collect hostname, os, arch
|
||||
- [ ] `lib/auth-store.ts` — read/write `~/.claudemesh/auth.json` with mode checks
|
||||
- [ ] `lib/api-client.ts` — typed fetch wrapper with bearer header
|
||||
- [ ] `commands/login.ts` — device-code flow + `--token` PAT path
|
||||
- [ ] `commands/logout.ts` — revoke + delete local
|
||||
- [ ] `commands/whoami.ts` — print current identity + token source
|
||||
- [ ] Token resolution helper (`--token` > `CLAUDEMESH_TOKEN` > `auth.json`)
|
||||
- [ ] Unit tests for auth-store and token resolution
|
||||
|
||||
### Phase 4 — CLI commands wired to API (3–4 hours)
|
||||
|
||||
- [ ] Rewrite `commands/create.ts` to call `POST /api/my/meshes`
|
||||
- [ ] New `commands/invite.ts` with `--email`, `--mesh`, `--role`, `--expires-in`
|
||||
- [ ] Rewrite `commands/sync.ts` to call `GET /api/my/meshes` and reconcile local config
|
||||
- [ ] Update `commands/list.ts` to show server-side meshes too
|
||||
- [ ] Integration tests against staging broker + web
|
||||
|
||||
### Phase 5 — Wizard integration (3–4 hours)
|
||||
|
||||
- [ ] Welcome screen new options (Sign in / Paste token / Create account / Join invite)
|
||||
- [ ] Auth screen as a flow step with `isComplete: s => s.user !== null`
|
||||
- [ ] Status rows pattern showing auth state during launch
|
||||
- [ ] First-run detection (no `auth.json`) → auto-route to login
|
||||
|
||||
### Phase 6 — Polish, docs, ship (2–3 hours)
|
||||
|
||||
- [ ] Update `README.md`, `apps/cli/README.md`, `docs/quickstart.md`
|
||||
- [ ] CHANGELOG entry for v0.11.0
|
||||
- [ ] Telemetry events for `auth.cli.login.{start,success,fail}`
|
||||
- [ ] Bump `apps/cli/package.json` to `0.11.0`
|
||||
- [ ] Publish to npm
|
||||
- [ ] Deploy broker / web (no broker changes, web for new routes)
|
||||
|
||||
**Total estimate:** 19–26 hours of focused work. Realistic: 3–4 days with testing and review.
|
||||
|
||||
## Dependencies between phases
|
||||
|
||||
```
|
||||
Phase 1 (backend) ──┬─→ Phase 2 (web routes)
|
||||
└─→ Phase 3 (CLI auth core)
|
||||
│
|
||||
└─→ Phase 4 (commands)
|
||||
│
|
||||
└─→ Phase 5 (wizard)
|
||||
│
|
||||
└─→ Phase 6 (ship)
|
||||
```
|
||||
|
||||
Phase 1 and 2 can be parallelized after the schema lands. Phase 3 needs Phase 1 endpoints live (even if on staging). Phase 4 onwards is strictly serial.
|
||||
|
||||
## Telemetry
|
||||
|
||||
Emit these events (PostHog or whatever the existing analytics are):
|
||||
|
||||
- `cli.login.started` — properties: `{ method: 'device-code' | 'pat' }`
|
||||
- `cli.login.succeeded` — properties: `{ method, user_id }`
|
||||
- `cli.login.failed` — properties: `{ method, reason }`
|
||||
- `cli.logout` — properties: `{ user_id }`
|
||||
- `cli.command.executed` — properties: `{ command, exit_code, duration_ms, authenticated: boolean }`
|
||||
- `cli.api.error` — properties: `{ endpoint, status, error_code }`
|
||||
|
||||
Telemetry is **opt-out**. First run shows a one-line notice: "claudemesh collects anonymized usage telemetry. Disable with `claudemesh telemetry off`."
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Better Auth `apiKey` plugin version** — confirm it's installed and at a version that supports `enableMetadata`. Check `pnpm why better-auth` in `apps/web`.
|
||||
2. **Audit log table** — does one already exist? If not, this spec adds three rows of log; not worth a new table for that. Use `console.log` with structured JSON to stderr and let the platform's log collector handle it.
|
||||
3. **Email sending** — `claudemesh invite --email` requires a transactional email path. Does the web app already have one (Resend, Postmark)? If yes, reuse. If no, defer the email send to a follow-up; the invite command can still create the invite and print the URL.
|
||||
4. **Token scopes** — v1 ships with no scopes; every token has full account access. Should we add `mesh:read`, `mesh:write`, `invite:create` scopes from day one, or wait? **Recommendation:** wait. YAGNI. Add when a user actually wants a read-only CI token.
|
||||
5. **PAT expiry default** — 90 days? 1 year? Never? Better Auth supports all three. **Recommendation:** 1 year default, user can pick "never" with explicit warning.
|
||||
6. **Mesh slug uniqueness in `claudemesh create`** — what happens if two users try to create meshes with the same slug? Existing API behavior should be tested. If it errors, the CLI should suggest `--slug platform-team-2`.
|
||||
7. **`claudemesh login` when already logged in** — re-authenticate (overwrite) or error ("already logged in, run logout first")? **Recommendation:** re-authenticate silently with a one-line notice ("Replacing existing session for Alejandro").
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
For v0.11.0 to ship, all of these must be true:
|
||||
|
||||
- [ ] `claudemesh login` on a fresh machine (no `auth.json`) opens browser, completes device-code flow, writes `auth.json`, runs in <30 seconds end-to-end
|
||||
- [ ] `claudemesh login --token cm_pat_…` works without browser
|
||||
- [ ] `claudemesh logout` revokes server-side and deletes local file
|
||||
- [ ] `claudemesh whoami` prints user identity and token source
|
||||
- [ ] `claudemesh create "Test mesh"` creates a real mesh on the server, joins it locally, and the user can see it on the dashboard
|
||||
- [ ] `claudemesh invite --email a@b.c --mesh test` creates an invite and prints the URL
|
||||
- [ ] `claudemesh launch` (bare) on a fresh machine walks login → mesh picker → name/role → Claude Code, all in one wizard
|
||||
- [ ] Dashboard `/dashboard/settings/cli-tokens` lists, creates, and revokes PATs
|
||||
- [ ] All flows work in `en` and `es`
|
||||
- [ ] Existing `claudemesh launch` invocations (with token already in `auth.json`) still work without prompting
|
||||
- [ ] Token in `auth.json` survives an hour of idle and continues to work (no aggressive expiry)
|
||||
- [ ] Revoking a token in the dashboard makes the next CLI call fail with a clear error
|
||||
- [ ] Documentation updated in `README.md`, `apps/cli/README.md`, `docs/quickstart.md`
|
||||
- [ ] CHANGELOG entry written
|
||||
- [ ] Published to npm as `claudemesh-cli@0.11.0`
|
||||
|
||||
## What this unlocks
|
||||
|
||||
Once this lands, every dashboard-only feature becomes one CLI command away. Future specs that depend on this:
|
||||
|
||||
- `claudemesh members list` / `claudemesh members add`
|
||||
- `claudemesh billing usage`
|
||||
- `claudemesh mesh archive`
|
||||
- `claudemesh stream subscribe` (live broker events)
|
||||
- `claudemesh skill publish` (publish a skill to mesh registry)
|
||||
- `claudemesh log tail` (mesh-wide audit log)
|
||||
|
||||
This is the foundational unlock. Everything else is incremental on top.
|
||||