Files
claudemesh/.artifacts/specs/2026-04-11-cli-v2-pass1.md
Alejandro Gutiérrez ee12510ef1
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-15 08:44:52 +01:00

1158 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# claudemesh-cli v2 — Pass 1
**Status:** spec (active implementation target)
**Created:** 2026-04-11
**Scope:** immediate v2 work — refactor + CLI user flows + v1 preservation
**Ships as:** `claudemesh-cli@1.0.0`
**Companion documents** (read these first):
- `2026-04-11-v1-feature-inventory.md` — every v1 feature that must keep working
- `2026-04-11-v2-parity-test-plan.md` — how we verify v2 preserves v1
- `2026-04-10-cli-v2-pass2-facade-pattern.md` — architectural rules (authoritative for both passes)
**Pass 2 references** (future work, not implementation targets):
- `2026-04-10-cli-v2-pass2-final-vision.md` — longer-term architecture vision
- `2026-04-10-cli-v2-pass2-ux-design.md` — interactive UX redesign
- `2026-04-10-cli-v2-pass2-local-first-storage.md` — local SQLite + Lamport + sync daemon
- `2026-04-10-cli-v2-pass2-shared-infrastructure.md` — broker hardening
---
## 1. Scope
### 1.1 What Pass 1 delivers
Three things, exactly:
1. **Folder structure refactor.** `apps/cli/src/` becomes `apps/cli-v2/src/` with a cleaner layout: `entrypoints/`, `cli/`, `commands/`, `services/` (feature-folders with facades), `ui/`, `mcp/`, `constants/`, `types/`, `utils/`, `locales/`, `templates/`. ESLint + dependency-cruiser enforce the facade pattern. The scaffold replaces v1's ad-hoc organization.
2. **CLI user flows** that remove the dependency on the web dashboard for common operations:
- `claudemesh login` — device-code OAuth against claudemesh.com
- `claudemesh register` — account creation flow (browser handoff for email verification)
- `claudemesh logout` — revoke session
- `claudemesh whoami` — identity check
- `claudemesh new` — create a mesh from the CLI (was dashboard-only in v1)
- `claudemesh invite` — generate an invite from the CLI (was dashboard-only in v1)
3. **Every v1 feature preserved** — the 79 MCP tools, the 85 broker WS message types, the welcome wizard, the launch flow, the status hooks, the crypto primitives, the invite parsing, the doctor checks. See `2026-04-11-v1-feature-inventory.md` for the complete list.
### 1.2 What Pass 1 does NOT deliver
Everything in the Pass 2 specs. Concretely:
- No local SQLite source of truth. v2 Pass 1 still hits the broker for every read/write (same as v1).
- No Lamport clock, no sync daemon, no outbox/inbox, no publish transaction, no NFC normalization, no write queue with state machine.
- No broker changes. The broker runs unchanged. Postgres, Neo4j, Qdrant, MinIO, Docker sandboxes — all stay on their v1 configurations.
- No hardened Postgres role isolation. v1's `search_path`-based scoping is preserved unchanged. Improving this is Pass 2.
- No MCP catalog tiering, no catalog audit process, no egress proxy for Docker sandboxes, no SSRF policy for URL watch. v1 behavior is preserved.
- No Telegram bridge changes. `apps/broker/src/telegram-bridge.ts` stays exactly as it is. The v2 CLI **preserves `claudemesh connect telegram`** as an interactive wizard (see §5.7) — this was a regression risk in an earlier draft of this spec. The v1 `disconnect telegram` subcommand is dropped because teardown is handled by the bridge itself (revoke inside Telegram) or by leaving the mesh.
- No new MCP catalog. Deployed MCPs still come from zip upload / git URL / npx package, same as v1. The bundled catalog (`apps/broker/src/mcp-catalog.ts`) is Pass 2 work.
- No ICU MessageFormat. v2 Pass 1 uses simple string templates. ICU plurals + per-locale length budgets are Pass 2.
- No `session_kind` enum with six output budgets. v2 Pass 1 keeps v1's imperative output patterns.
- No accessibility token-signal matrix. v2 Pass 1 keeps v1's TUI primitives. Pass 2 introduces the new design system.
- No new visual design system. v1's colors and icons stay. Pass 2 introduces the six-role palette.
- No trust surfaces / delight beats budget. Pass 2 concern.
- No new error structure with one-action recovery. v1's existing error messages stay.
- No 14 inviolable rules from the Pass 2 UX spec. Pass 2 concern.
If it's in a Pass 2 document, it is **not** part of Pass 1 unless this document explicitly says so.
### 1.3 The two compatibility contracts
**Contract A — the broker's WS protocol.** v2's CLI must speak the broker's v1 WS protocol byte-for-byte. Every one of the 85 WS message types in inventory §3 keeps the same envelope shape, the same field names, the same signature format. Production brokers deployed today must continue to serve v2 CLIs without any broker-side changes. This is the **only** compatibility contract that matters.
**Contract B — v1 feature parity (user-facing behavior).** Every item in the v1 inventory §12 "must preserve" list must keep working. This includes the 79 MCP tools, the 85 WS messages, the 18 HTTP endpoints, the backend integrations, the status engine, the scheduled message delivery, the URL watch, and the crypto round-trips. The v2 parity test suite is the verification.
**What is NOT a compatibility contract**:
- v1's CLI command names (`launch`, `connect telegram`, etc.) — v2 picks new names where better ones exist.
- v1's config file shape (`~/.claudemesh/config.json`) — v2 picks a new shape if needed.
- v1's JSON output shape (`--json` outputs) — v2 locks a new shape via `schema_version: "1.0"`.
- v1's env var names — v2 picks new ones.
- v1's exit codes — v2 uses the cleaner scheme from Pass 2 §11.2.
- v1's stdout text — v2 rewrites for consistency and future localization.
No deprecation windows, no migration runners, no aliases. There are no users to migrate.
---
## 2. The three Pass 1 intents
These are the decisions v2 Pass 1 is optimizing for. Everything else is either out of scope or inherits from v1.
### 2.1 Intent A — scalability through folder structure
**Problem**: v1's `apps/cli/src/` has 22 files, with business logic scattered across `commands/`, `mcp/`, `ws/`, `auth/`, `crypto/`, `invite/`, `state/`, `tui/`, and `lib/`. No enforced boundaries. `commands/launch.ts` is 775 lines. `mcp/server.ts` is 2,139 lines. `ws/client.ts` is 2,191 lines. Growing to v1.5 and beyond would compound the tangle.
**Pass 1 fix**: feature-folder services with facade pattern. Every service (`auth`, `mesh`, `invite`, `broker`, `api`, `crypto`, `config`, `state`, `device`, `clipboard`, `spawn`, `telemetry`, `health`, `update`, `i18n`, `lifecycle`, `logger`) lives in `services/<name>/` with `facade.ts` as the single public entry point. ESLint `boundaries` plugin + dependency-cruiser enforce that `ui/`, `commands/`, `cli/`, and `mcp/` import only from `services/*/facade.ts`.
The facade pattern is described in full in `2026-04-10-cli-v2-pass2-facade-pattern.md` (which applies to both passes). Pass 1 implements it completely.
**What this buys us**:
- Growing a service is local — add files inside its folder, extend its facade
- Cross-service calls are explicit — facades import other facades, never internal files
- Testing is trivial — mock the facade
- Code review is bounded — PRs touch single service folders
- Scaling contributors is possible — ownership can split along service lines
### 2.2 Intent B — CLI user flows (remove dashboard dependency)
**Problem**: v1 users must open the claudemesh.com dashboard to:
- Create an account
- Log in to the dashboard (OAuth flow)
- Create a mesh
- Generate an invite link
- Manage their profile
Only after those web steps can they use the CLI to join (`claudemesh join <url>`). This means a terminal-first developer always has to context-switch to a browser for anything beyond joining.
**Pass 1 fix**: add CLI commands that drive all these flows end-to-end.
| v2 Pass 1 command | Replaces |
|---|---|
| `claudemesh register` | Web dashboard signup page |
| `claudemesh login` | Web dashboard login + dashboard-to-CLI JWT sync |
| `claudemesh logout` | Web dashboard logout |
| `claudemesh whoami` | Web dashboard "who am I" profile view |
| `claudemesh new <name>` | Web dashboard "create mesh" form |
| `claudemesh invite [email]` | Web dashboard "generate invite" button |
These are the **only** net-new CLI commands in Pass 1. Everything else inherits from v1.
The flows use **device-code OAuth** against claudemesh.com's existing Better Auth backend. No new backend work is required beyond confirming the device-code endpoint exists (it may or may not — the inventory §4 confirms `POST /cli-sync` exists, which is the existing dashboard-sync flow; v2 extends this to a full login/register flow).
### 2.3 Intent C — preserve every v1 feature
**Problem**: the scope creep risk. Every review round has added "while we're refactoring, let's also fix X."
**Pass 1 fix**: the v1 feature inventory is the regression contract. The parity test suite is the verification. A PR cannot merge if it breaks any item in inventory §12.
Pass 1 is a **refactor**, not a rewrite. v1's code gets moved into new files, its imports get updated to use facades, its business logic stays intact. Where v1's implementation is long or messy, v2 Pass 1 may split it into multiple files — but the observable behavior stays identical.
---
## 3. Target source tree
```
apps/cli-v2/
├── package.json # name: claudemesh-cli, version: 0.11.0-alpha.0 → 1.0.0
├── tsconfig.json
├── bunfig.toml
├── build.ts # Bun bundler driver, enforces 1.2 MB gzipped ceiling
├── .eslintrc.cjs # boundaries plugin + 3 custom rules
├── dependency-cruiser.config.js # folder-level boundary enforcement
├── biome.json
├── CHANGELOG.md
├── README.md
├── bin/
│ └── claudemesh # shebang → dist/entrypoints/cli.js
├── src/
│ ├── entrypoints/
│ │ ├── cli.ts # interactive CLI entry — parses argv, dispatches
│ │ └── mcp.ts # `claudemesh mcp` → MCP stdio server
│ │
│ ├── cli/ # non-Ink I/O plumbing
│ │ ├── argv.ts # parse process.argv, detect positional invite URLs
│ │ ├── print.ts # stdout helpers (respects NO_COLOR / FORCE_COLOR)
│ │ ├── structured-io.ts # --json output with schema_version
│ │ ├── exit.ts # exit codes + cleanup hooks
│ │ ├── update-notice.ts # "new version available" banner (npm registry poll)
│ │ ├── handlers/
│ │ │ ├── signal.ts # SIGINT/SIGTERM graceful shutdown
│ │ │ └── error.ts # top-level error → user message
│ │ └── output/ # plain-text renderers for non-interactive commands
│ │ ├── list.ts
│ │ ├── peers.ts
│ │ ├── whoami.ts
│ │ └── version.ts
│ │
│ ├── commands/ # one verb per file; no "advanced" prefix
│ │ ├── index.ts # command registry
│ │ ├── join.ts # join a mesh from invite URL (v1 + v2 formats)
│ │ ├── new.ts # NEW — create a mesh via API
│ │ ├── invite.ts # NEW — generate invite via API
│ │ ├── list.ts # list joined meshes
│ │ ├── rename.ts # rename current mesh
│ │ ├── leave.ts # leave a mesh
│ │ ├── peers.ts # list peers
│ │ ├── send.ts # send a message
│ │ ├── inbox.ts # drain inbox
│ │ ├── state.ts # get / set / list state
│ │ ├── info.ts # mesh overview
│ │ ├── remember.ts # store memory
│ │ ├── recall.ts # search memories
│ │ ├── remind.ts # schedule reminder
│ │ ├── profile.ts # view / edit profile
│ │ ├── status.ts # broker connectivity check
│ │ ├── doctor.ts # diagnostic checks (port from v1)
│ │ ├── register.ts # NEW — account creation
│ │ ├── login.ts # NEW — device-code OAuth
│ │ ├── logout.ts # NEW — revoke session
│ │ ├── whoami.ts # NEW — identity check
│ │ ├── install.ts # register MCP + hooks with Claude Code
│ │ ├── uninstall.ts # remove MCP + hooks
│ │ ├── sync.ts # sync meshes from dashboard (existing /cli-sync)
│ │ ├── welcome.ts # welcome wizard (bare command, first-run only)
│ │ ├── hook.ts # internal Claude Code hook handler
│ │ ├── mcp.ts # internal `claudemesh mcp` dispatcher
│ │ └── seed-test-mesh.ts # dev-only
│ │
│ ├── services/ # feature-folders with facades
│ │ ├── auth/ # NEW CLI user flows + existing JWT sync
│ │ │ ├── client.ts # HTTP calls to /api/auth/cli/* endpoints
│ │ │ ├── device-code.ts # device-code flow orchestration
│ │ │ ├── token-store.ts # ~/.claudemesh/auth.json R/W with 0600
│ │ │ ├── dashboard-sync.ts # existing /cli-sync flow (port from v1 auth/sync-with-broker.ts)
│ │ │ ├── callback-listener.ts # local HTTP listener for OAuth callback (port from v1)
│ │ │ ├── open-browser.ts # cross-platform browser launcher (port from v1)
│ │ │ ├── implementation.ts
│ │ │ ├── schemas.ts
│ │ │ ├── errors.ts
│ │ │ ├── index.ts # getAuthService() factory
│ │ │ ├── facade.ts # loginWithDeviceCode, logout, whoAmI, register
│ │ │ └── auth.test.ts
│ │ │
│ │ ├── mesh/ # mesh CRUD + list + resolve-target
│ │ │ ├── client.ts # HTTP to /api/my/meshes
│ │ │ ├── list.ts # port from v1 commands/list.ts logic
│ │ │ ├── create.ts # NEW — POST /api/my/meshes
│ │ │ ├── rename.ts # PATCH /api/my/meshes/:slug
│ │ │ ├── leave.ts # port from v1 commands/leave.ts
│ │ │ ├── join.ts # port from v1 commands/join.ts
│ │ │ ├── resolve-target.ts # port from v1 launch.ts mesh picker logic
│ │ │ ├── implementation.ts
│ │ │ ├── schemas.ts
│ │ │ ├── errors.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── invite/ # invite generation + parsing + claiming
│ │ │ ├── generate.ts # NEW — POST /api/my/meshes/:slug/invites
│ │ │ ├── parse-url.ts # port from v1 invite/parse.ts + lib/invite-v2.ts
│ │ │ ├── claim.ts # port from v1 invite/enroll.ts
│ │ │ ├── send-email.ts # NEW — email delivery (if backend supports)
│ │ │ ├── implementation.ts
│ │ │ ├── schemas.ts
│ │ │ ├── errors.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── broker/ # WS client + peer crypto (port from v1 ws/)
│ │ │ ├── ws-client.ts # port from v1 ws/client.ts (2191 lines)
│ │ │ ├── manager.ts # port from v1 ws/manager.ts
│ │ │ ├── envelope.ts # port from v1 crypto/envelope.ts
│ │ │ ├── hello-sig.ts # port from v1 crypto/hello-sig.ts
│ │ │ ├── implementation.ts
│ │ │ ├── schemas.ts # WS message type definitions (all 85)
│ │ │ ├── errors.ts
│ │ │ ├── index.ts
│ │ │ ├── facade.ts # typed methods for each WS op
│ │ │ └── broker.test.ts
│ │ │
│ │ ├── api/ # base HTTP client
│ │ │ ├── client.ts # typed fetch wrapper
│ │ │ ├── my.ts # /api/my/* endpoint helpers
│ │ │ ├── public.ts # /api/public/* (invite claim)
│ │ │ ├── errors.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── crypto/ # Ed25519, NaCl, AES-GCM
│ │ │ ├── keypair.ts # port from v1 crypto/keypair.ts
│ │ │ ├── file-crypto.ts # port from v1 crypto/file-crypto.ts
│ │ │ ├── box.ts # NaCl crypto_box wrappers
│ │ │ ├── random.ts # secure random helpers
│ │ │ ├── index.ts
│ │ │ ├── facade.ts
│ │ │ └── crypto.test.ts
│ │ │
│ │ ├── config/ # ~/.claudemesh/config.json
│ │ │ ├── read.ts # port from v1 state/config.ts
│ │ │ ├── write.ts
│ │ │ ├── schemas.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── state/ # last-used cache (not mesh state — that's via broker)
│ │ │ ├── last-used.ts # remember last mesh, name, role for faster launch
│ │ │ ├── schemas.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── device/ # hostname, os, arch
│ │ │ ├── info.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── clipboard/ # pbpaste / xclip / wl-paste (optional)
│ │ │ ├── read.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── spawn/ # exec choke points
│ │ │ ├── claude.ts # exec claude binary (port from v1 launch.ts)
│ │ │ ├── browser.ts # open browser (port from v1 auth/open-browser.ts)
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── telemetry/ # opt-out usage events
│ │ │ ├── emit.ts
│ │ │ ├── opt-out.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── health/ # doctor check implementations
│ │ │ ├── check-node-version.ts
│ │ │ ├── check-claude-binary.ts
│ │ │ ├── check-mcp-registered.ts
│ │ │ ├── check-hooks-registered.ts
│ │ │ ├── check-config-perms.ts
│ │ │ ├── check-keypairs-valid.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts # runAllChecks, runCheck
│ │ │
│ │ ├── update/ # npm registry version check
│ │ │ ├── check.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── i18n/ # simple string templates (no ICU in Pass 1)
│ │ │ ├── resolve.ts # locale detection
│ │ │ ├── format.ts # {placeholder} substitution
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ ├── lifecycle/ # start/stop long-running services
│ │ │ ├── service-manager.ts
│ │ │ ├── index.ts
│ │ │ └── facade.ts
│ │ │
│ │ └── logger/ # structured logger
│ │ ├── logger.ts
│ │ ├── index.ts
│ │ └── facade.ts
│ │
│ ├── ui/ # Ink-based TUI (port from v1 tui/)
│ │ ├── styles.ts # colors + icons (port from v1 tui/colors.ts — v1 values, not Pass 2 redesign)
│ │ ├── screen.ts # port from v1 tui/screen.ts
│ │ ├── spinner.ts # port from v1 tui/spinner.ts
│ │ ├── welcome/ # welcome wizard screens (port from v1 commands/welcome.ts)
│ │ │ ├── WelcomeScreen.tsx
│ │ │ ├── RegisterStep.tsx # NEW for Pass 1
│ │ │ ├── LoginStep.tsx # NEW for Pass 1
│ │ │ ├── MeshPickerStep.tsx
│ │ │ └── index.ts
│ │ └── launch/ # launch flow (port from v1 commands/launch.ts interactive bits)
│ │ ├── LaunchFlow.tsx
│ │ └── index.ts
│ │
│ ├── mcp/ # stdio MCP server (port from v1 mcp/server.ts)
│ │ ├── server.ts # entry from entrypoints/mcp.ts
│ │ ├── router.ts # tool dispatch
│ │ ├── tools/ # one file per tool family (79 tools total)
│ │ │ ├── memory.ts # remember, recall, forget
│ │ │ ├── state.ts # set_state, get_state, list_state
│ │ │ ├── messaging.ts # send_message, list_peers, check_messages, message_status
│ │ │ ├── profile.ts # set_profile, set_status, set_summary, set_visible
│ │ │ ├── groups.ts # join_group, leave_group
│ │ │ ├── files.ts # share_file, get_file, list_files, file_status, delete_file, grant_file_access, read_peer_file, list_peer_files
│ │ │ ├── vectors.ts # vector_store, vector_search, vector_delete, list_collections
│ │ │ ├── graph.ts # graph_query, graph_execute
│ │ │ ├── sql.ts # mesh_query, mesh_execute, mesh_schema
│ │ │ ├── streams.ts # create_stream, publish, subscribe, list_streams
│ │ │ ├── contexts.ts # share_context, get_context, list_contexts
│ │ │ ├── tasks.ts # create_task, claim_task, complete_task, list_tasks
│ │ │ ├── scheduling.ts # schedule_reminder, list_scheduled, cancel_scheduled
│ │ │ ├── mesh-meta.ts # mesh_info, mesh_stats, mesh_clock, ping_mesh
│ │ │ ├── clock-write.ts # mesh_set_clock, mesh_pause_clock, mesh_resume_clock
│ │ │ ├── skills.ts # share_skill, get_skill, list_skills, remove_skill, mesh_skill_deploy
│ │ │ ├── mcp-registry-peer.ts # mesh_mcp_register, mesh_mcp_list, mesh_tool_call, mesh_mcp_remove
│ │ │ ├── mcp-registry-broker.ts # mesh_mcp_deploy, undeploy, update, logs, scope, schema, catalog
│ │ │ ├── vault.ts # vault_set, vault_list, vault_delete
│ │ │ ├── url-watch.ts # mesh_watch, mesh_unwatch, mesh_watches
│ │ │ ├── webhooks.ts # create_webhook, list_webhooks, delete_webhook
│ │ │ └── index.ts
│ │ ├── middleware/
│ │ │ ├── logging.ts
│ │ │ └── error-handler.ts
│ │ └── handlers/
│ │ ├── stdio.ts
│ │ └── jsonrpc.ts
│ │
│ ├── constants/
│ │ ├── paths.ts # XDG paths for ~/.claudemesh/
│ │ ├── urls.ts # default API / broker URLs
│ │ ├── timings.ts # polling intervals, timeouts
│ │ ├── exit-codes.ts # numeric exit code constants
│ │ └── index.ts
│ │
│ ├── types/
│ │ ├── api.ts # request/response types
│ │ ├── mesh.ts # mesh, member, invite types
│ │ ├── peer.ts
│ │ └── index.ts
│ │
│ ├── utils/
│ │ ├── levenshtein.ts # typo recovery
│ │ ├── slug.ts # hostname → slug
│ │ ├── url.ts # URL parsing
│ │ ├── format.ts # bytes, durations
│ │ ├── semver.ts # version comparison
│ │ ├── retry.ts # backoff helpers
│ │ └── index.ts
│ │
│ ├── locales/
│ │ ├── en.ts # simple string templates (no ICU in Pass 1)
│ │ └── index.ts
│ │
│ └── templates/ # mesh templates (port from v1 templates/index.ts)
│ ├── dev-team.ts
│ ├── research.ts
│ ├── ops-incident.ts
│ ├── simulation.ts
│ ├── personal.ts
│ └── index.ts
└── tests/ # See parity test plan for full layout
├── unit/ # colocated tests under src/services/*/
├── parity/ # v1/v2 behavioral equivalence (~70 files)
├── contract/ # WS protocol compatibility (~85 files)
├── mcp-tools/ # per-tool handler parity (79 files)
├── e2e/ # full journeys against real broker (~28 files)
├── golden/ # --json output shape (~12 files)
├── fixtures/
└── helpers/
```
**What's NOT in this tree** (versus Pass 2):
- No `src/services/store/` — no local SQLite source of truth
- No `src/services/broker/sync-daemon.ts` — no outbox/inbox sync
- No `src/services/broker/peer-crypto.ts` (beyond hello-sig + envelope that v1 already has)
- No `src/migrations/` — no config migrations because there's no v1 user base
- No `src/ui/accessibility/` — no announcement shim
- No `src/ui/session-kind.ts` — no output budget enum
- No `src/locales/es.ts` — Pass 1 is English-only (Spanish is Pass 2)
- No `src/services/broker/mcp-catalog.ts` — no CLI-side catalog mirror
The scaffold for Pass 1 creates ~200 files (similar to Pass 2 scaffold but leaner on the `services/store/`, `services/broker/sync-*`, `ui/accessibility/`, and Pass 2-specific areas).
---
## 4. CLI command surface
### 4.1 Main `--help` output
```
$ claudemesh --help
claudemesh — peer mesh for Claude Code sessions
v1.0.0
USAGE
claudemesh start a session in your mesh (creates one if needed)
claudemesh <url> join a mesh from an invite link
claudemesh new create a new mesh
claudemesh invite [email] generate an invite (copies to clipboard)
claudemesh list see your meshes
claudemesh rename <name> rename the current mesh
claudemesh leave [mesh] leave a mesh
claudemesh peers see who's in the current mesh
claudemesh send <to> <msg> send a message
claudemesh inbox drain pending messages
claudemesh state ... get, set, or list shared state
claudemesh remember <text> store a memory
claudemesh recall <query> search memories
claudemesh remind ... schedule a reminder
claudemesh profile view or edit your profile
When something's wrong
claudemesh doctor diagnose install/config/connection issues
claudemesh whoami show current identity
claudemesh status check broker connectivity
Authentication
claudemesh register create a claudemesh.com account from the CLI
claudemesh login sign in via browser
claudemesh logout sign out and clear credentials
Setup
claudemesh install register MCP server + status hooks with Claude Code
claudemesh uninstall remove MCP server + hooks
claudemesh sync refresh mesh list from your dashboard
claudemesh connect telegram link a Telegram bot to a mesh (interactive wizard)
Internal (for Claude Code and scripts)
claudemesh mcp stdio MCP server
claudemesh hook handle Claude Code hook events
claudemesh seed-test-mesh dev-only helper
```
### 4.2 Flat command namespace
There is no `advanced` prefix. There is no `launch` subcommand. `connect telegram` is preserved as an interactive wizard (see §5.7); `disconnect telegram` is dropped. Every command above is callable directly.
The main `--help` groups commands visually (USAGE block + "When something's wrong" + "Authentication" + "Setup" + "Internal") but this is display formatting, not namespacing. `claudemesh register` is a direct invocation, not `claudemesh advanced register`.
### 4.3 Bare command behavior
```
$ claudemesh
```
Depends on machine state:
- **First run, no config**: runs the welcome wizard (`commands/welcome.ts`) which detects first-time use and prompts for register/login/join/exit.
- **Returning user, config present**: launches a session directly. Picks the mesh from (in priority order): `--mesh` flag → last-used cache (`services/state/last-used.ts`) → interactive picker if ambiguous → the only joined mesh if there's just one.
- **Returning user, flag-first invocation** (`claudemesh --resume abc`, `claudemesh --name X`, `claudemesh -y`): same as returning user launch, with the flags passed to the launch handler.
- **Returning user, mesh already determined, clipboard has invite URL**: offers the invite as an alternative option before launching.
This is the v1 behavior preserved verbatim, minus the `launch` word.
### 4.4 Exit codes
```
0 success
1 user cancelled (Ctrl-C, declined prompt)
2 authentication failed
3 invalid arguments / unknown command
4 network error (broker or API unreachable — only when network was explicitly required)
5 not found (mesh, invite, peer)
6 already exists (slug collision, duplicate command)
7 permission denied (role, scope)
8 internal error (bug)
9 claude binary missing
```
These are defined in `src/constants/exit-codes.ts` and imported by every command handler.
### 4.5 Flag conventions
- `-y` / `--yes` — skip interactive confirmations
- `-q` / `--quiet` — suppress non-essential output
- `-v` / `--verbose` — increase log detail
- `--json` / `--output-format json` — machine-readable output with `schema_version: "1.0"`
- `--mesh <slug>` — override mesh selection
- `--token <value>` — override auth token (for scripts)
- `--help` / `-h` — per-command help
`-y` is the key flag. It enables scripted/non-interactive use by skipping every prompt. Commands that require a confirmation in interactive mode just proceed when `-y` is passed.
---
## 5. CLI user flows (net new)
All five user flows run against claudemesh.com's existing Better Auth + dashboard infrastructure. The broker already has `POST /cli-sync` (inventory §4) for JWT-based sync — v2 Pass 1 extends this to a full device-code OAuth flow.
### 5.1 `claudemesh register`
Creates a new claudemesh.com account via browser handoff (email verification is a browser-side step).
```
$ claudemesh register
Opening browser for account signup…
[browser opens to claudemesh.com/register?source=cli]
[user fills in email, password, verifies email]
[browser redirects to claudemesh.com/cli-auth with success token]
✔ Account created: alejandro@example.com
✔ Signed in.
```
Implementation:
1. CLI generates a one-time callback URL with a local port listener (`services/auth/callback-listener.ts`, ported from v1)
2. Opens browser to `claudemesh.com/register?source=cli&callback=http://localhost:<port>`
3. User completes signup + email verification on the web
4. claudemesh.com posts a session token back to the callback
5. CLI stores token in `~/.claudemesh/auth.json` via `services/auth/token-store.ts`
6. CLI calls `POST /cli-sync` to fetch any existing meshes (usually none for a new account)
**Backend requirement**: `claudemesh.com/register?source=cli&callback=<url>` must exist. If it doesn't, Pass 1 scope expands to add it to the web app (but this is a single Next.js page, not a backend architectural change).
### 5.2 `claudemesh login`
Device-code OAuth against claudemesh.com for existing accounts.
```
$ claudemesh login
Opening browser for sign-in…
If your browser didn't open, visit:
https://claudemesh.com/cli-auth?code=ABCD-EFGH
Waiting for confirmation…
✔ Signed in as Alejandro.
✔ Synced 3 meshes: platform-team, alejandro-mbp, claudefarm
```
Implementation:
1. CLI POSTs `/api/auth/cli/device-code/new` with device info (hostname, os, arch)
2. Receives `{ device_code, user_code, expires_at, verification_url }`
3. Opens browser to `verification_url?code=<user_code>`
4. Prints the user code to terminal as fallback
5. Polls `/api/auth/cli/device-code/<device_code>` every 1.5 seconds
6. On approval, receives `{ session_token, user }` + stores in `~/.claudemesh/auth.json`
7. Calls `POST /cli-sync` to fetch meshes
**Backend requirement**: `/api/auth/cli/device-code/*` endpoints must exist. These are net new. Pass 1 scope includes adding them to `apps/web/src/app/api/auth/cli/` as a thin wrapper around Better Auth's session creation. This is ~100 lines of backend TypeScript.
### 5.3 `claudemesh logout`
```
$ claudemesh logout
✔ Revoked session on claudemesh.com
✔ Removed ~/.claudemesh/auth.json
```
Implementation:
1. Calls `DELETE /api/my/sessions/current` (or equivalent Better Auth revoke endpoint)
2. Deletes `~/.claudemesh/auth.json`
3. On server revocation failure, still removes local file and warns:
```
✔ Removed local credentials.
⚠ Could not revoke session on claudemesh.com. Revoke manually at
https://claudemesh.com/dashboard/settings/sessions
```
### 5.4 `claudemesh whoami`
```
$ claudemesh whoami
Signed in as Alejandro (alejandro@example.com)
Token source: device-code (~/.claudemesh/auth.json)
Meshes: 3 owned, 1 guest
```
With `--json`:
```json
{
"schema_version": "1.0",
"signed_in": true,
"user": {
"id": "usr_abc123",
"display_name": "Alejandro",
"email": "alejandro@example.com"
},
"token_source": "device-code",
"meshes": { "owned": 3, "guest": 1 }
}
```
Implementation: reads `~/.claudemesh/auth.json`, calls `GET /api/my/profile` to verify + refresh, prints formatted output.
### 5.5 `claudemesh new <name>`
Creates a mesh on claudemesh.com from the CLI.
```
$ claudemesh new "Platform team"
✔ Created "platform-team" (id: msh_abc123)
✔ You're the owner
✔ Joined locally
Invite teammates with: claudemesh invite
```
Implementation:
1. Requires authenticated session (if not, triggers `login` flow first)
2. POSTs `/api/my/meshes` with `{ name, slug?: "platform-team" }` (slug auto-derived from name if not provided)
3. On slug collision, suggests alternative: `"A mesh called 'platform-team' already exists. Try 'platform-team-2'?"`
4. On success, stores the mesh in local config via `services/config/facade.ts`
5. Joins locally (generates Ed25519 keypair, stores in `~/.claudemesh/keys/<slug>.key`)
Flags:
- `--template <name>` — use a mesh template (reuses `templates/` directory ported from v1)
- `--description <text>` — optional description
**Backend requirement**: `POST /api/my/meshes` must exist. It may already exist (v1 backend has mesh creation endpoints per `member-api.ts`). If not, adding it is ~50 lines on top of existing schema patterns.
### 5.6 `claudemesh invite [email]`
Generates an invite URL for the current mesh, optionally sending email.
```
$ claudemesh invite
✔ Invite URL copied to clipboard:
https://claudemesh.com/i/AB12CD34
Expires in 7 days. Anyone with this link can join "platform-team".
```
With email:
```
$ claudemesh invite alice@example.com
✔ Sent to alice@example.com
✔ Also copied to clipboard
```
Implementation:
1. Resolves current mesh (via `services/mesh/resolve-target.ts`)
2. POSTs `/api/my/meshes/:slug/invites` with `{ email?, expires_in?: "7d", role?: "member" }`
3. Receives `{ url, code, expires_at }`
4. Copies to clipboard via `services/clipboard/facade.ts`
5. If email provided, backend sends an invitation email (reuses existing transactional email path in the web app)
Flags:
- `--mesh <slug>` — target mesh (default: current)
- `--expires <dur>` — expiry (default: `7d`)
- `--uses <n>` — max uses (default: unlimited)
- `--role <role>` — role for the invitee (default: `member`)
**Backend requirement**: `POST /api/my/meshes/:slug/invites` must exist. v1 has the inverse flow (`POST /api/public/invites/:code/claim`). Generating an invite is presumably already there since the dashboard has an "Invite" button — this just exposes it over the CLI.
### 5.7 `claudemesh connect telegram`
Interactive wizard that picks a mesh, requests a connect token from the broker, and shows a QR code + `t.me` deep link for the user to scan. Replaces v1's silent auto-pick of `config.meshes[0]` (which was a multi-mesh footgun).
```
$ claudemesh connect telegram
Connect Telegram to a mesh
──────────────────────────
Select mesh:
1) platform-team (owner, 4 members)
2) alejandro-mbp (owner, 1 member)
3) claudefarm (guest, 12 members)
[enter to confirm, ↑↓ to change, q to cancel]
✔ Token generated for "platform-team"
Scan with your phone camera, or tap the link:
█▀▀▀▀▀█ ▀▀ █▄▄▀ █▀▀▀▀▀█
█ ███ █ ██▄▄▄▄▀ █ ███ █
...
https://t.me/claudemeshbot?start=abc123…
Waiting for confirmation from Telegram… (Ctrl-C to skip)
✔ Connected as @alejandro_m
Messages to tg:alejandro_m now route to Telegram.
```
Implementation:
1. Loads joined meshes from `services/config/facade.ts`. If zero → error with `claudemesh join <url>` hint. If one → skip the picker. If multiple → render Ink mesh picker in `ui/telegram/ConnectWizard.tsx`.
2. Converts the chosen mesh's `brokerUrl` from `wss://` to `https://`, POSTs `{meshId, memberId, pubkey, secretKey}` to `<broker>/tg/token`.
3. Broker returns `{token, deepLink}`. Wizard renders `qrcode-terminal` ASCII block + the raw link so users on desktop can copy-paste.
4. Opens a short-lived WS subscription on the chosen mesh and waits for a `telegram_bridge_connected` push (new broker event — see §5.7.1 below). If received within 5 minutes, prints the success line with the resolved `tg:<username>`.
5. On Ctrl-C, prints `The link stays valid. Run 'claudemesh connect telegram --status' later to check.` and exits 0.
Flags:
- `--mesh <slug>` — skip the picker, target a specific mesh
- `--link` — print only the deep link, no QR, no wait (for scripting)
- `--status` — check whether the current mesh already has a Telegram bridge registered, without generating a new token
**Broker dependencies to verify before Phase 5:**
1. **Token TTL** — `/tg/token` currently returns `{token, deepLink}`. If the broker knows the TTL (it should — `generateTelegramConnectToken` encodes an expiry), extend the response to `{token, deepLink, expiresAt}`. This is a 3-line broker change and is the only broker touch permitted in Pass 1. Alternatively, the wizard just prints "link stays valid for a while" without a specific duration.
2. **Bridge-connected push event** — if the broker doesn't already emit a push when a Telegram bridge claims a token, the wizard falls back to polling `GET /mesh/:id/members` every 2s for a new `tg:*` member, with a 5-minute timeout. Polling is acceptable for Pass 1 — no broker change needed.
3. **Rate limit** — `/tg/token` is capped at 10 requests/hour/IP. Wizard must catch the 429 and print `You've requested too many Telegram tokens in the last hour. Try again in a few minutes.` instead of raw HTTP errors.
### 5.7.1 Optional broker edit (small, allowed)
The rule "broker unchanged in Pass 1" has one permitted exception: **extending the `/tg/token` response with `expiresAt`**. This is additive, unversioned, and costs three lines. If the v2 implementer judges even this too risky, the wizard simply omits the TTL display.
No other broker change is in scope.
---
## 6. Broker compatibility
### 6.1 The broker is unchanged
`apps/broker/src/` stays exactly as it is in v1. No files are added, modified, or removed. The broker:
- Listens on the same WS endpoint
- Handles the same 85 WS message types
- Serves the same 18 HTTP endpoints
- Uses the same Postgres schema
- Runs the same Neo4j, Qdrant, MinIO, Docker backends
- Executes the same Telegram bridge (`telegram-bridge.ts`, 1711 lines, unchanged)
- Applies the same rate limits, audit logging, status engine, priority delivery
**Why**: because there are no broker-side changes in scope, and touching the broker introduces risk without reward for Pass 1. Every Pass 2 improvement to the broker (role-per-mesh isolation, egress proxy, catalog audit, etc.) is deferred until its own decision point.
### 6.2 The CLI speaks v1's WS protocol
v2's `services/broker/ws-client.ts` is a port of v1's `ws/client.ts`. The port preserves:
- Every WS message type's envelope shape
- Every field name and type
- The Ed25519 hello signature format
- NaCl crypto_box envelope wrapping for `send_message`
- Reconnect logic, message queue, request/response correlation
The parity test layer 2 (WS contract tests) verifies this empirically by capturing v1's envelope for each message type and asserting v2's envelope matches byte-for-byte (modulo nonces and timestamps).
### 6.3 HTTP API compatibility
v2's `services/api/my.ts` and `services/api/public.ts` call the same endpoints v1 uses. `POST /cli-sync`, `POST /invites/:code/claim`, `POST /hook/set-status`, `POST /upload`, `GET /download/:id`, etc. — all unchanged.
The new user flow endpoints (`/api/auth/cli/device-code/*`, `POST /api/my/meshes`, `POST /api/my/meshes/:slug/invites`, `DELETE /api/my/sessions/current`, `GET /api/my/profile`) are net new but live in the claudemesh.com web app (`apps/web/`), not the broker. They're ~200 lines of Next.js API routes total.
### 6.4 Backend services unchanged
Postgres, Neo4j, Qdrant, MinIO, Docker — all on their v1 configurations. No role changes, no schema changes, no hardening, no egress controls. These all stay as Pass 2 work.
---
## 7. Implementation phases
Phases are organized by what gets built in each step. No time estimates — each phase ships when its acceptance criteria are met.
### Phase 0 — Scaffold
- Create `apps/cli-v2/` with the full source tree
- All files exist as stubs throwing `NotImplementedError` with the relevant spec reference in the header
- `package.json`, `tsconfig.json`, `bunfig.toml`, `build.ts`, `.eslintrc.cjs`, `dependency-cruiser.config.js`, `biome.json` all configured
- Custom ESLint plugin `tools/eslint-plugin-claudemesh/` scaffolded (3 rules)
- Test files for every layer of the parity plan scaffolded (~300 files)
- CI runs: lint green, type-check green, zero violations in dependency-cruiser + boundaries
- **Done when**: scaffold compiles and tests run (even if most tests are `NotImplementedError`)
### Phase 1 — Foundation layers
- `types/`, `constants/`, `utils/`, `locales/` fully implemented
- Pure services (`services/device/`, `services/clipboard/`, `services/spawn/`, `services/crypto/`, `services/i18n/`, `services/logger/`, `services/lifecycle/`)
- Unit tests for each (colocated)
- No user-visible behavior yet
- **Done when**: pure-layer unit tests all pass, 80%+ branch coverage on foundation services
### Phase 2 — Config + state + API client
- `services/config/` reads + writes `~/.claudemesh/config.json` with 0600 enforcement
- `services/state/last-used.ts` persists mesh picker cache
- `services/api/client.ts` typed fetch wrapper with retry
- `services/api/my.ts` + `services/api/public.ts` endpoint helpers
- Unit tests for each
- **Done when**: config round-trips, API client calls a mock server
### Phase 3 — Broker client (port from v1 ws/)
- `services/broker/ws-client.ts` ports v1's `ws/client.ts` into the facade pattern
- `services/broker/envelope.ts` and `hello-sig.ts` port v1's crypto primitives
- All 85 WS message types have typed facade methods
- **Contract test layer** (parity plan layer 2) goes green
- **Done when**: every WS contract test passes against captured v1 envelopes
### Phase 4 — MCP server (port from v1 mcp/server.ts)
- `mcp/server.ts` + `router.ts` + 21 tool family files
- Each tool family ports v1's handler logic unchanged, calling the new broker facade instead of v1's direct WS client
- **MCP tool parity test layer** (parity plan layer 3) goes green
- **Done when**: all 79 MCP tool parity tests pass
### Phase 5 — Auth + CLI user flows
- `services/auth/` full implementation: device-code flow, token store, dashboard sync, callback listener, browser opener
- `commands/register.ts`, `commands/login.ts`, `commands/logout.ts`, `commands/whoami.ts`
- Backend work (if needed): add `/api/auth/cli/device-code/*` endpoints to `apps/web/`
- `commands/new.ts`, `commands/invite.ts` — call the new backend endpoints
- End-to-end test of full flow: register → login → new mesh → invite → logout
- **Done when**: fresh user can register, create a mesh, invite a teammate, all from the CLI
### Phase 6 — Mesh + invite + remaining commands
- `services/mesh/` — list, rename, leave, join, resolve-target (port from v1)
- `services/invite/` — generate, parse-url, claim (port from v1)
- `commands/join.ts`, `commands/list.ts`, `commands/rename.ts`, `commands/leave.ts`, `commands/peers.ts`, `commands/send.ts`, `commands/inbox.ts`, `commands/state.ts`, `commands/info.ts`, `commands/remember.ts`, `commands/recall.ts`, `commands/remind.ts`, `commands/profile.ts`, `commands/status.ts`, `commands/sync.ts`
- `commands/install.ts`, `commands/uninstall.ts` — port from v1 (writes to `~/.claude.json` + `~/.claude/settings.json`)
- `commands/doctor.ts` — port from v1's 7 checks
- **Parity test layer** (parity plan layer 1) goes green for all non-launch commands
- **Done when**: every command in §4.1 main help works identically to v1
### Phase 7 — UI flows (welcome wizard + launch)
- `ui/welcome/` — welcome wizard screens (port from v1 commands/welcome.ts)
- `ui/launch/` — launch flow screens (port from v1 commands/launch.ts interactive bits)
- `commands/welcome.ts` → renders `ui/welcome/WelcomeScreen`
- Bare `claudemesh` dispatches to welcome (first run) or launch (returning)
- **Parity test layer** for first-run + launch journeys goes green
- **Done when**: welcome wizard and launch flow work identically to v1
### Phase 8 — Golden JSON + E2E tests
- `tests/golden/` — lock JSON output shape for every command that supports `--json`
- `tests/e2e/` — full journey tests against a real broker in testcontainers
- E2E tests cover the 28 flows from parity plan §6
- **Done when**: all e2e tests green on `main`, all golden tests green on PR
### Phase 9 — Docs + ship
- `apps/cli-v2/README.md` — usage, install, architecture overview
- `apps/cli-v2/CHANGELOG.md` — v1.0.0 entry listing changes from v0.10.5
- Root `README.md` updated to reference v2
- `docs/quickstart.md` rewritten for v2 command surface
- Migration notes: none (no users to migrate)
- **Done when**: docs complete, all layers green, ready to ship
### Phase 10 — Atomic swap + v1.0.0 publish
Single atomic commit:
```
rm -rf apps/cli && mv apps/cli-v2 apps/cli
git commit -m "v2: replace v1 CLI with the refactored v2 implementation"
```
Followed by:
```
cd apps/cli && pnpm publish --access public --no-git-checks
git tag v1.0.0
git push origin main --tags
```
No legacy preservation, no rollback window, no deprecation period. Clean break. (Feature is at `apps/cli/` after the swap, not `apps/cli-v2/`.)
---
## 8. Acceptance criteria
v2 Pass 1 ships when all of these are true:
### Test gates
- [ ] Every test file in `2026-04-11-v2-parity-test-plan.md` has a passing assertion (no `NotImplementedError` remaining)
- [ ] Layer 1 parity: ~70 test files covering v1 inventory §12 regression checks — all green
- [ ] Layer 2 contract: ~85 test files covering WS message types — all green against captured v1 envelopes
- [ ] Layer 3 MCP tool parity: 79 test files covering every MCP tool — all green
- [ ] Layer 4 e2e: 28 journey tests — all green against real broker in testcontainers
- [ ] Layer 5 golden JSON: 12 test files locking `--json` output shapes — all green
- [ ] Layer 6 facade units: colocated tests + boundary scanner — all green
- [ ] Layer 7 port-forwarded v1 tests: crypto round-trip + invite parse — all green
### Lint / type / structure gates
- [ ] `biome check` — zero violations
- [ ] `tsc --noEmit` — zero errors, strict mode
- [ ] ESLint `boundaries` plugin — zero violations (facade pattern holds)
- [ ] ESLint custom rules (`no-index-reexport-internal`, `type-imports-count-as-edges`, `no-dynamic-service-imports`) — zero violations
- [ ] `dependency-cruiser` — zero circular imports, zero layer violations, zero v1 imports (`no-v1-dependencies` rule)
- [ ] `no-restricted-imports` — zero violations
### Coverage gates
- [ ] `services/*/*.test.ts` — 80%+ branch coverage on non-broker services
- [ ] `services/broker/*` — 70%+ branch coverage via integration tests (WS client is the bulk of this)
- [ ] No file below 60% branch coverage without a documented reason in a PR comment
### Build / ship gates
- [ ] `bun build.ts` produces `dist/entrypoints/cli.js` and `dist/entrypoints/mcp.js`
- [ ] Gzipped JS bundle < 1.2 MB (enforced in `build.ts`)
- [ ] `bin/claudemesh` shebang works on macOS + Linux
- [ ] `pnpm publish` dry-run passes
- [ ] Published package installable via `npm i -g claudemesh-cli@1.0.0-rc.1`
- [ ] Cold start < 400 ms on Apple M2 Pro (benchmarked in `tests/bench/cold-start.bench.ts`)
### Behavioral gates (against v1 feature inventory §12)
- [ ] First-run install works on a fresh machine
- [ ] Welcome wizard shows on bare command with no config
- [ ] Launch works on returning machine (bare + flag-first)
- [ ] All 79 MCP tools dispatch correctly through the v2 MCP server
- [ ] Status priority engine (hook > manual > jsonl) unchanged
- [ ] Message queue priority delivery (now / next / low) unchanged
- [ ] Cryptographic integrity (Ed25519 signatures, NaCl envelopes, AES-GCM files) unchanged
- [ ] Scheduled reminders survive broker restart
- [ ] URL watch detects changes + survives broker restart
- [ ] Telegram bridge continues to work on the broker side (verified via e2e against broker directly, not via v2 CLI subcommand which is gone)
- [ ] Dashboard sync (`POST /cli-sync`) works
- [ ] Webhooks (`POST /hook/:meshId/:webhookId`) route external requests to mesh messages
- [ ] Doctor's 7 checks all pass on a clean install
### New behavior gates (net new in Pass 1)
- [ ] `claudemesh register` creates a new claudemesh.com account from the CLI
- [ ] `claudemesh login` completes the device-code flow
- [ ] `claudemesh logout` revokes the session + clears local credentials
- [ ] `claudemesh whoami` prints current identity
- [ ] `claudemesh new <name>` creates a mesh via `POST /api/my/meshes`
- [ ] `claudemesh invite` generates an invite via `POST /api/my/meshes/:slug/invites`
- [ ] `claudemesh invite <email>` sends a transactional email (if backend supports)
- [ ] Complete first-time user journey: `register → new → invite → send → logout` works end-to-end without opening the browser except for account signup email verification
### Regression gates (nothing should break)
- [ ] Zero previously-passing tests become failing
- [ ] No `--json` output shape change for any command that supported `--json` in v1
- [ ] Broker test suite (unchanged in Pass 1) stays green
When all gates are true, Pass 1 ships.
---
## 9. What gets renamed vs what gets rewritten
### Files that are ports (move + update imports, preserve logic)
These files' business logic is preserved verbatim from v1. The only changes are folder location, import paths, and the addition of a facade wrapper.
| v1 file | v2 location | Change |
|---|---|---|
| `cli/src/ws/client.ts` | `services/broker/ws-client.ts` | Import paths, facade wrap |
| `cli/src/ws/manager.ts` | `services/broker/manager.ts` | Import paths |
| `cli/src/crypto/envelope.ts` | `services/broker/envelope.ts` | Folder move only |
| `cli/src/crypto/hello-sig.ts` | `services/broker/hello-sig.ts` | Folder move only |
| `cli/src/crypto/keypair.ts` | `services/crypto/keypair.ts` | Facade wrap |
| `cli/src/crypto/file-crypto.ts` | `services/crypto/file-crypto.ts` | Facade wrap |
| `cli/src/auth/callback-listener.ts` | `services/auth/callback-listener.ts` | Folder move |
| `cli/src/auth/open-browser.ts` | `services/spawn/browser.ts` | Consolidated with launch spawn |
| `cli/src/auth/pairing-code.ts` | `services/auth/pairing-code.ts` | Folder move |
| `cli/src/auth/sync-with-broker.ts` | `services/auth/dashboard-sync.ts` | Folder move + facade wrap |
| `cli/src/invite/parse.ts` | `services/invite/parse-url.ts` | Folder move |
| `cli/src/invite/enroll.ts` | `services/invite/claim.ts` | Folder move |
| `cli/src/lib/invite-v2.ts` | `services/invite/parse-url.ts` (merged) | Consolidated |
| `cli/src/state/config.ts` | `services/config/` (split into read + write) | Split + facade wrap |
| `cli/src/mcp/server.ts` (2139 lines) | `mcp/server.ts` + `mcp/router.ts` + 21 tool family files | Split by family |
| `cli/src/mcp/tools.ts` | `mcp/tools/*.ts` — 21 family files | Split by family |
| `cli/src/templates/index.ts` | `templates/` — 5 individual files | Split by template name |
| `cli/src/commands/launch.ts` (775 lines) | `commands/welcome.ts` + `ui/launch/` + launch handler dispatched from bare command | Split |
| `cli/src/commands/welcome.ts` | `commands/welcome.ts` + `ui/welcome/` | Split |
| `cli/src/commands/install.ts` | `commands/install.ts` | Folder move, port ~538 lines |
| `cli/src/commands/doctor.ts` | `commands/doctor.ts` + `services/health/check-*.ts` | Split into health checks |
| `cli/src/commands/join.ts` | `commands/join.ts` + `services/mesh/join.ts` | Split into command + service |
| `cli/src/commands/list.ts` | `commands/list.ts` + `services/mesh/list.ts` | Split |
| `cli/src/commands/leave.ts` | `commands/leave.ts` + `services/mesh/leave.ts` | Split |
| `cli/src/commands/peers.ts` | `commands/peers.ts` | Folder move |
| `cli/src/commands/send.ts` | `commands/send.ts` | Folder move |
| `cli/src/commands/inbox.ts` | `commands/inbox.ts` | Folder move |
| `cli/src/commands/state.ts` | `commands/state.ts` | Folder move |
| `cli/src/commands/info.ts` | `commands/info.ts` | Folder move |
| `cli/src/commands/memory.ts` | `commands/remember.ts` + `commands/recall.ts` | Split into two commands |
| `cli/src/commands/remind.ts` | `commands/remind.ts` | Folder move |
| `cli/src/commands/profile.ts` | `commands/profile.ts` | Folder move |
| `cli/src/commands/status.ts` | `commands/status.ts` | Folder move |
| `cli/src/commands/sync.ts` | `commands/sync.ts` | Folder move |
| `cli/src/commands/hook.ts` | `commands/hook.ts` | Folder move |
| `cli/src/commands/create.ts` | `commands/new.ts` | **Renamed** (from `create` to `new`, matches spec) |
| `cli/src/commands/seed-test-mesh.ts` | `commands/seed-test-mesh.ts` | Folder move |
| `cli/src/commands/connect-telegram.ts` | `commands/connect-telegram.ts` + `ui/telegram/ConnectWizard.tsx` | Port + split: command stays thin, mesh picker + QR render live in Ink wizard (§5.7) |
| `cli/src/tui/colors.ts` | `ui/styles.ts` | Folder move, keep v1 values (Pass 2 redesigns) |
| `cli/src/tui/index.ts` | `ui/index.ts` | Folder move |
| `cli/src/tui/screen.ts` | `ui/screen.ts` | Folder move |
| `cli/src/tui/spinner.ts` | `ui/spinner.ts` | Folder move |
| `cli/src/env.ts` | `constants/urls.ts` + `constants/paths.ts` (split) | Consolidated into constants |
| `cli/src/version.ts` | `constants/version.ts` | Folder move |
| `cli/src/logo-spinner.ts` | `ui/logo-spinner.ts` | Folder move |
| `cli/src/index.ts` | `entrypoints/cli.ts` + `commands/index.ts` | Split entry point from command registry |
| `cli/src/__tests__/crypto-roundtrip.test.ts` | `tests/unit/crypto-roundtrip.test.ts` | Folder move |
| `cli/src/__tests__/invite-parse.test.ts` | `tests/unit/invite-parse.test.ts` | Folder move |
### Files that are NET NEW in Pass 1
| v2 file | Purpose |
|---|---|
| `commands/register.ts` | Account creation via browser handoff |
| `commands/login.ts` | Device-code OAuth |
| `commands/logout.ts` | Revoke session |
| `commands/whoami.ts` | Identity check |
| `commands/invite.ts` | Generate invite (was dashboard-only in v1) |
| `commands/rename.ts` | Rename a mesh (was dashboard-only in v1) |
| `services/auth/device-code.ts` | Device-code flow orchestration |
| `services/auth/token-store.ts` | `~/.claudemesh/auth.json` R/W |
| `services/auth/client.ts` | HTTP calls to new `/api/auth/cli/*` endpoints |
| `services/auth/facade.ts` | Public auth API |
| `services/mesh/create.ts` | `POST /api/my/meshes` |
| `services/mesh/facade.ts` | Public mesh API |
| `services/invite/generate.ts` | `POST /api/my/meshes/:slug/invites` |
| `services/invite/send-email.ts` | Email delivery through backend |
| `services/invite/facade.ts` | Public invite API |
| `services/api/facade.ts` | Public HTTP API |
| `services/config/facade.ts` | Public config API |
| `services/state/last-used.ts` | Last-used mesh/name/role cache |
| `services/state/facade.ts` | Public state API |
| `services/device/facade.ts` | Device info for device-code registration |
| `services/clipboard/facade.ts` | Clipboard read |
| `services/spawn/facade.ts` | Claude + browser spawning |
| `services/telemetry/facade.ts` | Usage event emission (opt-out) |
| `services/health/facade.ts` | Doctor checks |
| `services/update/facade.ts` | npm registry version poll |
| `services/i18n/facade.ts` | String templates (no ICU in Pass 1) |
| `services/lifecycle/facade.ts` | Service lifecycle manager |
| `services/logger/facade.ts` | Structured logger |
| `cli/argv.ts` | argv parsing |
| `cli/print.ts` | stdout helpers |
| `cli/structured-io.ts` | JSON output |
| `cli/exit.ts` | Exit code + cleanup hooks |
| `cli/update-notice.ts` | Update banner |
| `cli/handlers/signal.ts` | SIGINT/SIGTERM |
| `cli/handlers/error.ts` | Top-level error handler |
| `cli/output/list.ts` | Plain-text list renderer |
| `cli/output/peers.ts` | Plain-text peers renderer |
| `cli/output/whoami.ts` | Plain-text whoami renderer |
| `cli/output/version.ts` | Version output |
| `tools/eslint-plugin-claudemesh/` | Custom ESLint plugin (3 rules) |
| `tests/helpers/v1-runner.ts` | Spawns v1 CLI for parity comparisons |
| `tests/helpers/v2-runner.ts` | Spawns v2 CLI for parity comparisons |
| `tests/helpers/wire-capture.ts` | Records WS envelopes |
| `tests/helpers/mock-broker.ts` | In-memory broker |
| `tests/helpers/real-broker.ts` | Testcontainers broker harness |
### Files that are DELETED (no v2 replacement)
| v1 file | Why deleted |
|---|---|
| `cli/src/commands/disconnect-telegram.ts` | Bridge teardown is handled inside Telegram (`/revoke` in the bot) or by leaving the mesh; a CLI wrapper is purely cosmetic |
### Files that are NEW in the backend (apps/web/)
| File | Purpose |
|---|---|
| `apps/web/src/app/api/auth/cli/device-code/new/route.ts` | Issue device code |
| `apps/web/src/app/api/auth/cli/device-code/[code]/route.ts` | Poll device code status |
| `apps/web/src/app/api/auth/cli/device-code/[code]/approve/route.ts` | Approve from browser |
| `apps/web/src/app/[locale]/cli-auth/page.tsx` | Browser-side approval UI |
| `apps/web/src/app/api/my/meshes/route.ts` | POST = create mesh (may already exist) |
| `apps/web/src/app/api/my/meshes/[slug]/invites/route.ts` | POST = generate invite (may already exist) |
| `apps/web/src/app/api/my/profile/route.ts` | GET = whoami (may already exist) |
| `apps/web/src/app/api/my/sessions/current/route.ts` | DELETE = logout |
These are small Next.js API routes (~50 lines each) that wrap Better Auth's session management. The exact set depends on what already exists in the web app — an audit is part of Phase 5.
---
## 10. Decisions locked in by this spec
If you want to change any of these, update this spec and run the parity tests again.
1. **CLI command surface is flat.** No `advanced` prefix. No `launch` subcommand. No `connect telegram` / `disconnect telegram`. The main `--help` groups commands visually but the namespace is flat.
2. **Pass 1 preserves every v1 feature in the inventory.** No feature cuts, no "while we're at it, let's also change X." The only removals are CLI subcommands that don't add value (launch, connect-telegram, disconnect-telegram).
3. **The broker is unchanged in Pass 1.** No files added, modified, or removed. All broker improvements are deferred to Pass 2.
4. **Facade pattern is mandatory.** Every service has a `facade.ts` as the single public entry point. ESLint boundaries enforce this.
5. **No Pass 2 features sneak in.** No local SQLite, no Lamport, no sync daemon, no hardened Postgres, no MCP catalog tiering, no ICU, no accessibility matrix, no session_kind enum, no six-color palette redesign, no facade bypass path closures beyond what exists in the Pass 2 facade spec.
6. **No backwards compatibility hedges.** There are no users to migrate. v2 picks the best names, shapes, and conventions without alias support or deprecation windows.
7. **The parity test suite is the acceptance criteria.** Not vibes, not "looks good to me," not code review alone. A red test means not-done.
8. **The broker's WS protocol is the only compatibility contract that matters.** Everything above that layer (command names, config shape, JSON output, env vars, exit codes, stdout text) can change freely.
9. **Atomic swap, clean break.** When Pass 1 is ready, a single commit replaces `apps/cli` with `apps/cli-v2`. No legacy preservation.
10. **Pass 1 is done when the acceptance criteria in §8 are all green.** Nothing more, nothing less.
---
**End of Pass 1 spec.**