@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,266 @@
1
+ /**
2
+ * BrowserPool — manages browser sessions for visual extraction.
3
+ *
4
+ * Providers:
5
+ * - local → Playwright launches Chromium directly (default, zero-config)
6
+ * - remote → Connects to any CDP endpoint (Browserbase, E2B, etc.)
7
+ *
8
+ * Each platform gets its own BrowserContext with isolated cookies but shared browser.
9
+ * Session persistence via cookies saved to ~/.crowdlisten/sessions/{platform}/
10
+ */
11
+ import { chromium } from 'playwright';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as os from 'os';
15
+ const PLATFORM_PROFILES = {
16
+ twitter: {
17
+ viewport: { width: 1280, height: 900 },
18
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
19
+ },
20
+ tiktok: {
21
+ viewport: { width: 1280, height: 900 },
22
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
23
+ },
24
+ instagram: {
25
+ viewport: { width: 1280, height: 900 },
26
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
27
+ },
28
+ xiaohongshu: {
29
+ viewport: { width: 390, height: 844 },
30
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
31
+ locale: 'zh-CN',
32
+ isMobile: true,
33
+ },
34
+ };
35
+ export class BrowserPool {
36
+ browser = null;
37
+ contexts = new Map();
38
+ activePages = new Set();
39
+ maxContexts;
40
+ sessionDir;
41
+ headless;
42
+ provider;
43
+ cdpUrl;
44
+ constructor(options = {}) {
45
+ this.maxContexts = options.maxContexts ?? parseInt(process.env.VISUAL_MAX_CONCURRENCY || '5');
46
+ this.sessionDir = options.sessionDir ?? process.env.VISUAL_SESSION_DIR
47
+ ?? path.join(os.homedir(), '.crowdlisten', 'sessions');
48
+ this.headless = options.headless ?? (process.env.VISUAL_HEADLESS !== 'false');
49
+ this.provider = options.provider
50
+ ?? process.env.BROWSER_PROVIDER ?? 'local';
51
+ this.cdpUrl = options.cdpUrl ?? process.env.BROWSER_CDP_URL ?? null;
52
+ }
53
+ /**
54
+ * Launch or connect to a browser based on the configured provider.
55
+ */
56
+ async ensureBrowser() {
57
+ if (this.browser && this.browser.isConnected()) {
58
+ return this.browser;
59
+ }
60
+ this.browser = this.provider === 'remote'
61
+ ? await this.connectViaRemote()
62
+ : await this.launchLocal();
63
+ console.log(`[BrowserPool] Connected via provider: ${this.provider}`);
64
+ return this.browser;
65
+ }
66
+ /**
67
+ * Local provider: launch Chromium directly via Playwright (default).
68
+ */
69
+ async launchLocal() {
70
+ return chromium.launch({
71
+ headless: this.headless,
72
+ args: [
73
+ '--no-sandbox',
74
+ '--disable-blink-features=AutomationControlled',
75
+ '--disable-dev-shm-usage',
76
+ ],
77
+ });
78
+ }
79
+ /**
80
+ * Remote provider: connect to an existing CDP endpoint.
81
+ * Set BROWSER_CDP_URL to your endpoint (Browserbase, E2B, etc.)
82
+ */
83
+ async connectViaRemote() {
84
+ if (!this.cdpUrl) {
85
+ throw new Error('[BrowserPool] Remote provider requires BROWSER_CDP_URL. Set it to your CDP endpoint.\n' +
86
+ 'Examples:\n' +
87
+ ' BROWSER_CDP_URL=ws://localhost:9222\n' +
88
+ ' BROWSER_CDP_URL=wss://connect.browserbase.com?apiKey=YOUR_KEY');
89
+ }
90
+ console.log(`[BrowserPool] Connecting to remote browser: ${this.cdpUrl.substring(0, 50)}...`);
91
+ return chromium.connectOverCDP(this.cdpUrl);
92
+ }
93
+ getSessionPath(platform) {
94
+ return path.join(this.sessionDir, platform);
95
+ }
96
+ getCookiePath(platform) {
97
+ return path.join(this.getSessionPath(platform), 'cookies.json');
98
+ }
99
+ /**
100
+ * Acquire a Page for a specific platform.
101
+ * Creates a BrowserContext if one doesn't exist for this platform.
102
+ * Loads persisted cookies if available.
103
+ */
104
+ async acquire(platform) {
105
+ const browser = await this.ensureBrowser();
106
+ if (!this.contexts.has(platform)) {
107
+ if (this.contexts.size >= this.maxContexts) {
108
+ // Evict least recently used context
109
+ const oldest = this.contexts.keys().next().value;
110
+ if (oldest) {
111
+ await this.releaseContext(oldest);
112
+ }
113
+ }
114
+ const profile = PLATFORM_PROFILES[platform] ?? PLATFORM_PROFILES.twitter;
115
+ const context = await browser.newContext({
116
+ viewport: profile.viewport,
117
+ userAgent: profile.userAgent,
118
+ locale: profile.locale,
119
+ isMobile: profile.isMobile,
120
+ ignoreHTTPSErrors: true,
121
+ });
122
+ // Restore persisted cookies
123
+ await this.loadCookies(context, platform);
124
+ this.contexts.set(platform, context);
125
+ }
126
+ const context = this.contexts.get(platform);
127
+ const page = await context.newPage();
128
+ this.activePages.add(page);
129
+ return page;
130
+ }
131
+ /**
132
+ * Release a page back to the pool. Saves cookies and closes the page.
133
+ */
134
+ async release(page) {
135
+ this.activePages.delete(page);
136
+ // Find which platform context this page belongs to
137
+ for (const [platform, context] of this.contexts) {
138
+ if (context.pages().includes(page)) {
139
+ await this.saveCookies(context, platform);
140
+ break;
141
+ }
142
+ }
143
+ if (!page.isClosed()) {
144
+ await page.close();
145
+ }
146
+ }
147
+ /**
148
+ * Get a persistent context for a platform (reuses existing chrome profile).
149
+ * Used for platforms that need login persistence (Twitter, XHS).
150
+ * Only available with local provider — remote/docker use cookie persistence instead.
151
+ */
152
+ async acquirePersistent(platform) {
153
+ if (this.provider !== 'local') {
154
+ // Persistent contexts only work locally — fall back to cookie-based persistence
155
+ console.log(`[BrowserPool] Persistent context unavailable for ${this.provider} provider, using cookie persistence`);
156
+ const page = await this.acquire(platform);
157
+ const context = this.contexts.get(platform);
158
+ return { context, page };
159
+ }
160
+ const profilePath = this.getPersistentProfilePath(platform);
161
+ fs.mkdirSync(profilePath, { recursive: true });
162
+ const profile = PLATFORM_PROFILES[platform] ?? PLATFORM_PROFILES.twitter;
163
+ const context = await chromium.launchPersistentContext(profilePath, {
164
+ headless: this.headless,
165
+ viewport: profile.viewport,
166
+ userAgent: profile.userAgent,
167
+ locale: profile.locale,
168
+ args: [
169
+ '--no-sandbox',
170
+ '--disable-blink-features=AutomationControlled',
171
+ ],
172
+ });
173
+ this.contexts.set(`persistent_${platform}`, context);
174
+ const pages = context.pages();
175
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
176
+ this.activePages.add(page);
177
+ return { context, page };
178
+ }
179
+ getPersistentProfilePath(platform) {
180
+ const envMap = {
181
+ twitter: process.env.TWITTER_CHROME_PROFILE_PATH,
182
+ tiktok: process.env.TIKTOK_CHROME_PROFILE_PATH,
183
+ xiaohongshu: process.env.XHS_CHROME_PROFILE_PATH,
184
+ instagram: process.env.INSTAGRAM_CHROME_PROFILE_PATH,
185
+ };
186
+ return envMap[platform] ?? path.join(this.sessionDir, `${platform}-profile`);
187
+ }
188
+ async loadCookies(context, platform) {
189
+ const cookiePath = this.getCookiePath(platform);
190
+ try {
191
+ if (fs.existsSync(cookiePath)) {
192
+ const cookies = JSON.parse(fs.readFileSync(cookiePath, 'utf-8'));
193
+ if (Array.isArray(cookies) && cookies.length > 0) {
194
+ await context.addCookies(cookies);
195
+ console.log(`[BrowserPool] Loaded ${cookies.length} cookies for ${platform}`);
196
+ }
197
+ }
198
+ }
199
+ catch (err) {
200
+ console.warn(`[BrowserPool] Failed to load cookies for ${platform}:`, err);
201
+ }
202
+ }
203
+ async saveCookies(context, platform) {
204
+ const cookiePath = this.getCookiePath(platform);
205
+ try {
206
+ const sessionPath = this.getSessionPath(platform);
207
+ fs.mkdirSync(sessionPath, { recursive: true });
208
+ const cookies = await context.cookies();
209
+ fs.writeFileSync(cookiePath, JSON.stringify(cookies, null, 2));
210
+ console.log(`[BrowserPool] Saved ${cookies.length} cookies for ${platform}`);
211
+ }
212
+ catch (err) {
213
+ console.warn(`[BrowserPool] Failed to save cookies for ${platform}:`, err);
214
+ }
215
+ }
216
+ async releaseContext(platform) {
217
+ const context = this.contexts.get(platform);
218
+ if (context) {
219
+ await this.saveCookies(context, platform);
220
+ // Close all pages in this context
221
+ for (const page of context.pages()) {
222
+ this.activePages.delete(page);
223
+ }
224
+ await context.close();
225
+ this.contexts.delete(platform);
226
+ }
227
+ }
228
+ /**
229
+ * Close all contexts and the browser.
230
+ */
231
+ async cleanup() {
232
+ for (const [platform, context] of this.contexts) {
233
+ try {
234
+ await this.saveCookies(context, platform);
235
+ await context.close();
236
+ }
237
+ catch (err) {
238
+ console.warn(`[BrowserPool] Error cleaning up ${platform}:`, err);
239
+ }
240
+ }
241
+ this.contexts.clear();
242
+ this.activePages.clear();
243
+ if (this.browser) {
244
+ await this.browser.close();
245
+ this.browser = null;
246
+ }
247
+ console.log('[BrowserPool] Cleanup complete');
248
+ }
249
+ get activeContextCount() {
250
+ return this.contexts.size;
251
+ }
252
+ get activePageCount() {
253
+ return this.activePages.size;
254
+ }
255
+ get currentProvider() {
256
+ return this.provider;
257
+ }
258
+ }
259
+ // Singleton pool instance
260
+ let _poolInstance = null;
261
+ export function getBrowserPool(options) {
262
+ if (!_poolInstance) {
263
+ _poolInstance = new BrowserPool(options);
264
+ }
265
+ return _poolInstance;
266
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * RequestInterceptor — captures JSON responses from internal platform API calls.
3
+ * Registers response listeners on a Playwright Page, filters by URL patterns,
4
+ * and stores structured JSON for later retrieval.
5
+ */
6
+ import { Page } from 'playwright';
7
+ export interface InterceptedResponse {
8
+ url: string;
9
+ status: number;
10
+ data: any;
11
+ timestamp: number;
12
+ matchedPattern: string;
13
+ }
14
+ export declare class RequestInterceptor {
15
+ private intercepted;
16
+ private patterns;
17
+ private seenHashes;
18
+ private listening;
19
+ /**
20
+ * Set up response interception on a page for the given URL patterns.
21
+ * Patterns are substring-matched against response URLs.
22
+ */
23
+ setup(page: Page, urlPatterns: string[]): Promise<void>;
24
+ /**
25
+ * Get all intercepted responses matching a specific pattern.
26
+ */
27
+ getIntercepted(pattern?: string): InterceptedResponse[];
28
+ /**
29
+ * Get all intercepted data payloads (just the JSON bodies).
30
+ */
31
+ getAllData(pattern?: string): any[];
32
+ /**
33
+ * Wait for a response matching a specific pattern, with timeout.
34
+ */
35
+ waitForResponse(page: Page, pattern: string, timeout?: number): Promise<InterceptedResponse | null>;
36
+ /**
37
+ * Clear all captured data.
38
+ */
39
+ clear(): void;
40
+ /**
41
+ * Stop listening for responses.
42
+ */
43
+ stop(): void;
44
+ get count(): number;
45
+ private computeHash;
46
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * RequestInterceptor — captures JSON responses from internal platform API calls.
3
+ * Registers response listeners on a Playwright Page, filters by URL patterns,
4
+ * and stores structured JSON for later retrieval.
5
+ */
6
+ export class RequestInterceptor {
7
+ intercepted = [];
8
+ patterns = [];
9
+ seenHashes = new Set();
10
+ listening = false;
11
+ /**
12
+ * Set up response interception on a page for the given URL patterns.
13
+ * Patterns are substring-matched against response URLs.
14
+ */
15
+ async setup(page, urlPatterns) {
16
+ this.patterns = urlPatterns;
17
+ this.intercepted = [];
18
+ this.seenHashes.clear();
19
+ this.listening = true;
20
+ page.on('response', async (response) => {
21
+ if (!this.listening)
22
+ return;
23
+ const url = response.url();
24
+ const matchedPattern = this.patterns.find(p => url.includes(p));
25
+ if (!matchedPattern)
26
+ return;
27
+ try {
28
+ const contentType = response.headers()['content-type'] || '';
29
+ if (!contentType.includes('json') && !contentType.includes('javascript')) {
30
+ return;
31
+ }
32
+ const status = response.status();
33
+ if (status < 200 || status >= 400)
34
+ return;
35
+ const body = await response.json().catch(() => null);
36
+ if (!body)
37
+ return;
38
+ // Deduplicate by hashing URL + stringified body
39
+ const hash = this.computeHash(url, body);
40
+ if (this.seenHashes.has(hash))
41
+ return;
42
+ this.seenHashes.add(hash);
43
+ this.intercepted.push({
44
+ url,
45
+ status,
46
+ data: body,
47
+ timestamp: Date.now(),
48
+ matchedPattern,
49
+ });
50
+ }
51
+ catch {
52
+ // Non-JSON response or body read failure — skip silently
53
+ }
54
+ });
55
+ }
56
+ /**
57
+ * Get all intercepted responses matching a specific pattern.
58
+ */
59
+ getIntercepted(pattern) {
60
+ if (!pattern)
61
+ return [...this.intercepted];
62
+ return this.intercepted.filter(r => r.matchedPattern === pattern || r.url.includes(pattern));
63
+ }
64
+ /**
65
+ * Get all intercepted data payloads (just the JSON bodies).
66
+ */
67
+ getAllData(pattern) {
68
+ return this.getIntercepted(pattern).map(r => r.data);
69
+ }
70
+ /**
71
+ * Wait for a response matching a specific pattern, with timeout.
72
+ */
73
+ async waitForResponse(page, pattern, timeout = 15000) {
74
+ // Check if we already have it
75
+ const existing = this.intercepted.find(r => r.url.includes(pattern));
76
+ if (existing)
77
+ return existing;
78
+ return new Promise((resolve) => {
79
+ const startTime = Date.now();
80
+ const check = setInterval(() => {
81
+ const match = this.intercepted.find(r => r.url.includes(pattern) && r.timestamp > startTime);
82
+ if (match) {
83
+ clearInterval(check);
84
+ resolve(match);
85
+ }
86
+ else if (Date.now() - startTime > timeout) {
87
+ clearInterval(check);
88
+ resolve(null);
89
+ }
90
+ }, 200);
91
+ });
92
+ }
93
+ /**
94
+ * Clear all captured data.
95
+ */
96
+ clear() {
97
+ this.intercepted = [];
98
+ this.seenHashes.clear();
99
+ }
100
+ /**
101
+ * Stop listening for responses.
102
+ */
103
+ stop() {
104
+ this.listening = false;
105
+ }
106
+ get count() {
107
+ return this.intercepted.length;
108
+ }
109
+ computeHash(url, body) {
110
+ // Simple hash for deduplication — URL path + first 200 chars of stringified body
111
+ const urlPath = new URL(url).pathname;
112
+ const bodyStr = JSON.stringify(body).substring(0, 200);
113
+ return `${urlPath}::${bodyStr}`;
114
+ }
115
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CrowdListen CLI
4
+ * Cross-channel feedback analysis for AI agents.
5
+ * Extracts audience signal (pain points, feature requests, sentiment) from
6
+ * social platforms into structured JSON. stdout = data, stderr = errors.
7
+ */
8
+ export {};
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CrowdListen CLI
4
+ * Cross-channel feedback analysis for AI agents.
5
+ * Extracts audience signal (pain points, feature requests, sentiment) from
6
+ * social platforms into structured JSON. stdout = data, stderr = errors.
7
+ */
8
+ import { Command } from 'commander';
9
+ import { createService } from './service-config.js';
10
+ import { searchContent, getContentComments, analyzeContent, clusterOpinions, getTrendingContent, getUserContent, getPlatformStatus, healthCheck, deepAnalyze, extractInsights, researchSynthesis, extractWithVision, } from './handlers.js';
11
+ const program = new Command();
12
+ program
13
+ .name('crowdlisten')
14
+ .description('Cross-channel feedback analysis for AI agents')
15
+ .version('2.0.0');
16
+ // Shared service instance — initialized lazily
17
+ let service = null;
18
+ let initialized = false;
19
+ async function getService() {
20
+ if (!service) {
21
+ service = createService();
22
+ }
23
+ if (!initialized) {
24
+ const results = await service.initialize();
25
+ const ok = Object.entries(results).filter(([, s]) => s).map(([p]) => p);
26
+ if (ok.length === 0) {
27
+ console.error('Error: No platforms initialized successfully');
28
+ process.exit(1);
29
+ }
30
+ console.error(`[crowdlisten] Platforms: ${ok.join(', ')}`);
31
+ initialized = true;
32
+ }
33
+ return service;
34
+ }
35
+ function output(data) {
36
+ console.log(JSON.stringify(data, null, 2));
37
+ }
38
+ async function run(fn) {
39
+ try {
40
+ const result = await fn();
41
+ output(result);
42
+ process.exit(0);
43
+ }
44
+ catch (err) {
45
+ console.error(`Error: ${err.message || err}`);
46
+ process.exit(1);
47
+ }
48
+ }
49
+ // --- Commands ---
50
+ program
51
+ .command('search <platform> <query>')
52
+ .description('Search social media for audience conversations')
53
+ .option('-l, --limit <n>', 'Max results', '10')
54
+ .option('--vision', 'Force vision extraction mode')
55
+ .action(async (platform, query, opts) => {
56
+ await run(async () => {
57
+ const svc = await getService();
58
+ return searchContent(svc, {
59
+ platform,
60
+ query,
61
+ limit: parseInt(opts.limit),
62
+ useVision: opts.vision || false,
63
+ });
64
+ });
65
+ });
66
+ program
67
+ .command('comments <platform> <contentId>')
68
+ .description('Get comments for a specific post/video')
69
+ .option('-l, --limit <n>', 'Max comments', '20')
70
+ .option('--vision', 'Force vision extraction mode')
71
+ .action(async (platform, contentId, opts) => {
72
+ await run(async () => {
73
+ const svc = await getService();
74
+ return getContentComments(svc, {
75
+ platform,
76
+ contentId,
77
+ limit: parseInt(opts.limit),
78
+ useVision: opts.vision || false,
79
+ });
80
+ });
81
+ });
82
+ program
83
+ .command('analyze <platform> <contentId>')
84
+ .description('Full analysis pipeline: comments + clustering + sentiment')
85
+ .option('-d, --depth <level>', 'Analysis depth (surface|standard|deep|comprehensive)', 'standard')
86
+ .option('--no-clustering', 'Disable opinion clustering')
87
+ .action(async (platform, contentId, opts) => {
88
+ const depth = opts.depth;
89
+ // Route deep/comprehensive to paid agent API
90
+ if (depth === 'deep' || depth === 'comprehensive') {
91
+ await run(async () => deepAnalyze({ platform, contentId, analysisDepth: depth }));
92
+ }
93
+ else {
94
+ await run(async () => {
95
+ const svc = await getService();
96
+ return analyzeContent(svc, {
97
+ platform,
98
+ contentId,
99
+ analysisDepth: depth,
100
+ });
101
+ });
102
+ }
103
+ });
104
+ program
105
+ .command('cluster <platform> <contentId>')
106
+ .description('Cluster opinions from comments using embeddings')
107
+ .option('-n, --clusters <n>', 'Number of clusters', '5')
108
+ .option('--no-examples', 'Exclude example comments')
109
+ .action(async (platform, contentId, opts) => {
110
+ await run(async () => {
111
+ const svc = await getService();
112
+ return clusterOpinions(svc, {
113
+ platform,
114
+ contentId,
115
+ clusterCount: parseInt(opts.clusters),
116
+ includeExamples: opts.examples !== false,
117
+ weightByEngagement: true,
118
+ });
119
+ });
120
+ });
121
+ program
122
+ .command('trending <platform>')
123
+ .description('Get trending content from a platform')
124
+ .option('-l, --limit <n>', 'Max results', '10')
125
+ .action(async (platform, opts) => {
126
+ await run(async () => {
127
+ const svc = await getService();
128
+ return getTrendingContent(svc, { platform, limit: parseInt(opts.limit) });
129
+ });
130
+ });
131
+ program
132
+ .command('user <platform> <userId>')
133
+ .description('Get content from a specific user')
134
+ .option('-l, --limit <n>', 'Max results', '10')
135
+ .action(async (platform, userId, opts) => {
136
+ await run(async () => {
137
+ const svc = await getService();
138
+ return getUserContent(svc, { platform, userId, limit: parseInt(opts.limit) });
139
+ });
140
+ });
141
+ program
142
+ .command('vision <url>')
143
+ .description('Extract content from any URL using vision (LLM screenshot analysis)')
144
+ .option('-m, --mode <mode>', 'Extraction mode (posts|comments|raw)', 'posts')
145
+ .option('-l, --limit <n>', 'Max results', '10')
146
+ .action(async (url, opts) => {
147
+ await run(async () => {
148
+ return extractWithVision({
149
+ url,
150
+ mode: opts.mode,
151
+ limit: parseInt(opts.limit),
152
+ });
153
+ });
154
+ });
155
+ program
156
+ .command('status')
157
+ .description('Show available platforms and capabilities')
158
+ .action(async () => {
159
+ await run(async () => {
160
+ const svc = await getService();
161
+ return getPlatformStatus(svc);
162
+ });
163
+ });
164
+ program
165
+ .command('health')
166
+ .description('Check health of all platforms')
167
+ .action(async () => {
168
+ await run(async () => {
169
+ const svc = await getService();
170
+ return healthCheck(svc);
171
+ });
172
+ });
173
+ // --- Paid Commands (require CROWDLISTEN_API_KEY) ---
174
+ program
175
+ .command('insights <platform> <contentId>')
176
+ .description('Extract structured insights with categorization and confidence (paid)')
177
+ .option('-c, --categories <list>', 'Comma-separated insight categories', '')
178
+ .action(async (platform, contentId, opts) => {
179
+ await run(async () => extractInsights({
180
+ platform,
181
+ contentId,
182
+ categories: opts.categories ? opts.categories.split(',') : undefined,
183
+ }));
184
+ });
185
+ program
186
+ .command('research <query>')
187
+ .description('Multi-source research synthesis with AI analysis (paid)')
188
+ .option('-p, --platforms <list>', 'Comma-separated platforms', 'reddit,twitter,youtube')
189
+ .option('-d, --depth <level>', 'Research depth (quick|standard|deep)', 'standard')
190
+ .action(async (query, opts) => {
191
+ await run(async () => researchSynthesis({
192
+ query,
193
+ platforms: opts.platforms.split(','),
194
+ depth: opts.depth,
195
+ }));
196
+ });
197
+ // CLI only — MCP server is now unified in the parent package's index.ts
198
+ // If called with no args via piped stdin, print help
199
+ if (process.argv.length <= 2 && !process.stdin.isTTY) {
200
+ console.error('CrowdListen insights CLI. Use the unified MCP server instead:');
201
+ console.error(' npx @crowdlisten/harness');
202
+ process.exit(0);
203
+ }
204
+ else {
205
+ program.parse();
206
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Base adapter class providing common functionality for all social media platforms
3
+ * Implements shared features like rate limiting, error handling, and logging
4
+ *
5
+ * Analysis (clustering, enrichment) has been moved to the CrowdListen API.
6
+ * This class now only handles data retrieval.
7
+ */
8
+ import { SocialMediaPlatform, PlatformType, PlatformConfig, PlatformCapabilities, Post, Comment, ContentAnalysis } from '../interfaces/SocialMediaPlatform.js';
9
+ export declare abstract class BaseAdapter implements SocialMediaPlatform {
10
+ protected config: PlatformConfig;
11
+ protected isInitialized: boolean;
12
+ protected lastRequestTime: number;
13
+ protected requestCount: number;
14
+ protected rateLimitWindow: number;
15
+ protected maxRequestsPerWindow: number;
16
+ constructor(config: PlatformConfig);
17
+ protected enforceRateLimit(): Promise<void>;
18
+ protected sleep(ms: number): Promise<void>;
19
+ protected handleError(error: any, operation: string): never;
20
+ protected isRateLimitError(error: any): boolean;
21
+ protected isAuthError(error: any): boolean;
22
+ protected isNotFoundError(error: any): boolean;
23
+ protected log(message: string, level?: 'info' | 'warn' | 'error'): void;
24
+ protected validateUserId(userId: string): void;
25
+ protected validateContentId(contentId: string): void;
26
+ protected validateLimit(limit: number): void;
27
+ analyzeContent(contentId: string, enableClustering?: boolean): Promise<ContentAnalysis>;
28
+ protected ensureInitialized(): void;
29
+ abstract getTrendingContent(limit?: number): Promise<Post[]>;
30
+ abstract getUserContent(userId: string, limit?: number): Promise<Post[]>;
31
+ abstract searchContent(query: string, limit?: number): Promise<Post[]>;
32
+ abstract getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
33
+ abstract getPlatformName(): PlatformType;
34
+ abstract getSupportedFeatures(): PlatformCapabilities;
35
+ abstract initialize(): Promise<boolean>;
36
+ cleanup(): Promise<void>;
37
+ }