@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,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
+ }