Add the foundation for deploying and managing MCP servers on the VPS
broker, with per-peer credential vaults and visibility scopes.
Architecture:
- One Docker container per mesh with a Node supervisor
- Each MCP server runs as a child process with its own stdio pipe
- claudemesh launch installs native MCP entries in ~/.claude.json
- Mid-session deploys fall back to svc__* dynamic tools + list_changed
New components:
- DB: mesh.service + mesh.vault_entry tables, mesh.skill extensions
- Broker: 19 wire protocol types, 11 message handlers, service catalog
in hello_ack with scope filtering, service-manager.ts (775 lines)
- CLI: 13 tool definitions, 12 WS client methods, tool call handlers,
startServiceProxy() for native MCP proxy mode
- Launch: catalog fetch, native MCP entry install, stale sweep, cleanup,
MCP_TIMEOUT=30s, MAX_MCP_OUTPUT_TOKENS=50k
Security: path sanitization on service names, column whitelist on
upsertService, returning()-based delete checks, vault E2E encryption.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After MCP registration and hooks setup, `claudemesh install` now checks
the config for joined meshes. If empty, it prints actionable guidance
(join command + dashboard URL) instead of the generic "Next:" line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace in-memory-only setTimeout scheduling with a DB-backed system
that survives broker restarts. Adds:
- `scheduled_message` table in mesh schema (Drizzle + raw CREATE TABLE
for zero-downtime deploys)
- Minimal 5-field cron parser (no dependencies) with next-fire-time
calculation for recurring entries
- On broker boot, all non-cancelled entries are loaded from PostgreSQL
and timers re-armed automatically
- CLI `schedule_reminder` MCP tool accepts optional `cron` expression
- CLI `remind` command accepts `--cron` flag
- One-shot reminders remain backward compatible — no cron field = same
behavior as before
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 4 missing tools (cancel_scheduled, grant_file_access, list_scheduled,
schedule_reminder) and sort the array alphabetically for maintainability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Predefined mesh configurations (dev-team, research, ops-incident,
simulation, personal) let users bootstrap meshes with groups, roles,
state keys, and system prompt hints. Templates are bundled at build
time via Bun's JSON import support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend the WS hello handshake with optional peerType, channel, and model
fields so peers can advertise what kind of client they are. The broker
stores these in-memory on PeerConn and returns them (along with cwd) in
the peers_list response. CLI peers command and MCP list_peers tool now
display the new metadata.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace manual switch + HELP string with citty defineCommand/runMain.
Flag definitions in index.ts are now the single source of truth for
--help output. Remove parseArgs() from launch.ts; accept citty-parsed
flags + rawArgs (-- passthrough to claude preserved).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
broker: expand member groups to ancestor paths at drain time (pull model)
- @flexicar message reaches peers in @flexicar/core, @flexicar/output, etc.
- Resolved at drainForMember — no DB changes, fully backward-compatible
- Any depth: flexicar/team/backend also matches @flexicar and @flexicar/team
cli: wire --role all the way through to session config + env
- Config.role field added
- launch.ts stores role in sessionConfig, passes CLAUDEMESH_ROLE env var
- mcp/server.ts includes role in identity string
- manager.ts auto-joins groups from config on WS connect (--groups flag now works)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--inbox: count-only notifications, no content in context
--no-messages: tools only, zero prompt injection risk
Default: push (real-time, current behavior)
Wizard shows mode picker when no flag provided.
MCP instructions tell Claude its current mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase A of the claudemesh spec. Peers can now join named groups
with roles, and messages route to @group targets.
Broker:
- @group routing in fan-out (matches peer group membership)
- @all alias for broadcast
- join_group/leave_group WS messages + DB persistence
- list_peers returns group metadata
- drainForMember matches @group targetSpecs in SQL
CLI:
- join_group/leave_group MCP tools
- send_message supports @group targets
- list_peers shows group membership
- PeerInfo includes groups array
- Peer name cache for push notifications
Launch:
- --role flag (optional peer role)
- --groups flag (comma-separated, e.g. "frontend:lead,reviewers")
- Interactive wizard for role + groups when flags omitted
- Groups written to session config for broker hello
Spec: SPEC.md added with full v0.2 vision (groups, state, memory)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Write displayName into tmpdir config.json so the MCP server reads
it directly. Env vars from claudemesh launch may not propagate to
MCP child processes spawned by Claude Code. Config file is reliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claudemesh launch now passes --dangerously-skip-permissions to
claude so peers can chat without per-tool-call approval prompts.
Shows a clear explanation before launch; user confirms with Enter.
Skip with -y/--yes for CI or repeat launches.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each WS connection generates its own ed25519 keypair (sessionPubkey)
sent in the hello handshake. The broker stores it on the presence
row and uses it for message routing + list_peers. This gives every
`claudemesh launch` a unique crypto identity without burning invite
uses — member auth stays permanent, session identity is ephemeral.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
withPayload crashes ALL routes with React #130 in standalone
output — even with admin page replaced by redirect. The wrapper
injects a client-side ConfigProvider that fails hydration.
Removed: withPayload wrapper, entire (payload) route group.
Kept: payload.config.ts, migrations, blog/changelog server-side
queries with graceful DB fallback.
Payload admin runs on local dev only (add withPayload back in
next.config when running pnpm dev). Production content via
static TSX pages or future API-based publishing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove Payload's /api/[...slug] route that conflicts with existing
/api/[...route]. Blog/changelog pages use Payload's local API.
Includes cli install.ts backup + assertNoMcpLoss guards (from
worktree agent).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Running \`claudemesh\` with no args now detects install state and
prints context-appropriate guidance: suggests \`install\` if MCP
not registered, \`join\` if no meshes, \`launch\` if ready.
Replaces the static HELP dump with a first-run wizard that meets
users where they are.
Static HELP still available via --help.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three Tier-2 polish commands for debugging + discoverability:
- claudemesh --version / -v: print CLI version (baked from
package.json at build time via Bun JSON import).
- claudemesh status: WS-probe each joined mesh's broker, report
reachability per mesh. Exit 1 if any broker unreachable.
- claudemesh doctor: run 6 preconditions — Node>=20, claude on PATH,
MCP registered, hooks registered, config file parses + chmod 0600,
mesh keypairs validate. Each check has a pass/fail + fix hint.
Exit 0 if all pass.
Help text now leads with version (\"claudemesh v0.1.3 —\").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `claudemesh launch [args]` that spawns Claude Code with
--dangerously-load-development-channels server:claudemesh so peer
messages arrive as <channel> system reminders mid-turn instead of
pull-only via check_messages. Windows uses shell:true to resolve
claude.cmd from PATHEXT.
Prints an info banner before spawning that explains the channel's
scope (peer text injection only), the trust model (treat as
untrusted input), and that existing tool-approval prompts remain
the safety net. --quiet skips the banner.
Install output now mentions `claudemesh launch` as the recommended
launch path; plain `claude` still works for pull-only mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full parity with claude-peers:
1. Push-injection (the "tap on shoulder" UX)
- MCP server now declares experimental.claude/channel capability
- BrokerClient onPush handlers emit server.notification({
method: "notifications/claude/channel",
params: { content, meta: {from_id, from_name, mesh_slug,
mesh_id, priority, sent_at, delivered_at, kind}}
})
- Claude Code injects each push as <channel source="claudemesh">
system reminder, so the receiver session sees inbound messages
WITHOUT calling check_messages manually
- Updated MCP instructions with the "RESPOND IMMEDIATELY" framing
(adapted from claude-peers)
2. Status hooks in install (default-on, --no-hooks to opt out)
- new apps/cli/src/commands/hook.ts: reads stdin JSON (Claude Code
hook payload), extracts cwd+session_id, POSTs /hook/set-status
to every joined mesh's broker in parallel with process.ppid +
1s timeout per POST. Silent fail, fire-and-forget.
- install.ts: writes to ~/.claude/settings.json registering
`claudemesh hook idle` on Stop + `claudemesh hook working` on
UserPromptSubmit. Idempotent, preserves other hook entries.
- uninstall.ts: removes both hook entries + MCP entry; leaves
unrelated hook/MCP entries alone.
- dedupes by brokerUrl (multiple meshes on same broker → one POST)
3. CLI surface
- new subcommand: `claudemesh hook <status>` (internal, but
exposed so Claude Code can invoke it via the hook shell command)
- `install --no-hooks` for users who want bare MCP registration
- --help updated
Coexistence with claude-peers: both tools register Stop and
UserPromptSubmit hooks, each POSTs to its own broker. Claude Code
fires multiple hooks per event without conflict.
npm version 0.1.0 → 0.1.1 (patch).
Verified:
- install with hooks → 2 entries added to settings.json ✓
- install --no-hooks → "Hooks skipped" ✓
- uninstall → both MCP entry + 2 hook entries removed ✓
- `echo '{...}' | claudemesh hook idle` with no joined meshes →
silent no-op ("no joined meshes, nothing to do") ✓
- MCP initialize response includes experimental.claude/channel ✓
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pairs with claudemesh-2's new /join/[token] landing page. Users can
now paste a clickable HTTPS URL instead of the dev-only ic:// scheme.
apps/cli/src/invite/parse.ts — new extractInviteToken() handles
four input formats before handing the raw base64url token to the
existing parseInviteLink pipeline:
- https://claudemesh.com/join/<token> (primary, clickable)
- https://claudemesh.com/<locale>/join/<token> (i18n prefix)
- ic://join/<token> (still supported, dev)
- <raw-token> (last resort: bare base64url)
User-facing strings updated to the HTTPS form:
- cli help: "join <url>"
- install success message
- list (no-meshes) hint
- MCP server "no meshes" error
- README.md primary example
- docs/QUICKSTART.md Path A + Path B
Verified extractInviteToken() on all 4 formats — each returns the
same base64url token → same broker /join lookup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claudemesh/cli was already taken on npm by an unrelated project
(claudemesh "domain packages", v1.0.7). PM picked option A: publish
unscoped as claudemesh-cli. Binary name stays "claudemesh" — users
type the natural thing on install:
npm install -g claudemesh-cli
claudemesh install
claudemesh join ic://join/...
renamed references everywhere:
- apps/cli/package.json: name
- apps/cli/README.md: title + install command
- apps/cli/src/{index.ts, mcp/server.ts, commands/install.ts} headers
- docs/QUICKSTART.md: install command, version banner, npx hint
- docs/roadmap.md: package name
also (PM journey-friction #5): surface the "restart Claude Code" step
LOUDLY in install output. Added a yellow-bold warning line after the
✓ success lines so new users don't miss the restart step (MCP tools
only load on Claude Code restart).
⚠ RESTART CLAUDE CODE for MCP tools to appear.
ANSI colors gated on isTTY + NO_COLOR/TERM=dumb guards.
bundle rebuilt. ready for npm publish pending user's `npm adduser`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Makes @claudemesh/cli installable globally via npm without requiring
bun on user machines. (Bun stays the dev runtime; bundled output is
node-compatible.)
- bun build --target=node --outfile dist/index.js produces a 2.69MB
standalone bundle with node-shebang banner
- package.json: add description/keywords/author/license/homepage/
repository, set bin to ./dist/index.js, files=[dist, README, LICENSE],
publishConfig.access=public, engines.node >=20
- prepublishOnly auto-runs the build
- pin zod from catalog: to 4.1.13 (npm rejects catalog: refs)
- swap Bun.spawnSync → node:child_process.spawnSync in install.ts
(the only Bun-global usage in the package)
- strip shebang from src/index.ts (banner supplies it post-bundle)
install command now runs in two modes:
- BUNDLED (npm i -g): detects dist/index.js path, writes MCP entry
with command "claudemesh" (relies on the global bin shim on PATH)
- SOURCE (bun src/index.ts, dev): preflights bun, writes MCP entry
with command "bun <absolute-path> mcp"
verified end-to-end:
- node dist/index.js --help prints usage ✓
- node dist/index.js install writes correct ~/.claude.json ✓
- node dist/index.js mcp | tools/list returns all 5 tools ✓
- bun src/index.ts install (dev mode) still works ✓
NOT PUBLISHED YET — @claudemesh/cli is owned by an unrelated project
on npm. Awaiting user decision on alternative name (claudemesh-cli,
@alezmad/claudemesh-cli, or new org scope). Bundle is name-agnostic
and will reuse regardless.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous flow printed a \`claude mcp add ...\` command and asked
users to paste it. That's 2 steps, a typo surface, and a point of
user dropoff. Replace with direct read-modify-write of ~/.claude.json.
install:
- preflights bun on PATH (clear error + Bun.com link if missing)
- verifies the MCP entry file exists on disk
- reads ~/.claude.json (empty object if absent)
- adds/updates mcpServers.claudemesh with resolved absolute path
- writes back with 0600 perms, creates parent dir if needed
- read-back verification (bails loudly if post-write state is wrong)
- idempotent: re-running returns "unchanged" if entry already matches
- preserves existing mcpServers entries + other top-level config keys
uninstall:
- removes the claudemesh entry if present
- no-ops cleanly when entry or config file doesn't exist
- doesn't touch anything else
Both print a clear post-action hint: "Restart Claude Code to load
the MCP server. Then join a mesh with claudemesh join <invite-link>".
verified locally with HOME=/tmp/fake-home:
- fresh install → ✓ added, config emitted correctly
- re-install → ✓ unchanged (idempotent)
- install alongside existing "other-mcp" entry → both preserved,
plus unrelated top-level keys kept verbatim
- uninstall → ✓ removed, claudemesh gone, other entries intact
- uninstall again → · not present (no error)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
End-to-end join: user runs `claudemesh join ic://join/<base64>` and
walks away with a signed member record + persistent keypair.
new modules:
- src/crypto/keypair.ts: libsodium ed25519 keypair generation. Format
is crypto_sign_keypair raw bytes, hex-encoded (32-byte pub, 64-byte
secret = seed || pub). Same format libsodium will need in Step 18
for sign/verify.
- src/invite/parse.ts: ic://join/<base64url(JSON)> parser with Zod
shape validation + expiry check. encodeInviteLink helper for tests.
- src/invite/enroll.ts: POST /join to broker, converts ws:// to http://
transparently.
rewritten join command wires them together:
1. parse invite → 2. generate keypair → 3. POST /join → 4. persist
config → 5. print success.
state/config.ts: saveConfig now chmods the file to 0600 after write,
since it holds ed25519 secret keys. No-op on Windows.
signature verification (step 18) + invite-token one-time-use tracking
are deferred. For now the invite link is a plain bearer token; any
client with the link can join.
verified end-to-end via apps/cli/scripts/join-roundtrip.ts:
build invite → run join subprocess → load new config → connect as
new member → send A→B → receive push. Flow passes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
broker-client: full WS client with hello handshake + ack, auto-reconnect
with exponential backoff (1s → 30s capped), in-memory outbound queue
(max 100) during reconnect, 500-entry push buffer for check_messages.
MCP tool integration:
- send_message: "slug:target" prefix or single-mesh fast path
- check_messages: drains push buffers across all clients
- set_status: fans manual override across all connected meshes
- set_summary: stubbed (broker protocol extension needed)
- list_peers: stubbed — lists connected mesh slugs + statuses
manager module holds Map<meshId, BrokerClient>, starts on MCP server
boot for every joined mesh in ~/.claudemesh/config.json.
new CLI command: seed-test-mesh injects a mesh row for dev testing.
also fixes a broker-side hello race: handleHello sent hello_ack before
the caller closure assigned presenceId, so clients sending right after
the ack hit the no_hello check. Fix: return presenceId, caller sets
closure var, THEN sends hello_ack. Queue drain is fire-and-forget now.
round-trip verified: two clients, A→B, push received with correct
senderPubkey + ciphertext. 44/44 broker tests still pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>