feat(sdk+cli): bridge peer — forward a topic between two meshes
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

A bridge holds memberships in two meshes and relays messages on a
single topic between them. Federation-lite without a broker-to-broker
protocol.

SDK additions:
- Bridge class (start, stop, EventEmitter for forwarded/dropped/error)
- MeshClient.joinTopic / leaveTopic / createTopic methods
- Loop prevention: plaintext hop counter prefix __cmh<n>: with maxHops
  default 2; echo guard via senderPubkey == own session pubkey

CLI additions:
- claudemesh bridge run <config.yaml> long-lived process
- claudemesh bridge init prints config template
- Zero-dep YAML parser for the flat bridge config shape

The hop prefix is visible in message bodies — minor wart, fixed in
v0.3.0 by moving loop tracking into broker primitives.

SDK kept as devDependency since Bun bundles it into dist; no impact
on npm publish or runtime resolution.

Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-02 13:41:50 +01:00
parent 9418d0ee30
commit 9dd1e401b0
7 changed files with 432 additions and 0 deletions

View File

@@ -365,6 +365,32 @@ export class MeshClient extends EventEmitter {
this.ws.send(JSON.stringify({ type: "set_status", status }));
}
// --- Topics (v0.2.0) ---
// Conversational primitive within a mesh. To receive topic-tagged
// messages, you must subscribe via `joinTopic`.
async createTopic(args: {
name: string;
description?: string;
visibility?: "public" | "private" | "dm";
}): Promise<void> {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({ type: "topic_create", ...args }));
}
async joinTopic(
topic: string,
role?: "lead" | "member" | "observer",
): Promise<void> {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({ type: "topic_join", topic, role }));
}
async leaveTopic(topic: string): Promise<void> {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({ type: "topic_leave", topic }));
}
// --- Internals ---
private makeReqId(): string {