← back to Norma
agents/twitch-agent/skills/report.js
164 lines
/**
* Twitch Report Skill
*
* Aggregates Twitch agent stats and pushes to Pulse.
* Reports: channels monitored, messages sent, streams found, chat status.
*/
const { logAction, getActionSummary } = require('../../shared/audit-logger');
const { reportAgentStatus, pushSocialFeed } = require('../../shared/pulse-reporter');
const { query: dbQuery } = require('../../shared/db');
const { getChatStatus, getJoinedChannels, getRecentMessages } = require('../lib/twitch');
const AGENT = 'twitch-agent';
const PLATFORM = 'twitch';
/**
* @param {Object} body - Request body
* @param {number} [body.hours_back=4] - How many hours back to report
* @returns {Promise<Object>}
*/
module.exports = async function report(body) {
const hoursBack = body.hours_back || 4;
// Get action summary for the time window
const actionSummary = await getActionSummary(AGENT, hoursBack);
// Get current chat status
const chatStatus = getChatStatus();
const joinedChannels = getJoinedChannels();
const recentMessages = getRecentMessages();
// Get monitor targets from database
let monitorStats = { total: 0, active: 0, totalItemsFound: 0 };
try {
const { rows } = await dbQuery(
`SELECT
COUNT(*)::int as total,
COUNT(*) FILTER (WHERE is_active = true)::int as active,
COALESCE(SUM(items_found), 0)::int as total_items_found
FROM sdcc_agent_monitors
WHERE agent = $1 AND platform = $2`,
[AGENT, PLATFORM]
);
if (rows[0]) {
monitorStats = {
total: rows[0].total,
active: rows[0].active,
totalItemsFound: rows[0].total_items_found,
};
}
} catch (err) {
console.error(`[${AGENT}] Failed to get monitor stats:`, err.message);
}
// Get action counts for the period
let actionCounts = { discovers: 0, posts: 0, replies: 0, monitors: 0 };
try {
const { rows } = await dbQuery(
`SELECT action_type, COUNT(*)::int as count
FROM sdcc_agent_actions
WHERE agent = $1 AND created_at > NOW() - INTERVAL '1 hour' * $2
GROUP BY action_type`,
[AGENT, hoursBack]
);
for (const row of rows) {
if (row.action_type === 'discover') actionCounts.discovers = row.count;
if (row.action_type === 'post') actionCounts.posts = row.count;
if (row.action_type === 'reply') actionCounts.replies = row.count;
if (row.action_type === 'monitor') actionCounts.monitors = row.count;
}
} catch (err) {
console.error(`[${AGENT}] Failed to get action counts:`, err.message);
}
// Keyword message stats
const keywordsByChannel = {};
for (const msg of recentMessages) {
if (!keywordsByChannel[msg.channel]) {
keywordsByChannel[msg.channel] = { count: 0, keywords: new Set() };
}
keywordsByChannel[msg.channel].count++;
for (const kw of msg.keywords) {
keywordsByChannel[msg.channel].keywords.add(kw);
}
}
const channelStats = Object.entries(keywordsByChannel).map(([channel, data]) => ({
channel,
messageCount: data.count,
keywords: [...data.keywords],
}));
const reportData = {
agent: AGENT,
platform: PLATFORM,
period_hours: hoursBack,
actions: actionSummary,
action_counts: actionCounts,
chat: {
...chatStatus,
channelsMonitored: joinedChannels.length,
recentKeywordMessages: recentMessages.length,
channelBreakdown: channelStats,
},
monitors: monitorStats,
uptime: process.uptime(),
reported_at: new Date().toISOString(),
};
// Push status to Pulse
const pulseResult = await reportAgentStatus(AGENT, {
state: chatStatus.connected ? 'running' : 'idle',
uptime: process.uptime(),
lastAction: actionSummary.length > 0 ? actionSummary[0] : null,
stats: {
channelsMonitored: joinedChannels.length,
keywordMessagesDetected: recentMessages.length,
actionCounts,
monitors: monitorStats,
},
});
// Push notable keyword messages as social feed items
const topMessages = recentMessages
.sort((a, b) => b.keywords.length - a.keywords.length)
.slice(0, 5);
for (const msg of topMessages) {
await pushSocialFeed({
agent: AGENT,
platform: PLATFORM,
type: 'mention',
title: `Keyword match in #${msg.channel}`,
url: `https://twitch.tv/${msg.channel}`,
content: `@${msg.username}: "${msg.message.substring(0, 200)}"`,
metadata: {
channel: msg.channel,
username: msg.username,
keywords: msg.keywords,
timestamp: msg.timestamp,
},
});
}
// Log the report action
await logAction({
agent: AGENT,
actionType: 'report',
platform: PLATFORM,
responseData: {
periodHours: hoursBack,
channelsMonitored: joinedChannels.length,
actionsInPeriod: Object.values(actionCounts).reduce((sum, c) => sum + c, 0),
keywordMessages: recentMessages.length,
},
status: 'success',
});
return {
...reportData,
pulse_response: pulseResult,
};
};