← back to Norma

agents/discord-agent/skills/monitor.js

199 lines

/**
 * Discord Monitor Skill
 *
 * Manages monitoring configuration for Discord channels and keywords.
 * Stores/updates monitor entries in the sdcc_agent_monitors table.
 * Returns current monitor configuration.
 *
 * Note: Active real-time monitoring is event-driven via Discord.js client
 * events in the bot, not cron-based. This skill manages the configuration.
 */

const { logAction } = require('../../shared/audit-logger');
const { getRecentMessages, scoreRelevance, hasCredentials, KEYWORDS } = require('../lib/discord');
const { pushSocialFeed } = require('../../shared/pulse-reporter');
const { query: dbQuery } = require('../../shared/db');

const AGENT = 'discord-agent';
const PLATFORM = 'discord';

/**
 * @param {Object} body - Request body
 * @param {string[]} [body.channel_ids] - Channel IDs to add/update as monitors
 * @param {string[]} [body.keywords] - Keywords to monitor for
 * @param {boolean} [body.check_now=false] - If true, fetch recent messages from monitored channels now
 * @param {number} [body.limit=50] - Messages per channel when checking
 * @param {boolean} [body.list_only=false] - If true, just return current monitors
 * @returns {Promise<Object>}
 */
module.exports = async function monitor(body) {
  const checkNow = body.check_now || false;
  const listOnly = body.list_only || false;
  const limit = Math.min(body.limit || 50, 100);

  // Get current monitors from database
  let currentMonitors = [];
  try {
    const { rows } = await dbQuery(
      `SELECT id, target, keywords, monitor_type, is_active,
              items_found, last_checked_at, created_at
       FROM sdcc_agent_monitors
       WHERE agent = $1 AND platform = $2
       ORDER BY created_at DESC`,
      [AGENT, PLATFORM]
    );
    currentMonitors = rows;
  } catch (err) {
    console.error(`[${AGENT}] Failed to fetch monitors:`, err.message);
  }

  // If list_only, just return current state
  if (listOnly) {
    return {
      monitors: currentMonitors,
      total: currentMonitors.length,
      active: currentMonitors.filter((m) => m.is_active).length,
      bot_connected: hasCredentials(),
    };
  }

  // Add/update channel monitors if provided
  const addedMonitors = [];
  if (body.channel_ids && body.channel_ids.length > 0) {
    const keywords = body.keywords || KEYWORDS.slice(0, 5);

    for (const channelId of body.channel_ids) {
      try {
        const { rows } = await dbQuery(
          `INSERT INTO sdcc_agent_monitors
            (agent, platform, target, monitor_type, keywords, is_active)
           VALUES ($1, $2, $3, 'channel', $4, true)
           ON CONFLICT (agent, platform, target)
           DO UPDATE SET keywords = $4, is_active = true, updated_at = NOW()
           RETURNING *`,
          [AGENT, PLATFORM, channelId, JSON.stringify(keywords)]
        );
        if (rows[0]) {
          addedMonitors.push(rows[0]);
        }
      } catch (err) {
        console.error(`[${AGENT}] Failed to upsert monitor for channel ${channelId}:`, err.message);
        addedMonitors.push({
          target: channelId,
          error: err.message,
        });
      }
    }
  }

  // Check monitored channels now if requested
  let checkResults = [];
  if (checkNow) {
    const activeMonitors = currentMonitors.filter((m) => m.is_active);
    const channelsToCheck = addedMonitors.length > 0
      ? addedMonitors.filter((m) => m.id).map((m) => m.target)
      : activeMonitors.map((m) => m.target);

    for (const channelId of channelsToCheck) {
      try {
        const messages = await getRecentMessages(channelId, limit);

        // Score and filter relevant messages
        const scored = messages
          .filter((msg) => !msg.isBot)
          .map((msg) => ({
            ...msg,
            relevance_score: scoreRelevance(msg.content),
          }));

        const relevant = scored.filter((m) => m.relevance_score > 10);

        checkResults.push({
          channelId,
          total_messages: messages.length,
          relevant_count: relevant.length,
          top_messages: relevant.slice(0, 5),
        });

        // Update monitor stats
        const mon = [...currentMonitors, ...addedMonitors].find((m) => m.target === channelId);
        if (mon && mon.id) {
          await dbQuery(
            `UPDATE sdcc_agent_monitors
             SET last_checked_at = NOW(), items_found = items_found + $1, updated_at = NOW()
             WHERE id = $2`,
            [relevant.length, mon.id]
          );
        }

        // Push high-relevance messages to Pulse
        for (const msg of relevant.slice(0, 3)) {
          await pushSocialFeed({
            agent: AGENT,
            platform: PLATFORM,
            type: 'discussion',
            title: `${msg.author}: ${msg.content.substring(0, 80)}`,
            content: msg.content.substring(0, 300),
            metadata: {
              message_id: msg.id,
              channel_id: channelId,
              author: msg.author,
              relevance_score: msg.relevance_score,
            },
          });
        }
      } catch (err) {
        console.error(`[${AGENT}] Check failed for channel ${channelId}:`, err.message);
        checkResults.push({
          channelId,
          total_messages: 0,
          relevant_count: 0,
          error: err.message,
        });
      }
    }
  }

  // Refresh monitor list after changes
  let updatedMonitors = currentMonitors;
  if (addedMonitors.length > 0) {
    try {
      const { rows } = await dbQuery(
        `SELECT id, target, keywords, monitor_type, is_active,
                items_found, last_checked_at, created_at
         FROM sdcc_agent_monitors
         WHERE agent = $1 AND platform = $2
         ORDER BY created_at DESC`,
        [AGENT, PLATFORM]
      );
      updatedMonitors = rows;
    } catch (err) {
      console.error(`[${AGENT}] Failed to refresh monitors:`, err.message);
    }
  }

  // Log the action
  await logAction({
    agent: AGENT,
    actionType: 'monitor',
    platform: PLATFORM,
    responseData: {
      monitors_total: updatedMonitors.length,
      monitors_active: updatedMonitors.filter((m) => m.is_active).length,
      channels_added: addedMonitors.length,
      channels_checked: checkResults.length,
    },
    status: 'success',
  });

  return {
    monitors: updatedMonitors,
    total: updatedMonitors.length,
    active: updatedMonitors.filter((m) => m.is_active).length,
    added: addedMonitors.length > 0 ? addedMonitors : undefined,
    check_results: checkResults.length > 0 ? checkResults : undefined,
    bot_connected: hasCredentials(),
    monitored_at: new Date().toISOString(),
  };
};