feat(cli): websocket client + MCP tool integration
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>
This commit is contained in:
55
apps/cli/src/ws/manager.ts
Normal file
55
apps/cli/src/ws/manager.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Process-wide registry of BrokerClient connections, keyed by meshId.
|
||||
*
|
||||
* The MCP server lazily starts a client per joined mesh on startup,
|
||||
* keeps them alive for the life of the process, and uses them to
|
||||
* service MCP tool calls.
|
||||
*/
|
||||
|
||||
import { BrokerClient } from "./client";
|
||||
import type { Config, JoinedMesh } from "../state/config";
|
||||
import { env } from "../env";
|
||||
|
||||
const clients = new Map<string, BrokerClient>();
|
||||
|
||||
/** Ensure a BrokerClient exists + is connecting/open for this mesh. */
|
||||
export async function ensureClient(mesh: JoinedMesh): Promise<BrokerClient> {
|
||||
const existing = clients.get(mesh.meshId);
|
||||
if (existing) return existing;
|
||||
const client = new BrokerClient(mesh, { debug: env.CLAUDEMESH_DEBUG });
|
||||
clients.set(mesh.meshId, client);
|
||||
try {
|
||||
await client.connect();
|
||||
} catch {
|
||||
// Connect failed → client is in "reconnecting" state, leave it
|
||||
// wired so tool calls can surface the status.
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/** Start clients for every joined mesh. Called once on MCP server start. */
|
||||
export async function startClients(config: Config): Promise<void> {
|
||||
await Promise.allSettled(config.meshes.map(ensureClient));
|
||||
}
|
||||
|
||||
/** Look up a client by mesh slug (human-friendly) or meshId. */
|
||||
export function findClient(needle: string): BrokerClient | null {
|
||||
// Try meshId first, then slug.
|
||||
const byId = clients.get(needle);
|
||||
if (byId) return byId;
|
||||
for (const c of clients.values()) {
|
||||
if (c.meshSlug === needle) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** All clients across all meshes. */
|
||||
export function allClients(): BrokerClient[] {
|
||||
return [...clients.values()];
|
||||
}
|
||||
|
||||
/** Close every client (shutdown hook). */
|
||||
export function stopAll(): void {
|
||||
for (const c of clients.values()) c.close();
|
||||
clients.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user