@crowdlisten/harness 1.0.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.
Files changed (109) hide show
  1. package/AGENTS.md +167 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/dist/agent-proxy.d.ts +24 -0
  5. package/dist/agent-proxy.js +140 -0
  6. package/dist/agent-tools.d.ts +736 -0
  7. package/dist/agent-tools.js +409 -0
  8. package/dist/context/api.d.ts +5 -0
  9. package/dist/context/api.js +164 -0
  10. package/dist/context/cli.d.ts +19 -0
  11. package/dist/context/cli.js +108 -0
  12. package/dist/context/extractor.d.ts +12 -0
  13. package/dist/context/extractor.js +43 -0
  14. package/dist/context/index.d.ts +12 -0
  15. package/dist/context/index.js +11 -0
  16. package/dist/context/matcher.d.ts +39 -0
  17. package/dist/context/matcher.js +246 -0
  18. package/dist/context/parser.d.ts +28 -0
  19. package/dist/context/parser.js +157 -0
  20. package/dist/context/pipeline.d.ts +26 -0
  21. package/dist/context/pipeline.js +56 -0
  22. package/dist/context/prompts.d.ts +6 -0
  23. package/dist/context/prompts.js +60 -0
  24. package/dist/context/providers.d.ts +6 -0
  25. package/dist/context/providers.js +106 -0
  26. package/dist/context/redactor.d.ts +10 -0
  27. package/dist/context/redactor.js +68 -0
  28. package/dist/context/server.d.ts +5 -0
  29. package/dist/context/server.js +134 -0
  30. package/dist/context/store.d.ts +12 -0
  31. package/dist/context/store.js +82 -0
  32. package/dist/context/types.d.ts +79 -0
  33. package/dist/context/types.js +4 -0
  34. package/dist/context/user-state.d.ts +40 -0
  35. package/dist/context/user-state.js +144 -0
  36. package/dist/index.d.ts +14 -0
  37. package/dist/index.js +385 -0
  38. package/dist/insights/browser/BrowserPool.d.ts +87 -0
  39. package/dist/insights/browser/BrowserPool.js +266 -0
  40. package/dist/insights/browser/RequestInterceptor.d.ts +46 -0
  41. package/dist/insights/browser/RequestInterceptor.js +115 -0
  42. package/dist/insights/cli.d.ts +8 -0
  43. package/dist/insights/cli.js +206 -0
  44. package/dist/insights/core/base/BaseAdapter.d.ts +37 -0
  45. package/dist/insights/core/base/BaseAdapter.js +123 -0
  46. package/dist/insights/core/health/HealthMonitor.d.ts +75 -0
  47. package/dist/insights/core/health/HealthMonitor.js +171 -0
  48. package/dist/insights/core/interfaces/SocialMediaPlatform.d.ts +125 -0
  49. package/dist/insights/core/interfaces/SocialMediaPlatform.js +42 -0
  50. package/dist/insights/core/utils/DataNormalizer.d.ts +53 -0
  51. package/dist/insights/core/utils/DataNormalizer.js +349 -0
  52. package/dist/insights/core/utils/InstagramUrlUtils.d.ts +11 -0
  53. package/dist/insights/core/utils/InstagramUrlUtils.js +60 -0
  54. package/dist/insights/core/utils/TikTokUrlUtils.d.ts +10 -0
  55. package/dist/insights/core/utils/TikTokUrlUtils.js +57 -0
  56. package/dist/insights/handlers.d.ts +157 -0
  57. package/dist/insights/handlers.js +246 -0
  58. package/dist/insights/index.d.ts +437 -0
  59. package/dist/insights/index.js +426 -0
  60. package/dist/insights/platforms/instagram/InstagramAdapter.d.ts +34 -0
  61. package/dist/insights/platforms/instagram/InstagramAdapter.js +342 -0
  62. package/dist/insights/platforms/moltbook/MoltbookAdapter.d.ts +31 -0
  63. package/dist/insights/platforms/moltbook/MoltbookAdapter.js +227 -0
  64. package/dist/insights/platforms/reddit/RedditAdapter.d.ts +21 -0
  65. package/dist/insights/platforms/reddit/RedditAdapter.js +212 -0
  66. package/dist/insights/platforms/tiktok/TikTokAdapter.d.ts +34 -0
  67. package/dist/insights/platforms/tiktok/TikTokAdapter.js +269 -0
  68. package/dist/insights/platforms/twitter/TwitterAdapter.d.ts +23 -0
  69. package/dist/insights/platforms/twitter/TwitterAdapter.js +211 -0
  70. package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.d.ts +35 -0
  71. package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.js +258 -0
  72. package/dist/insights/platforms/youtube/YouTubeAdapter.d.ts +22 -0
  73. package/dist/insights/platforms/youtube/YouTubeAdapter.js +254 -0
  74. package/dist/insights/service-config.d.ts +7 -0
  75. package/dist/insights/service-config.js +60 -0
  76. package/dist/insights/services/UnifiedSocialMediaService.d.ts +94 -0
  77. package/dist/insights/services/UnifiedSocialMediaService.js +259 -0
  78. package/dist/insights/vision/VisionExtractor.d.ts +46 -0
  79. package/dist/insights/vision/VisionExtractor.js +236 -0
  80. package/dist/learnings.d.ts +50 -0
  81. package/dist/learnings.js +130 -0
  82. package/dist/openapi.d.ts +29 -0
  83. package/dist/openapi.js +169 -0
  84. package/dist/server-factory.d.ts +20 -0
  85. package/dist/server-factory.js +41 -0
  86. package/dist/suggestions.d.ts +16 -0
  87. package/dist/suggestions.js +72 -0
  88. package/dist/telemetry.d.ts +44 -0
  89. package/dist/telemetry.js +93 -0
  90. package/dist/tools/registry.d.ts +65 -0
  91. package/dist/tools/registry.js +256 -0
  92. package/dist/tools.d.ts +2433 -0
  93. package/dist/tools.js +2294 -0
  94. package/dist/transport/http.d.ts +15 -0
  95. package/dist/transport/http.js +154 -0
  96. package/package.json +76 -0
  97. package/skills/catalog.json +272 -0
  98. package/skills/community-catalog.json +4202 -0
  99. package/skills/competitive-analysis/SKILL.md +174 -0
  100. package/skills/content-creator/SKILL.md +256 -0
  101. package/skills/content-strategy/SKILL.md +222 -0
  102. package/skills/data-storytelling/SKILL.md +248 -0
  103. package/skills/heuristic-evaluation/SKILL.md +201 -0
  104. package/skills/market-research-reports/SKILL.md +184 -0
  105. package/skills/user-stories/SKILL.md +178 -0
  106. package/skills/ux-researcher/SKILL.md +239 -0
  107. package/web-dist/assets/index-B1b25lNd.css +1 -0
  108. package/web-dist/assets/index-CDWHwHbl.js +64 -0
  109. package/web-dist/index.html +16 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * YouTube Platform Adapter
3
+ * Uses YouTube Data API v3 for video search, trending, comments, and channel content
4
+ */
5
+ import { BaseAdapter } from '../../core/base/BaseAdapter.js';
6
+ import { DataNormalizer } from '../../core/utils/DataNormalizer.js';
7
+ import { NotFoundError } from '../../core/interfaces/SocialMediaPlatform.js';
8
+ import axios from 'axios';
9
+ export class YouTubeAdapter extends BaseAdapter {
10
+ client = null;
11
+ apiKey = '';
12
+ constructor(config) {
13
+ super(config);
14
+ // YouTube API quota is 10,000 units/day; keep requests conservative
15
+ this.maxRequestsPerWindow = 20;
16
+ }
17
+ async initialize() {
18
+ try {
19
+ this.apiKey = this.config.credentials?.apiKey || process.env.YOUTUBE_API_KEY || '';
20
+ if (!this.apiKey) {
21
+ this.log('No YouTube API key provided', 'warn');
22
+ return false;
23
+ }
24
+ this.client = axios.create({
25
+ baseURL: 'https://www.googleapis.com/youtube/v3',
26
+ timeout: 15000
27
+ });
28
+ this.isInitialized = true;
29
+ this.log('YouTube adapter initialized successfully');
30
+ return true;
31
+ }
32
+ catch (error) {
33
+ this.log('Failed to initialize YouTube adapter', 'error');
34
+ this.isInitialized = false;
35
+ return false;
36
+ }
37
+ }
38
+ async getTrendingContent(limit = 10) {
39
+ this.ensureInitialized();
40
+ this.validateLimit(limit);
41
+ try {
42
+ await this.enforceRateLimit();
43
+ const response = await this.client.get('/videos', {
44
+ params: {
45
+ part: 'snippet,statistics',
46
+ chart: 'mostPopular',
47
+ regionCode: 'US',
48
+ maxResults: Math.min(limit, 50),
49
+ key: this.apiKey
50
+ }
51
+ });
52
+ const posts = [];
53
+ for (const item of (response.data.items || []).slice(0, limit)) {
54
+ posts.push(DataNormalizer.normalizePost(item, 'youtube'));
55
+ }
56
+ this.log(`Retrieved ${posts.length} trending YouTube videos`);
57
+ return posts;
58
+ }
59
+ catch (error) {
60
+ this.handleError(error, 'getTrendingContent');
61
+ }
62
+ }
63
+ async getUserContent(userId, limit = 10) {
64
+ this.ensureInitialized();
65
+ this.validateUserId(userId);
66
+ this.validateLimit(limit);
67
+ try {
68
+ await this.enforceRateLimit();
69
+ // First resolve the channel ID — userId could be a channel ID or a handle
70
+ let channelId = userId;
71
+ if (!userId.startsWith('UC')) {
72
+ const handle = userId.startsWith('@') ? userId : `@${userId}`;
73
+ const channelRes = await this.client.get('/channels', {
74
+ params: {
75
+ part: 'id',
76
+ forHandle: handle,
77
+ key: this.apiKey
78
+ }
79
+ });
80
+ channelId = channelRes.data.items?.[0]?.id;
81
+ if (!channelId) {
82
+ throw new NotFoundError('youtube', `Channel ${userId}`);
83
+ }
84
+ }
85
+ await this.enforceRateLimit();
86
+ // Search for videos on this channel
87
+ const response = await this.client.get('/search', {
88
+ params: {
89
+ part: 'snippet',
90
+ channelId,
91
+ type: 'video',
92
+ order: 'date',
93
+ maxResults: Math.min(limit, 50),
94
+ key: this.apiKey
95
+ }
96
+ });
97
+ // Get video statistics in a separate call
98
+ const videoIds = (response.data.items || [])
99
+ .map((item) => item.id?.videoId)
100
+ .filter(Boolean)
101
+ .join(',');
102
+ let statsMap = {};
103
+ if (videoIds) {
104
+ await this.enforceRateLimit();
105
+ const statsRes = await this.client.get('/videos', {
106
+ params: {
107
+ part: 'statistics',
108
+ id: videoIds,
109
+ key: this.apiKey
110
+ }
111
+ });
112
+ for (const v of statsRes.data.items || []) {
113
+ statsMap[v.id] = v.statistics;
114
+ }
115
+ }
116
+ const posts = [];
117
+ for (const item of (response.data.items || []).slice(0, limit)) {
118
+ const videoId = item.id?.videoId;
119
+ const merged = {
120
+ id: videoId,
121
+ snippet: item.snippet,
122
+ statistics: statsMap[videoId] || {}
123
+ };
124
+ posts.push(DataNormalizer.normalizePost(merged, 'youtube'));
125
+ }
126
+ this.log(`Retrieved ${posts.length} videos from YouTube channel ${userId}`);
127
+ return posts;
128
+ }
129
+ catch (error) {
130
+ if (error.response?.status === 404) {
131
+ throw new NotFoundError('youtube', `Channel ${userId}`, error);
132
+ }
133
+ this.handleError(error, 'getUserContent');
134
+ }
135
+ }
136
+ async searchContent(query, limit = 10) {
137
+ this.ensureInitialized();
138
+ this.validateLimit(limit);
139
+ if (!query || query.trim().length === 0) {
140
+ throw new Error('Search query cannot be empty');
141
+ }
142
+ try {
143
+ await this.enforceRateLimit();
144
+ const response = await this.client.get('/search', {
145
+ params: {
146
+ part: 'snippet',
147
+ q: query.trim(),
148
+ type: 'video',
149
+ order: 'relevance',
150
+ maxResults: Math.min(limit, 50),
151
+ key: this.apiKey
152
+ }
153
+ });
154
+ // Get video statistics
155
+ const videoIds = (response.data.items || [])
156
+ .map((item) => item.id?.videoId)
157
+ .filter(Boolean)
158
+ .join(',');
159
+ let statsMap = {};
160
+ if (videoIds) {
161
+ await this.enforceRateLimit();
162
+ const statsRes = await this.client.get('/videos', {
163
+ params: {
164
+ part: 'statistics',
165
+ id: videoIds,
166
+ key: this.apiKey
167
+ }
168
+ });
169
+ for (const v of statsRes.data.items || []) {
170
+ statsMap[v.id] = v.statistics;
171
+ }
172
+ }
173
+ const posts = [];
174
+ for (const item of (response.data.items || []).slice(0, limit)) {
175
+ const videoId = item.id?.videoId;
176
+ const merged = {
177
+ id: videoId,
178
+ snippet: item.snippet,
179
+ statistics: statsMap[videoId] || {}
180
+ };
181
+ posts.push(DataNormalizer.normalizePost(merged, 'youtube'));
182
+ }
183
+ this.log(`Found ${posts.length} YouTube videos for query: ${query}`);
184
+ return posts;
185
+ }
186
+ catch (error) {
187
+ this.handleError(error, 'searchContent');
188
+ }
189
+ }
190
+ async getContentComments(contentId, limit = 20) {
191
+ this.ensureInitialized();
192
+ this.validateContentId(contentId);
193
+ this.validateLimit(limit);
194
+ try {
195
+ await this.enforceRateLimit();
196
+ const response = await this.client.get('/commentThreads', {
197
+ params: {
198
+ part: 'snippet,replies',
199
+ videoId: contentId,
200
+ maxResults: Math.min(limit, 100),
201
+ order: 'relevance',
202
+ key: this.apiKey
203
+ }
204
+ });
205
+ const comments = [];
206
+ for (const item of (response.data.items || []).slice(0, limit)) {
207
+ comments.push(DataNormalizer.normalizeComment(item, 'youtube'));
208
+ }
209
+ this.log(`Retrieved ${comments.length} comments for YouTube video ${contentId}`);
210
+ return comments;
211
+ }
212
+ catch (error) {
213
+ if (error.response?.status === 403 &&
214
+ error.response?.data?.error?.errors?.[0]?.reason === 'commentsDisabled') {
215
+ this.log(`Comments are disabled for video ${contentId}`, 'warn');
216
+ return [];
217
+ }
218
+ this.handleError(error, 'getContentComments');
219
+ }
220
+ }
221
+ getPlatformName() {
222
+ return 'youtube';
223
+ }
224
+ getSupportedFeatures() {
225
+ return {
226
+ supportsTrending: true,
227
+ supportsUserContent: true,
228
+ supportsSearch: true,
229
+ supportsComments: true,
230
+ supportsAnalysis: true
231
+ };
232
+ }
233
+ isRateLimitError(error) {
234
+ return error.response?.status === 429 ||
235
+ error.response?.data?.error?.errors?.[0]?.reason === 'quotaExceeded' ||
236
+ error.response?.data?.error?.errors?.[0]?.reason === 'rateLimitExceeded';
237
+ }
238
+ isAuthError(error) {
239
+ return error.response?.status === 401 ||
240
+ error.response?.status === 403;
241
+ }
242
+ isNotFoundError(error) {
243
+ return error.response?.status === 404;
244
+ }
245
+ async cleanup() {
246
+ try {
247
+ this.client = null;
248
+ await super.cleanup();
249
+ }
250
+ catch (error) {
251
+ this.log('Error during YouTube cleanup', 'warn');
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CrowdListen Service Configuration
3
+ * Shared service factory used by MCP and CLI entry points.
4
+ */
5
+ import { UnifiedSocialMediaService, UnifiedServiceConfig } from './services/UnifiedSocialMediaService.js';
6
+ export declare function createServiceConfig(): UnifiedServiceConfig;
7
+ export declare function createService(): UnifiedSocialMediaService;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * CrowdListen Service Configuration
3
+ * Shared service factory used by MCP and CLI entry points.
4
+ */
5
+ import * as dotenv from 'dotenv';
6
+ import { UnifiedSocialMediaService } from './services/UnifiedSocialMediaService.js';
7
+ dotenv.config();
8
+ export function createServiceConfig() {
9
+ const config = {
10
+ platforms: {},
11
+ globalOptions: {
12
+ timeout: 30000,
13
+ retries: 3,
14
+ fallbackStrategy: 'continue',
15
+ },
16
+ };
17
+ // Twitter — uses twitter-scraper, needs TWITTER_USERNAME + TWITTER_PASSWORD
18
+ config.platforms.twitter = {
19
+ platform: 'twitter',
20
+ credentials: {},
21
+ };
22
+ // TikTok — browser adapter
23
+ config.platforms.tiktok = {
24
+ platform: 'tiktok',
25
+ credentials: {},
26
+ };
27
+ // Instagram — browser adapter
28
+ config.platforms.instagram = {
29
+ platform: 'instagram',
30
+ credentials: {},
31
+ };
32
+ // Xiaohongshu — browser adapter
33
+ config.platforms.xiaohongshu = {
34
+ platform: 'xiaohongshu',
35
+ credentials: {},
36
+ };
37
+ // Reddit (no credentials needed for public content)
38
+ config.platforms.reddit = {
39
+ platform: 'reddit',
40
+ credentials: {},
41
+ };
42
+ // YouTube
43
+ if (process.env.YOUTUBE_API_KEY) {
44
+ config.platforms.youtube = {
45
+ platform: 'youtube',
46
+ credentials: { apiKey: process.env.YOUTUBE_API_KEY },
47
+ };
48
+ }
49
+ // Moltbook
50
+ if (process.env.MOLTBOOK_API_KEY) {
51
+ config.platforms.moltbook = {
52
+ platform: 'moltbook',
53
+ credentials: { apiKey: process.env.MOLTBOOK_API_KEY },
54
+ };
55
+ }
56
+ return config;
57
+ }
58
+ export function createService() {
59
+ return new UnifiedSocialMediaService(createServiceConfig());
60
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Unified Social Media Service
3
+ * Coordinates all platform adapters and provides a single interface.
4
+ * Each platform has one adapter — no visual vs legacy distinction.
5
+ */
6
+ import { PlatformType, PlatformConfig, Post, Comment } from '../core/interfaces/SocialMediaPlatform.js';
7
+ export interface UnifiedServiceConfig {
8
+ platforms: {
9
+ tiktok?: PlatformConfig;
10
+ twitter?: PlatformConfig;
11
+ reddit?: PlatformConfig;
12
+ instagram?: PlatformConfig;
13
+ youtube?: PlatformConfig;
14
+ moltbook?: PlatformConfig;
15
+ xiaohongshu?: PlatformConfig;
16
+ };
17
+ globalOptions?: {
18
+ timeout?: number;
19
+ retries?: number;
20
+ fallbackStrategy?: 'fail' | 'continue' | 'mock';
21
+ };
22
+ }
23
+ export declare class UnifiedSocialMediaService {
24
+ private adapters;
25
+ private config;
26
+ private isInitialized;
27
+ constructor(config: UnifiedServiceConfig);
28
+ /**
29
+ * Initialize all configured platform adapters.
30
+ * One adapter per platform — flat, no tiers.
31
+ */
32
+ initialize(): Promise<{
33
+ [key in PlatformType]?: boolean;
34
+ }>;
35
+ /**
36
+ * Get trending content from a specific platform
37
+ */
38
+ getTrendingContent(platform: PlatformType, limit?: number): Promise<Post[]>;
39
+ /**
40
+ * Get trending content from all available platforms
41
+ */
42
+ getAllTrendingContent(limit?: number): Promise<{
43
+ [key in PlatformType]?: Post[];
44
+ }>;
45
+ /**
46
+ * Get user content from a specific platform
47
+ */
48
+ getUserContent(platform: PlatformType, userId: string, limit?: number): Promise<Post[]>;
49
+ /**
50
+ * Search content on a specific platform
51
+ */
52
+ searchContent(platform: PlatformType, query: string, limit?: number): Promise<Post[]>;
53
+ /**
54
+ * Search content across all available platforms
55
+ */
56
+ searchAllPlatforms(query: string, limit?: number): Promise<{
57
+ [key in PlatformType]?: Post[];
58
+ }>;
59
+ /**
60
+ * Get comments for content on a specific platform
61
+ */
62
+ getContentComments(platform: PlatformType, contentId: string, limit?: number): Promise<Comment[]>;
63
+ /**
64
+ * Get the list of initialized platform types.
65
+ * Used by HealthMonitor to know which platforms to probe.
66
+ */
67
+ getInitializedPlatforms(): PlatformType[];
68
+ /**
69
+ * Get available platforms and their capabilities
70
+ */
71
+ getAvailablePlatforms(): {
72
+ [key in PlatformType]?: any;
73
+ };
74
+ /**
75
+ * Get combined trending content with platform attribution
76
+ */
77
+ getCombinedTrendingContent(limit?: number): Promise<Post[]>;
78
+ /**
79
+ * Search across all platforms and return combined results
80
+ */
81
+ getCombinedSearchResults(query: string, limit?: number): Promise<Post[]>;
82
+ /**
83
+ * Platform health check
84
+ */
85
+ healthCheck(): Promise<{
86
+ [key in PlatformType]?: 'healthy' | 'degraded' | 'down';
87
+ }>;
88
+ /**
89
+ * Cleanup all adapters and browser pool
90
+ */
91
+ cleanup(): Promise<void>;
92
+ private getAdapter;
93
+ private calculateRelevance;
94
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Unified Social Media Service
3
+ * Coordinates all platform adapters and provides a single interface.
4
+ * Each platform has one adapter — no visual vs legacy distinction.
5
+ */
6
+ import { SocialMediaError } from '../core/interfaces/SocialMediaPlatform.js';
7
+ import { TwitterAdapter } from '../platforms/twitter/TwitterAdapter.js';
8
+ import { RedditAdapter } from '../platforms/reddit/RedditAdapter.js';
9
+ import { YouTubeAdapter } from '../platforms/youtube/YouTubeAdapter.js';
10
+ import { MoltbookAdapter } from '../platforms/moltbook/MoltbookAdapter.js';
11
+ import { TikTokAdapter } from '../platforms/tiktok/TikTokAdapter.js';
12
+ import { InstagramAdapter } from '../platforms/instagram/InstagramAdapter.js';
13
+ import { XiaohongshuAdapter } from '../platforms/xiaohongshu/XiaohongshuAdapter.js';
14
+ import { getBrowserPool } from '../browser/BrowserPool.js';
15
+ export class UnifiedSocialMediaService {
16
+ adapters = new Map();
17
+ config;
18
+ isInitialized = false;
19
+ constructor(config) {
20
+ this.config = config;
21
+ }
22
+ /**
23
+ * Initialize all configured platform adapters.
24
+ * One adapter per platform — flat, no tiers.
25
+ */
26
+ async initialize() {
27
+ const results = {};
28
+ const adapterMap = [
29
+ { platform: 'twitter', create: () => new TwitterAdapter(this.config.platforms.twitter) },
30
+ { platform: 'tiktok', create: () => new TikTokAdapter(this.config.platforms.tiktok) },
31
+ { platform: 'instagram', create: () => new InstagramAdapter(this.config.platforms.instagram) },
32
+ { platform: 'xiaohongshu', create: () => new XiaohongshuAdapter(this.config.platforms.xiaohongshu) },
33
+ { platform: 'reddit', create: () => new RedditAdapter(this.config.platforms.reddit) },
34
+ { platform: 'youtube', create: () => new YouTubeAdapter(this.config.platforms.youtube) },
35
+ { platform: 'moltbook', create: () => new MoltbookAdapter(this.config.platforms.moltbook) },
36
+ ];
37
+ for (const { platform, create } of adapterMap) {
38
+ if (!this.config.platforms[platform])
39
+ continue;
40
+ try {
41
+ const adapter = create();
42
+ const success = await adapter.initialize();
43
+ if (success) {
44
+ this.adapters.set(platform, adapter);
45
+ }
46
+ results[platform] = success;
47
+ }
48
+ catch (error) {
49
+ console.error(`Failed to initialize ${platform} adapter:`, error);
50
+ results[platform] = false;
51
+ }
52
+ }
53
+ this.isInitialized = true;
54
+ console.log(`Unified Service initialized with ${this.adapters.size} platforms:`, Array.from(this.adapters.keys()));
55
+ return results;
56
+ }
57
+ /**
58
+ * Get trending content from a specific platform
59
+ */
60
+ async getTrendingContent(platform, limit) {
61
+ const adapter = this.getAdapter(platform);
62
+ return await adapter.getTrendingContent(limit);
63
+ }
64
+ /**
65
+ * Get trending content from all available platforms
66
+ */
67
+ async getAllTrendingContent(limit) {
68
+ const results = {};
69
+ const limitPerPlatform = limit ? Math.ceil(limit / this.adapters.size) : 10;
70
+ const promises = Array.from(this.adapters.entries()).map(async ([platform, adapter]) => {
71
+ try {
72
+ const posts = await adapter.getTrendingContent(limitPerPlatform);
73
+ results[platform] = posts;
74
+ }
75
+ catch (error) {
76
+ console.error(`Failed to get trending content from ${platform}:`, error);
77
+ if (this.config.globalOptions?.fallbackStrategy === 'continue') {
78
+ results[platform] = [];
79
+ }
80
+ }
81
+ });
82
+ await Promise.allSettled(promises);
83
+ return results;
84
+ }
85
+ /**
86
+ * Get user content from a specific platform
87
+ */
88
+ async getUserContent(platform, userId, limit) {
89
+ const adapter = this.getAdapter(platform);
90
+ return await adapter.getUserContent(userId, limit);
91
+ }
92
+ /**
93
+ * Search content on a specific platform
94
+ */
95
+ async searchContent(platform, query, limit) {
96
+ const adapter = this.getAdapter(platform);
97
+ return await adapter.searchContent(query, limit);
98
+ }
99
+ /**
100
+ * Search content across all available platforms
101
+ */
102
+ async searchAllPlatforms(query, limit) {
103
+ const results = {};
104
+ const limitPerPlatform = limit ? Math.ceil(limit / this.adapters.size) : 10;
105
+ const promises = Array.from(this.adapters.entries()).map(async ([platform, adapter]) => {
106
+ try {
107
+ const posts = await adapter.searchContent(query, limitPerPlatform);
108
+ results[platform] = posts;
109
+ }
110
+ catch (error) {
111
+ console.error(`Failed to search ${platform} for "${query}":`, error);
112
+ if (this.config.globalOptions?.fallbackStrategy === 'continue') {
113
+ results[platform] = [];
114
+ }
115
+ }
116
+ });
117
+ await Promise.allSettled(promises);
118
+ return results;
119
+ }
120
+ /**
121
+ * Get comments for content on a specific platform
122
+ */
123
+ async getContentComments(platform, contentId, limit) {
124
+ const adapter = this.getAdapter(platform);
125
+ return await adapter.getContentComments(contentId, limit);
126
+ }
127
+ /**
128
+ * Get the list of initialized platform types.
129
+ * Used by HealthMonitor to know which platforms to probe.
130
+ */
131
+ getInitializedPlatforms() {
132
+ return Array.from(this.adapters.keys());
133
+ }
134
+ /**
135
+ * Get available platforms and their capabilities
136
+ */
137
+ getAvailablePlatforms() {
138
+ const platforms = {};
139
+ for (const [platform, adapter] of this.adapters) {
140
+ platforms[platform] = {
141
+ name: platform,
142
+ capabilities: adapter.getSupportedFeatures(),
143
+ initialized: true
144
+ };
145
+ }
146
+ return platforms;
147
+ }
148
+ /**
149
+ * Get combined trending content with platform attribution
150
+ */
151
+ async getCombinedTrendingContent(limit = 30) {
152
+ const allTrending = await this.getAllTrendingContent(limit);
153
+ const combinedPosts = [];
154
+ for (const [, posts] of Object.entries(allTrending)) {
155
+ if (posts && Array.isArray(posts)) {
156
+ combinedPosts.push(...posts);
157
+ }
158
+ }
159
+ combinedPosts.sort((a, b) => {
160
+ const aEngagement = (a.engagement.likes || 0) + (a.engagement.comments || 0);
161
+ const bEngagement = (b.engagement.likes || 0) + (b.engagement.comments || 0);
162
+ if (aEngagement !== bEngagement) {
163
+ return bEngagement - aEngagement;
164
+ }
165
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
166
+ });
167
+ return combinedPosts.slice(0, limit);
168
+ }
169
+ /**
170
+ * Search across all platforms and return combined results
171
+ */
172
+ async getCombinedSearchResults(query, limit = 30) {
173
+ const allResults = await this.searchAllPlatforms(query, limit);
174
+ const combinedPosts = [];
175
+ for (const [, posts] of Object.entries(allResults)) {
176
+ if (posts && Array.isArray(posts)) {
177
+ combinedPosts.push(...posts);
178
+ }
179
+ }
180
+ combinedPosts.sort((a, b) => {
181
+ const aRelevance = this.calculateRelevance(a.content, query);
182
+ const bRelevance = this.calculateRelevance(b.content, query);
183
+ if (aRelevance !== bRelevance) {
184
+ return bRelevance - aRelevance;
185
+ }
186
+ const aEngagement = (a.engagement.likes || 0) + (a.engagement.comments || 0);
187
+ const bEngagement = (b.engagement.likes || 0) + (b.engagement.comments || 0);
188
+ return bEngagement - aEngagement;
189
+ });
190
+ return combinedPosts.slice(0, limit);
191
+ }
192
+ /**
193
+ * Platform health check
194
+ */
195
+ async healthCheck() {
196
+ const health = {};
197
+ const promises = Array.from(this.adapters.entries()).map(async ([platform, adapter]) => {
198
+ try {
199
+ await adapter.getTrendingContent(1);
200
+ health[platform] = 'healthy';
201
+ }
202
+ catch (error) {
203
+ console.warn(`Health check failed for ${platform}:`, error);
204
+ health[platform] = 'down';
205
+ }
206
+ });
207
+ await Promise.allSettled(promises);
208
+ return health;
209
+ }
210
+ /**
211
+ * Cleanup all adapters and browser pool
212
+ */
213
+ async cleanup() {
214
+ const cleanupPromises = Array.from(this.adapters.values()).map(adapter => adapter.cleanup().catch(error => console.warn('Cleanup error:', error)));
215
+ await Promise.allSettled(cleanupPromises);
216
+ // Cleanup browser pool if any browser-based adapters were used
217
+ const browserPlatforms = ['tiktok', 'instagram', 'xiaohongshu'];
218
+ const hasBrowserAdapters = browserPlatforms.some(p => this.adapters.has(p));
219
+ if (hasBrowserAdapters) {
220
+ try {
221
+ const pool = getBrowserPool();
222
+ await pool.cleanup();
223
+ }
224
+ catch (error) {
225
+ console.warn('Browser pool cleanup error:', error);
226
+ }
227
+ }
228
+ this.adapters.clear();
229
+ this.isInitialized = false;
230
+ console.log('Unified Social Media Service cleaned up');
231
+ }
232
+ getAdapter(platform) {
233
+ if (!this.isInitialized) {
234
+ throw new SocialMediaError('Service not initialized', 'NOT_INITIALIZED', 'tiktok');
235
+ }
236
+ const adapter = this.adapters.get(platform);
237
+ if (!adapter) {
238
+ throw new SocialMediaError(`Platform ${platform} not available`, 'PLATFORM_NOT_AVAILABLE', platform);
239
+ }
240
+ return adapter;
241
+ }
242
+ calculateRelevance(content, query) {
243
+ if (!content || !query)
244
+ return 0;
245
+ const contentLower = content.toLowerCase();
246
+ const queryLower = query.toLowerCase();
247
+ const queryWords = queryLower.split(/\s+/);
248
+ let score = 0;
249
+ if (contentLower.includes(queryLower)) {
250
+ score += 10;
251
+ }
252
+ for (const word of queryWords) {
253
+ if (contentLower.includes(word)) {
254
+ score += 2;
255
+ }
256
+ }
257
+ return score;
258
+ }
259
+ }