From df98d3ef950f93e97eaae9a758fb3be347d6dff1 Mon Sep 17 00:00:00 2001 From: Clawd Date: Wed, 28 Jan 2026 10:09:25 +0000 Subject: [PATCH] Initial commit: MQTT to Clawdbot bridge - Subscribe to MQTT topics, forward to Clawdbot webhook - Configurable via environment variables - Supports wake (system event) and agent (isolated) modes - Includes systemd service template - Home Assistant examples in README --- .env.example | 14 +++ .gitignore | 4 + README.md | 164 ++++++++++++++++++++++++++++++++ index.js | 175 +++++++++++++++++++++++++++++++++++ mqtt-clawdbot-bridge.service | 22 +++++ package.json | 17 ++++ 6 files changed, 396 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 mqtt-clawdbot-bridge.service create mode 100644 package.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6fcbdc4 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# MQTT Configuration +MQTT_URL=mqtt://your-broker:1883 +MQTT_USERNAME= +MQTT_PASSWORD= +MQTT_TOPICS=clawd/#,home/alerts/# +MQTT_CLIENT_ID=clawdbot-bridge + +# Clawdbot Configuration +CLAWDBOT_URL=http://127.0.0.1:18789 +CLAWDBOT_TOKEN=your-webhook-token-here +CLAWDBOT_MODE=wake # 'wake' for system events, 'agent' for isolated runs + +# Logging +LOG_LEVEL=info # debug, info, warn, error diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daf1dd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f02093 --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# MQTT → Clawdbot Bridge + +Subscribe to MQTT topics and forward messages to [Clawdbot](https://github.com/clawdbot/clawdbot) via webhook. + +Perfect for triggering your AI assistant from Home Assistant, IoT devices, or any MQTT-enabled system — without exposing your Clawdbot gateway to the internet. + +## How It Works + +``` +[Your Device] → [MQTT Broker] → [This Bridge] → [Clawdbot Webhook] → [AI Response] +``` + +The bridge runs locally alongside Clawdbot, subscribes to MQTT topics, and calls the local webhook endpoint when messages arrive. + +## Quick Start + +### 1. Install + +```bash +git clone https://gitea.olex.me/olex/mqtt-clawdbot-bridge.git +cd mqtt-clawdbot-bridge +npm install +``` + +### 2. Configure + +```bash +cp .env.example .env +# Edit .env with your settings +``` + +Required settings: +- `MQTT_URL` — Your MQTT broker (e.g., `mqtt://192.168.1.100:1883`) +- `CLAWDBOT_TOKEN` — Webhook token from your Clawdbot config + +### 3. Enable Clawdbot Webhooks + +Add to your `~/.clawdbot/clawdbot.json`: + +```json +{ + "hooks": { + "enabled": true, + "token": "your-secret-token", + "path": "/hooks" + } +} +``` + +Restart the gateway: `clawdbot gateway restart` + +### 4. Run + +```bash +npm start +``` + +## Configuration + +All configuration is via environment variables (or `.env` file): + +| Variable | Default | Description | +|----------|---------|-------------| +| `MQTT_URL` | `mqtt://localhost:1883` | MQTT broker URL | +| `MQTT_USERNAME` | — | MQTT username (optional) | +| `MQTT_PASSWORD` | — | MQTT password (optional) | +| `MQTT_TOPICS` | `clawd/#` | Comma-separated topics to subscribe | +| `MQTT_CLIENT_ID` | random | MQTT client identifier | +| `CLAWDBOT_URL` | `http://127.0.0.1:18789` | Clawdbot gateway URL | +| `CLAWDBOT_TOKEN` | — | Webhook token (**required**) | +| `CLAWDBOT_MODE` | `wake` | `wake` (system event) or `agent` (isolated run) | +| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` | + +## Webhook Modes + +### `wake` (default) +Triggers a system event in the main session. Good for alerts and notifications that should appear in your regular chat context. + +### `agent` +Runs an isolated agent session. Good for tasks that need a dedicated response without cluttering main chat history. + +## Running as a Service + +### systemd (Linux) + +```bash +# Copy files +sudo cp -r . /opt/mqtt-clawdbot-bridge +sudo cp mqtt-clawdbot-bridge.service /etc/systemd/system/ + +# Configure +sudo cp .env.example /opt/mqtt-clawdbot-bridge/.env +sudo nano /opt/mqtt-clawdbot-bridge/.env + +# Enable and start +sudo systemctl daemon-reload +sudo systemctl enable mqtt-clawdbot-bridge +sudo systemctl start mqtt-clawdbot-bridge + +# Check status +sudo systemctl status mqtt-clawdbot-bridge +journalctl -u mqtt-clawdbot-bridge -f +``` + +## Home Assistant Example + +Publish to MQTT when a door opens: + +```yaml +automation: + - alias: "Notify Clawd - Front Door" + trigger: + - platform: state + entity_id: binary_sensor.front_door + to: "on" + action: + - service: mqtt.publish + data: + topic: "clawd/alerts/door" + payload: "Front door opened at {{ now().strftime('%H:%M') }}" +``` + +Or send JSON with more context: + +```yaml + action: + - service: mqtt.publish + data: + topic: "clawd/alerts/motion" + payload: > + {"message": "Motion detected in {{ trigger.to_state.attributes.friendly_name }}", + "entity": "{{ trigger.entity_id }}", + "time": "{{ now().isoformat() }}"} +``` + +## Message Format + +The bridge accepts: + +- **Plain text**: Forwarded as-is +- **JSON with `message`/`text`/`payload` field**: That field is extracted and forwarded + +Messages are prefixed with the topic for context: +``` +[MQTT clawd/alerts/door] Front door opened at 10:30 +``` + +## Troubleshooting + +### Bridge can't connect to MQTT +- Check broker URL and credentials +- Verify network connectivity: `mosquitto_sub -h -t '#'` + +### Messages not reaching Clawdbot +- Verify `CLAWDBOT_TOKEN` matches your gateway config +- Check gateway is running: `clawdbot gateway status` +- Enable debug logging: `LOG_LEVEL=debug npm start` + +### Webhook returns 401 +- Token mismatch — check `hooks.token` in Clawdbot config matches `CLAWDBOT_TOKEN` + +## License + +MIT diff --git a/index.js b/index.js new file mode 100644 index 0000000..e4eb917 --- /dev/null +++ b/index.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +/** + * MQTT → Clawdbot Bridge + * + * Subscribes to MQTT topics and forwards messages to Clawdbot via webhook. + * + * Configuration via environment variables: + * MQTT_URL - MQTT broker URL (default: mqtt://localhost:1883) + * MQTT_USERNAME - MQTT username (optional) + * MQTT_PASSWORD - MQTT password (optional) + * MQTT_TOPICS - Comma-separated list of topics to subscribe (default: clawd/#) + * CLAWDBOT_URL - Clawdbot gateway URL (default: http://127.0.0.1:18789) + * CLAWDBOT_TOKEN - Clawdbot webhook token (required) + * CLAWDBOT_MODE - Webhook mode: 'wake' or 'agent' (default: wake) + * LOG_LEVEL - Logging level: debug, info, warn, error (default: info) + */ + +import mqtt from 'mqtt'; + +// Configuration +const config = { + mqtt: { + url: process.env.MQTT_URL || 'mqtt://localhost:1883', + username: process.env.MQTT_USERNAME || undefined, + password: process.env.MQTT_PASSWORD || undefined, + topics: (process.env.MQTT_TOPICS || 'clawd/#').split(',').map(t => t.trim()), + clientId: process.env.MQTT_CLIENT_ID || `clawdbot-bridge-${Math.random().toString(16).slice(2, 8)}`, + }, + clawdbot: { + url: process.env.CLAWDBOT_URL || 'http://127.0.0.1:18789', + token: process.env.CLAWDBOT_TOKEN, + mode: process.env.CLAWDBOT_MODE || 'wake', // 'wake' or 'agent' + }, + logLevel: process.env.LOG_LEVEL || 'info', +}; + +// Logging +const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; +const currentLogLevel = LOG_LEVELS[config.logLevel] ?? 1; + +const log = { + debug: (...args) => currentLogLevel <= 0 && console.log('[DEBUG]', new Date().toISOString(), ...args), + info: (...args) => currentLogLevel <= 1 && console.log('[INFO]', new Date().toISOString(), ...args), + warn: (...args) => currentLogLevel <= 2 && console.warn('[WARN]', new Date().toISOString(), ...args), + error: (...args) => currentLogLevel <= 3 && console.error('[ERROR]', new Date().toISOString(), ...args), +}; + +// Validate config +if (!config.clawdbot.token) { + log.error('CLAWDBOT_TOKEN is required. Set it in your environment.'); + process.exit(1); +} + +/** + * Send message to Clawdbot webhook + */ +async function sendToClawdbot(topic, payload) { + const endpoint = config.clawdbot.mode === 'agent' + ? `${config.clawdbot.url}/hooks/agent` + : `${config.clawdbot.url}/hooks/wake`; + + const body = config.clawdbot.mode === 'agent' + ? { message: `[MQTT ${topic}] ${payload}`, name: 'MQTT' } + : { text: `[MQTT ${topic}] ${payload}`, mode: 'now' }; + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.clawdbot.token}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`HTTP ${response.status}: ${text}`); + } + + log.info(`Forwarded to Clawdbot: ${topic} → ${payload.substring(0, 100)}`); + return true; + } catch (err) { + log.error(`Failed to forward to Clawdbot: ${err.message}`); + return false; + } +} + +/** + * Parse MQTT payload (handles JSON and plain text) + */ +function parsePayload(payload) { + const str = payload.toString(); + try { + const json = JSON.parse(str); + // If JSON has a 'message' or 'text' field, use that + return json.message || json.text || json.payload || str; + } catch { + return str; + } +} + +/** + * Main entry point + */ +async function main() { + log.info('Starting MQTT → Clawdbot Bridge'); + log.info(`MQTT broker: ${config.mqtt.url}`); + log.info(`Topics: ${config.mqtt.topics.join(', ')}`); + log.info(`Clawdbot: ${config.clawdbot.url} (mode: ${config.clawdbot.mode})`); + + // Connect to MQTT + const mqttOptions = { + clientId: config.mqtt.clientId, + clean: true, + reconnectPeriod: 5000, + }; + + if (config.mqtt.username) { + mqttOptions.username = config.mqtt.username; + mqttOptions.password = config.mqtt.password; + } + + const client = mqtt.connect(config.mqtt.url, mqttOptions); + + client.on('connect', () => { + log.info('Connected to MQTT broker'); + + // Subscribe to topics + for (const topic of config.mqtt.topics) { + client.subscribe(topic, (err) => { + if (err) { + log.error(`Failed to subscribe to ${topic}: ${err.message}`); + } else { + log.info(`Subscribed to: ${topic}`); + } + }); + } + }); + + client.on('message', async (topic, payload) => { + const message = parsePayload(payload); + log.debug(`Received: ${topic} → ${message}`); + await sendToClawdbot(topic, message); + }); + + client.on('error', (err) => { + log.error(`MQTT error: ${err.message}`); + }); + + client.on('reconnect', () => { + log.warn('Reconnecting to MQTT broker...'); + }); + + client.on('close', () => { + log.warn('MQTT connection closed'); + }); + + // Graceful shutdown + const shutdown = () => { + log.info('Shutting down...'); + client.end(true, () => { + process.exit(0); + }); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +main().catch((err) => { + log.error(`Fatal error: ${err.message}`); + process.exit(1); +}); diff --git a/mqtt-clawdbot-bridge.service b/mqtt-clawdbot-bridge.service new file mode 100644 index 0000000..ad2cd08 --- /dev/null +++ b/mqtt-clawdbot-bridge.service @@ -0,0 +1,22 @@ +[Unit] +Description=MQTT to Clawdbot Bridge +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/mqtt-clawdbot-bridge +ExecStart=/usr/bin/node index.js +Restart=always +RestartSec=10 + +# Load environment from file +EnvironmentFile=/opt/mqtt-clawdbot-bridge/.env + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/package.json b/package.json new file mode 100644 index 0000000..6e1b8ff --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "mqtt-clawdbot-bridge", + "version": "1.0.0", + "description": "Bridge MQTT messages to Clawdbot triggers via webhook", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "keywords": ["mqtt", "clawdbot", "automation", "home-assistant", "webhook"], + "author": "Clawd", + "license": "MIT", + "dependencies": { + "mqtt": "^5.0.0" + } +}