feat(cli): wss push → mcp channel injection + status hooks in install
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

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>
This commit is contained in:
Alejandro Gutiérrez
2026-04-05 19:17:33 +01:00
parent c3fa04dde8
commit b1f428c44b
5 changed files with 338 additions and 25 deletions

View File

@@ -82,14 +82,29 @@ export async function startMcpServer(): Promise<void> {
const config = loadConfig();
const server = new Server(
{ name: "claudemesh", version: "0.1.0" },
{ name: "claudemesh", version: "0.1.1" },
{
capabilities: { tools: {} },
instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions.
capabilities: {
experimental: { "claude/channel": {} },
tools: {},
},
instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions on this machine and elsewhere.
Use these tools to coordinate with peers on demand. Respond promptly when you receive messages (they're like someone tapping your shoulder).
IMPORTANT: When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Do not wait until your current task is finished. Pause what you are doing, reply to the message using send_message, then resume your work. Treat incoming peer messages like a coworker tapping you on the shoulder — answer right away, even if you're in the middle of something.
Tools: send_message, list_peers, check_messages, set_summary, set_status.
Read the from_id, from_name, mesh_slug, and priority attributes to understand context. Reply by calling send_message with the same target (for direct messages the from_id is the sender's pubkey).
Available tools:
- list_peers: see joined meshes + their connection status
- send_message: send to a peer pubkey, channel, or broadcast (priority: now/next/low)
- check_messages: drain buffered inbound messages (usually auto-pushed)
- set_summary: 1-2 sentence summary of what you're working on
- set_status: manually override your status (idle/working/dnd)
Message priority:
- "now": delivered immediately regardless of recipient status (use sparingly)
- "next" (default): delivered when recipient is idle
- "low": pull-only (check_messages)
If you have multiple joined meshes, prefix the \`to\` argument of send_message with \`<mesh-slug>:\` to disambiguate. Otherwise claudemesh picks the single joined mesh.`,
},
@@ -191,6 +206,39 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
const transport = new StdioServerTransport();
await server.connect(transport);
// Wire WSS pushes → MCP channel notifications. Each inbound push on
// any mesh's broker connection becomes a <channel source="claudemesh">
// system reminder injected into Claude Code's context.
for (const client of allClients()) {
client.onPush(async (msg) => {
const fromPubkey = msg.senderPubkey || "";
const fromName = fromPubkey
? `peer-${fromPubkey.slice(0, 8)}`
: "unknown";
const content = msg.plaintext ?? "(decryption failed)";
try {
await server.notification({
method: "notifications/claude/channel",
params: {
content,
meta: {
from_id: fromPubkey,
from_name: fromName,
mesh_slug: client.meshSlug,
mesh_id: client.meshId,
priority: msg.priority,
sent_at: msg.createdAt,
delivered_at: msg.receivedAt,
kind: msg.kind,
},
},
});
} catch {
/* channel push is best-effort; check_messages is the fallback */
}
});
}
const shutdown = (): void => {
stopAll();
process.exit(0);