Three bugs compounding when multiple peers share a display name:
1. list_peers (MCP + CLI) truncated pubkey to 12 hex chars with an
ellipsis. A truncated pubkey cannot be used as a routing key, so
the caller had no way to disambiguate visually.
2. send_message required the full 64-hex pubkey and refused prefix
input, forcing callers to rely on --json output to get a full key.
3. Name-based resolution returned the first exact match without
filtering the caller's own session — so "send to <my-own-name>"
would bounce against the broker's self-send guard when another
session of the same user was the intended target.
Fixes:
- list_peers now prints 16-char pubkey prefix labelled "pubkey: …"
(MCP) and appends it to CLI output
- send_message accepts any 8–64 hex-char prefix and resolves against
live peer lists across joined meshes; unique match routes, multi-
match returns a disambiguation error listing each candidate's
displayName + pubkey + cwd
- Name matches now skip the caller's own session pubkey; multiple
same-named matches fail loudly with a copy-pasteable pubkey
disambiguation hint instead of silently picking one
- Full 64-char pubkeys without a live match still queue at the
broker (preserves offline-delivery semantics)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs that combined to make Claude's peer-send look successful even
when the recipient didn't exist:
1. resolveClient fell through to 'let the broker try' when a single
mesh was joined and the name didn't match any peer. The broker
queued the message against the literal unknown string, matched no
peer in fan-out, but returned a messageId — so the CLI reported
'✓ lezg → msgId' for a peer that was never there.
Now: refuse to send, list the known peer names.
2. list_peers showed the same pubkey multiple times with different
display_names (one per live session) without hinting that they
were the same member — so Claude treated them as distinct people.
Now: annotate with '[shares key with N other session(s)]' so the
caller understands one pubkey = one identity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
Claude Code's MCP prompts path doesn't support the context field
natively. When a skill has context:"fork", prepend an instruction
telling the model to use the Agent tool with the specified agent
type and model.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Welcome was silently dropped when sent before Claude Code's
notifications/initialized. Add 2s delay after WS connects to
ensure the MCP handshake is complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move startClients() to run after server.connect(), not before.
MCP server is available to Claude Code in <0.5s instead of ~30s.
Tool handlers gracefully return errors until WS is ready.
Push event wiring happens in background callback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claudemesh MCP server now declares prompts:{} and resources:{} capabilities.
Mesh skills auto-appear as /claudemesh:skill-name slash commands in Claude Code
via prompts/list+get, and as skill://claudemesh/{name} resources for the
upcoming MCP_SKILLS protocol. share_skill accepts optional metadata (when_to_use,
allowed_tools, model, context, agent) stored in the manifest jsonb column.
Change notifications sent on share/remove so Claude Code refreshes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
System messages (watch_triggered, mcp_deployed, peer_joined, etc.)
have senderPubkey='system' with empty ciphertext. The push handler
now formats them as readable plaintext instead of failing to decrypt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runner /load now accepts gitUrl, npxPackage, or sourcePath. It handles
git clone and npm install internally. Broker no longer needs shared
volume for source extraction — just tells the runner what to fetch.
CLI mesh_mcp_deploy now supports npx_package as a third source type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Wave 3I handlers (vault_set, vault_list, vault_delete, mesh_mcp_deploy,
mesh_mcp_undeploy, mesh_mcp_update, mesh_mcp_logs, mesh_mcp_scope,
mesh_mcp_schema, mesh_mcp_catalog, mesh_skill_deploy) were lost during
the re-apply phase. Tools were registered in tools/list but returned
"Unknown tool" because the switch cases in server.ts were missing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Persistent MCP servers (opt-in via `persistent: true`) survive host
disconnects — they appear as offline in mcp_list and auto-restore when
the host reconnects. Ephemeral servers (default) still clean up on
disconnect. Offline servers return a clear error on mcp_call with
time-since-disconnect info.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Save groups, profile, visibility, summary, display name, and cumulative
stats to a new mesh.peer_state table on disconnect. On reconnect (same
meshId + memberId), restore them automatically — hello groups take
precedence over stored groups if provided. Broadcast peer_returned
system event with last-seen time and summary to other peers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Peers now receive [system] notifications when MCP servers join or
leave the mesh, with tool names and hosting peer info.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Teaches AI when to use filesystem (local), read_peer_file (remote
<1MB), or share_file (persistent, no size limit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When read_peer_file targets a local peer (same hostname), prepend a
hint with the direct filesystem path. Still executes the relay as
fallback — AI learns the shortcut without being blocked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Peers report os.hostname() in the hello handshake. list_peers shows
[local] or [remote] tag per peer. MCP instructions teach AI to read
local peers' files directly via filesystem instead of relay.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire up MCP tool handlers for the peer file sharing relay. Peers can
now read files and list directories from other peers' local filesystems
through the mesh broker. Includes name-to-pubkey resolution, base64
decode, and instructions table update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds share_skill, get_skill, list_skills, and remove_skill across the full
stack (Drizzle schema, broker CRUD + WS handlers, CLI client methods, MCP
tools). Skills are mesh-scoped, unique by name, and searchable via ILIKE
on name/description/tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Peers self-report resource usage via set_stats; stats visible in
list_peers responses and the new mesh_stats MCP tool. CLI auto-reports
every 60s and tracks messagesIn/Out, toolCalls, uptime, and errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Peers can register MCP servers with the mesh and other peers can invoke
those tools through the existing claudemesh connection without restarting.
Broker: in-memory MCP registry with mcp_register/unregister/list/call
handlers, call forwarding to hosting peer with 30s timeout, and automatic
cleanup on peer disconnect.
CLI: mcpRegister/mcpUnregister/mcpList/mcpCall client methods, inbound
mcp_call_forward handler, and 4 new MCP tools (mesh_mcp_register,
mesh_mcp_list, mesh_tool_call, mesh_mcp_remove).
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>
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>
When a peer connects or disconnects, the broker now broadcasts a
system push (subtype: "system") to all other peers in the same mesh.
The CLI formats these as [system] channel notifications so AI sessions
can react to topology changes without polling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- claim_task/complete_task: send taskId not id
- graph_result: read msg.records not msg.rows
- message_status: try all mesh clients, not only first
- broker: omit state_result for set_state (fixes get_state cross-contamination)
- error handler: unblock first pending resolver on unmatched broker errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
broker: owner also fetches sealedKey from mesh.file_key (not skipped),
only non-owners are blocked when key is missing
cli: explicit error when encrypted file has no sealedKey (no silent raw download)
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>
Node.js stdout to a pipe is buffered. Without periodic event loop
activity, WS callback → server.notification() → stdout.write() may
not flush until the next I/O event. A 1s setInterval (NOT unref'd)
keeps the event loop ticking so notifications flush immediately.
This is why claude-intercom worked: its 1s HTTP poll kept the event
loop active as a side effect. Claudemesh's passive WS listener let
the event loop settle, causing stdout to buffer indefinitely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sends test messages to self through the full pipeline per priority
and measures round-trip timing. Reports send→ack and send→receive
latency. Detects broker priority gating (status=working holds next/low).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert poll-based drain (v0.5.2 overcorrection). Claude Code source
confirms notifications are processed event-driven via React
useEffect, not polled. The WS onPush → server.notification() path
is correct.
Added section 13 to SPEC.md documenting the full Claude Code
notification pipeline, feature gates, priority gating, and common
push delivery issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace WS onPush→notification with timer-based buffer drain.
The old claude-intercom used 1s polling and worked reliably.
WS async callbacks may not flush stdio properly for MCP
notifications. Polling on a timer ensures consistent delivery.
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
Files: MinIO-backed file sharing built into the broker.
share_file for persistent mesh files, send_message(file:) for
ephemeral attachments. Presigned URLs for download, access
tracking per peer.
Broker infra: MinIO in docker-compose, internal network.
HTTP POST /upload endpoint. WS handlers for get_file,
list_files, file_status, delete_file.
Multi-target: send_message(to:) accepts string or array.
Targets deduplicated before delivery.
Targeted views: MCP instructions teach Claude to send
tailored messages per audience instead of generic broadcasts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase B + C + message delivery status.
State: shared key-value store per mesh. set_state pushes changes
to all peers. get_state/list_state for reads. Peers coordinate
through shared facts instead of messages.
Memory: persistent knowledge with full-text search (tsvector).
remember/recall/forget. New peers recall context from past sessions.
message_status: check delivery status with per-recipient detail
(delivered/held/disconnected).
Multicast fix: broadcast and @group messages now push directly to
all connected peers instead of racing through queue drain.
MCP instructions: dynamic identity injection (name, groups, role),
comprehensive tool reference, group coordination guide.
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>
onPush now queries list_peers to resolve the sender's pubkey to their
display name. Instructions updated to tell Claude to reply by name
instead of raw pubkey. Fixes two-way messaging between named peers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>