feat(broker+cli): multi-tenant telegram bridge with 4 entry points
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

- DB: mesh.telegram_bridge table + migration
- Broker: telegram-bridge.ts (Grammy bot + WS pool + routing)
- Broker: telegram-token.ts (JWT connect tokens)
- Broker: POST /tg/token endpoint + bridge boot on startup
- CLI: claudemesh connect/disconnect telegram commands
- Spec: docs/telegram-bridge-spec.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-09 10:03:11 +01:00
parent c914f2b7db
commit 126bbfeb2c
11 changed files with 2120 additions and 3 deletions

View File

@@ -0,0 +1,65 @@
import { loadConfig } from "../state/config";
export async function connectTelegram(args: string[]): Promise<void> {
const config = loadConfig();
if (config.meshes.length === 0) {
console.error("No meshes joined. Run 'claudemesh join' first.");
process.exit(1);
}
const mesh = config.meshes[0]!;
const linkOnly = args.includes("--link");
// Convert WS broker URL to HTTP
const brokerHttp = mesh.brokerUrl
.replace("wss://", "https://")
.replace("ws://", "http://")
.replace("/ws", "");
console.log("Requesting Telegram connect token...");
const res = await fetch(`${brokerHttp}/tg/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
meshId: mesh.meshId,
memberId: mesh.memberId,
pubkey: mesh.pubkey,
secretKey: mesh.secretKey,
}),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
console.error(`Failed: ${(err as any).error ?? res.statusText}`);
process.exit(1);
}
const { token, deepLink } = (await res.json()) as {
token: string;
deepLink: string;
};
if (linkOnly) {
console.log(deepLink);
return;
}
// Print QR code using simple block characters
console.log("\n Connect Telegram to your mesh:\n");
console.log(` ${deepLink}\n`);
console.log(" Open this link on your phone, or scan the QR code");
console.log(" with your Telegram camera.\n");
// Try to generate QR with qrcode-terminal if available
try {
const QRCode = require("qrcode-terminal");
QRCode.generate(deepLink, { small: true }, (code: string) => {
console.log(code);
});
} catch {
// qrcode-terminal not available, link is enough
console.log(" (Install qrcode-terminal for QR code display)");
}
}

View File

@@ -0,0 +1,3 @@
export async function disconnectTelegram(): Promise<void> {
console.log("To disconnect Telegram, send /disconnect in the bot chat.");
}

View File

@@ -29,6 +29,8 @@ import { runRemember, runRecall } from "./commands/memory";
import { runInfo } from "./commands/info";
import { runRemind } from "./commands/remind";
import { runCreate } from "./commands/create";
import { runSync } from "./commands/sync";
import { runProfile, type ProfileFlags } from "./commands/profile";
import { VERSION } from "./version";
const launch = defineCommand({
@@ -270,6 +272,26 @@ const main = defineCommand({
await runRemind(args, positionals);
},
}),
sync: defineCommand({
meta: { name: "sync", description: "Sync meshes from your dashboard account" },
args: {
force: { type: "boolean", description: "Re-link account even if already linked", default: false },
},
async run({ args }) { await runSync(args); },
}),
profile: defineCommand({
meta: { name: "profile", description: "View or edit your member profile" },
args: {
mesh: { type: "string", description: "Mesh slug (auto-selected if only one joined)" },
"role-tag": { type: "string", description: "Set role tag (e.g. 'backend-dev', 'lead')" },
groups: { type: "string", description: "Set groups as 'group:role,...' (e.g. 'eng:lead,review')" },
"message-mode": { type: "string", description: "'push' | 'inbox' | 'off'" },
name: { type: "string", description: "Set display name" },
member: { type: "string", description: "Edit another member (admin only)" },
json: { type: "boolean", description: "Output as JSON", default: false },
},
async run({ args }) { await runProfile(args as ProfileFlags); },
}),
status: defineCommand({
meta: { name: "status", description: "Check broker connectivity for each joined mesh" },
async run() { await runStatus(); },