@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,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instagram Platform Adapter — flat browser adapter using API interception.
|
|
3
|
+
*
|
|
4
|
+
* Uses BrowserPool + RequestInterceptor to capture Instagram's internal API
|
|
5
|
+
* responses (GraphQL + v1 API). No tiers, no fallback chains.
|
|
6
|
+
*
|
|
7
|
+
* API targets:
|
|
8
|
+
* - /graphql/query/ — posts, user content
|
|
9
|
+
* - /api/v1/tags/ — hashtag search
|
|
10
|
+
* - /api/v1/feed/ — explore/user feed
|
|
11
|
+
* - /api/v1/media/ — media endpoints
|
|
12
|
+
*/
|
|
13
|
+
import { BaseAdapter } from '../../core/base/BaseAdapter.js';
|
|
14
|
+
import { getBrowserPool } from '../../browser/BrowserPool.js';
|
|
15
|
+
import { RequestInterceptor } from '../../browser/RequestInterceptor.js';
|
|
16
|
+
const API_PATTERNS = [
|
|
17
|
+
'/graphql/query/',
|
|
18
|
+
'/api/v1/tags/',
|
|
19
|
+
'/api/v1/feed/',
|
|
20
|
+
'/api/v1/media/',
|
|
21
|
+
'/api/v1/web/search/',
|
|
22
|
+
'/web/search/topsearch/',
|
|
23
|
+
];
|
|
24
|
+
export class InstagramAdapter extends BaseAdapter {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
super(config);
|
|
27
|
+
// Browser platforms risk IP blocks above ~2/min sustained; 5/min allows interactive bursts
|
|
28
|
+
this.maxRequestsPerWindow = 5;
|
|
29
|
+
}
|
|
30
|
+
async initialize() {
|
|
31
|
+
this.isInitialized = true;
|
|
32
|
+
this.log('Instagram adapter initialized (API interception mode)');
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
async searchContent(query, limit = 10) {
|
|
36
|
+
this.ensureInitialized();
|
|
37
|
+
await this.enforceRateLimit();
|
|
38
|
+
const tag = query.replace(/^#/, '');
|
|
39
|
+
const url = `https://www.instagram.com/explore/tags/${encodeURIComponent(tag)}/`;
|
|
40
|
+
return this.interceptPosts(url, limit);
|
|
41
|
+
}
|
|
42
|
+
async getTrendingContent(limit = 10) {
|
|
43
|
+
this.ensureInitialized();
|
|
44
|
+
await this.enforceRateLimit();
|
|
45
|
+
return this.interceptPosts('https://www.instagram.com/explore/', limit);
|
|
46
|
+
}
|
|
47
|
+
async getUserContent(userId, limit = 10) {
|
|
48
|
+
this.ensureInitialized();
|
|
49
|
+
this.validateUserId(userId);
|
|
50
|
+
await this.enforceRateLimit();
|
|
51
|
+
const username = userId.startsWith('@') ? userId.slice(1) : userId;
|
|
52
|
+
return this.interceptPosts(`https://www.instagram.com/${username}/`, limit);
|
|
53
|
+
}
|
|
54
|
+
async getContentComments(contentId, limit = 20) {
|
|
55
|
+
this.ensureInitialized();
|
|
56
|
+
this.validateContentId(contentId);
|
|
57
|
+
await this.enforceRateLimit();
|
|
58
|
+
const url = contentId.includes('instagram.com/')
|
|
59
|
+
? contentId
|
|
60
|
+
: `https://www.instagram.com/p/${contentId}/`;
|
|
61
|
+
const pool = getBrowserPool();
|
|
62
|
+
const page = await pool.acquire('instagram');
|
|
63
|
+
const interceptor = new RequestInterceptor();
|
|
64
|
+
try {
|
|
65
|
+
await interceptor.setup(page, API_PATTERNS);
|
|
66
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
67
|
+
await this.waitAndScroll(page);
|
|
68
|
+
const apiData = interceptor.getAllData();
|
|
69
|
+
if (apiData.length === 0)
|
|
70
|
+
return [];
|
|
71
|
+
return this.structureComments(apiData).slice(0, limit);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.handleError(error, `getContentComments(${contentId})`);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
interceptor.stop();
|
|
78
|
+
await pool.release(page);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ── Interception pipeline ──────────────────────────────────────────────
|
|
82
|
+
async interceptPosts(url, limit) {
|
|
83
|
+
const pool = getBrowserPool();
|
|
84
|
+
const page = await pool.acquire('instagram');
|
|
85
|
+
const interceptor = new RequestInterceptor();
|
|
86
|
+
try {
|
|
87
|
+
await interceptor.setup(page, API_PATTERNS);
|
|
88
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
89
|
+
await this.waitAndScroll(page);
|
|
90
|
+
const apiData = interceptor.getAllData();
|
|
91
|
+
if (apiData.length === 0)
|
|
92
|
+
return [];
|
|
93
|
+
return this.structurePosts(apiData).slice(0, limit);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.handleError(error, `interceptPosts(${url})`);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
interceptor.stop();
|
|
100
|
+
await pool.release(page);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async waitAndScroll(page) {
|
|
104
|
+
try {
|
|
105
|
+
await page.waitForLoadState('networkidle', { timeout: 15000 });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Non-fatal
|
|
109
|
+
}
|
|
110
|
+
for (let i = 0; i < 4; i++) {
|
|
111
|
+
await page.evaluate(() => window.scrollBy(0, window.innerHeight));
|
|
112
|
+
await page.waitForTimeout(2000);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ── Data normalization (preserved from VisualInstagramAdapter) ─────────
|
|
116
|
+
structurePosts(interceptedData) {
|
|
117
|
+
const posts = [];
|
|
118
|
+
const seenIds = new Set();
|
|
119
|
+
for (const data of interceptedData) {
|
|
120
|
+
try {
|
|
121
|
+
// GraphQL query response shapes
|
|
122
|
+
const edges = this.extractMediaEdges(data);
|
|
123
|
+
for (const edge of edges) {
|
|
124
|
+
const node = edge.node || edge;
|
|
125
|
+
const post = this.normalizeMediaNode(node);
|
|
126
|
+
if (post && !seenIds.has(post.id)) {
|
|
127
|
+
seenIds.add(post.id);
|
|
128
|
+
posts.push(post);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Tag search shape
|
|
132
|
+
const tagMedia = data?.data?.hashtag?.edge_hashtag_to_media?.edges
|
|
133
|
+
|| data?.data?.hashtag?.edge_hashtag_to_top_posts?.edges
|
|
134
|
+
|| [];
|
|
135
|
+
for (const edge of tagMedia) {
|
|
136
|
+
const post = this.normalizeMediaNode(edge.node || edge);
|
|
137
|
+
if (post && !seenIds.has(post.id)) {
|
|
138
|
+
seenIds.add(post.id);
|
|
139
|
+
posts.push(post);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// v1 API shape (items array)
|
|
143
|
+
const items = data?.items || data?.ranked_items || data?.media || [];
|
|
144
|
+
if (Array.isArray(items)) {
|
|
145
|
+
for (const item of items) {
|
|
146
|
+
const post = this.normalizeV1Item(item);
|
|
147
|
+
if (post && !seenIds.has(post.id)) {
|
|
148
|
+
seenIds.add(post.id);
|
|
149
|
+
posts.push(post);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Skip malformed response
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return posts;
|
|
159
|
+
}
|
|
160
|
+
structureComments(interceptedData) {
|
|
161
|
+
const comments = [];
|
|
162
|
+
const seenIds = new Set();
|
|
163
|
+
for (const data of interceptedData) {
|
|
164
|
+
try {
|
|
165
|
+
// GraphQL comments shape
|
|
166
|
+
const commentEdges = data?.data?.shortcode_media?.edge_media_to_parent_comment?.edges
|
|
167
|
+
|| data?.data?.shortcode_media?.edge_media_to_comment?.edges
|
|
168
|
+
|| [];
|
|
169
|
+
for (const edge of commentEdges) {
|
|
170
|
+
const comment = this.normalizeCommentNode(edge.node || edge);
|
|
171
|
+
if (comment && !seenIds.has(comment.id)) {
|
|
172
|
+
seenIds.add(comment.id);
|
|
173
|
+
comments.push(comment);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// v1 API comments shape
|
|
177
|
+
const commentItems = data?.comments || [];
|
|
178
|
+
if (Array.isArray(commentItems)) {
|
|
179
|
+
for (const item of commentItems) {
|
|
180
|
+
const comment = this.normalizeV1Comment(item);
|
|
181
|
+
if (comment && !seenIds.has(comment.id)) {
|
|
182
|
+
seenIds.add(comment.id);
|
|
183
|
+
comments.push(comment);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Skip malformed response
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return comments;
|
|
193
|
+
}
|
|
194
|
+
extractMediaEdges(data) {
|
|
195
|
+
const userEdges = data?.data?.user?.edge_owner_to_timeline_media?.edges;
|
|
196
|
+
if (userEdges)
|
|
197
|
+
return userEdges;
|
|
198
|
+
const exploreEdges = data?.data?.user?.edge_web_feed_timeline?.edges;
|
|
199
|
+
if (exploreEdges)
|
|
200
|
+
return exploreEdges;
|
|
201
|
+
const discoverEdges = data?.data?.web_discover_media?.edges;
|
|
202
|
+
if (discoverEdges)
|
|
203
|
+
return discoverEdges;
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
normalizeMediaNode(node) {
|
|
207
|
+
try {
|
|
208
|
+
const id = node.id || node.pk || '';
|
|
209
|
+
if (!id)
|
|
210
|
+
return null;
|
|
211
|
+
const owner = node.owner || {};
|
|
212
|
+
const caption = node.edge_media_to_caption?.edges?.[0]?.node?.text
|
|
213
|
+
|| node.caption?.text
|
|
214
|
+
|| '';
|
|
215
|
+
return {
|
|
216
|
+
id: String(id),
|
|
217
|
+
platform: 'instagram',
|
|
218
|
+
author: {
|
|
219
|
+
id: owner.id || '',
|
|
220
|
+
username: owner.username || '',
|
|
221
|
+
displayName: owner.full_name || owner.username || '',
|
|
222
|
+
profileImageUrl: owner.profile_pic_url,
|
|
223
|
+
},
|
|
224
|
+
content: caption,
|
|
225
|
+
mediaUrl: node.display_url || node.thumbnail_src || '',
|
|
226
|
+
engagement: {
|
|
227
|
+
likes: node.edge_media_preview_like?.count || node.like_count || 0,
|
|
228
|
+
comments: node.edge_media_to_comment?.count || node.comment_count || 0,
|
|
229
|
+
views: node.video_view_count || 0,
|
|
230
|
+
},
|
|
231
|
+
timestamp: node.taken_at_timestamp
|
|
232
|
+
? new Date(node.taken_at_timestamp * 1000)
|
|
233
|
+
: new Date(),
|
|
234
|
+
url: `https://www.instagram.com/p/${node.shortcode || id}/`,
|
|
235
|
+
hashtags: this.extractHashtags(caption),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
normalizeV1Item(item) {
|
|
243
|
+
try {
|
|
244
|
+
const id = item.pk || item.id || '';
|
|
245
|
+
if (!id)
|
|
246
|
+
return null;
|
|
247
|
+
const user = item.user || {};
|
|
248
|
+
const caption = item.caption?.text || '';
|
|
249
|
+
return {
|
|
250
|
+
id: String(id),
|
|
251
|
+
platform: 'instagram',
|
|
252
|
+
author: {
|
|
253
|
+
id: user.pk || user.id || '',
|
|
254
|
+
username: user.username || '',
|
|
255
|
+
displayName: user.full_name || '',
|
|
256
|
+
profileImageUrl: user.profile_pic_url,
|
|
257
|
+
verified: user.is_verified,
|
|
258
|
+
},
|
|
259
|
+
content: caption,
|
|
260
|
+
mediaUrl: item.image_versions2?.candidates?.[0]?.url || item.thumbnail_url || '',
|
|
261
|
+
engagement: {
|
|
262
|
+
likes: item.like_count || 0,
|
|
263
|
+
comments: item.comment_count || 0,
|
|
264
|
+
views: item.view_count || item.play_count || 0,
|
|
265
|
+
},
|
|
266
|
+
timestamp: item.taken_at ? new Date(item.taken_at * 1000) : new Date(),
|
|
267
|
+
url: `https://www.instagram.com/p/${item.code || id}/`,
|
|
268
|
+
hashtags: this.extractHashtags(caption),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
normalizeCommentNode(node) {
|
|
276
|
+
try {
|
|
277
|
+
const id = node.id || node.pk || '';
|
|
278
|
+
if (!id)
|
|
279
|
+
return null;
|
|
280
|
+
const user = node.owner || {};
|
|
281
|
+
const replies = (node.edge_threaded_comments?.edges || [])
|
|
282
|
+
.map((e) => this.normalizeCommentNode(e.node || e))
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
return {
|
|
285
|
+
id: String(id),
|
|
286
|
+
author: {
|
|
287
|
+
id: user.id || '',
|
|
288
|
+
username: user.username || '',
|
|
289
|
+
displayName: user.username || '',
|
|
290
|
+
profileImageUrl: user.profile_pic_url,
|
|
291
|
+
},
|
|
292
|
+
text: node.text || '',
|
|
293
|
+
timestamp: node.created_at
|
|
294
|
+
? new Date(node.created_at * 1000)
|
|
295
|
+
: new Date(),
|
|
296
|
+
likes: node.edge_liked_by?.count || 0,
|
|
297
|
+
replies: replies.length > 0 ? replies : undefined,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
normalizeV1Comment(item) {
|
|
305
|
+
try {
|
|
306
|
+
const id = item.pk || item.id || '';
|
|
307
|
+
if (!id)
|
|
308
|
+
return null;
|
|
309
|
+
const user = item.user || {};
|
|
310
|
+
return {
|
|
311
|
+
id: String(id),
|
|
312
|
+
author: {
|
|
313
|
+
id: user.pk || user.id || '',
|
|
314
|
+
username: user.username || '',
|
|
315
|
+
displayName: user.full_name || '',
|
|
316
|
+
},
|
|
317
|
+
text: item.text || '',
|
|
318
|
+
timestamp: item.created_at ? new Date(item.created_at * 1000) : new Date(),
|
|
319
|
+
likes: item.comment_like_count || 0,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
extractHashtags(text) {
|
|
327
|
+
return (text.match(/#\w+/g) || []).map(h => h.slice(1));
|
|
328
|
+
}
|
|
329
|
+
// ── Platform identity ──────────────────────────────────────────────────
|
|
330
|
+
getPlatformName() {
|
|
331
|
+
return 'instagram';
|
|
332
|
+
}
|
|
333
|
+
getSupportedFeatures() {
|
|
334
|
+
return {
|
|
335
|
+
supportsTrending: true,
|
|
336
|
+
supportsUserContent: true,
|
|
337
|
+
supportsSearch: true,
|
|
338
|
+
supportsComments: true,
|
|
339
|
+
supportsAnalysis: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moltbook Platform Adapter
|
|
3
|
+
* Uses Moltbook REST API for content retrieval and comment extraction.
|
|
4
|
+
*
|
|
5
|
+
* API Endpoints:
|
|
6
|
+
* - GET /api/v1/search?q=QUERY&type=all&limit=N
|
|
7
|
+
* - GET /api/v1/posts/{ID}/comments?sort=best
|
|
8
|
+
* - GET /api/v1/posts?sort=hot (trending)
|
|
9
|
+
*
|
|
10
|
+
* Auth: Bearer token via MOLTBOOK_API_KEY
|
|
11
|
+
* Rate limit: 60 reads/min
|
|
12
|
+
*/
|
|
13
|
+
import { BaseAdapter } from '../../core/base/BaseAdapter.js';
|
|
14
|
+
import { Post, Comment, PlatformCapabilities, PlatformType, PlatformConfig } from '../../core/interfaces/SocialMediaPlatform.js';
|
|
15
|
+
export declare class MoltbookAdapter extends BaseAdapter {
|
|
16
|
+
private client;
|
|
17
|
+
constructor(config: PlatformConfig);
|
|
18
|
+
initialize(): Promise<boolean>;
|
|
19
|
+
getTrendingContent(limit?: number): Promise<Post[]>;
|
|
20
|
+
getUserContent(userId: string, limit?: number): Promise<Post[]>;
|
|
21
|
+
searchContent(query: string, limit?: number): Promise<Post[]>;
|
|
22
|
+
getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
|
|
23
|
+
getPlatformName(): PlatformType;
|
|
24
|
+
getSupportedFeatures(): PlatformCapabilities;
|
|
25
|
+
protected isRateLimitError(error: any): boolean;
|
|
26
|
+
protected isAuthError(error: any): boolean;
|
|
27
|
+
protected isNotFoundError(error: any): boolean;
|
|
28
|
+
cleanup(): Promise<void>;
|
|
29
|
+
private normalizePost;
|
|
30
|
+
private normalizeComment;
|
|
31
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moltbook Platform Adapter
|
|
3
|
+
* Uses Moltbook REST API for content retrieval and comment extraction.
|
|
4
|
+
*
|
|
5
|
+
* API Endpoints:
|
|
6
|
+
* - GET /api/v1/search?q=QUERY&type=all&limit=N
|
|
7
|
+
* - GET /api/v1/posts/{ID}/comments?sort=best
|
|
8
|
+
* - GET /api/v1/posts?sort=hot (trending)
|
|
9
|
+
*
|
|
10
|
+
* Auth: Bearer token via MOLTBOOK_API_KEY
|
|
11
|
+
* Rate limit: 60 reads/min
|
|
12
|
+
*/
|
|
13
|
+
import { BaseAdapter } from '../../core/base/BaseAdapter.js';
|
|
14
|
+
import { NotFoundError } from '../../core/interfaces/SocialMediaPlatform.js';
|
|
15
|
+
import axios from 'axios';
|
|
16
|
+
export class MoltbookAdapter extends BaseAdapter {
|
|
17
|
+
client = null;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
super(config);
|
|
20
|
+
this.maxRequestsPerWindow = 60;
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
try {
|
|
24
|
+
const apiKey = this.config.credentials?.apiKey || '';
|
|
25
|
+
const headers = {
|
|
26
|
+
'User-Agent': 'crowdlisten-mcp/1.0.0',
|
|
27
|
+
'Accept': 'application/json',
|
|
28
|
+
};
|
|
29
|
+
if (apiKey) {
|
|
30
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
31
|
+
}
|
|
32
|
+
this.client = axios.create({
|
|
33
|
+
baseURL: 'https://moltbook.com/api/v1',
|
|
34
|
+
headers,
|
|
35
|
+
timeout: 10000,
|
|
36
|
+
});
|
|
37
|
+
this.isInitialized = true;
|
|
38
|
+
this.log('Moltbook adapter initialized successfully', 'info');
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
this.log('Failed to initialize Moltbook adapter', 'error');
|
|
43
|
+
this.isInitialized = false;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async getTrendingContent(limit = 10) {
|
|
48
|
+
this.ensureInitialized();
|
|
49
|
+
this.validateLimit(limit);
|
|
50
|
+
try {
|
|
51
|
+
await this.enforceRateLimit();
|
|
52
|
+
const response = await this.client.get('/posts', {
|
|
53
|
+
params: { sort: 'hot', limit },
|
|
54
|
+
});
|
|
55
|
+
const items = response.data?.posts || response.data?.data || response.data?.results || [];
|
|
56
|
+
const posts = [];
|
|
57
|
+
for (const item of (Array.isArray(items) ? items : []).slice(0, limit)) {
|
|
58
|
+
posts.push(this.normalizePost(item));
|
|
59
|
+
}
|
|
60
|
+
this.log(`Retrieved ${posts.length} trending Moltbook posts`, 'info');
|
|
61
|
+
return posts;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.handleError(error, 'getTrendingContent');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async getUserContent(userId, limit = 10) {
|
|
68
|
+
this.ensureInitialized();
|
|
69
|
+
this.validateUserId(userId);
|
|
70
|
+
this.validateLimit(limit);
|
|
71
|
+
try {
|
|
72
|
+
await this.enforceRateLimit();
|
|
73
|
+
const response = await this.client.get(`/users/${userId}/posts`, {
|
|
74
|
+
params: { limit },
|
|
75
|
+
});
|
|
76
|
+
const items = response.data?.posts || response.data?.data || [];
|
|
77
|
+
const posts = [];
|
|
78
|
+
for (const item of (Array.isArray(items) ? items : []).slice(0, limit)) {
|
|
79
|
+
posts.push(this.normalizePost(item));
|
|
80
|
+
}
|
|
81
|
+
this.log(`Retrieved ${posts.length} posts from Moltbook user ${userId}`, 'info');
|
|
82
|
+
return posts;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.response?.status === 404) {
|
|
86
|
+
throw new NotFoundError('moltbook', `User ${userId}`, error);
|
|
87
|
+
}
|
|
88
|
+
this.handleError(error, 'getUserContent');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async searchContent(query, limit = 10) {
|
|
92
|
+
this.ensureInitialized();
|
|
93
|
+
this.validateLimit(limit);
|
|
94
|
+
if (!query || query.trim().length === 0) {
|
|
95
|
+
throw new Error('Search query cannot be empty');
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await this.enforceRateLimit();
|
|
99
|
+
const response = await this.client.get('/search', {
|
|
100
|
+
params: { q: query.trim(), type: 'all', limit },
|
|
101
|
+
});
|
|
102
|
+
const items = response.data?.posts || response.data?.results || response.data?.data || [];
|
|
103
|
+
const posts = [];
|
|
104
|
+
for (const item of (Array.isArray(items) ? items : []).slice(0, limit)) {
|
|
105
|
+
posts.push(this.normalizePost(item));
|
|
106
|
+
}
|
|
107
|
+
this.log(`Found ${posts.length} Moltbook posts for query: ${query}`, 'info');
|
|
108
|
+
return posts;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.handleError(error, 'searchContent');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async getContentComments(contentId, limit = 20) {
|
|
115
|
+
this.ensureInitialized();
|
|
116
|
+
this.validateContentId(contentId);
|
|
117
|
+
this.validateLimit(limit);
|
|
118
|
+
try {
|
|
119
|
+
await this.enforceRateLimit();
|
|
120
|
+
const response = await this.client.get(`/posts/${contentId}/comments`, {
|
|
121
|
+
params: { sort: 'best', limit },
|
|
122
|
+
});
|
|
123
|
+
const items = response.data?.comments || response.data?.data || response.data?.results || [];
|
|
124
|
+
const comments = [];
|
|
125
|
+
const flatten = (list, depth = 0) => {
|
|
126
|
+
for (const item of list) {
|
|
127
|
+
if (comments.length >= limit)
|
|
128
|
+
return;
|
|
129
|
+
comments.push(this.normalizeComment(item));
|
|
130
|
+
const replies = item.replies || item.children || [];
|
|
131
|
+
if (replies.length > 0 && depth < 3) {
|
|
132
|
+
flatten(replies, depth + 1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
flatten(Array.isArray(items) ? items : []);
|
|
137
|
+
this.log(`Retrieved ${comments.length} comments for Moltbook post ${contentId}`);
|
|
138
|
+
return comments.slice(0, limit);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error.response?.status === 404) {
|
|
142
|
+
throw new NotFoundError('moltbook', `Post ${contentId}`, error);
|
|
143
|
+
}
|
|
144
|
+
this.handleError(error, 'getContentComments');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
getPlatformName() {
|
|
148
|
+
return 'moltbook';
|
|
149
|
+
}
|
|
150
|
+
getSupportedFeatures() {
|
|
151
|
+
return {
|
|
152
|
+
supportsTrending: true,
|
|
153
|
+
supportsUserContent: true,
|
|
154
|
+
supportsSearch: true,
|
|
155
|
+
supportsComments: true,
|
|
156
|
+
supportsAnalysis: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
isRateLimitError(error) {
|
|
160
|
+
return error.response?.status === 429 || error.message?.includes('rate limit');
|
|
161
|
+
}
|
|
162
|
+
isAuthError(error) {
|
|
163
|
+
return error.response?.status === 401 || error.response?.status === 403;
|
|
164
|
+
}
|
|
165
|
+
isNotFoundError(error) {
|
|
166
|
+
return error.response?.status === 404;
|
|
167
|
+
}
|
|
168
|
+
async cleanup() {
|
|
169
|
+
try {
|
|
170
|
+
this.client = null;
|
|
171
|
+
await super.cleanup();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
this.log('Error during Moltbook cleanup', 'warn');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// --- Private helpers ---
|
|
178
|
+
normalizePost(raw) {
|
|
179
|
+
const author = raw.author || {};
|
|
180
|
+
const community = raw.community?.name || raw.community_name || '';
|
|
181
|
+
const postId = raw.id || '';
|
|
182
|
+
const user = {
|
|
183
|
+
id: author.id || author.username || '',
|
|
184
|
+
username: author.username || author.name || '',
|
|
185
|
+
displayName: author.display_name || author.username || '',
|
|
186
|
+
followerCount: author.follower_count,
|
|
187
|
+
verified: author.verified || false,
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
id: String(postId),
|
|
191
|
+
platform: 'moltbook',
|
|
192
|
+
author: user,
|
|
193
|
+
content: raw.body || raw.content || raw.title || '',
|
|
194
|
+
mediaUrl: raw.media_url || raw.image_url,
|
|
195
|
+
engagement: {
|
|
196
|
+
likes: raw.score || raw.upvotes || 0,
|
|
197
|
+
comments: raw.comment_count || raw.num_comments || 0,
|
|
198
|
+
shares: raw.share_count || 0,
|
|
199
|
+
views: raw.view_count || 0,
|
|
200
|
+
},
|
|
201
|
+
timestamp: raw.created_at ? new Date(raw.created_at) : new Date(),
|
|
202
|
+
url: raw.url || (community ? `https://moltbook.com/m/${community}/posts/${postId}` : ''),
|
|
203
|
+
hashtags: raw.tags || raw.hashtags || [],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
normalizeComment(raw) {
|
|
207
|
+
const author = raw.author || {};
|
|
208
|
+
const user = {
|
|
209
|
+
id: author.id || author.username || '',
|
|
210
|
+
username: author.username || author.name || 'anonymous',
|
|
211
|
+
displayName: author.display_name || author.username,
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
id: String(raw.id || ''),
|
|
215
|
+
author: user,
|
|
216
|
+
text: raw.body || raw.content || raw.text || '',
|
|
217
|
+
timestamp: raw.created_at ? new Date(raw.created_at) : new Date(),
|
|
218
|
+
likes: raw.score || raw.upvotes || 0,
|
|
219
|
+
replies: raw.replies ? raw.replies.map((r) => this.normalizeComment(r)) : undefined,
|
|
220
|
+
engagement: {
|
|
221
|
+
upvotes: raw.upvotes || raw.score || 0,
|
|
222
|
+
downvotes: raw.downvotes || 0,
|
|
223
|
+
score: raw.score || 0,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Reddit Platform Adapter
|
|
3
|
+
* Uses Reddit's public JSON API for basic functionality
|
|
4
|
+
*/
|
|
5
|
+
import { BaseAdapter } from '../../core/base/BaseAdapter.js';
|
|
6
|
+
import { Post, Comment, PlatformCapabilities, PlatformType, PlatformConfig } from '../../core/interfaces/SocialMediaPlatform.js';
|
|
7
|
+
export declare class RedditAdapter extends BaseAdapter {
|
|
8
|
+
private client;
|
|
9
|
+
constructor(config: PlatformConfig);
|
|
10
|
+
initialize(): Promise<boolean>;
|
|
11
|
+
getTrendingContent(limit?: number): Promise<Post[]>;
|
|
12
|
+
getUserContent(userId: string, limit?: number): Promise<Post[]>;
|
|
13
|
+
searchContent(query: string, limit?: number): Promise<Post[]>;
|
|
14
|
+
getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
|
|
15
|
+
getPlatformName(): PlatformType;
|
|
16
|
+
getSupportedFeatures(): PlatformCapabilities;
|
|
17
|
+
protected isRateLimitError(error: any): boolean;
|
|
18
|
+
protected isAuthError(error: any): boolean;
|
|
19
|
+
protected isNotFoundError(error: any): boolean;
|
|
20
|
+
cleanup(): Promise<void>;
|
|
21
|
+
}
|