@crowdlisten/harness 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +167 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/agent-proxy.d.ts +24 -0
- package/dist/agent-proxy.js +140 -0
- package/dist/agent-tools.d.ts +736 -0
- package/dist/agent-tools.js +409 -0
- package/dist/context/api.d.ts +5 -0
- package/dist/context/api.js +164 -0
- package/dist/context/cli.d.ts +19 -0
- package/dist/context/cli.js +108 -0
- package/dist/context/extractor.d.ts +12 -0
- package/dist/context/extractor.js +43 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +11 -0
- package/dist/context/matcher.d.ts +39 -0
- package/dist/context/matcher.js +246 -0
- package/dist/context/parser.d.ts +28 -0
- package/dist/context/parser.js +157 -0
- package/dist/context/pipeline.d.ts +26 -0
- package/dist/context/pipeline.js +56 -0
- package/dist/context/prompts.d.ts +6 -0
- package/dist/context/prompts.js +60 -0
- package/dist/context/providers.d.ts +6 -0
- package/dist/context/providers.js +106 -0
- package/dist/context/redactor.d.ts +10 -0
- package/dist/context/redactor.js +68 -0
- package/dist/context/server.d.ts +5 -0
- package/dist/context/server.js +134 -0
- package/dist/context/store.d.ts +12 -0
- package/dist/context/store.js +82 -0
- package/dist/context/types.d.ts +79 -0
- package/dist/context/types.js +4 -0
- package/dist/context/user-state.d.ts +40 -0
- package/dist/context/user-state.js +144 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +385 -0
- package/dist/insights/browser/BrowserPool.d.ts +87 -0
- package/dist/insights/browser/BrowserPool.js +266 -0
- package/dist/insights/browser/RequestInterceptor.d.ts +46 -0
- package/dist/insights/browser/RequestInterceptor.js +115 -0
- package/dist/insights/cli.d.ts +8 -0
- package/dist/insights/cli.js +206 -0
- package/dist/insights/core/base/BaseAdapter.d.ts +37 -0
- package/dist/insights/core/base/BaseAdapter.js +123 -0
- package/dist/insights/core/health/HealthMonitor.d.ts +75 -0
- package/dist/insights/core/health/HealthMonitor.js +171 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.d.ts +125 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.js +42 -0
- package/dist/insights/core/utils/DataNormalizer.d.ts +53 -0
- package/dist/insights/core/utils/DataNormalizer.js +349 -0
- package/dist/insights/core/utils/InstagramUrlUtils.d.ts +11 -0
- package/dist/insights/core/utils/InstagramUrlUtils.js +60 -0
- package/dist/insights/core/utils/TikTokUrlUtils.d.ts +10 -0
- package/dist/insights/core/utils/TikTokUrlUtils.js +57 -0
- package/dist/insights/handlers.d.ts +157 -0
- package/dist/insights/handlers.js +246 -0
- package/dist/insights/index.d.ts +437 -0
- package/dist/insights/index.js +426 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.d.ts +34 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.js +342 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.d.ts +31 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.js +227 -0
- package/dist/insights/platforms/reddit/RedditAdapter.d.ts +21 -0
- package/dist/insights/platforms/reddit/RedditAdapter.js +212 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.d.ts +34 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.js +269 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.d.ts +23 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.js +211 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.d.ts +35 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.js +258 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.d.ts +22 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.js +254 -0
- package/dist/insights/service-config.d.ts +7 -0
- package/dist/insights/service-config.js +60 -0
- package/dist/insights/services/UnifiedSocialMediaService.d.ts +94 -0
- package/dist/insights/services/UnifiedSocialMediaService.js +259 -0
- package/dist/insights/vision/VisionExtractor.d.ts +46 -0
- package/dist/insights/vision/VisionExtractor.js +236 -0
- package/dist/learnings.d.ts +50 -0
- package/dist/learnings.js +130 -0
- package/dist/openapi.d.ts +29 -0
- package/dist/openapi.js +169 -0
- package/dist/server-factory.d.ts +20 -0
- package/dist/server-factory.js +41 -0
- package/dist/suggestions.d.ts +16 -0
- package/dist/suggestions.js +72 -0
- package/dist/telemetry.d.ts +44 -0
- package/dist/telemetry.js +93 -0
- package/dist/tools/registry.d.ts +65 -0
- package/dist/tools/registry.js +256 -0
- package/dist/tools.d.ts +2433 -0
- package/dist/tools.js +2294 -0
- package/dist/transport/http.d.ts +15 -0
- package/dist/transport/http.js +154 -0
- package/package.json +76 -0
- package/skills/catalog.json +272 -0
- package/skills/community-catalog.json +4202 -0
- package/skills/competitive-analysis/SKILL.md +174 -0
- package/skills/content-creator/SKILL.md +256 -0
- package/skills/content-strategy/SKILL.md +222 -0
- package/skills/data-storytelling/SKILL.md +248 -0
- package/skills/heuristic-evaluation/SKILL.md +201 -0
- package/skills/market-research-reports/SKILL.md +184 -0
- package/skills/user-stories/SKILL.md +178 -0
- package/skills/ux-researcher/SKILL.md +239 -0
- package/web-dist/assets/index-B1b25lNd.css +1 -0
- package/web-dist/assets/index-CDWHwHbl.js +64 -0
- package/web-dist/index.html +16 -0
|
@@ -0,0 +1,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|