Skill: zero2ai-hub/skill-agent-broadcast (version 1.0.1)

--- name: skill-agent-broadcast version: 1.0.0 description: Broadcast a message to multiple OpenClaw group sessions simultaneously. Use for cross-agent coordination, alerts, and announcements. metadata: openclaw: requires: { bins: ["node"] } --- # skill-agent-broadcast Cross-group signal router. Send one message to multiple OpenClaw Telegram/Discord groups simultaneously. ## Usage ```bash # Broadcast to named groups node scripts/broadcast.js --message "Deploy complete!" --groups "github-ops,amazon-ops" # Broadcast to all registered groups node scripts/broadcast.js --message "⚡ System alert" --groups all # Use raw Telegram chat IDs node scripts/broadcast.js --message "Hello" --groups "-1003871838436,-1003578613620" # Custom delay between sends node scripts/broadcast.js --message "Update" --groups all --delay 1000 ``` ## Arguments | Arg | Default | Description | |---|---|---| | `--message` / `-m` | required | Message text to broadcast | | `--groups` / `-g` | all | Comma-separated group names or IDs. Use `all` for all registered groups | | `--channel` | `telegram` | Channel type: `telegram` or `discord` | | `--delay` | `500` | Milliseconds between sends (rate limiting) | ## Environment Variables | Var | Default | Description | |---|---|---| | `OPENCLAW_PORT` | `3000` | OpenClaw gateway port | | `OPENCLAW_TOKEN` | — | Gateway auth token | | `GROUPS_CONFIG_PATH` | `config/groups.json` | Path to group registry JSON | ## Group Registry Edit `config/groups.json` to add/remove groups: ```json { "github-ops": "-1003871838436", "social-media": "-1003578613620", "amazon-ops": "-1003898064257" } ``` ## Output Delivery receipt per group: ``` ✅ github-ops (-1003871838436) ✅ amazon-ops (-1003898064257) ❌ social-media (-1003578613620) (status: 500) ✅ Broadcast complete: 2 sent, 1 failed ```

path: _meta.json
content: name: skill-agent-broadcast
version: 1.0.0
description: Broadcast a message to multiple OpenClaw group sessions simultaneously. Use for cross-agent coordination, alerts, and announcements.
author: Zero2Ai-hub
license: MIT

path: config/groups.json
content: github-ops: -1003871838436
social-media: -1003578613620
amazon-ops: -1003898064257
dropship-ops: -1003577202952
reporting: -1003475582700

path: scripts/broadcast.js
content: #!/usr/bin/env node
/**
 * broadcast.js — Cross-group signal router for OpenClaw
 *
 * Usage:
 *   node broadcast.js --message "Hello!" --groups "github-ops,social-media"
 *   node broadcast.js --message "Alert!" --groups "-1003871838436,-1003578613620"
 *   node broadcast.js --message "All hands!" --groups all --channel telegram
 *
 * Env vars:
 *   OPENCLAW_PORT   — Gateway port (default: 3000)
 *   OPENCLAW_TOKEN  — Gateway auth token
 *   GROUPS_CONFIG_PATH — Path to groups registry JSON
 */

'use strict';

const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');

// ── Config ────────────────────────────────────────────────────────────────────
const GATEWAY_PORT = parseInt(process.env.OPENCLAW_PORT || '3000', 10);
const GATEWAY_TOKEN = process.env.OPENCLAW_TOKEN || '';

const GROUPS_CONFIG_PATH = process.env.GROUPS_CONFIG_PATH ||
  path.join(__dirname, '..', 'config', 'groups.json');

function loadGroups() {
  try {
    return JSON.parse(fs.readFileSync(GROUPS_CONFIG_PATH, 'utf8'));
  } catch {
    return {};
  }
}

function parseArgs() {
  const args = process.argv.slice(2);
  const opts = { message: null, groups: null, channel: 'telegram', delay: 500 };
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--message' || args[i] === '-m') opts.message = args[++i];
    else if (args[i] === '--groups' || args[i] === '-g') opts.groups = args[++i];
    else if (args[i] === '--channel') opts.channel = args[++i];
    else if (args[i] === '--delay') opts.delay = parseInt(args[++i], 10);
  }
  return opts;
}

function resolveGroups(groupsArg, registry) {
  if (!groupsArg) return Object.values(registry);
  if (groupsArg === 'all') return Object.values(registry);

  return groupsArg.split(',').map(g => {
    g = g.trim();
    // If it's a negative number (Telegram chat ID), use as-is
    if (/^-?\d+$/.test(g)) return g;
    // Otherwise resolve by name
    const resolved = registry[g] || registry[g.replace(/-/g, '_')];
    if (!resolved) {
      console.warn(`⚠️  Unknown group name: "${g}" — skipping`);
      return null;
    }
    return resolved;
  }).filter(Boolean);
}

function sleep(ms) {
  return new Promise(r => setTimeout(r, ms));
}

function sendToGateway(channel, to, message, token) {
  return new Promise((resolve) => {
    const body = JSON.stringify({ channel, to, message });
    const options = {
      hostname: 'localhost',
      port: GATEWAY_PORT,
      path: '/api/send',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
      },
    };

    const req = http.request(options, (res) => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        resolve({ status: res.statusCode, body: data });
      });
    });
    req.on('error', (err) => {
      resolve({ status: 0, body: err.message });
    });
    req.write(body);
    req.end();
  });
}

async function main() {
  const opts = parseArgs();

  if (!opts.message) {
    console.error('❌ --message is required');
    console.error('   Usage: node broadcast.js --message "Hello!" --groups "github-ops,amazon-ops"');
    process.exit(1);
  }

  const registry = loadGroups();
  const targets = resolveGroups(opts.groups, registry);

  if (targets.length === 0) {
    console.error('❌ No valid targets resolved. Check --groups and config/groups.json');
    process.exit(1);
  }

  console.log(`\n📡 Broadcasting to ${targets.length} group(s) via ${opts.channel}`);
  console.log(`   Message: ${opts.message.slice(0, 80)}${opts.message.length > 80 ? '...' : ''}\n`);

  const results = [];

  for (const target of targets) {
    const { status, body } = await sendToGateway(opts.channel, target, opts.message, GATEWAY_TOKEN);
    const ok = status === 200 || status === 201;
    const label = Object.entries(registry).find(([, v]) => v === target)?.[0] || target;
    const receipt = ok ? '✅' : `❌ (${status})`;
    console.log(`  ${receipt} ${label} (${target})`);
    results.push({ target, label, status, ok });

    if (opts.delay > 0 && targets.indexOf(target) < targets.length - 1) {
      await sleep(opts.delay);
    }
  }

  const succeeded = results.filter(r => r.ok).length;
  const failed = results.filter(r => !r.ok).length;
  console.log(`\n✅ Broadcast complete: ${succeeded} sent, ${failed} failed`);

  if (failed > 0) process.exit(1);
}

main().catch(err => {
  console.error(`FATAL: ${err.message}`);
  process.exit(1);
});


path: scripts/formatters.js
content: 'use strict';

const STRENGTH_BADGE = {
  low: '🟡',
  medium: '🟠',
  high: '🔴',
};

function strengthLine(strength) {
  const badge = STRENGTH_BADGE[strength] || '⚪';
  return `${badge} Signal Strength: ${strength.toUpperCase()}`;
}

function ts() {
  return new Date().toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
}

const formatters = {
  product_launch(content, strength) {
    const { name = 'Unknown', price = '—', sku = '', url = '', description = '' } = content;
    return [
      `🚀 *PRODUCT LAUNCH*`,
      ``,
      `📦 *${name}*`,
      sku ? `🔑 SKU: \`${sku}\`` : null,
      `💰 Price: *$${price}*`,
      description ? `📝 ${description}` : null,
      url ? `🔗 ${url}` : null,
      ``,
      strengthLine(strength),
      `🕐 ${ts()}`,
    ].filter(l => l !== null).join('\n');
  },

  price_alert(content, strength) {
    const { name = 'Unknown', sku = '', old_price, new_price, change_pct, marketplace = '' } = content;
    const arrow = (old_price && new_price && new_price < old_price) ? '📉' : '📈';
    return [
      `${arrow} *PRICE ALERT*`,
      ``,
      `📦 *${name}*`,
      sku ? `🔑 SKU: \`${sku}\`` : null,
      marketplace ? `🏪 Marketplace: ${marketplace}` : null,
      old_price != null ? `💵 Old Price: $${old_price}` : null,
      new_price != null ? `💰 New Price: *$${new_price}*` : null,
      change_pct != null ? `📊 Change: ${change_pct > 0 ? '+' : ''}${change_pct}%` : null,
      ``,
      strengthLine(strength),
      `🕐 ${ts()}`,
    ].filter(l => l !== null).join('\n');
  },

  ad_signal(content, strength) {
    const { campaign = '', action = '', budget, acos, roas, note = '' } = content;
    return [
      `📣 *AD SIGNAL*`,
      ``,
      campaign ? `🎯 Campaign: *${campaign}*` : null,
      action ? `⚡ Action: ${action}` : null,
      budget != null ? `💰 Budget: $${budget}` : null,
      acos != null ? `📊 ACoS: ${acos}%` : null,
      roas != null ? `📈 ROAS: ${roas}x` : null,
      note ? `📝 ${note}` : null,
      ``,
      strengthLine(strength),
      `🕐 ${ts()}`,
    ].filter(l => l !== null).join('\n');
  },

  stock_alert(content, strength) {
    const { name = 'Unknown', sku = '', units, warehouse = '', status = '', threshold } = content;
    const icon = (units != null && threshold != null && units < threshold) ? '⚠️' : '📦';
    return [
      `${icon} *STOCK ALERT*`,
      ``,
      `📦 *${name}*`,
      sku ? `🔑 SKU: \`${sku}\`` : null,
      warehouse ? `🏭 Warehouse: ${warehouse}` : null,
      units != null ? `📊 Units: *${units}*` : null,
      threshold != null ? `🚨 Threshold: ${threshold}` : null,
      status ? `📋 Status: ${status}` : null,
      ``,
      strengthLine(strength),
      `🕐 ${ts()}`,
    ].filter(l => l !== null).join('\n');
  },

  custom(content, strength) {
    const { title = 'Signal', body = '', emoji = '📡' } = content;
    const extra = Object.entries(content)
      .filter(([k]) => !['title', 'body', 'emoji'].includes(k))
      .map(([k, v]) => `• *${k}*: ${v}`)
      .join('\n');
    return [
      `${emoji} *${title.toUpperCase()}*`,
      ``,
      body || null,
      extra || null,
      ``,
      strengthLine(strength),
      `🕐 ${ts()}`,
    ].filter(l => l !== null).join('\n');
  },
};

module.exports = { formatters };


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)