@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,212 @@
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 { DataNormalizer } from '../../core/utils/DataNormalizer.js';
7
+ import { NotFoundError } from '../../core/interfaces/SocialMediaPlatform.js';
8
+ import axios from 'axios';
9
+ export class RedditAdapter extends BaseAdapter {
10
+ client = null;
11
+ constructor(config) {
12
+ super(config);
13
+ this.maxRequestsPerWindow = 60;
14
+ }
15
+ async initialize() {
16
+ try {
17
+ this.client = axios.create({
18
+ baseURL: 'https://www.reddit.com',
19
+ headers: {
20
+ 'User-Agent': 'crowdlisten-mcp/1.0.0'
21
+ },
22
+ timeout: 10000
23
+ });
24
+ this.isInitialized = true;
25
+ this.log('Reddit adapter initialized successfully (HTTP access)', 'info');
26
+ return true;
27
+ }
28
+ catch (error) {
29
+ this.log('Failed to initialize Reddit adapter', 'error');
30
+ this.isInitialized = false;
31
+ return false;
32
+ }
33
+ }
34
+ async getTrendingContent(limit = 10) {
35
+ this.ensureInitialized();
36
+ this.validateLimit(limit);
37
+ try {
38
+ await this.enforceRateLimit();
39
+ const response = await this.client.get('/r/popular.json', {
40
+ params: { limit }
41
+ });
42
+ const posts = [];
43
+ const items = response.data?.data?.children || [];
44
+ for (const item of items.slice(0, limit)) {
45
+ const postData = item.data;
46
+ const post = DataNormalizer.normalizePost({
47
+ id: postData.id,
48
+ title: postData.title,
49
+ selftext: postData.selftext || '',
50
+ author: postData.author,
51
+ score: postData.score,
52
+ num_comments: postData.num_comments,
53
+ created_utc: postData.created_utc,
54
+ permalink: postData.permalink,
55
+ url: postData.url || `https://reddit.com${postData.permalink}`,
56
+ subreddit: postData.subreddit
57
+ }, 'reddit');
58
+ posts.push(post);
59
+ }
60
+ this.log(`Retrieved ${posts.length} trending Reddit 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(`/user/${userId}.json`, {
74
+ params: { limit }
75
+ });
76
+ const posts = [];
77
+ const items = response.data?.data?.children || [];
78
+ for (const item of items.slice(0, limit)) {
79
+ const postData = item.data;
80
+ if (postData.title) { // Only posts, not comments
81
+ const post = DataNormalizer.normalizePost({
82
+ id: postData.id,
83
+ title: postData.title,
84
+ selftext: postData.selftext || '',
85
+ author: postData.author || userId,
86
+ score: postData.score,
87
+ num_comments: postData.num_comments,
88
+ created_utc: postData.created_utc,
89
+ permalink: postData.permalink,
90
+ url: postData.url || `https://reddit.com${postData.permalink}`,
91
+ subreddit: postData.subreddit
92
+ }, 'reddit');
93
+ posts.push(post);
94
+ }
95
+ }
96
+ this.log(`Retrieved ${posts.length} posts from Reddit user ${userId}`, 'info');
97
+ return posts;
98
+ }
99
+ catch (error) {
100
+ if (error.response?.status === 404) {
101
+ throw new NotFoundError('reddit', `User ${userId}`, error);
102
+ }
103
+ this.handleError(error, 'getUserContent');
104
+ }
105
+ }
106
+ async searchContent(query, limit = 10) {
107
+ this.ensureInitialized();
108
+ this.validateLimit(limit);
109
+ if (!query || query.trim().length === 0) {
110
+ throw new Error('Search query cannot be empty');
111
+ }
112
+ try {
113
+ await this.enforceRateLimit();
114
+ const response = await this.client.get('/search.json', {
115
+ params: {
116
+ q: query.trim(),
117
+ limit,
118
+ sort: 'relevance',
119
+ type: 'link'
120
+ }
121
+ });
122
+ const posts = [];
123
+ const items = response.data?.data?.children || [];
124
+ for (const item of items.slice(0, limit)) {
125
+ const postData = item.data;
126
+ const post = DataNormalizer.normalizePost({
127
+ id: postData.id,
128
+ title: postData.title,
129
+ selftext: postData.selftext || '',
130
+ author: postData.author,
131
+ score: postData.score,
132
+ num_comments: postData.num_comments,
133
+ created_utc: postData.created_utc,
134
+ permalink: postData.permalink,
135
+ url: postData.url || `https://reddit.com${postData.permalink}`,
136
+ subreddit: postData.subreddit
137
+ }, 'reddit');
138
+ posts.push(post);
139
+ }
140
+ this.log(`Found ${posts.length} Reddit posts for query: ${query}`, 'info');
141
+ return posts;
142
+ }
143
+ catch (error) {
144
+ this.handleError(error, 'searchContent');
145
+ }
146
+ }
147
+ async getContentComments(contentId, limit = 20) {
148
+ this.ensureInitialized();
149
+ this.validateContentId(contentId);
150
+ this.validateLimit(limit);
151
+ try {
152
+ await this.enforceRateLimit();
153
+ // Reddit /comments/{postId}.json returns [postListing, commentListing]
154
+ const response = await this.client.get(`/comments/${contentId}.json`, {
155
+ params: {
156
+ limit,
157
+ depth: 5,
158
+ sort: 'top'
159
+ }
160
+ });
161
+ const commentListing = response.data?.[1];
162
+ const children = commentListing?.data?.children || [];
163
+ const comments = [];
164
+ for (const child of children) {
165
+ // Skip "more" stubs (load-more placeholders)
166
+ if (child.kind !== 't1' || !child.data)
167
+ continue;
168
+ comments.push(DataNormalizer.normalizeComment(child.data, 'reddit'));
169
+ }
170
+ this.log(`Retrieved ${comments.length} comments for Reddit post ${contentId}`);
171
+ return comments.slice(0, limit);
172
+ }
173
+ catch (error) {
174
+ if (error.response?.status === 404) {
175
+ throw new NotFoundError('reddit', `Post ${contentId}`, error);
176
+ }
177
+ this.handleError(error, 'getContentComments');
178
+ }
179
+ }
180
+ getPlatformName() {
181
+ return 'reddit';
182
+ }
183
+ getSupportedFeatures() {
184
+ return {
185
+ supportsTrending: true,
186
+ supportsUserContent: true,
187
+ supportsSearch: true,
188
+ supportsComments: true,
189
+ supportsAnalysis: true
190
+ };
191
+ }
192
+ isRateLimitError(error) {
193
+ return error.response?.status === 429 ||
194
+ error.message?.includes('rate limit');
195
+ }
196
+ isAuthError(error) {
197
+ return error.response?.status === 401 ||
198
+ error.response?.status === 403;
199
+ }
200
+ isNotFoundError(error) {
201
+ return error.response?.status === 404;
202
+ }
203
+ async cleanup() {
204
+ try {
205
+ this.client = null;
206
+ await super.cleanup();
207
+ }
208
+ catch (error) {
209
+ this.log('Error during Reddit cleanup', 'warn');
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * TikTok Platform Adapter — flat browser adapter using API interception.
3
+ *
4
+ * Uses BrowserPool + RequestInterceptor to capture TikTok's internal API
5
+ * responses. No tiers, no fallback chains.
6
+ *
7
+ * API targets:
8
+ * - /api/search/item/ — search results
9
+ * - /api/comment/list/ — comment threads
10
+ * - /api/recommend/item_list/ — trending/FYP
11
+ * - /api/post/item_list/ — user posts
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 TikTokAdapter extends BaseAdapter {
16
+ constructor(config: PlatformConfig);
17
+ initialize(): Promise<boolean>;
18
+ searchContent(query: string, limit?: number): Promise<Post[]>;
19
+ getTrendingContent(limit?: number): Promise<Post[]>;
20
+ getUserContent(userId: string, limit?: number): Promise<Post[]>;
21
+ getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
22
+ private interceptPosts;
23
+ /**
24
+ * TikTok uses IntersectionObserver for lazy loading — stub it before navigation.
25
+ */
26
+ private setupPage;
27
+ private waitAndScroll;
28
+ private structurePosts;
29
+ private structureComments;
30
+ private normalizeItem;
31
+ private normalizeComment;
32
+ getPlatformName(): PlatformType;
33
+ getSupportedFeatures(): PlatformCapabilities;
34
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * TikTok Platform Adapter — flat browser adapter using API interception.
3
+ *
4
+ * Uses BrowserPool + RequestInterceptor to capture TikTok's internal API
5
+ * responses. No tiers, no fallback chains.
6
+ *
7
+ * API targets:
8
+ * - /api/search/item/ — search results
9
+ * - /api/comment/list/ — comment threads
10
+ * - /api/recommend/item_list/ — trending/FYP
11
+ * - /api/post/item_list/ — user posts
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
+ '/api/search/item/',
18
+ '/api/search/general/',
19
+ '/api/comment/list/',
20
+ '/api/recommend/item_list/',
21
+ '/api/post/item_list/',
22
+ '/v1/search/',
23
+ ];
24
+ export class TikTokAdapter 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('TikTok adapter initialized (API interception mode)');
33
+ return true;
34
+ }
35
+ async searchContent(query, limit = 10) {
36
+ this.ensureInitialized();
37
+ await this.enforceRateLimit();
38
+ const url = `https://www.tiktok.com/search?q=${encodeURIComponent(query)}`;
39
+ return this.interceptPosts(url, limit);
40
+ }
41
+ async getTrendingContent(limit = 10) {
42
+ this.ensureInitialized();
43
+ await this.enforceRateLimit();
44
+ return this.interceptPosts('https://www.tiktok.com/explore', limit);
45
+ }
46
+ async getUserContent(userId, limit = 10) {
47
+ this.ensureInitialized();
48
+ this.validateUserId(userId);
49
+ await this.enforceRateLimit();
50
+ const username = userId.startsWith('@') ? userId : `@${userId}`;
51
+ return this.interceptPosts(`https://www.tiktok.com/${username}`, limit);
52
+ }
53
+ async getContentComments(contentId, limit = 20) {
54
+ this.ensureInitialized();
55
+ this.validateContentId(contentId);
56
+ await this.enforceRateLimit();
57
+ const url = contentId.includes('tiktok.com/')
58
+ ? contentId
59
+ : `https://www.tiktok.com/video/${contentId}`;
60
+ const pool = getBrowserPool();
61
+ const page = await pool.acquire('tiktok');
62
+ const interceptor = new RequestInterceptor();
63
+ try {
64
+ await this.setupPage(page);
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('tiktok');
85
+ const interceptor = new RequestInterceptor();
86
+ try {
87
+ await this.setupPage(page);
88
+ await interceptor.setup(page, API_PATTERNS);
89
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
90
+ await this.waitAndScroll(page);
91
+ const apiData = interceptor.getAllData();
92
+ if (apiData.length === 0)
93
+ return [];
94
+ return this.structurePosts(apiData).slice(0, limit);
95
+ }
96
+ catch (error) {
97
+ this.handleError(error, `interceptPosts(${url})`);
98
+ }
99
+ finally {
100
+ interceptor.stop();
101
+ await pool.release(page);
102
+ }
103
+ }
104
+ /**
105
+ * TikTok uses IntersectionObserver for lazy loading — stub it before navigation.
106
+ */
107
+ async setupPage(page) {
108
+ await page.addInitScript(() => {
109
+ if (typeof globalThis.IntersectionObserver === 'undefined') {
110
+ globalThis.IntersectionObserver = class {
111
+ _cb;
112
+ constructor(cb) { this._cb = cb; }
113
+ observe(target) {
114
+ setTimeout(() => this._cb([{
115
+ isIntersecting: true,
116
+ intersectionRatio: 1,
117
+ target,
118
+ }]), 50);
119
+ }
120
+ unobserve() { }
121
+ disconnect() { }
122
+ };
123
+ }
124
+ });
125
+ }
126
+ async waitAndScroll(page) {
127
+ try {
128
+ await page.waitForLoadState('networkidle', { timeout: 15000 });
129
+ }
130
+ catch {
131
+ // Non-fatal
132
+ }
133
+ for (let i = 0; i < 5; i++) {
134
+ await page.evaluate(() => window.scrollBy(0, window.innerHeight));
135
+ await page.waitForTimeout(2000);
136
+ }
137
+ }
138
+ // ── Data normalization (preserved from VisualTikTokAdapter) ────────────
139
+ structurePosts(interceptedData) {
140
+ const posts = [];
141
+ const seenIds = new Set();
142
+ for (const data of interceptedData) {
143
+ try {
144
+ const items = data?.data || data?.item_list || data?.itemList || [];
145
+ if (Array.isArray(items)) {
146
+ for (const item of items) {
147
+ const post = this.normalizeItem(item);
148
+ if (post && !seenIds.has(post.id)) {
149
+ seenIds.add(post.id);
150
+ posts.push(post);
151
+ }
152
+ }
153
+ }
154
+ const searchItems = data?.data?.item_list || data?.data?.items || [];
155
+ if (Array.isArray(searchItems) && searchItems !== items) {
156
+ for (const item of searchItems) {
157
+ const post = this.normalizeItem(item);
158
+ if (post && !seenIds.has(post.id)) {
159
+ seenIds.add(post.id);
160
+ posts.push(post);
161
+ }
162
+ }
163
+ }
164
+ }
165
+ catch {
166
+ // Skip malformed response
167
+ }
168
+ }
169
+ return posts;
170
+ }
171
+ structureComments(interceptedData) {
172
+ const comments = [];
173
+ const seenIds = new Set();
174
+ for (const data of interceptedData) {
175
+ try {
176
+ const commentList = data?.comments || data?.data?.comments || [];
177
+ if (Array.isArray(commentList)) {
178
+ for (const item of commentList) {
179
+ const comment = this.normalizeComment(item);
180
+ if (comment && !seenIds.has(comment.id)) {
181
+ seenIds.add(comment.id);
182
+ comments.push(comment);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ catch {
188
+ // Skip malformed response
189
+ }
190
+ }
191
+ return comments;
192
+ }
193
+ normalizeItem(item) {
194
+ try {
195
+ const id = item.id || item.aweme_id || item.video?.id || '';
196
+ if (!id)
197
+ return null;
198
+ const author = item.author || {};
199
+ const stats = item.stats || item.statistics || {};
200
+ const desc = item.desc || item.title || item.video?.title || '';
201
+ const createTime = item.createTime || item.create_time;
202
+ return {
203
+ id: String(id),
204
+ platform: 'tiktok',
205
+ author: {
206
+ id: author.id || author.uid || '',
207
+ username: author.uniqueId || author.unique_id || author.nickname || '',
208
+ displayName: author.nickname || '',
209
+ followerCount: author.followerCount || author.follower_count,
210
+ verified: author.verified,
211
+ profileImageUrl: author.avatarThumb || author.avatar_thumb,
212
+ },
213
+ content: desc,
214
+ mediaUrl: item.video?.cover || item.video?.dynamicCover || '',
215
+ engagement: {
216
+ likes: stats.diggCount || stats.digg_count || stats.likeCount || 0,
217
+ comments: stats.commentCount || stats.comment_count || 0,
218
+ shares: stats.shareCount || stats.share_count || 0,
219
+ views: stats.playCount || stats.play_count || 0,
220
+ },
221
+ timestamp: createTime ? new Date(createTime * 1000) : new Date(),
222
+ url: `https://www.tiktok.com/@${author.uniqueId || author.unique_id || 'user'}/video/${id}`,
223
+ hashtags: (item.textExtra || [])
224
+ .filter((t) => t.hashtagName || t.hashtag_name)
225
+ .map((t) => t.hashtagName || t.hashtag_name),
226
+ };
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }
232
+ normalizeComment(item) {
233
+ try {
234
+ const id = item.cid || item.id || '';
235
+ if (!id)
236
+ return null;
237
+ const user = item.user || {};
238
+ const createTime = item.create_time || item.createTime;
239
+ return {
240
+ id: String(id),
241
+ author: {
242
+ id: user.uid || user.id || '',
243
+ username: user.unique_id || user.uniqueId || user.nickname || '',
244
+ displayName: user.nickname || '',
245
+ },
246
+ text: item.text || '',
247
+ timestamp: createTime ? new Date(createTime * 1000) : new Date(),
248
+ likes: item.digg_count || item.diggCount || item.likes || 0,
249
+ replies: (item.reply_comment || []).map((r) => this.normalizeComment(r)).filter(Boolean),
250
+ };
251
+ }
252
+ catch {
253
+ return null;
254
+ }
255
+ }
256
+ // ── Platform identity ──────────────────────────────────────────────────
257
+ getPlatformName() {
258
+ return 'tiktok';
259
+ }
260
+ getSupportedFeatures() {
261
+ return {
262
+ supportsTrending: true,
263
+ supportsUserContent: true,
264
+ supportsSearch: true,
265
+ supportsComments: true,
266
+ supportsAnalysis: true,
267
+ };
268
+ }
269
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Twitter/X Platform Adapter
3
+ * Uses @the-convocation/twitter-scraper for HTTP-only access to Twitter's
4
+ * internal API via cookie auth. No browser needed.
5
+ *
6
+ * Auth: TWITTER_USERNAME + TWITTER_PASSWORD env vars, or stored cookies.
7
+ */
8
+ import { BaseAdapter } from '../../core/base/BaseAdapter.js';
9
+ import { Post, Comment, PlatformCapabilities, PlatformType, PlatformConfig } from '../../core/interfaces/SocialMediaPlatform.js';
10
+ export declare class TwitterAdapter extends BaseAdapter {
11
+ private scraper;
12
+ constructor(config: PlatformConfig);
13
+ initialize(): Promise<boolean>;
14
+ searchContent(query: string, limit?: number): Promise<Post[]>;
15
+ getTrendingContent(limit?: number): Promise<Post[]>;
16
+ getUserContent(userId: string, limit?: number): Promise<Post[]>;
17
+ getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
18
+ private normalizeTweet;
19
+ private normalizeTweetAsComment;
20
+ getPlatformName(): PlatformType;
21
+ getSupportedFeatures(): PlatformCapabilities;
22
+ cleanup(): Promise<void>;
23
+ }