bridge-echo 0.2.0

HTTP bridge for Claude Code CLI
#!/usr/bin/env node
// Discord gateway listener — forwards #echo messages to bridge-echo
// Runs as a systemd service. Routes responses to Discord text or voice
// depending on whether an active voice session exists.

const WebSocket = require('ws');

const BOT_TOKEN = 'MTQ3NDg4NDU0NTI3ODExNTk3MQ.GKjMUM.JhcV4FLADqE7H_1lqY0qSgs90RbOYxklz1VU6A';
const ALLOWED_USERS = ['693836830436753409'];
const CHAT_CHANNEL = '1474894225786016007';
const BRIDGE_ECHO_URL = 'http://127.0.0.1:3100/chat';

const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json';

let ws;
let heartbeatInterval = null;
let lastSequence = null;
let sessionId = null;
let resumeGatewayUrl = null;

function log(msg) {
  console.log(`[${new Date().toISOString()}] ${msg}`);
}

function sendPayload(op, d) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ op, d }));
  }
}

function heartbeat() {
  sendPayload(1, lastSequence);
}

function identify() {
  sendPayload(2, {
    token: BOT_TOKEN,
    intents: (1 << 9) | (1 << 15), // GUILD_MESSAGES | MESSAGE_CONTENT
    properties: { os: 'linux', browser: 'echo-bridge', device: 'echo-bridge' }
  });
}

async function forwardToBridge(message) {
  try {
    const resp = await fetch(BRIDGE_ECHO_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message: message.content,
        channel: 'discord',
        sender: 'D',
        metadata: {
          discord_channel_id: message.channel_id,
        },
        callback: {
          type: 'discord',
        }
      }),
    });

    if (resp.status === 202) {
      log('Message accepted — response will be delivered via callback');
    } else if (!resp.ok) {
      log(`bridge-echo returned ${resp.status}`);
    }
  } catch (err) {
    log(`Failed to forward to bridge-echo: ${err.message}`);
  }
}

function connect(url) {
  ws = new WebSocket(url || GATEWAY_URL);

  ws.on('open', () => log('Gateway connected'));

  ws.on('message', (data) => {
    const payload = JSON.parse(data);
    const { op, t, s, d } = payload;

    if (s) lastSequence = s;

    switch (op) {
      case 10: // Hello
        const interval = d.heartbeat_interval;
        log(`Heartbeat interval: ${interval}ms`);
        if (heartbeatInterval) clearInterval(heartbeatInterval);
        heartbeatInterval = setInterval(heartbeat, interval);
        heartbeat();
        if (sessionId && resumeGatewayUrl) {
          log('Resuming session...');
          sendPayload(6, { token: BOT_TOKEN, session_id: sessionId, seq: lastSequence });
        } else {
          identify();
        }
        break;

      case 11: // Heartbeat ACK
        break;

      case 7: // Reconnect
        log('Server requested reconnect');
        ws.close();
        break;

      case 9: // Invalid Session
        log('Invalid session, re-identifying...');
        sessionId = null;
        setTimeout(identify, 2000);
        break;

      case 0: // Dispatch
        if (t === 'READY') {
          sessionId = d.session_id;
          resumeGatewayUrl = d.resume_gateway_url;
          log(`Ready! Session: ${sessionId}, User: ${d.user.username}`);
        }

        if (t === 'MESSAGE_CREATE') {
          // Security gates
          if (d.author.bot) break;
          if (!ALLOWED_USERS.includes(d.author.id)) break;
          if (d.channel_id !== CHAT_CHANNEL) break;
          if (!d.content) break;

          log(`Message from ${d.author.username}: ${d.content.substring(0, 50)}...`);
          forwardToBridge(d);
        }
        break;
    }
  });

  ws.on('close', (code, reason) => {
    log(`Disconnected: ${code} ${reason}`);
    if (heartbeatInterval) clearInterval(heartbeatInterval);

    // Reconnect after delay (unless code 4004 = invalid token)
    if (code !== 4004) {
      const delay = code === 4000 ? 1000 : 5000;
      log(`Reconnecting in ${delay}ms...`);
      setTimeout(() => connect(resumeGatewayUrl || GATEWAY_URL), delay);
    } else {
      log('FATAL: Authentication failed. Check bot token.');
      process.exit(1);
    }
  });

  ws.on('error', (err) => {
    log(`WebSocket error: ${err.message}`);
  });
}

log('Starting Discord gateway listener...');
connect();