@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.
- package/AGENTS.md +167 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/agent-proxy.d.ts +24 -0
- package/dist/agent-proxy.js +140 -0
- package/dist/agent-tools.d.ts +736 -0
- package/dist/agent-tools.js +409 -0
- package/dist/context/api.d.ts +5 -0
- package/dist/context/api.js +164 -0
- package/dist/context/cli.d.ts +19 -0
- package/dist/context/cli.js +108 -0
- package/dist/context/extractor.d.ts +12 -0
- package/dist/context/extractor.js +43 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +11 -0
- package/dist/context/matcher.d.ts +39 -0
- package/dist/context/matcher.js +246 -0
- package/dist/context/parser.d.ts +28 -0
- package/dist/context/parser.js +157 -0
- package/dist/context/pipeline.d.ts +26 -0
- package/dist/context/pipeline.js +56 -0
- package/dist/context/prompts.d.ts +6 -0
- package/dist/context/prompts.js +60 -0
- package/dist/context/providers.d.ts +6 -0
- package/dist/context/providers.js +106 -0
- package/dist/context/redactor.d.ts +10 -0
- package/dist/context/redactor.js +68 -0
- package/dist/context/server.d.ts +5 -0
- package/dist/context/server.js +134 -0
- package/dist/context/store.d.ts +12 -0
- package/dist/context/store.js +82 -0
- package/dist/context/types.d.ts +79 -0
- package/dist/context/types.js +4 -0
- package/dist/context/user-state.d.ts +40 -0
- package/dist/context/user-state.js +144 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +385 -0
- package/dist/insights/browser/BrowserPool.d.ts +87 -0
- package/dist/insights/browser/BrowserPool.js +266 -0
- package/dist/insights/browser/RequestInterceptor.d.ts +46 -0
- package/dist/insights/browser/RequestInterceptor.js +115 -0
- package/dist/insights/cli.d.ts +8 -0
- package/dist/insights/cli.js +206 -0
- package/dist/insights/core/base/BaseAdapter.d.ts +37 -0
- package/dist/insights/core/base/BaseAdapter.js +123 -0
- package/dist/insights/core/health/HealthMonitor.d.ts +75 -0
- package/dist/insights/core/health/HealthMonitor.js +171 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.d.ts +125 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.js +42 -0
- package/dist/insights/core/utils/DataNormalizer.d.ts +53 -0
- package/dist/insights/core/utils/DataNormalizer.js +349 -0
- package/dist/insights/core/utils/InstagramUrlUtils.d.ts +11 -0
- package/dist/insights/core/utils/InstagramUrlUtils.js +60 -0
- package/dist/insights/core/utils/TikTokUrlUtils.d.ts +10 -0
- package/dist/insights/core/utils/TikTokUrlUtils.js +57 -0
- package/dist/insights/handlers.d.ts +157 -0
- package/dist/insights/handlers.js +246 -0
- package/dist/insights/index.d.ts +437 -0
- package/dist/insights/index.js +426 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.d.ts +34 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.js +342 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.d.ts +31 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.js +227 -0
- package/dist/insights/platforms/reddit/RedditAdapter.d.ts +21 -0
- package/dist/insights/platforms/reddit/RedditAdapter.js +212 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.d.ts +34 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.js +269 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.d.ts +23 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.js +211 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.d.ts +35 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.js +258 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.d.ts +22 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.js +254 -0
- package/dist/insights/service-config.d.ts +7 -0
- package/dist/insights/service-config.js +60 -0
- package/dist/insights/services/UnifiedSocialMediaService.d.ts +94 -0
- package/dist/insights/services/UnifiedSocialMediaService.js +259 -0
- package/dist/insights/vision/VisionExtractor.d.ts +46 -0
- package/dist/insights/vision/VisionExtractor.js +236 -0
- package/dist/learnings.d.ts +50 -0
- package/dist/learnings.js +130 -0
- package/dist/openapi.d.ts +29 -0
- package/dist/openapi.js +169 -0
- package/dist/server-factory.d.ts +20 -0
- package/dist/server-factory.js +41 -0
- package/dist/suggestions.d.ts +16 -0
- package/dist/suggestions.js +72 -0
- package/dist/telemetry.d.ts +44 -0
- package/dist/telemetry.js +93 -0
- package/dist/tools/registry.d.ts +65 -0
- package/dist/tools/registry.js +256 -0
- package/dist/tools.d.ts +2433 -0
- package/dist/tools.js +2294 -0
- package/dist/transport/http.d.ts +15 -0
- package/dist/transport/http.js +154 -0
- package/package.json +76 -0
- package/skills/catalog.json +272 -0
- package/skills/community-catalog.json +4202 -0
- package/skills/competitive-analysis/SKILL.md +174 -0
- package/skills/content-creator/SKILL.md +256 -0
- package/skills/content-strategy/SKILL.md +222 -0
- package/skills/data-storytelling/SKILL.md +248 -0
- package/skills/heuristic-evaluation/SKILL.md +201 -0
- package/skills/market-research-reports/SKILL.md +184 -0
- package/skills/user-stories/SKILL.md +178 -0
- package/skills/ux-researcher/SKILL.md +239 -0
- package/web-dist/assets/index-B1b25lNd.css +1 -0
- package/web-dist/assets/index-CDWHwHbl.js +64 -0
- package/web-dist/index.html +16 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data normalization utilities for converting platform-specific data
|
|
3
|
+
* into standardized formats
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Normalize user data from different platforms
|
|
7
|
+
*/
|
|
8
|
+
export class DataNormalizer {
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a post from any platform to standard format
|
|
11
|
+
*/
|
|
12
|
+
static normalizePost(rawData, platform, baseUrl = '') {
|
|
13
|
+
switch (platform) {
|
|
14
|
+
case 'tiktok':
|
|
15
|
+
return this.normalizeTikTokPost(rawData);
|
|
16
|
+
case 'twitter':
|
|
17
|
+
return this.normalizeTwitterPost(rawData);
|
|
18
|
+
case 'reddit':
|
|
19
|
+
return this.normalizeRedditPost(rawData);
|
|
20
|
+
case 'instagram':
|
|
21
|
+
return this.normalizeInstagramPost(rawData);
|
|
22
|
+
case 'youtube':
|
|
23
|
+
return this.normalizeYouTubePost(rawData);
|
|
24
|
+
default:
|
|
25
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalize user data from any platform
|
|
30
|
+
*/
|
|
31
|
+
static normalizeUser(rawData, platform) {
|
|
32
|
+
switch (platform) {
|
|
33
|
+
case 'tiktok':
|
|
34
|
+
return this.normalizeTikTokUser(rawData);
|
|
35
|
+
case 'twitter':
|
|
36
|
+
return this.normalizeTwitterUser(rawData);
|
|
37
|
+
case 'reddit':
|
|
38
|
+
return this.normalizeRedditUser(rawData);
|
|
39
|
+
case 'instagram':
|
|
40
|
+
return this.normalizeInstagramUser(rawData);
|
|
41
|
+
case 'youtube':
|
|
42
|
+
return this.normalizeYouTubeUser(rawData);
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalize comment data from any platform
|
|
49
|
+
*/
|
|
50
|
+
static normalizeComment(rawData, platform) {
|
|
51
|
+
switch (platform) {
|
|
52
|
+
case 'tiktok':
|
|
53
|
+
return this.normalizeTikTokComment(rawData);
|
|
54
|
+
case 'twitter':
|
|
55
|
+
return this.normalizeTwitterComment(rawData);
|
|
56
|
+
case 'reddit':
|
|
57
|
+
return this.normalizeRedditComment(rawData);
|
|
58
|
+
case 'instagram':
|
|
59
|
+
return this.normalizeInstagramComment(rawData);
|
|
60
|
+
case 'youtube':
|
|
61
|
+
return this.normalizeYouTubeComment(rawData);
|
|
62
|
+
default:
|
|
63
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// TikTok normalization methods
|
|
67
|
+
static normalizeTikTokPost(data) {
|
|
68
|
+
return {
|
|
69
|
+
id: data.id || data.aweme_id || '',
|
|
70
|
+
platform: 'tiktok',
|
|
71
|
+
author: this.normalizeTikTokUser(data.author || {}),
|
|
72
|
+
content: data.desc || data.description || '',
|
|
73
|
+
mediaUrl: data.video?.play_addr?.url_list?.[0] || '',
|
|
74
|
+
engagement: {
|
|
75
|
+
likes: data.stats?.diggCount || data.statistics?.digg_count || 0,
|
|
76
|
+
comments: data.stats?.commentCount || data.statistics?.comment_count || 0,
|
|
77
|
+
shares: data.stats?.shareCount || data.statistics?.share_count || 0,
|
|
78
|
+
views: data.stats?.playCount || data.statistics?.play_count || 0
|
|
79
|
+
},
|
|
80
|
+
timestamp: new Date((data.createTime || data.create_time || Date.now() / 1000) * 1000),
|
|
81
|
+
url: data.webVideoUrl || `https://www.tiktok.com/@${data.author?.uniqueId}/video/${data.id}`,
|
|
82
|
+
hashtags: this.extractHashtags(data.desc || data.description || '')
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
static normalizeTikTokUser(data) {
|
|
86
|
+
return {
|
|
87
|
+
id: data.id || data.uid || '',
|
|
88
|
+
username: data.uniqueId || data.unique_id || '',
|
|
89
|
+
displayName: data.nickname || data.nick_name || '',
|
|
90
|
+
followerCount: data.followerCount || data.follower_count || 0,
|
|
91
|
+
verified: data.verified || false,
|
|
92
|
+
profileImageUrl: data.avatarLarger || data.avatar_larger || '',
|
|
93
|
+
bio: data.signature || ''
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
static normalizeTikTokComment(data) {
|
|
97
|
+
return {
|
|
98
|
+
id: data.cid || data.comment_id || '',
|
|
99
|
+
author: this.normalizeTikTokUser(data.user || {}),
|
|
100
|
+
text: data.text || '',
|
|
101
|
+
timestamp: new Date((data.create_time || Date.now() / 1000) * 1000),
|
|
102
|
+
likes: data.digg_count || 0,
|
|
103
|
+
replies: data.reply_comment?.map((reply) => this.normalizeTikTokComment(reply)) || []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Twitter normalization methods (agent-twitter-client Tweet shape)
|
|
107
|
+
static normalizeTwitterPost(data) {
|
|
108
|
+
return {
|
|
109
|
+
id: data.id || '',
|
|
110
|
+
platform: 'twitter',
|
|
111
|
+
author: this.normalizeTwitterUser(data),
|
|
112
|
+
content: data.text || '',
|
|
113
|
+
mediaUrl: data.photos?.[0]?.url || data.videos?.[0]?.preview || '',
|
|
114
|
+
engagement: {
|
|
115
|
+
likes: data.likes || 0,
|
|
116
|
+
comments: data.replies || 0,
|
|
117
|
+
shares: data.retweets || 0,
|
|
118
|
+
views: data.views || 0
|
|
119
|
+
},
|
|
120
|
+
timestamp: data.timeParsed ? new Date(data.timeParsed) :
|
|
121
|
+
data.timestamp ? new Date(data.timestamp * 1000) : new Date(),
|
|
122
|
+
url: data.permanentUrl || `https://twitter.com/${data.username || 'user'}/status/${data.id}`,
|
|
123
|
+
hashtags: data.hashtags || this.extractHashtags(data.text || '')
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
static normalizeTwitterUser(data) {
|
|
127
|
+
return {
|
|
128
|
+
id: data.userId || data.id || '',
|
|
129
|
+
username: data.username || '',
|
|
130
|
+
displayName: data.name || '',
|
|
131
|
+
followerCount: 0,
|
|
132
|
+
verified: false,
|
|
133
|
+
profileImageUrl: '',
|
|
134
|
+
bio: ''
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
static normalizeTwitterComment(data) {
|
|
138
|
+
// Twitter replies normalized as comments
|
|
139
|
+
return {
|
|
140
|
+
id: data.id || '',
|
|
141
|
+
author: this.normalizeTwitterUser(data),
|
|
142
|
+
text: data.text || '',
|
|
143
|
+
timestamp: data.timeParsed ? new Date(data.timeParsed) :
|
|
144
|
+
data.timestamp ? new Date(data.timestamp * 1000) : new Date(),
|
|
145
|
+
likes: data.likes || 0,
|
|
146
|
+
replies: []
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Reddit normalization methods
|
|
150
|
+
static normalizeRedditPost(data) {
|
|
151
|
+
return {
|
|
152
|
+
id: data.id || '',
|
|
153
|
+
platform: 'reddit',
|
|
154
|
+
author: this.normalizeRedditUser(data.author_display_name || data.author || ''),
|
|
155
|
+
content: data.selftext || data.title || '',
|
|
156
|
+
mediaUrl: data.url_overridden_by_dest || data.url || '',
|
|
157
|
+
engagement: {
|
|
158
|
+
likes: data.score || data.ups || 0,
|
|
159
|
+
comments: data.num_comments || data.comment_count || 0,
|
|
160
|
+
shares: 0, // Reddit doesn't have shares
|
|
161
|
+
views: 0 // Reddit doesn't track views publicly
|
|
162
|
+
},
|
|
163
|
+
timestamp: new Date((data.created_utc || data.created || Date.now() / 1000) * 1000),
|
|
164
|
+
url: `https://reddit.com${data.permalink}`,
|
|
165
|
+
hashtags: []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
static normalizeRedditUser(data) {
|
|
169
|
+
if (typeof data === 'string') {
|
|
170
|
+
return {
|
|
171
|
+
id: data,
|
|
172
|
+
username: data,
|
|
173
|
+
displayName: data
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
id: data.id || data.name || '',
|
|
178
|
+
username: data.name || data.display_name || '',
|
|
179
|
+
displayName: data.display_name || data.name || '',
|
|
180
|
+
followerCount: 0, // Reddit doesn't expose follower counts
|
|
181
|
+
verified: data.is_gold || false,
|
|
182
|
+
profileImageUrl: data.icon_img || '',
|
|
183
|
+
bio: data.public_description || ''
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
static normalizeRedditComment(data) {
|
|
187
|
+
// Reddit nests replies as a Listing: replies.data.children[]
|
|
188
|
+
// replies can also be an empty string when there are no replies
|
|
189
|
+
const replyChildren = data.replies?.data?.children || [];
|
|
190
|
+
const replies = replyChildren
|
|
191
|
+
.filter((child) => child.kind === 't1' && child.data)
|
|
192
|
+
.map((child) => this.normalizeRedditComment(child.data));
|
|
193
|
+
return {
|
|
194
|
+
id: data.id || '',
|
|
195
|
+
author: this.normalizeRedditUser(data.author_display_name || data.author || ''),
|
|
196
|
+
text: data.body || '',
|
|
197
|
+
timestamp: new Date((data.created_utc || Date.now() / 1000) * 1000),
|
|
198
|
+
likes: data.score || data.ups || 0,
|
|
199
|
+
replies
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Instagram normalization methods
|
|
203
|
+
static normalizeInstagramPost(data) {
|
|
204
|
+
return {
|
|
205
|
+
id: data.id || data.pk || '',
|
|
206
|
+
platform: 'instagram',
|
|
207
|
+
author: this.normalizeInstagramUser(data.user || data.owner || {}),
|
|
208
|
+
content: data.caption?.text || data.edge_media_to_caption?.edges?.[0]?.node?.text || '',
|
|
209
|
+
mediaUrl: data.image_versions2?.candidates?.[0]?.url || data.display_url || '',
|
|
210
|
+
engagement: {
|
|
211
|
+
likes: data.like_count || data.edge_media_preview_like?.count || 0,
|
|
212
|
+
comments: data.comment_count || data.edge_media_to_comment?.count || 0,
|
|
213
|
+
shares: 0, // Instagram doesn't expose shares
|
|
214
|
+
views: data.view_count || data.video_view_count || 0
|
|
215
|
+
},
|
|
216
|
+
timestamp: new Date((data.taken_at || data.taken_at_timestamp || Date.now() / 1000) * 1000),
|
|
217
|
+
url: `https://www.instagram.com/p/${data.code || data.shortcode}/`,
|
|
218
|
+
hashtags: this.extractHashtags(data.caption?.text || '')
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
static normalizeInstagramUser(data) {
|
|
222
|
+
return {
|
|
223
|
+
id: data.pk || data.id || '',
|
|
224
|
+
username: data.username || '',
|
|
225
|
+
displayName: data.full_name || data.username || '',
|
|
226
|
+
followerCount: data.follower_count || data.edge_followed_by?.count || 0,
|
|
227
|
+
verified: data.is_verified || false,
|
|
228
|
+
profileImageUrl: data.profile_pic_url || data.profile_pic_url_hd || '',
|
|
229
|
+
bio: data.biography || data.bio || ''
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
static normalizeInstagramComment(data) {
|
|
233
|
+
return {
|
|
234
|
+
id: data.pk || data.id || '',
|
|
235
|
+
author: this.normalizeInstagramUser(data.user || {}),
|
|
236
|
+
text: data.text || '',
|
|
237
|
+
timestamp: new Date((data.created_at || data.created_at_utc || Date.now() / 1000) * 1000),
|
|
238
|
+
likes: data.comment_like_count || 0,
|
|
239
|
+
replies: data.child_comment_count ? [] : [] // Instagram API complex for replies
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// YouTube normalization methods
|
|
243
|
+
static normalizeYouTubePost(data) {
|
|
244
|
+
const snippet = data.snippet || {};
|
|
245
|
+
const stats = data.statistics || {};
|
|
246
|
+
const videoId = data.id?.videoId || data.id || '';
|
|
247
|
+
return {
|
|
248
|
+
id: videoId,
|
|
249
|
+
platform: 'youtube',
|
|
250
|
+
author: this.normalizeYouTubeUser(snippet),
|
|
251
|
+
content: snippet.title || '',
|
|
252
|
+
mediaUrl: snippet.thumbnails?.high?.url || snippet.thumbnails?.default?.url || '',
|
|
253
|
+
engagement: {
|
|
254
|
+
likes: parseInt(stats.likeCount || '0', 10),
|
|
255
|
+
comments: parseInt(stats.commentCount || '0', 10),
|
|
256
|
+
shares: 0,
|
|
257
|
+
views: parseInt(stats.viewCount || '0', 10)
|
|
258
|
+
},
|
|
259
|
+
timestamp: new Date(snippet.publishedAt || Date.now()),
|
|
260
|
+
url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
261
|
+
hashtags: this.extractHashtags(snippet.title || '')
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
static normalizeYouTubeUser(data) {
|
|
265
|
+
return {
|
|
266
|
+
id: data.channelId || '',
|
|
267
|
+
username: data.channelTitle || '',
|
|
268
|
+
displayName: data.channelTitle || '',
|
|
269
|
+
followerCount: 0,
|
|
270
|
+
verified: false,
|
|
271
|
+
profileImageUrl: '',
|
|
272
|
+
bio: ''
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
static normalizeYouTubeComment(data) {
|
|
276
|
+
const topLevel = data.snippet?.topLevelComment?.snippet || {};
|
|
277
|
+
const replies = (data.replies?.comments || []).map((reply) => {
|
|
278
|
+
const rs = reply.snippet || {};
|
|
279
|
+
return {
|
|
280
|
+
id: reply.id || '',
|
|
281
|
+
author: {
|
|
282
|
+
id: rs.authorChannelId?.value || '',
|
|
283
|
+
username: rs.authorDisplayName || '',
|
|
284
|
+
displayName: rs.authorDisplayName || ''
|
|
285
|
+
},
|
|
286
|
+
text: rs.textDisplay || rs.textOriginal || '',
|
|
287
|
+
timestamp: new Date(rs.publishedAt || Date.now()),
|
|
288
|
+
likes: rs.likeCount || 0,
|
|
289
|
+
replies: []
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
id: data.id || '',
|
|
294
|
+
author: {
|
|
295
|
+
id: topLevel.authorChannelId?.value || '',
|
|
296
|
+
username: topLevel.authorDisplayName || '',
|
|
297
|
+
displayName: topLevel.authorDisplayName || ''
|
|
298
|
+
},
|
|
299
|
+
text: topLevel.textDisplay || topLevel.textOriginal || '',
|
|
300
|
+
timestamp: new Date(topLevel.publishedAt || Date.now()),
|
|
301
|
+
likes: topLevel.likeCount || 0,
|
|
302
|
+
replies
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Extract hashtags from text
|
|
307
|
+
*/
|
|
308
|
+
static extractHashtags(text) {
|
|
309
|
+
const hashtagRegex = /#[\w\u00c0-\u024f\u1e00-\u1eff]+/gi;
|
|
310
|
+
const matches = text.match(hashtagRegex);
|
|
311
|
+
return matches ? matches.map(tag => tag.toLowerCase()) : [];
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Calculate engagement rate
|
|
315
|
+
*/
|
|
316
|
+
static calculateEngagementRate(likes, comments, shares = 0, followerCount) {
|
|
317
|
+
if (followerCount === 0)
|
|
318
|
+
return 0;
|
|
319
|
+
return ((likes + comments + shares) / followerCount) * 100;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Sanitize and validate text content
|
|
323
|
+
*/
|
|
324
|
+
static sanitizeText(text) {
|
|
325
|
+
if (!text)
|
|
326
|
+
return '';
|
|
327
|
+
return text
|
|
328
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control characters
|
|
329
|
+
.trim()
|
|
330
|
+
.substring(0, 2000); // Limit length
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Normalize timestamp from various formats
|
|
334
|
+
*/
|
|
335
|
+
static normalizeTimestamp(timestamp) {
|
|
336
|
+
if (!timestamp)
|
|
337
|
+
return new Date();
|
|
338
|
+
if (timestamp instanceof Date)
|
|
339
|
+
return timestamp;
|
|
340
|
+
if (typeof timestamp === 'string') {
|
|
341
|
+
return new Date(timestamp);
|
|
342
|
+
}
|
|
343
|
+
if (typeof timestamp === 'number') {
|
|
344
|
+
// Handle both seconds and milliseconds
|
|
345
|
+
return new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp);
|
|
346
|
+
}
|
|
347
|
+
return new Date();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Instagram URL utilities for detection, redirect resolution, and shortcode extraction.
|
|
3
|
+
*/
|
|
4
|
+
export declare class InstagramUrlUtils {
|
|
5
|
+
private static readonly INSTAGRAM_HOST_PATTERN;
|
|
6
|
+
static isInstagramUrl(input: string): boolean;
|
|
7
|
+
static isReelUrl(input: string): boolean;
|
|
8
|
+
static resolveUrl(input: string): Promise<string>;
|
|
9
|
+
static extractShortcode(input: string): string | null;
|
|
10
|
+
private static tryParseUrl;
|
|
11
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
/**
|
|
3
|
+
* Shared Instagram URL utilities for detection, redirect resolution, and shortcode extraction.
|
|
4
|
+
*/
|
|
5
|
+
export class InstagramUrlUtils {
|
|
6
|
+
static INSTAGRAM_HOST_PATTERN = /(^|\.)instagram\.com$/i;
|
|
7
|
+
static isInstagramUrl(input) {
|
|
8
|
+
const parsed = this.tryParseUrl(input);
|
|
9
|
+
if (!parsed)
|
|
10
|
+
return false;
|
|
11
|
+
return this.INSTAGRAM_HOST_PATTERN.test(parsed.hostname);
|
|
12
|
+
}
|
|
13
|
+
static isReelUrl(input) {
|
|
14
|
+
const parsed = this.tryParseUrl(input);
|
|
15
|
+
if (!parsed)
|
|
16
|
+
return false;
|
|
17
|
+
return /\/(reel|reels)\//i.test(parsed.pathname);
|
|
18
|
+
}
|
|
19
|
+
static async resolveUrl(input) {
|
|
20
|
+
const parsed = this.tryParseUrl(input);
|
|
21
|
+
if (!parsed) {
|
|
22
|
+
return input;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const response = await axios.get(parsed.toString(), {
|
|
26
|
+
maxRedirects: 5,
|
|
27
|
+
timeout: 10000,
|
|
28
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
29
|
+
});
|
|
30
|
+
const finalUrl = response.request?.res?.responseUrl;
|
|
31
|
+
return typeof finalUrl === 'string' && finalUrl.length > 0 ? finalUrl : parsed.toString();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return parsed.toString();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
static extractShortcode(input) {
|
|
38
|
+
const parsed = this.tryParseUrl(input);
|
|
39
|
+
if (!parsed) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
// Match /reel/{shortcode}/ or /p/{shortcode}/ or /reels/{shortcode}/
|
|
43
|
+
const match = parsed.pathname.match(/\/(reel|reels|p)\/([A-Za-z0-9_-]+)/);
|
|
44
|
+
if (match?.[2]) {
|
|
45
|
+
return match[2];
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
static tryParseUrl(input) {
|
|
50
|
+
if (!input || input.trim().length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return new URL(input.trim());
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TikTok URL utilities for detection, redirect resolution, and video ID extraction.
|
|
3
|
+
*/
|
|
4
|
+
export declare class TikTokUrlUtils {
|
|
5
|
+
private static readonly TIKTOK_HOST_PATTERN;
|
|
6
|
+
static isTikTokUrl(input: string): boolean;
|
|
7
|
+
static resolveUrl(input: string): Promise<string>;
|
|
8
|
+
static extractVideoId(input: string): string | null;
|
|
9
|
+
private static tryParseUrl;
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
/**
|
|
3
|
+
* Shared TikTok URL utilities for detection, redirect resolution, and video ID extraction.
|
|
4
|
+
*/
|
|
5
|
+
export class TikTokUrlUtils {
|
|
6
|
+
static TIKTOK_HOST_PATTERN = /(^|\.)tiktok\.com$/i;
|
|
7
|
+
static isTikTokUrl(input) {
|
|
8
|
+
const parsed = this.tryParseUrl(input);
|
|
9
|
+
if (!parsed)
|
|
10
|
+
return false;
|
|
11
|
+
return this.TIKTOK_HOST_PATTERN.test(parsed.hostname);
|
|
12
|
+
}
|
|
13
|
+
static async resolveUrl(input) {
|
|
14
|
+
const parsed = this.tryParseUrl(input);
|
|
15
|
+
if (!parsed) {
|
|
16
|
+
return input;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const response = await axios.get(parsed.toString(), {
|
|
20
|
+
maxRedirects: 5,
|
|
21
|
+
timeout: 10000,
|
|
22
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
23
|
+
});
|
|
24
|
+
const finalUrl = response.request?.res?.responseUrl;
|
|
25
|
+
return typeof finalUrl === 'string' && finalUrl.length > 0 ? finalUrl : parsed.toString();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return parsed.toString();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
static extractVideoId(input) {
|
|
32
|
+
const parsed = this.tryParseUrl(input);
|
|
33
|
+
if (!parsed) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const pathMatch = parsed.pathname.match(/\/video\/(\d+)/);
|
|
37
|
+
if (pathMatch?.[1]) {
|
|
38
|
+
return pathMatch[1];
|
|
39
|
+
}
|
|
40
|
+
const itemId = parsed.searchParams.get('item_id') || parsed.searchParams.get('aweme_id');
|
|
41
|
+
if (itemId && /^\d+$/.test(itemId)) {
|
|
42
|
+
return itemId;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
static tryParseUrl(input) {
|
|
47
|
+
if (!input || input.trim().length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return new URL(input.trim());
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrowdListen Shared Handlers
|
|
3
|
+
* Pure functions that return plain objects — used by CLI and MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Retrieval handlers (free, local): search, comments, trending, user content, vision
|
|
6
|
+
* Analysis handlers (paid, API): analyze, cluster, enrich, deep_analyze, insights, research
|
|
7
|
+
*/
|
|
8
|
+
import { UnifiedSocialMediaService } from './services/UnifiedSocialMediaService.js';
|
|
9
|
+
import { HealthMonitor } from './core/health/HealthMonitor.js';
|
|
10
|
+
export interface SearchArgs {
|
|
11
|
+
platform: string;
|
|
12
|
+
query: string;
|
|
13
|
+
limit?: number;
|
|
14
|
+
useVision?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface CommentsArgs {
|
|
17
|
+
platform: string;
|
|
18
|
+
contentId: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
useVision?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface AnalyzeArgs {
|
|
23
|
+
platform: string;
|
|
24
|
+
contentId: string;
|
|
25
|
+
analysisDepth?: 'surface' | 'standard' | 'deep' | 'comprehensive';
|
|
26
|
+
}
|
|
27
|
+
export interface ClusterArgs {
|
|
28
|
+
platform: string;
|
|
29
|
+
contentId: string;
|
|
30
|
+
clusterCount?: number;
|
|
31
|
+
includeExamples?: boolean;
|
|
32
|
+
weightByEngagement?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface EnrichArgs {
|
|
35
|
+
platform: string;
|
|
36
|
+
contentId: string;
|
|
37
|
+
question?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface TrendingArgs {
|
|
40
|
+
platform: string;
|
|
41
|
+
limit?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface UserContentArgs {
|
|
44
|
+
platform: string;
|
|
45
|
+
userId: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ExtractUrlArgs {
|
|
49
|
+
url: string;
|
|
50
|
+
mode?: 'posts' | 'comments' | 'raw';
|
|
51
|
+
limit?: number;
|
|
52
|
+
}
|
|
53
|
+
export declare function extractWithVision(args: ExtractUrlArgs): Promise<{
|
|
54
|
+
raw?: string | undefined;
|
|
55
|
+
count?: number | undefined;
|
|
56
|
+
comments?: import("./core/interfaces/SocialMediaPlatform.js").Comment[] | undefined;
|
|
57
|
+
posts?: import("./core/interfaces/SocialMediaPlatform.js").Post[] | undefined;
|
|
58
|
+
url: string;
|
|
59
|
+
mode: "posts" | "comments" | "raw";
|
|
60
|
+
provider: string;
|
|
61
|
+
extractionMethod: string;
|
|
62
|
+
}>;
|
|
63
|
+
export declare function getTrendingContent(service: UnifiedSocialMediaService, args: TrendingArgs): Promise<{
|
|
64
|
+
platform: string;
|
|
65
|
+
count: number;
|
|
66
|
+
posts: import("./core/interfaces/SocialMediaPlatform.js").Post[];
|
|
67
|
+
}>;
|
|
68
|
+
export declare function getUserContent(service: UnifiedSocialMediaService, args: UserContentArgs): Promise<{
|
|
69
|
+
platform: string;
|
|
70
|
+
userId: string;
|
|
71
|
+
count: number;
|
|
72
|
+
posts: import("./core/interfaces/SocialMediaPlatform.js").Post[];
|
|
73
|
+
}>;
|
|
74
|
+
export declare function searchContent(service: UnifiedSocialMediaService, args: SearchArgs): Promise<{
|
|
75
|
+
raw?: string | undefined;
|
|
76
|
+
count?: number | undefined;
|
|
77
|
+
comments?: import("./core/interfaces/SocialMediaPlatform.js").Comment[] | undefined;
|
|
78
|
+
posts?: import("./core/interfaces/SocialMediaPlatform.js").Post[] | undefined;
|
|
79
|
+
url: string;
|
|
80
|
+
mode: "posts" | "comments" | "raw";
|
|
81
|
+
provider: string;
|
|
82
|
+
extractionMethod: string;
|
|
83
|
+
} | {
|
|
84
|
+
platform: string;
|
|
85
|
+
query: string;
|
|
86
|
+
count: number;
|
|
87
|
+
posts: import("./core/interfaces/SocialMediaPlatform.js").Post[];
|
|
88
|
+
}>;
|
|
89
|
+
export declare function getContentComments(service: UnifiedSocialMediaService, args: CommentsArgs): Promise<{
|
|
90
|
+
raw?: string | undefined;
|
|
91
|
+
count?: number | undefined;
|
|
92
|
+
comments?: import("./core/interfaces/SocialMediaPlatform.js").Comment[] | undefined;
|
|
93
|
+
posts?: import("./core/interfaces/SocialMediaPlatform.js").Post[] | undefined;
|
|
94
|
+
url: string;
|
|
95
|
+
mode: "posts" | "comments" | "raw";
|
|
96
|
+
provider: string;
|
|
97
|
+
extractionMethod: string;
|
|
98
|
+
} | {
|
|
99
|
+
platform: string;
|
|
100
|
+
contentId: string;
|
|
101
|
+
count: number;
|
|
102
|
+
comments: import("./core/interfaces/SocialMediaPlatform.js").Comment[];
|
|
103
|
+
}>;
|
|
104
|
+
export declare function getPlatformStatus(service: UnifiedSocialMediaService): {
|
|
105
|
+
availablePlatforms: {
|
|
106
|
+
tiktok?: any;
|
|
107
|
+
twitter?: any;
|
|
108
|
+
reddit?: any;
|
|
109
|
+
instagram?: any;
|
|
110
|
+
youtube?: any;
|
|
111
|
+
moltbook?: any;
|
|
112
|
+
xiaohongshu?: any;
|
|
113
|
+
};
|
|
114
|
+
totalPlatforms: number;
|
|
115
|
+
};
|
|
116
|
+
export declare function healthCheck(service: UnifiedSocialMediaService, monitor?: HealthMonitor): Promise<{
|
|
117
|
+
overall: import("./index.js").HealthStatus;
|
|
118
|
+
healthStatus: Record<string, unknown>;
|
|
119
|
+
source: string;
|
|
120
|
+
lastFullCheck: string | null;
|
|
121
|
+
timestamp: string;
|
|
122
|
+
} | {
|
|
123
|
+
healthStatus: {
|
|
124
|
+
tiktok?: "healthy" | "degraded" | "down" | undefined;
|
|
125
|
+
twitter?: "healthy" | "degraded" | "down" | undefined;
|
|
126
|
+
reddit?: "healthy" | "degraded" | "down" | undefined;
|
|
127
|
+
instagram?: "healthy" | "degraded" | "down" | undefined;
|
|
128
|
+
youtube?: "healthy" | "degraded" | "down" | undefined;
|
|
129
|
+
moltbook?: "healthy" | "degraded" | "down" | undefined;
|
|
130
|
+
xiaohongshu?: "healthy" | "degraded" | "down" | undefined;
|
|
131
|
+
};
|
|
132
|
+
timestamp: string;
|
|
133
|
+
overall?: undefined;
|
|
134
|
+
source?: undefined;
|
|
135
|
+
lastFullCheck?: undefined;
|
|
136
|
+
}>;
|
|
137
|
+
export declare function analyzeContent(service: UnifiedSocialMediaService, args: AnalyzeArgs): Promise<any>;
|
|
138
|
+
export declare function clusterOpinions(service: UnifiedSocialMediaService, args: ClusterArgs): Promise<any>;
|
|
139
|
+
export declare function enrichContent(service: UnifiedSocialMediaService, args: EnrichArgs): Promise<any>;
|
|
140
|
+
export interface DeepAnalyzeArgs {
|
|
141
|
+
platform: string;
|
|
142
|
+
contentId: string;
|
|
143
|
+
analysisDepth: 'deep' | 'comprehensive';
|
|
144
|
+
}
|
|
145
|
+
export interface InsightsArgs {
|
|
146
|
+
platform: string;
|
|
147
|
+
contentId: string;
|
|
148
|
+
categories?: string[];
|
|
149
|
+
}
|
|
150
|
+
export interface ResearchArgs {
|
|
151
|
+
query: string;
|
|
152
|
+
platforms?: string[];
|
|
153
|
+
depth?: 'quick' | 'standard' | 'deep';
|
|
154
|
+
}
|
|
155
|
+
export declare function deepAnalyze(args: DeepAnalyzeArgs): Promise<any>;
|
|
156
|
+
export declare function extractInsights(args: InsightsArgs): Promise<any>;
|
|
157
|
+
export declare function researchSynthesis(args: ResearchArgs): Promise<any>;
|