Files
claudemesh/packages/connector-slack/src/bridge.ts
Alejandro Gutiérrez 5563f90733 feat: add @claudemesh/sdk package for non-Claude-Code clients
Standalone TypeScript SDK that any process can use to join a mesh and
send/receive messages. Implements the same WS protocol and libsodium
crypto_box encryption as the CLI, with an EventEmitter-based API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:53:22 +01:00

98 lines
2.8 KiB
TypeScript

/**
* Bridge — bidirectional message relay between Slack and a claudemesh mesh.
*
* Slack -> Mesh: messages from the Slack channel are broadcast to mesh peers.
* Mesh -> Slack: push messages addressed to this connector (or broadcast)
* are posted to the Slack channel.
*/
import type { SlackClient } from "./slack";
import type { MeshClient } from "./mesh-client";
import type { SlackConnectorConfig } from "./config";
export class Bridge {
private slack: SlackClient;
private mesh: MeshClient;
private config: SlackConnectorConfig;
private unsubSlack: (() => void) | null = null;
private unsubMesh: (() => void) | null = null;
/** Track message IDs we've relayed to avoid echo loops. */
private recentRelayed = new Set<string>();
private cleanupTimer: NodeJS.Timeout | null = null;
constructor(
slack: SlackClient,
mesh: MeshClient,
config: SlackConnectorConfig,
) {
this.slack = slack;
this.mesh = mesh;
this.config = config;
}
/**
* Start the bidirectional relay.
*/
start(): void {
// --- Slack -> Mesh ---
this.unsubSlack = this.slack.onMessage((msg) => {
const channelName = this.config.slackChannelId;
const formatted = `[${msg.displayName} via Slack #${channelName}] ${msg.text}`;
// Broadcast to all mesh peers
this.mesh.broadcast(formatted).catch((err) => {
console.error("[bridge] Failed to relay Slack->Mesh:", err);
});
});
// --- Mesh -> Slack ---
this.unsubMesh = this.mesh.onPush((push) => {
// Skip messages we ourselves sent (echo prevention)
if (this.recentRelayed.has(push.messageId)) {
this.recentRelayed.delete(push.messageId);
return;
}
// Skip system events (peer_joined, peer_left) — too noisy for Slack
if (push.subtype === "system") return;
const plaintext = push.plaintext;
if (!plaintext) return;
// Resolve sender name from the push metadata
const senderName = push.senderName || push.senderPubkey.slice(0, 8);
const formatted = `*[${senderName}]*: ${plaintext}`;
this.slack.postMessage(formatted).catch((err) => {
console.error("[bridge] Failed to relay Mesh->Slack:", err);
});
});
// Periodically clean the echo-prevention set to prevent memory leaks
this.cleanupTimer = setInterval(() => {
this.recentRelayed.clear();
}, 60_000);
console.log("[bridge] Relay started");
}
/**
* Stop the relay and clean up subscriptions.
*/
stop(): void {
if (this.unsubSlack) {
this.unsubSlack();
this.unsubSlack = null;
}
if (this.unsubMesh) {
this.unsubMesh();
this.unsubMesh = null;
}
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
console.log("[bridge] Relay stopped");
}
}