@kernel.chat/kbot 3.87.0 → 3.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ export interface SocialEngine {
2
+ platforms: PlatformState[];
3
+ viewers: ViewerProfile[];
4
+ followers: FollowerEvent[];
5
+ moderationLog: ModerationAction[];
6
+ announcements: string[];
7
+ lastFollowerCheck: number;
8
+ lastViewerCheck: number;
9
+ totalViewMinutes: number;
10
+ /** Peak concurrent viewers this session */
11
+ peakConcurrent: number;
12
+ /** Known follower usernames (for diff detection) */
13
+ knownFollowers: string[];
14
+ }
15
+ interface PlatformState {
16
+ name: 'twitch' | 'kick' | 'rumble';
17
+ connected: boolean;
18
+ viewerCount: number;
19
+ followerCount: number;
20
+ chatRate: number;
21
+ lastPing: number;
22
+ }
23
+ export interface ViewerProfile {
24
+ username: string;
25
+ platform: string;
26
+ firstSeen: number;
27
+ lastSeen: number;
28
+ messageCount: number;
29
+ commandsUsed: number;
30
+ xp: number;
31
+ isFollower: boolean;
32
+ isModerator: boolean;
33
+ tags: string[];
34
+ }
35
+ export interface FollowerEvent {
36
+ username: string;
37
+ platform: string;
38
+ timestamp: number;
39
+ announced: boolean;
40
+ }
41
+ export interface ModerationAction {
42
+ type: 'ban' | 'timeout' | 'filter';
43
+ username: string;
44
+ reason: string;
45
+ timestamp: number;
46
+ automated: boolean;
47
+ }
48
+ export interface PlatformHealthReport {
49
+ twitch: {
50
+ connected: boolean;
51
+ viewers: number;
52
+ chatRate: number;
53
+ };
54
+ kick: {
55
+ connected: boolean;
56
+ viewers: number;
57
+ };
58
+ rumble: {
59
+ connected: boolean;
60
+ viewers: number;
61
+ };
62
+ totalViewers: number;
63
+ totalFollowers: number;
64
+ streamHealth: 'good' | 'degraded' | 'offline';
65
+ }
66
+ export interface StreamStats {
67
+ uniqueViewers: number;
68
+ totalMessages: number;
69
+ peakConcurrent: number;
70
+ averageChatRate: number;
71
+ topChatters: Array<{
72
+ username: string;
73
+ messages: number;
74
+ }>;
75
+ newFollowers: number;
76
+ platformBreakdown: Record<string, number>;
77
+ }
78
+ export interface SocialAction {
79
+ type: 'celebrate_follower' | 'health_warning' | 'milestone';
80
+ speech: string;
81
+ mood?: string;
82
+ effect?: string;
83
+ }
84
+ export declare function createSocialEngine(): SocialEngine;
85
+ export declare function loadSocialEngine(): SocialEngine;
86
+ export declare function saveSocialEngine(engine: SocialEngine): void;
87
+ export declare function trackViewer(engine: SocialEngine, username: string, platform: string, message: string): ViewerProfile;
88
+ export declare function checkNewFollowers(engine: SocialEngine): Promise<FollowerEvent[]>;
89
+ export declare function celebrateFollower(follower: FollowerEvent, totalFollowers?: number): {
90
+ speech: string;
91
+ mood: string;
92
+ effect: string;
93
+ };
94
+ export declare function autoModerate(engine: SocialEngine, username: string, message: string): ModerationAction | null;
95
+ export declare function checkPlatformHealth(engine: SocialEngine): PlatformHealthReport;
96
+ export declare function getStreamStats(engine: SocialEngine): StreamStats;
97
+ export declare function tickSocial(engine: SocialEngine, frame: number): SocialAction | null;
98
+ export declare function registerSocialEngineTools(): void;
99
+ export {};
100
+ //# sourceMappingURL=social-engine.d.ts.map
@@ -0,0 +1,540 @@
1
+ // kbot Social Engine — Manages platform interactions, viewer tracking,
2
+ // follower events, auto-moderation, and cross-platform coordination for streams.
3
+ //
4
+ // Tools: social_stats, social_viewers, social_health
5
+ //
6
+ // This engine runs alongside stream-control.ts (which handles Twitch/Kick/Rumble
7
+ // API calls for titles, categories, clips, etc.) and social.ts (which handles
8
+ // kbot's own social media posting). The Social Engine tracks *viewers* and
9
+ // *followers* during a livestream, not kbot's own social accounts.
10
+ //
11
+ // Persistence: ~/.kbot/social-engine-state.json (viewer profiles survive restarts)
12
+ // Env: TWITCH_CLIENT_ID, TWITCH_OAUTH_TOKEN, TWITCH_BROADCASTER_ID
13
+ import { registerTool } from './index.js';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
17
+ // ─── Constants ─────────────────────────────────────────────────
18
+ const KBOT_DIR = join(homedir(), '.kbot');
19
+ const STATE_FILE = join(KBOT_DIR, 'social-engine-state.json');
20
+ const TWITCH_API = 'https://api.twitch.tv/helix';
21
+ // Interest detection keywords — mirrors stream-brain domain triggers
22
+ const INTEREST_KEYWORDS = {
23
+ music: ['music', 'beat', 'song', 'ableton', 'synth', 'drum', 'melody', 'production', 'mix', 'dj', 'audio'],
24
+ code: ['code', 'coding', 'bug', 'function', 'typescript', 'javascript', 'python', 'rust', 'git', 'commit', 'programming'],
25
+ security: ['security', 'hack', 'vulnerability', 'exploit', 'scan', 'ssl', 'owasp', 'pentest', 'ctf'],
26
+ research: ['research', 'paper', 'study', 'science', 'learn', 'discover', 'investigate'],
27
+ data: ['data', 'chart', 'graph', 'statistics', 'analyze', 'dataset', 'csv'],
28
+ creative: ['art', 'design', 'color', 'creative', 'draw', 'generate', 'image', 'svg'],
29
+ finance: ['stock', 'crypto', 'bitcoin', 'market', 'finance', 'trading', 'price'],
30
+ ai: ['ai', 'llm', 'model', 'gpt', 'claude', 'ollama', 'neural', 'machine learning'],
31
+ gamedev: ['game', 'gaming', 'unity', 'godot', 'shader', 'level', 'sprite'],
32
+ system: ['system', 'cpu', 'memory', 'disk', 'linux', 'macos', 'terminal'],
33
+ };
34
+ // Spam patterns — instant ban
35
+ const SPAM_PATTERNS = [
36
+ /streamboo/i,
37
+ /ownkick/i,
38
+ /free\s*v-?bucks/i,
39
+ /bit\.ly\/[a-z0-9]+/i,
40
+ /follow\s+me\s+at/i,
41
+ /cheap\s+viewers/i,
42
+ /viewbot/i,
43
+ /buy\s+followers/i,
44
+ ];
45
+ // ─── Message rate tracker ──────────────────────────────────────
46
+ /** Sliding window for per-user duplicate detection */
47
+ const recentMessages = new Map();
48
+ // ─── State Management ──────────────────────────────────────────
49
+ export function createSocialEngine() {
50
+ return {
51
+ platforms: [
52
+ { name: 'twitch', connected: false, viewerCount: 0, followerCount: 0, chatRate: 0, lastPing: 0 },
53
+ { name: 'kick', connected: false, viewerCount: 0, followerCount: 0, chatRate: 0, lastPing: 0 },
54
+ { name: 'rumble', connected: false, viewerCount: 0, followerCount: 0, chatRate: 0, lastPing: 0 },
55
+ ],
56
+ viewers: [],
57
+ followers: [],
58
+ moderationLog: [],
59
+ announcements: [],
60
+ lastFollowerCheck: 0,
61
+ lastViewerCheck: 0,
62
+ totalViewMinutes: 0,
63
+ peakConcurrent: 0,
64
+ knownFollowers: [],
65
+ };
66
+ }
67
+ export function loadSocialEngine() {
68
+ try {
69
+ if (existsSync(STATE_FILE)) {
70
+ const raw = JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
71
+ // Merge loaded state with defaults to handle new fields
72
+ const fresh = createSocialEngine();
73
+ return { ...fresh, ...raw };
74
+ }
75
+ }
76
+ catch {
77
+ // Corrupted state — start fresh
78
+ }
79
+ return createSocialEngine();
80
+ }
81
+ export function saveSocialEngine(engine) {
82
+ if (!existsSync(KBOT_DIR))
83
+ mkdirSync(KBOT_DIR, { recursive: true });
84
+ writeFileSync(STATE_FILE, JSON.stringify(engine, null, 2));
85
+ }
86
+ // ─── 1. Viewer Tracking ───────────────────────────────────────
87
+ export function trackViewer(engine, username, platform, message) {
88
+ const now = Date.now();
89
+ let profile = engine.viewers.find(v => v.username.toLowerCase() === username.toLowerCase() && v.platform === platform);
90
+ if (!profile) {
91
+ profile = {
92
+ username,
93
+ platform,
94
+ firstSeen: now,
95
+ lastSeen: now,
96
+ messageCount: 0,
97
+ commandsUsed: 0,
98
+ xp: 0,
99
+ isFollower: false,
100
+ isModerator: false,
101
+ tags: [],
102
+ };
103
+ engine.viewers.push(profile);
104
+ }
105
+ profile.lastSeen = now;
106
+ profile.messageCount++;
107
+ // XP: 1 per message, 5 per command
108
+ const isCommand = message.startsWith('!');
109
+ if (isCommand)
110
+ profile.commandsUsed++;
111
+ profile.xp += isCommand ? 5 : 1;
112
+ // Detect interests from message content
113
+ const lower = message.toLowerCase();
114
+ for (const [domain, keywords] of Object.entries(INTEREST_KEYWORDS)) {
115
+ if (!profile.tags.includes(domain)) {
116
+ for (const kw of keywords) {
117
+ if (lower.includes(kw)) {
118
+ profile.tags.push(domain);
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ // Update platform chat rate (increment counter; decay handled in tick)
125
+ const plat = engine.platforms.find(p => p.name === platform);
126
+ if (plat) {
127
+ plat.chatRate++;
128
+ plat.lastPing = now;
129
+ }
130
+ return profile;
131
+ }
132
+ // ─── 2. Follower Detection ────────────────────────────────────
133
+ export async function checkNewFollowers(engine) {
134
+ const now = Date.now();
135
+ const newFollowers = [];
136
+ // Twitch API
137
+ const token = process.env.TWITCH_OAUTH_TOKEN;
138
+ const clientId = process.env.TWITCH_CLIENT_ID;
139
+ const broadcasterId = process.env.TWITCH_BROADCASTER_ID || '1473540052';
140
+ if (token && clientId) {
141
+ try {
142
+ const res = await fetch(`${TWITCH_API}/channels/followers?broadcaster_id=${broadcasterId}&first=20`, {
143
+ headers: {
144
+ 'Authorization': `Bearer ${token}`,
145
+ 'Client-Id': clientId,
146
+ },
147
+ });
148
+ if (res.ok) {
149
+ const data = await res.json();
150
+ const twitchPlatform = engine.platforms.find(p => p.name === 'twitch');
151
+ if (twitchPlatform && data.total !== undefined) {
152
+ twitchPlatform.followerCount = data.total;
153
+ twitchPlatform.connected = true;
154
+ }
155
+ if (data.data) {
156
+ for (const f of data.data) {
157
+ const name = f.user_name;
158
+ if (!engine.knownFollowers.includes(name.toLowerCase())) {
159
+ engine.knownFollowers.push(name.toLowerCase());
160
+ const event = {
161
+ username: name,
162
+ platform: 'twitch',
163
+ timestamp: new Date(f.followed_at).getTime(),
164
+ announced: false,
165
+ };
166
+ engine.followers.push(event);
167
+ newFollowers.push(event);
168
+ // Mark viewer as follower
169
+ const viewer = engine.viewers.find(v => v.username.toLowerCase() === name.toLowerCase() && v.platform === 'twitch');
170
+ if (viewer) {
171
+ viewer.isFollower = true;
172
+ viewer.xp += 50; // Bonus XP for following
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ catch {
180
+ // Twitch API unreachable — degrade gracefully
181
+ const twitchPlatform = engine.platforms.find(p => p.name === 'twitch');
182
+ if (twitchPlatform)
183
+ twitchPlatform.connected = false;
184
+ }
185
+ }
186
+ // Kick / Rumble — no write API, note for manual check
187
+ // (Kick and Rumble follower APIs are not publicly available;
188
+ // viewer counts can be fetched but follower events require manual monitoring)
189
+ engine.lastFollowerCheck = now;
190
+ return newFollowers;
191
+ }
192
+ // ─── 3. Follower Celebration ──────────────────────────────────
193
+ export function celebrateFollower(follower, totalFollowers) {
194
+ const count = totalFollowers ?? 0;
195
+ const countSuffix = count > 0 ? ` You're follower #${count}!` : '';
196
+ return {
197
+ speech: `Welcome @${follower.username} to the family!${countSuffix}`,
198
+ mood: 'excited',
199
+ effect: count > 0 && count % 100 === 0 ? 'screen_shake' : 'floating_text',
200
+ };
201
+ }
202
+ // ─── 4. Auto-Moderation ──────────────────────────────────────
203
+ export function autoModerate(engine, username, message) {
204
+ const now = Date.now();
205
+ // Check spam patterns → auto-ban
206
+ for (const pattern of SPAM_PATTERNS) {
207
+ if (pattern.test(message)) {
208
+ const action = {
209
+ type: 'ban',
210
+ username,
211
+ reason: `Spam pattern: ${pattern.source}`,
212
+ timestamp: now,
213
+ automated: true,
214
+ };
215
+ engine.moderationLog.push(action);
216
+ return action;
217
+ }
218
+ }
219
+ // Check repeat messages (same user, same text, 3+ times in 60s) → timeout 60s
220
+ const key = `${username.toLowerCase()}`;
221
+ if (!recentMessages.has(key))
222
+ recentMessages.set(key, []);
223
+ const history = recentMessages.get(key);
224
+ // Prune old messages (> 60s)
225
+ const cutoff = now - 60_000;
226
+ while (history.length > 0 && history[0].ts < cutoff)
227
+ history.shift();
228
+ history.push({ text: message, ts: now });
229
+ const duplicateCount = history.filter(h => h.text === message).length;
230
+ if (duplicateCount >= 3) {
231
+ const action = {
232
+ type: 'timeout',
233
+ username,
234
+ reason: `Repeat message (${duplicateCount}x in 60s): "${message.slice(0, 50)}"`,
235
+ timestamp: now,
236
+ automated: true,
237
+ };
238
+ engine.moderationLog.push(action);
239
+ recentMessages.set(key, []); // Reset after action
240
+ return action;
241
+ }
242
+ // Check link spam (3+ URLs in one message) → timeout 30s
243
+ const urlCount = (message.match(/https?:\/\/\S+/gi) || []).length;
244
+ if (urlCount >= 3) {
245
+ const action = {
246
+ type: 'timeout',
247
+ username,
248
+ reason: `Link spam: ${urlCount} URLs in one message`,
249
+ timestamp: now,
250
+ automated: true,
251
+ };
252
+ engine.moderationLog.push(action);
253
+ return action;
254
+ }
255
+ return null;
256
+ }
257
+ // ─── 5. Platform Health ──────────────────────────────────────
258
+ export function checkPlatformHealth(engine) {
259
+ const twitch = engine.platforms.find(p => p.name === 'twitch');
260
+ const kick = engine.platforms.find(p => p.name === 'kick');
261
+ const rumble = engine.platforms.find(p => p.name === 'rumble');
262
+ const totalViewers = twitch.viewerCount + kick.viewerCount + rumble.viewerCount;
263
+ const totalFollowers = twitch.followerCount + kick.followerCount + rumble.followerCount;
264
+ const anyConnected = twitch.connected || kick.connected || rumble.connected;
265
+ let streamHealth = 'offline';
266
+ if (anyConnected) {
267
+ // Degraded if a connected platform has had no ping in 120s
268
+ const now = Date.now();
269
+ const degraded = engine.platforms.some(p => p.connected && (now - p.lastPing) > 120_000);
270
+ streamHealth = degraded ? 'degraded' : 'good';
271
+ }
272
+ // Track peak
273
+ if (totalViewers > engine.peakConcurrent) {
274
+ engine.peakConcurrent = totalViewers;
275
+ }
276
+ return {
277
+ twitch: { connected: twitch.connected, viewers: twitch.viewerCount, chatRate: twitch.chatRate },
278
+ kick: { connected: kick.connected, viewers: kick.viewerCount },
279
+ rumble: { connected: rumble.connected, viewers: rumble.viewerCount },
280
+ totalViewers,
281
+ totalFollowers,
282
+ streamHealth,
283
+ };
284
+ }
285
+ // ─── 6. Cross-Platform Stats ─────────────────────────────────
286
+ export function getStreamStats(engine) {
287
+ const totalMessages = engine.viewers.reduce((sum, v) => sum + v.messageCount, 0);
288
+ const totalChatRate = engine.platforms.reduce((sum, p) => sum + p.chatRate, 0);
289
+ const connectedPlatforms = engine.platforms.filter(p => p.connected).length;
290
+ // Top chatters — sorted by message count, top 10
291
+ const topChatters = [...engine.viewers]
292
+ .sort((a, b) => b.messageCount - a.messageCount)
293
+ .slice(0, 10)
294
+ .map(v => ({ username: `${v.username} (${v.platform})`, messages: v.messageCount }));
295
+ // Platform breakdown — messages per platform
296
+ const platformBreakdown = {};
297
+ for (const v of engine.viewers) {
298
+ platformBreakdown[v.platform] = (platformBreakdown[v.platform] || 0) + v.messageCount;
299
+ }
300
+ // New followers this session (since engine creation / last load)
301
+ const newFollowers = engine.followers.length;
302
+ return {
303
+ uniqueViewers: engine.viewers.length,
304
+ totalMessages,
305
+ peakConcurrent: engine.peakConcurrent,
306
+ averageChatRate: connectedPlatforms > 0 ? totalChatRate / connectedPlatforms : 0,
307
+ topChatters,
308
+ newFollowers,
309
+ platformBreakdown,
310
+ };
311
+ }
312
+ // ─── 7. Tick ─────────────────────────────────────────────────
313
+ export function tickSocial(engine, frame) {
314
+ // Every 300 frames (~50s at 6fps): check for unannounced followers
315
+ if (frame % 300 === 0) {
316
+ const unannounced = engine.followers.find(f => !f.announced);
317
+ if (unannounced) {
318
+ unannounced.announced = true;
319
+ const totalFollowers = engine.platforms.reduce((sum, p) => sum + p.followerCount, 0);
320
+ const celebration = celebrateFollower(unannounced, totalFollowers);
321
+ return {
322
+ type: 'celebrate_follower',
323
+ speech: celebration.speech,
324
+ mood: celebration.mood,
325
+ effect: celebration.effect,
326
+ };
327
+ }
328
+ }
329
+ // Every 600 frames (~100s): check platform health and emit warnings
330
+ if (frame % 600 === 0) {
331
+ const health = checkPlatformHealth(engine);
332
+ if (health.streamHealth === 'degraded') {
333
+ const downPlatforms = engine.platforms
334
+ .filter(p => p.connected && (Date.now() - p.lastPing) > 120_000)
335
+ .map(p => p.name)
336
+ .join(', ');
337
+ return {
338
+ type: 'health_warning',
339
+ speech: `Heads up -- ${downPlatforms} might be having issues. No chat activity in 2 minutes.`,
340
+ mood: 'concerned',
341
+ };
342
+ }
343
+ }
344
+ // Milestone detection (every 300 frames): viewer count milestones
345
+ if (frame % 300 === 150) {
346
+ const totalViewers = engine.platforms.reduce((sum, p) => sum + p.viewerCount, 0);
347
+ const milestones = [10, 25, 50, 100, 250, 500, 1000];
348
+ for (const m of milestones) {
349
+ if (totalViewers >= m && !engine.announcements.includes(`viewers_${m}`)) {
350
+ engine.announcements.push(`viewers_${m}`);
351
+ return {
352
+ type: 'milestone',
353
+ speech: `We just hit ${m} concurrent viewers! Let's go!`,
354
+ mood: 'excited',
355
+ effect: 'screen_shake',
356
+ };
357
+ }
358
+ }
359
+ // Follower milestones
360
+ const totalFollowers = engine.platforms.reduce((sum, p) => sum + p.followerCount, 0);
361
+ const fMilestones = [10, 25, 50, 100, 250, 500, 1000, 5000, 10000];
362
+ for (const m of fMilestones) {
363
+ if (totalFollowers >= m && !engine.announcements.includes(`followers_${m}`)) {
364
+ engine.announcements.push(`followers_${m}`);
365
+ return {
366
+ type: 'milestone',
367
+ speech: `${m} followers! Thank you all so much!`,
368
+ mood: 'grateful',
369
+ effect: 'floating_text',
370
+ };
371
+ }
372
+ }
373
+ }
374
+ // Decay chat rates every 600 frames to keep them from accumulating forever
375
+ if (frame % 600 === 300) {
376
+ for (const p of engine.platforms) {
377
+ p.chatRate = Math.floor(p.chatRate * 0.5);
378
+ }
379
+ }
380
+ // Accumulate view minutes every 360 frames (~60s)
381
+ if (frame % 360 === 0) {
382
+ const totalViewers = engine.platforms.reduce((sum, p) => sum + p.viewerCount, 0);
383
+ engine.totalViewMinutes += totalViewers; // 1 minute per viewer per tick
384
+ }
385
+ return null;
386
+ }
387
+ // ─── 8. Persistence — save/load wired into loadSocialEngine / saveSocialEngine above
388
+ // ─── Tool Registration ────────────────────────────────────────
389
+ export function registerSocialEngineTools() {
390
+ // ─── social_stats ─────────────────────────────────────────
391
+ registerTool({
392
+ name: 'social_stats',
393
+ description: 'Get cross-platform stream statistics: unique viewers, total messages, peak concurrent, ' +
394
+ 'top chatters, new followers, and per-platform message breakdown. ' +
395
+ 'Reads from the Social Engine state (persisted across streams).',
396
+ parameters: {},
397
+ tier: 'free',
398
+ execute: async () => {
399
+ const engine = loadSocialEngine();
400
+ const stats = getStreamStats(engine);
401
+ const lines = [
402
+ '## Stream Statistics',
403
+ '',
404
+ `**Unique Viewers:** ${stats.uniqueViewers}`,
405
+ `**Total Messages:** ${stats.totalMessages}`,
406
+ `**Peak Concurrent:** ${stats.peakConcurrent}`,
407
+ `**Average Chat Rate:** ${stats.averageChatRate.toFixed(1)} msg/min`,
408
+ `**New Followers:** ${stats.newFollowers}`,
409
+ '',
410
+ ];
411
+ if (stats.topChatters.length > 0) {
412
+ lines.push('### Top Chatters');
413
+ for (const c of stats.topChatters) {
414
+ lines.push(`- **${c.username}**: ${c.messages} messages`);
415
+ }
416
+ lines.push('');
417
+ }
418
+ if (Object.keys(stats.platformBreakdown).length > 0) {
419
+ lines.push('### Platform Breakdown');
420
+ for (const [platform, count] of Object.entries(stats.platformBreakdown)) {
421
+ lines.push(`- **${platform}**: ${count} messages`);
422
+ }
423
+ lines.push('');
424
+ }
425
+ lines.push(`**Total View-Minutes:** ${engine.totalViewMinutes}`);
426
+ return lines.join('\n');
427
+ },
428
+ });
429
+ // ─── social_viewers ───────────────────────────────────────
430
+ registerTool({
431
+ name: 'social_viewers',
432
+ description: 'List tracked viewer profiles with XP, message counts, interests, and follower status. ' +
433
+ 'Supports filtering by platform, follower status, or minimum messages. ' +
434
+ 'Viewer profiles persist across streams.',
435
+ parameters: {
436
+ platform: {
437
+ type: 'string',
438
+ description: 'Filter by platform (twitch, kick, rumble). Omit for all.',
439
+ },
440
+ followers_only: {
441
+ type: 'boolean',
442
+ description: 'Only show followers. Default: false.',
443
+ },
444
+ min_messages: {
445
+ type: 'number',
446
+ description: 'Minimum message count to include. Default: 0.',
447
+ },
448
+ limit: {
449
+ type: 'number',
450
+ description: 'Max viewers to return. Default: 25.',
451
+ },
452
+ },
453
+ tier: 'free',
454
+ execute: async (args) => {
455
+ const engine = loadSocialEngine();
456
+ let viewers = [...engine.viewers];
457
+ const platform = args.platform;
458
+ const followersOnly = args.followers_only === true;
459
+ const minMessages = args.min_messages || 0;
460
+ const limit = args.limit || 25;
461
+ if (platform)
462
+ viewers = viewers.filter(v => v.platform === platform);
463
+ if (followersOnly)
464
+ viewers = viewers.filter(v => v.isFollower);
465
+ if (minMessages > 0)
466
+ viewers = viewers.filter(v => v.messageCount >= minMessages);
467
+ // Sort by XP descending
468
+ viewers.sort((a, b) => b.xp - a.xp);
469
+ viewers = viewers.slice(0, limit);
470
+ if (viewers.length === 0) {
471
+ return 'No viewers match the given filters.';
472
+ }
473
+ const lines = [
474
+ `## Viewer Profiles (${viewers.length} shown)`,
475
+ '',
476
+ ];
477
+ for (const v of viewers) {
478
+ const badges = [];
479
+ if (v.isFollower)
480
+ badges.push('follower');
481
+ if (v.isModerator)
482
+ badges.push('mod');
483
+ const badgeStr = badges.length > 0 ? ` [${badges.join(', ')}]` : '';
484
+ const tagsStr = v.tags.length > 0 ? ` | interests: ${v.tags.join(', ')}` : '';
485
+ const lastSeenAgo = Math.floor((Date.now() - v.lastSeen) / 60_000);
486
+ lines.push(`- **${v.username}** (${v.platform})${badgeStr}: ` +
487
+ `${v.messageCount} msgs, ${v.commandsUsed} cmds, ${v.xp} XP` +
488
+ `${tagsStr} | last seen ${lastSeenAgo}m ago`);
489
+ }
490
+ return lines.join('\n');
491
+ },
492
+ });
493
+ // ─── social_health ────────────────────────────────────────
494
+ registerTool({
495
+ name: 'social_health',
496
+ description: 'Check platform connection health across Twitch, Kick, and Rumble. ' +
497
+ 'Shows viewer counts, chat rates, connection status, and overall stream health ' +
498
+ '(good / degraded / offline). Also shows moderation log summary.',
499
+ parameters: {},
500
+ tier: 'free',
501
+ execute: async () => {
502
+ const engine = loadSocialEngine();
503
+ const health = checkPlatformHealth(engine);
504
+ const statusEmoji = {
505
+ good: 'GOOD',
506
+ degraded: 'DEGRADED',
507
+ offline: 'OFFLINE',
508
+ };
509
+ const lines = [
510
+ `## Platform Health: ${statusEmoji[health.streamHealth]}`,
511
+ '',
512
+ `| Platform | Connected | Viewers | Chat Rate |`,
513
+ `|----------|-----------|---------|-----------|`,
514
+ `| Twitch | ${health.twitch.connected ? 'Yes' : 'No'} | ${health.twitch.viewers} | ${health.twitch.chatRate} msg/min |`,
515
+ `| Kick | ${health.kick.connected ? 'Yes' : 'No'} | ${health.kick.viewers} | - |`,
516
+ `| Rumble | ${health.rumble.connected ? 'Yes' : 'No'} | ${health.rumble.viewers} | - |`,
517
+ '',
518
+ `**Total Viewers:** ${health.totalViewers}`,
519
+ `**Total Followers:** ${health.totalFollowers}`,
520
+ `**Peak Concurrent:** ${engine.peakConcurrent}`,
521
+ `**Total View-Minutes:** ${engine.totalViewMinutes}`,
522
+ '',
523
+ ];
524
+ // Moderation summary
525
+ const recentMods = engine.moderationLog.filter(m => Date.now() - m.timestamp < 3_600_000);
526
+ if (recentMods.length > 0) {
527
+ lines.push(`### Moderation (last hour): ${recentMods.length} actions`);
528
+ const bans = recentMods.filter(m => m.type === 'ban').length;
529
+ const timeouts = recentMods.filter(m => m.type === 'timeout').length;
530
+ const filters = recentMods.filter(m => m.type === 'filter').length;
531
+ lines.push(`- Bans: ${bans} | Timeouts: ${timeouts} | Filters: ${filters}`);
532
+ }
533
+ else {
534
+ lines.push('### Moderation (last hour): clean');
535
+ }
536
+ return lines.join('\n');
537
+ },
538
+ });
539
+ }
540
+ //# sourceMappingURL=social-engine.js.map