@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,40 @@
1
+ /**
2
+ * UserState — Persistent state for progressive skill pack disclosure.
3
+ *
4
+ * Stored at ~/.crowdlisten/state.json
5
+ *
6
+ * Auto-migration:
7
+ * - Existing users (have auth.json or context.json): all packs active
8
+ * - New users: core pack only
9
+ */
10
+ export type TelemetryLevel = "off" | "anonymous" | "community";
11
+ export interface UserState {
12
+ version: 2;
13
+ activePacks: string[];
14
+ contextBlockCount: number;
15
+ preferences: {
16
+ maxActivePacks: number;
17
+ autoActivate: string[];
18
+ telemetry: TelemetryLevel;
19
+ proactiveSuggestions: boolean;
20
+ crossProjectLearnings: boolean;
21
+ };
22
+ installationId: string;
23
+ onboardingCompleted: string[];
24
+ }
25
+ /**
26
+ * Load user state. Auto-creates with migration logic if not found.
27
+ */
28
+ export declare function loadUserState(): UserState;
29
+ /**
30
+ * Save user state to disk.
31
+ */
32
+ export declare function saveUserState(state: UserState): void;
33
+ /**
34
+ * Activate a skill pack.
35
+ */
36
+ export declare function activatePack(packId: string): UserState;
37
+ /**
38
+ * Deactivate a skill pack.
39
+ */
40
+ export declare function deactivatePack(packId: string): UserState;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * UserState — Persistent state for progressive skill pack disclosure.
3
+ *
4
+ * Stored at ~/.crowdlisten/state.json
5
+ *
6
+ * Auto-migration:
7
+ * - Existing users (have auth.json or context.json): all packs active
8
+ * - New users: core pack only
9
+ */
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import * as os from "os";
13
+ import * as crypto from "crypto";
14
+ const BASE_DIR = path.join(os.homedir(), ".crowdlisten");
15
+ const STATE_FILE = path.join(BASE_DIR, "state.json");
16
+ const AUTH_FILE = path.join(BASE_DIR, "auth.json");
17
+ const CONTEXT_FILE = path.join(BASE_DIR, "context.json");
18
+ // All tool packs (excluding core which is always on, and legacy which is hidden)
19
+ const ALL_PACKS = [
20
+ "planning",
21
+ "knowledge",
22
+ "social-listening",
23
+ "audience-analysis",
24
+ "sessions",
25
+ "setup",
26
+ ];
27
+ function defaultNewUserState() {
28
+ return {
29
+ version: 2,
30
+ activePacks: [],
31
+ contextBlockCount: 0,
32
+ preferences: {
33
+ maxActivePacks: 10,
34
+ autoActivate: [],
35
+ telemetry: "off",
36
+ proactiveSuggestions: true,
37
+ crossProjectLearnings: false,
38
+ },
39
+ installationId: crypto.randomUUID(),
40
+ onboardingCompleted: [],
41
+ };
42
+ }
43
+ function defaultExistingUserState() {
44
+ return {
45
+ version: 2,
46
+ activePacks: [...ALL_PACKS, "legacy"],
47
+ contextBlockCount: 0,
48
+ preferences: {
49
+ maxActivePacks: 10,
50
+ autoActivate: [...ALL_PACKS],
51
+ telemetry: "off",
52
+ proactiveSuggestions: true,
53
+ crossProjectLearnings: false,
54
+ },
55
+ installationId: crypto.randomUUID(),
56
+ onboardingCompleted: [],
57
+ };
58
+ }
59
+ /**
60
+ * Migrate v1 state to v2 in-place.
61
+ */
62
+ function migrateV1toV2(v1) {
63
+ return {
64
+ version: 2,
65
+ activePacks: v1.activePacks,
66
+ contextBlockCount: v1.contextBlockCount,
67
+ preferences: {
68
+ ...v1.preferences,
69
+ telemetry: "off",
70
+ proactiveSuggestions: true,
71
+ crossProjectLearnings: false,
72
+ },
73
+ installationId: crypto.randomUUID(),
74
+ onboardingCompleted: [],
75
+ };
76
+ }
77
+ function ensureDir() {
78
+ if (!fs.existsSync(BASE_DIR)) {
79
+ fs.mkdirSync(BASE_DIR, { recursive: true });
80
+ }
81
+ }
82
+ /**
83
+ * Check if this is an existing user (has auth or context data).
84
+ */
85
+ function isExistingUser() {
86
+ return fs.existsSync(AUTH_FILE) || fs.existsSync(CONTEXT_FILE);
87
+ }
88
+ // ─── Public API ─────────────────────────────────────────────────────────────
89
+ /**
90
+ * Load user state. Auto-creates with migration logic if not found.
91
+ */
92
+ export function loadUserState() {
93
+ try {
94
+ if (fs.existsSync(STATE_FILE)) {
95
+ const raw = fs.readFileSync(STATE_FILE, "utf-8");
96
+ const parsed = JSON.parse(raw);
97
+ // v2 — current version
98
+ if (parsed.version === 2)
99
+ return parsed;
100
+ // v1 → v2 migration
101
+ if (parsed.version === 1) {
102
+ const migrated = migrateV1toV2(parsed);
103
+ saveUserState(migrated);
104
+ return migrated;
105
+ }
106
+ }
107
+ }
108
+ catch {
109
+ // Corrupted file — will recreate
110
+ }
111
+ // Auto-migration: create initial state
112
+ const state = isExistingUser()
113
+ ? defaultExistingUserState()
114
+ : defaultNewUserState();
115
+ saveUserState(state);
116
+ return state;
117
+ }
118
+ /**
119
+ * Save user state to disk.
120
+ */
121
+ export function saveUserState(state) {
122
+ ensureDir();
123
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
124
+ }
125
+ /**
126
+ * Activate a skill pack.
127
+ */
128
+ export function activatePack(packId) {
129
+ const state = loadUserState();
130
+ if (!state.activePacks.includes(packId)) {
131
+ state.activePacks.push(packId);
132
+ saveUserState(state);
133
+ }
134
+ return state;
135
+ }
136
+ /**
137
+ * Deactivate a skill pack.
138
+ */
139
+ export function deactivatePack(packId) {
140
+ const state = loadUserState();
141
+ state.activePacks = state.activePacks.filter(id => id !== packId);
142
+ saveUserState(state);
143
+ return state;
144
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CrowdListen — Unified MCP Server
4
+ *
5
+ * Single server combining planning, social listening, skill packs, and
6
+ * knowledge management with progressive disclosure.
7
+ *
8
+ * Pattern: Start with ~4 discovery tools → activate skill packs on demand
9
+ * → fire tools/list_changed so the agent sees new tools.
10
+ *
11
+ * First time: npx @crowdlisten/harness login
12
+ * Then add to your agent config and go.
13
+ */
14
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CrowdListen — Unified MCP Server
4
+ *
5
+ * Single server combining planning, social listening, skill packs, and
6
+ * knowledge management with progressive disclosure.
7
+ *
8
+ * Pattern: Start with ~4 discovery tools → activate skill packs on demand
9
+ * → fire tools/list_changed so the agent sees new tools.
10
+ *
11
+ * First time: npx @crowdlisten/harness login
12
+ * Then add to your agent config and go.
13
+ */
14
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
17
+ import { createClient } from "@supabase/supabase-js";
18
+ import * as http from "http";
19
+ import * as crypto from "crypto";
20
+ import * as path from "path";
21
+ import { fileURLToPath } from "url";
22
+ import { loadAuth, saveAuth, clearAuth, openBrowser, callbackHtml, autoInstallMcp, handleTool, TOOLS, MCP_ENTRY, AUTH_FILE, } from "./tools.js";
23
+ import { runSetupContext, runContextCLI, runContextWeb, } from "./context/cli.js";
24
+ import { loadUserState } from "./context/user-state.js";
25
+ import { initializeRegistry, registerTools, getToolsForPacks, isInsightsTool, } from "./tools/registry.js";
26
+ import { INSIGHTS_TOOLS, handleInsightsTool, cleanupInsights, } from "./insights/index.js";
27
+ import { recordEvent, shouldOnboard, getOnboardingPrompt, buildToolPackMap } from "./telemetry.js";
28
+ import { checkSuggestion } from "./suggestions.js";
29
+ // ─── Constants ──────────────────────────────────────────────────────────────
30
+ const CROWDLISTEN_SUPABASE_URL = process.env.CROWDLISTEN_URL || "https://fnvlxtzonwybshtvrzit.supabase.co";
31
+ const CROWDLISTEN_ANON_KEY = process.env.CROWDLISTEN_ANON_KEY ||
32
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZudmx4dHpvbnd5YnNodHZyeml0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY4NjExMjksImV4cCI6MjA3MjQzNzEyOX0.KAoEVMAVxqANcHBrjT5Et_9xiMZGP7LzdVSoSDLxpaA";
33
+ const CROWDLISTEN_APP_URL = process.env.CROWDLISTEN_APP_URL || "https://crowdlisten.com";
34
+ // Resolve skills directory relative to this file
35
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
+ const SKILLS_DIR = path.resolve(__dirname, "..", "skills");
37
+ // ─── Browser-Based Login ────────────────────────────────────────────────────
38
+ async function interactiveLogin() {
39
+ const state = crypto.randomBytes(16).toString("hex");
40
+ return new Promise((resolve, reject) => {
41
+ const server = http.createServer(async (req, res) => {
42
+ const url = new URL(req.url || "/", `http://localhost`);
43
+ if (url.pathname === "/callback") {
44
+ const accessToken = url.searchParams.get("access_token");
45
+ const refreshToken = url.searchParams.get("refresh_token");
46
+ const returnedState = url.searchParams.get("state");
47
+ const userId = url.searchParams.get("user_id");
48
+ const email = url.searchParams.get("email");
49
+ const errorMsg = url.searchParams.get("error");
50
+ res.writeHead(200, { "Content-Type": "text/html" });
51
+ if (errorMsg || !accessToken || !refreshToken || returnedState !== state) {
52
+ res.end(callbackHtml(false, errorMsg || "Authentication failed"));
53
+ console.error(`\n❌ Login failed: ${errorMsg || "Invalid callback"}`);
54
+ server.close();
55
+ reject(new Error(errorMsg || "Login failed"));
56
+ return;
57
+ }
58
+ const auth = {
59
+ access_token: accessToken,
60
+ refresh_token: refreshToken,
61
+ user_id: userId || "",
62
+ email: email || "",
63
+ expires_at: undefined,
64
+ };
65
+ const supabase = createClient(CROWDLISTEN_SUPABASE_URL, CROWDLISTEN_ANON_KEY);
66
+ const { data, error } = await supabase.auth.setSession({
67
+ access_token: accessToken,
68
+ refresh_token: refreshToken,
69
+ });
70
+ if (error || !data.session) {
71
+ res.end(callbackHtml(false, "Token verification failed"));
72
+ console.error("\n❌ Token verification failed");
73
+ server.close();
74
+ reject(new Error("Token verification failed"));
75
+ return;
76
+ }
77
+ auth.user_id = data.user?.id || auth.user_id;
78
+ auth.email = data.user?.email || auth.email;
79
+ auth.expires_at = data.session.expires_at;
80
+ auth.access_token = data.session.access_token;
81
+ auth.refresh_token = data.session.refresh_token;
82
+ saveAuth(auth);
83
+ res.end(callbackHtml(true));
84
+ console.error(`\n✅ Logged in as ${auth.email}`);
85
+ console.error(` Saved to ${AUTH_FILE}\n`);
86
+ const installed = await autoInstallMcp();
87
+ if (installed.length > 0) {
88
+ console.error(`🔌 Auto-configured MCP for: ${installed.join(", ")}\n`);
89
+ console.error(" Restart your coding agent to connect.\n");
90
+ }
91
+ else {
92
+ console.error("To connect manually, add this to your agent's MCP config:\n");
93
+ console.error(JSON.stringify({ mcpServers: {
94
+ crowdlisten: MCP_ENTRY,
95
+ } }, null, 2));
96
+ console.error("");
97
+ }
98
+ server.close();
99
+ resolve(auth);
100
+ }
101
+ else {
102
+ res.writeHead(404);
103
+ res.end("Not found");
104
+ }
105
+ });
106
+ server.listen(0, "127.0.0.1", () => {
107
+ const addr = server.address();
108
+ if (!addr || typeof addr === "string") {
109
+ reject(new Error("Failed to start local server"));
110
+ return;
111
+ }
112
+ const port = addr.port;
113
+ const callbackUrl = `http://localhost:${port}/callback`;
114
+ const loginUrl = `${CROWDLISTEN_APP_URL}/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${state}`;
115
+ console.error("\n🔐 CrowdListen Login\n");
116
+ console.error("Opening your browser to sign in...\n");
117
+ openBrowser(loginUrl);
118
+ console.error(`If the browser didn't open, go to:\n${loginUrl}\n`);
119
+ console.error("Waiting for authentication...");
120
+ });
121
+ setTimeout(() => {
122
+ console.error("\n⏰ Login timed out. Please try again.");
123
+ server.close();
124
+ reject(new Error("Login timed out"));
125
+ }, 5 * 60 * 1000);
126
+ });
127
+ }
128
+ // ─── Authenticated Supabase Client ──────────────────────────────────────────
129
+ async function getAuthedClient() {
130
+ let auth = loadAuth();
131
+ if (!auth) {
132
+ console.error("Not logged in. Run: npx @crowdlisten/harness login");
133
+ process.exit(1);
134
+ }
135
+ const supabase = createClient(CROWDLISTEN_SUPABASE_URL, CROWDLISTEN_ANON_KEY, {
136
+ auth: {
137
+ autoRefreshToken: true,
138
+ persistSession: false,
139
+ },
140
+ });
141
+ const { data, error } = await supabase.auth.setSession({
142
+ access_token: auth.access_token,
143
+ refresh_token: auth.refresh_token,
144
+ });
145
+ if (error || !data.session) {
146
+ console.error("Session expired. Please login again: npx @crowdlisten/harness login");
147
+ clearAuth();
148
+ process.exit(1);
149
+ }
150
+ if (data.session.access_token !== auth.access_token) {
151
+ auth = {
152
+ ...auth,
153
+ access_token: data.session.access_token,
154
+ refresh_token: data.session.refresh_token,
155
+ expires_at: data.session.expires_at,
156
+ };
157
+ saveAuth(auth);
158
+ }
159
+ return { supabase, userId: auth.user_id };
160
+ }
161
+ // ─── CLI Entry Point ────────────────────────────────────────────────────────
162
+ const command = process.argv[2];
163
+ if (command === "login") {
164
+ interactiveLogin().then(() => process.exit(0));
165
+ }
166
+ else if (command === "logout") {
167
+ clearAuth();
168
+ console.error("✅ Logged out. Auth cleared.");
169
+ process.exit(0);
170
+ }
171
+ else if (command === "whoami") {
172
+ const auth = loadAuth();
173
+ if (auth) {
174
+ console.error(`Logged in as: ${auth.email} (${auth.user_id})`);
175
+ }
176
+ else {
177
+ console.error("Not logged in. Run: npx @crowdlisten/harness login");
178
+ }
179
+ process.exit(0);
180
+ }
181
+ else if (command === "setup") {
182
+ autoInstallMcp().then((installed) => {
183
+ if (installed.length > 0) {
184
+ console.error(`🔌 Installed MCP config for: ${installed.join(", ")}`);
185
+ console.error(" Restart your coding agent(s) to connect.");
186
+ }
187
+ else {
188
+ console.error("No new agent configs to update.");
189
+ console.error("Already installed, or no agent config files found.\n");
190
+ console.error("Supported agents: Claude Code, Cursor, Gemini CLI, Codex, Amp, OpenClaw");
191
+ console.error("If your agent isn't listed, add this to its MCP config:\n");
192
+ console.error(JSON.stringify({
193
+ crowdlisten: MCP_ENTRY,
194
+ }, null, 2));
195
+ }
196
+ process.exit(0);
197
+ });
198
+ }
199
+ else if (command === "context") {
200
+ const fileArg = process.argv[3];
201
+ if (fileArg) {
202
+ runContextCLI(fileArg).then(() => process.exit(0));
203
+ }
204
+ else {
205
+ runContextWeb();
206
+ }
207
+ }
208
+ else if (command === "setup-context") {
209
+ runSetupContext().then(() => process.exit(0));
210
+ }
211
+ else if (command === "serve") {
212
+ startHttpServer();
213
+ }
214
+ else if (command === "openapi") {
215
+ printOpenApiSpec();
216
+ }
217
+ else if (command === "help" || command === "--help" || command === "-h") {
218
+ console.error(`
219
+ CrowdListen — Unified MCP Server
220
+
221
+ COMMANDS:
222
+ login Sign in + auto-configure your coding agents
223
+ setup Re-run auto-configure for agent MCP configs
224
+ logout Clear saved credentials
225
+ whoami Show current user
226
+ serve Start Streamable HTTP transport (port 3848)
227
+ openapi Print OpenAPI 3.0 spec to stdout
228
+ context Launch skill pack dashboard (port 3847)
229
+ context <file> Process a file through the context pipeline (CLI mode)
230
+ setup-context Configure LLM provider for context extraction
231
+ help Show this help
232
+
233
+ QUICK START:
234
+
235
+ npx @crowdlisten/harness login
236
+
237
+ That's it. Login auto-detects and configures Claude Code,
238
+ Cursor, Gemini CLI, Codex, and Amp. Just restart your agent.
239
+
240
+ Your agent starts with 4 discovery tools:
241
+ list_skill_packs, activate_skill_pack, remember, recall
242
+
243
+ Activate packs to unlock more: planning, social-listening, etc.
244
+
245
+ REMOTE ACCESS:
246
+
247
+ npx @crowdlisten/harness serve # Start HTTP server on :3848
248
+ curl localhost:3848/health # Health check
249
+ curl localhost:3848/openapi.json # OpenAPI spec
250
+ `);
251
+ process.exit(0);
252
+ }
253
+ else {
254
+ // Default: run as MCP server (stdio)
255
+ startMcpServer();
256
+ }
257
+ // ─── MCP Server (stdio, progressive disclosure) ─────────────────────────────
258
+ async function startMcpServer() {
259
+ const { supabase: sb, userId } = await getAuthedClient();
260
+ // Initialize the skill pack registry with all tool definitions
261
+ initializeRegistry(SKILLS_DIR);
262
+ registerTools(TOOLS);
263
+ registerTools(INSIGHTS_TOOLS);
264
+ const server = new Server({ name: "crowdlisten", version: "1.0.0" }, { capabilities: { tools: { listChanged: true } } });
265
+ // Dynamic ListTools — returns tools based on active packs
266
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
267
+ const state = loadUserState();
268
+ const tools = getToolsForPacks(state.activePacks);
269
+ return { tools };
270
+ });
271
+ // Build tool→pack mapping for telemetry tagging
272
+ const toolPackMap = buildToolPackMap([
273
+ { id: "core", toolNames: ["list_skill_packs", "activate_skill_pack", "remember", "recall", "set_preferences"] },
274
+ { id: "planning", toolNames: ["list_tasks", "create_task", "get_task", "update_task", "claim_task", "complete_task", "log_progress", "delete_task", "create_plan", "get_plan", "update_plan"] },
275
+ { id: "knowledge", toolNames: ["query_context", "add_context", "record_learning", "log_learning", "search_learnings"] },
276
+ { id: "social-listening", toolNames: ["search_content", "get_content_comments", "get_trending_content", "get_user_content", "get_platform_status", "health_check", "extract_url"] },
277
+ { id: "audience-analysis", toolNames: ["analyze_content", "cluster_opinions", "enrich_content", "deep_analyze", "extract_insights", "research_synthesis"] },
278
+ { id: "sessions", toolNames: ["start_session", "list_sessions", "update_session"] },
279
+ { id: "setup", toolNames: ["get_or_create_global_board", "list_projects", "list_boards", "create_board", "migrate_to_global_board"] },
280
+ { id: "spec-delivery", toolNames: ["get_specs", "get_spec_detail", "start_spec"] },
281
+ ]);
282
+ // Unified CallTool handler with telemetry + suggestions interceptor
283
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
284
+ const { name, arguments: toolArgs } = request.params;
285
+ const args = (toolArgs || {});
286
+ const startTime = Date.now();
287
+ try {
288
+ let result;
289
+ if (isInsightsTool(name)) {
290
+ result = await handleInsightsTool(name, args);
291
+ }
292
+ else {
293
+ result = await handleTool(sb, userId, name, args);
294
+ }
295
+ // After activate_skill_pack, fire tools/list_changed
296
+ if (name === "activate_skill_pack") {
297
+ try {
298
+ const parsed = JSON.parse(result);
299
+ if (parsed._needsListChanged) {
300
+ delete parsed._needsListChanged;
301
+ result = JSON.stringify(parsed, null, 2);
302
+ await server.sendToolListChanged();
303
+ }
304
+ }
305
+ catch {
306
+ // Non-fatal
307
+ }
308
+ }
309
+ // ── POST hooks: telemetry, onboarding, suggestions ──────────
310
+ const state = loadUserState();
311
+ const durationMs = Date.now() - startTime;
312
+ const pack = toolPackMap.get(name) || "unknown";
313
+ // Telemetry recording
314
+ recordEvent({ event: "tool_call", tool: name, pack, duration_ms: durationMs, success: true }, state.preferences.telemetry, state.installationId);
315
+ // Build response content
316
+ const content = [
317
+ { type: "text", text: result },
318
+ ];
319
+ // Onboarding check — append prompt if a step is pending
320
+ const pendingStep = shouldOnboard(state);
321
+ if (pendingStep) {
322
+ const prompt = getOnboardingPrompt(pendingStep);
323
+ if (prompt) {
324
+ content.push({
325
+ type: "text",
326
+ text: JSON.stringify({
327
+ _onboarding: {
328
+ step: pendingStep,
329
+ title: prompt.title,
330
+ message: prompt.message,
331
+ },
332
+ }),
333
+ });
334
+ }
335
+ }
336
+ // Proactive suggestions check
337
+ const suggestion = checkSuggestion(result, state.activePacks, state.preferences.proactiveSuggestions);
338
+ if (suggestion) {
339
+ content.push({
340
+ type: "text",
341
+ text: JSON.stringify({ _suggestion: suggestion }),
342
+ });
343
+ }
344
+ return { content };
345
+ }
346
+ catch (err) {
347
+ // Record failed telemetry
348
+ const state = loadUserState();
349
+ const durationMs = Date.now() - startTime;
350
+ const pack = toolPackMap.get(name) || "unknown";
351
+ recordEvent({ event: "tool_call", tool: name, pack, duration_ms: durationMs, success: false }, state.preferences.telemetry, state.installationId);
352
+ const message = err instanceof Error ? err.message : String(err);
353
+ return {
354
+ content: [
355
+ { type: "text", text: JSON.stringify({ error: message }) },
356
+ ],
357
+ isError: true,
358
+ };
359
+ }
360
+ });
361
+ // Graceful shutdown
362
+ const cleanup = async () => {
363
+ console.error("[Shutdown] Cleaning up...");
364
+ await cleanupInsights();
365
+ process.exit(0);
366
+ };
367
+ process.on("SIGINT", cleanup);
368
+ process.on("SIGTERM", cleanup);
369
+ server.onerror = (error) => console.error("[MCP Error]", error);
370
+ const transport = new StdioServerTransport();
371
+ await server.connect(transport);
372
+ console.error("CrowdListen unified MCP server running (progressive disclosure)");
373
+ }
374
+ // ─── HTTP Server (Streamable HTTP) ──────────────────────────────────────────
375
+ async function startHttpServer() {
376
+ const { startHttpTransport } = await import("./transport/http.js");
377
+ const port = parseInt(process.argv[3] || "3848", 10);
378
+ await startHttpTransport(port);
379
+ }
380
+ // ─── OpenAPI Spec ───────────────────────────────────────────────────────────
381
+ async function printOpenApiSpec() {
382
+ const { generateOpenApiSpec } = await import("./openapi.js");
383
+ console.log(JSON.stringify(generateOpenApiSpec(), null, 2));
384
+ process.exit(0);
385
+ }
@@ -0,0 +1,87 @@
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 { BrowserContext, Page } from 'playwright';
12
+ export interface PlatformProfile {
13
+ viewport: {
14
+ width: number;
15
+ height: number;
16
+ };
17
+ userAgent: string;
18
+ locale?: string;
19
+ isMobile?: boolean;
20
+ }
21
+ export type BrowserProvider = 'local' | 'remote';
22
+ export interface BrowserPoolOptions {
23
+ maxContexts?: number;
24
+ sessionDir?: string;
25
+ headless?: boolean;
26
+ /** Browser provider: 'local' (default) or 'remote' (CDP endpoint) */
27
+ provider?: BrowserProvider;
28
+ /** CDP endpoint URL for remote provider (e.g., ws://localhost:9222 or Browserbase URL) */
29
+ cdpUrl?: string;
30
+ }
31
+ export declare class BrowserPool {
32
+ private browser;
33
+ private contexts;
34
+ private activePages;
35
+ private maxContexts;
36
+ private sessionDir;
37
+ private headless;
38
+ private provider;
39
+ private cdpUrl;
40
+ constructor(options?: BrowserPoolOptions);
41
+ /**
42
+ * Launch or connect to a browser based on the configured provider.
43
+ */
44
+ private ensureBrowser;
45
+ /**
46
+ * Local provider: launch Chromium directly via Playwright (default).
47
+ */
48
+ private launchLocal;
49
+ /**
50
+ * Remote provider: connect to an existing CDP endpoint.
51
+ * Set BROWSER_CDP_URL to your endpoint (Browserbase, E2B, etc.)
52
+ */
53
+ private connectViaRemote;
54
+ private getSessionPath;
55
+ private getCookiePath;
56
+ /**
57
+ * Acquire a Page for a specific platform.
58
+ * Creates a BrowserContext if one doesn't exist for this platform.
59
+ * Loads persisted cookies if available.
60
+ */
61
+ acquire(platform: string): Promise<Page>;
62
+ /**
63
+ * Release a page back to the pool. Saves cookies and closes the page.
64
+ */
65
+ release(page: Page): Promise<void>;
66
+ /**
67
+ * Get a persistent context for a platform (reuses existing chrome profile).
68
+ * Used for platforms that need login persistence (Twitter, XHS).
69
+ * Only available with local provider — remote/docker use cookie persistence instead.
70
+ */
71
+ acquirePersistent(platform: string): Promise<{
72
+ context: BrowserContext;
73
+ page: Page;
74
+ }>;
75
+ private getPersistentProfilePath;
76
+ private loadCookies;
77
+ private saveCookies;
78
+ private releaseContext;
79
+ /**
80
+ * Close all contexts and the browser.
81
+ */
82
+ cleanup(): Promise<void>;
83
+ get activeContextCount(): number;
84
+ get activePageCount(): number;
85
+ get currentProvider(): BrowserProvider;
86
+ }
87
+ export declare function getBrowserPool(options?: BrowserPoolOptions): BrowserPool;