@easyfunnel/mcp 0.1.1 → 0.1.2
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/dist/index.js +1358 -80
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -52,12 +52,46 @@ var ApiClient = class {
|
|
|
52
52
|
`/projects/${projectId}/funnels/${funnelId}?${params.toString()}`
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
|
+
async listFunnels(projectId) {
|
|
56
|
+
return this.request(`/projects/${projectId}/funnels`);
|
|
57
|
+
}
|
|
58
|
+
async deleteFunnel(projectId, funnelId) {
|
|
59
|
+
return this.request(`/projects/${projectId}/funnels/${funnelId}`, {
|
|
60
|
+
method: "DELETE"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async updateFunnel(projectId, funnelId, updates) {
|
|
64
|
+
return this.request(`/projects/${projectId}/funnels/${funnelId}`, {
|
|
65
|
+
method: "PATCH",
|
|
66
|
+
body: JSON.stringify(updates)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async sendTestEvent(projectApiKey, sessionId) {
|
|
70
|
+
const url = `${this.baseUrl}/api/collect`;
|
|
71
|
+
const res = await fetch(url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
api_key: projectApiKey,
|
|
76
|
+
events: [
|
|
77
|
+
{
|
|
78
|
+
session_id: sessionId,
|
|
79
|
+
event_name: "__ef_test",
|
|
80
|
+
properties: { source: "mcp_validation" },
|
|
81
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
return { status: res.status, body: await res.text() };
|
|
87
|
+
}
|
|
55
88
|
async queryEvents(projectId, params) {
|
|
56
89
|
const searchParams = new URLSearchParams();
|
|
57
90
|
searchParams.set("query_type", params.query_type);
|
|
58
91
|
if (params.event_name) searchParams.set("event_name", params.event_name);
|
|
59
92
|
if (params.time_range) searchParams.set("time_range", params.time_range);
|
|
60
93
|
if (params.group_by) searchParams.set("group_by", params.group_by);
|
|
94
|
+
if (params.property_name) searchParams.set("property_name", params.property_name);
|
|
61
95
|
if (params.limit) searchParams.set("limit", params.limit.toString());
|
|
62
96
|
return this.request(
|
|
63
97
|
`/projects/${projectId}/events?${searchParams.toString()}`
|
|
@@ -120,7 +154,7 @@ var import_path = require("path");
|
|
|
120
154
|
var import_child_process = require("child_process");
|
|
121
155
|
var setupSdkDefinition = {
|
|
122
156
|
name: "setup_sdk",
|
|
123
|
-
description: "Install the
|
|
157
|
+
description: "Install the EasyFunnel SDK, write the env var, and wrap the app with the provider. Supports Next.js, Vite, CRA, SvelteKit, and plain HTML.",
|
|
124
158
|
inputSchema: {
|
|
125
159
|
type: "object",
|
|
126
160
|
properties: {
|
|
@@ -134,13 +168,39 @@ var setupSdkDefinition = {
|
|
|
134
168
|
},
|
|
135
169
|
framework: {
|
|
136
170
|
type: "string",
|
|
137
|
-
enum: ["nextjs", "
|
|
171
|
+
enum: ["nextjs", "vite", "cra", "sveltekit", "html"],
|
|
138
172
|
description: "The framework used in the project"
|
|
139
173
|
}
|
|
140
174
|
},
|
|
141
175
|
required: ["project_api_key", "project_root", "framework"]
|
|
142
176
|
}
|
|
143
177
|
};
|
|
178
|
+
var frameworkConfigs = {
|
|
179
|
+
nextjs: {
|
|
180
|
+
envFile: ".env.local",
|
|
181
|
+
envVarName: "NEXT_PUBLIC_EASYFUNNEL_KEY",
|
|
182
|
+
envAccessor: "process.env.NEXT_PUBLIC_EASYFUNNEL_KEY!",
|
|
183
|
+
layoutPaths: ["app/layout.tsx", "app/layout.jsx", "src/app/layout.tsx", "src/app/layout.jsx"]
|
|
184
|
+
},
|
|
185
|
+
vite: {
|
|
186
|
+
envFile: ".env",
|
|
187
|
+
envVarName: "VITE_EASYFUNNEL_KEY",
|
|
188
|
+
envAccessor: "import.meta.env.VITE_EASYFUNNEL_KEY",
|
|
189
|
+
layoutPaths: ["src/App.tsx", "src/App.jsx", "src/main.tsx", "src/main.jsx"]
|
|
190
|
+
},
|
|
191
|
+
cra: {
|
|
192
|
+
envFile: ".env",
|
|
193
|
+
envVarName: "REACT_APP_EASYFUNNEL_KEY",
|
|
194
|
+
envAccessor: "process.env.REACT_APP_EASYFUNNEL_KEY!",
|
|
195
|
+
layoutPaths: ["src/App.tsx", "src/App.jsx", "src/index.tsx", "src/index.jsx"]
|
|
196
|
+
},
|
|
197
|
+
sveltekit: {
|
|
198
|
+
envFile: ".env",
|
|
199
|
+
envVarName: "PUBLIC_EASYFUNNEL_KEY",
|
|
200
|
+
envAccessor: "import.meta.env.PUBLIC_EASYFUNNEL_KEY",
|
|
201
|
+
layoutPaths: ["src/routes/+layout.svelte"]
|
|
202
|
+
}
|
|
203
|
+
};
|
|
144
204
|
function detectPackageManager(projectRoot) {
|
|
145
205
|
if ((0, import_fs.existsSync)((0, import_path.join)(projectRoot, "bun.lockb"))) return "bun";
|
|
146
206
|
if ((0, import_fs.existsSync)((0, import_path.join)(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -149,101 +209,142 @@ function detectPackageManager(projectRoot) {
|
|
|
149
209
|
}
|
|
150
210
|
async function setupSdk(args) {
|
|
151
211
|
const { project_api_key, project_root, framework } = args;
|
|
152
|
-
|
|
212
|
+
if (!project_api_key?.startsWith("ef_")) {
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: `Error: Invalid API key format. Project keys start with "ef_". Got: "${project_api_key || "(empty)"}"
|
|
218
|
+
|
|
219
|
+
Account keys ("efa_") are for the MCP server, not the SDK. Use list_projects to find your project API key.`
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
153
224
|
if (framework === "html") {
|
|
154
225
|
return {
|
|
155
226
|
content: [
|
|
156
227
|
{
|
|
157
228
|
type: "text",
|
|
158
|
-
text: `
|
|
229
|
+
text: `SDK SETUP COMPLETE
|
|
230
|
+
|
|
231
|
+
Add this script tag to your <head>:
|
|
232
|
+
|
|
233
|
+
<script defer data-api-key="${project_api_key}" src="https://easyfunnel.co/sdk.js"></script>
|
|
159
234
|
|
|
160
|
-
|
|
235
|
+
This will automatically track page views and clicks on elements with data-ef-track attributes.
|
|
161
236
|
|
|
162
|
-
|
|
237
|
+
Next: After adding the script, I'll verify everything works with a test event.`
|
|
163
238
|
}
|
|
164
239
|
]
|
|
165
240
|
};
|
|
166
241
|
}
|
|
242
|
+
const config = frameworkConfigs[framework];
|
|
243
|
+
const steps = [];
|
|
244
|
+
const filesModified = [];
|
|
167
245
|
const pm = detectPackageManager(project_root);
|
|
168
246
|
const installCmd = pm === "npm" ? "npm install @easyfunnel/sdk @easyfunnel/react" : `${pm} add @easyfunnel/sdk @easyfunnel/react`;
|
|
169
247
|
try {
|
|
170
248
|
(0, import_child_process.execSync)(installCmd, { cwd: project_root, stdio: "pipe" });
|
|
171
|
-
|
|
249
|
+
steps.push(`[done] Installed @easyfunnel/sdk and @easyfunnel/react via ${pm}`);
|
|
250
|
+
filesModified.push({ file: "package.json", action: "Added SDK dependencies" });
|
|
251
|
+
} catch {
|
|
252
|
+
steps.push(`[skip] Package install failed (may already be installed)`);
|
|
172
253
|
}
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
254
|
+
const envPath = (0, import_path.join)(project_root, config.envFile);
|
|
255
|
+
const envLine = `${config.envVarName}=${project_api_key}`;
|
|
256
|
+
let envWritten = false;
|
|
176
257
|
if ((0, import_fs.existsSync)(envPath)) {
|
|
177
258
|
const content = (0, import_fs.readFileSync)(envPath, "utf-8");
|
|
178
|
-
if (!content.includes(
|
|
179
|
-
(0, import_fs.writeFileSync)(envPath, content + "\n" + envLine + "\n");
|
|
180
|
-
|
|
259
|
+
if (!content.includes(config.envVarName)) {
|
|
260
|
+
(0, import_fs.writeFileSync)(envPath, content.trimEnd() + "\n" + envLine + "\n");
|
|
261
|
+
envWritten = true;
|
|
262
|
+
} else {
|
|
263
|
+
steps.push(`[skip] ${config.envVarName} already exists in ${config.envFile}`);
|
|
181
264
|
}
|
|
182
265
|
} else {
|
|
183
266
|
(0, import_fs.writeFileSync)(envPath, envLine + "\n");
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
if (
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
(
|
|
190
|
-
(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
267
|
+
envWritten = true;
|
|
268
|
+
}
|
|
269
|
+
if (envWritten) {
|
|
270
|
+
const verifyContent = (0, import_fs.readFileSync)(envPath, "utf-8");
|
|
271
|
+
if (verifyContent.includes(project_api_key)) {
|
|
272
|
+
steps.push(`[done] Added ${config.envVarName}=${project_api_key.slice(0, 8)}... to ${config.envFile}`);
|
|
273
|
+
filesModified.push({ file: config.envFile, action: "Added API key" });
|
|
274
|
+
} else {
|
|
275
|
+
steps.push(`[FAIL] Wrote to ${config.envFile} but verification failed`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let providerWrapped = false;
|
|
279
|
+
for (const relPath of config.layoutPaths) {
|
|
280
|
+
const fullPath = (0, import_path.join)(project_root, relPath);
|
|
281
|
+
if (!(0, import_fs.existsSync)(fullPath)) continue;
|
|
282
|
+
let content = (0, import_fs.readFileSync)(fullPath, "utf-8");
|
|
283
|
+
if (content.includes("EasyFunnelProvider")) {
|
|
284
|
+
steps.push(`[skip] EasyFunnelProvider already present in ${relPath}`);
|
|
285
|
+
providerWrapped = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
if (framework === "sveltekit") {
|
|
289
|
+
const importLine2 = `<script>
|
|
290
|
+
import { EasyFunnelProvider } from '@easyfunnel/react'
|
|
291
|
+
</script>
|
|
292
|
+
|
|
293
|
+
`;
|
|
294
|
+
content = importLine2 + content;
|
|
295
|
+
(0, import_fs.writeFileSync)(fullPath, content);
|
|
296
|
+
steps.push(`[done] Added EasyFunnelProvider import to ${relPath}`);
|
|
297
|
+
filesModified.push({ file: relPath, action: "Added provider import" });
|
|
298
|
+
providerWrapped = true;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
const importLine = `import { EasyFunnelProvider } from '@easyfunnel/react'
|
|
302
|
+
`;
|
|
303
|
+
content = importLine + content;
|
|
304
|
+
if (content.includes("{children}")) {
|
|
305
|
+
content = content.replace(
|
|
306
|
+
/(\{children\})/,
|
|
307
|
+
`<EasyFunnelProvider apiKey={${config.envAccessor}}>
|
|
203
308
|
$1
|
|
204
309
|
</EasyFunnelProvider>`
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} else if (framework === "react") {
|
|
213
|
-
const appPaths = [
|
|
214
|
-
(0, import_path.join)(project_root, "src", "App.tsx"),
|
|
215
|
-
(0, import_path.join)(project_root, "src", "App.jsx"),
|
|
216
|
-
(0, import_path.join)(project_root, "App.tsx"),
|
|
217
|
-
(0, import_path.join)(project_root, "App.jsx")
|
|
218
|
-
];
|
|
219
|
-
for (const appPath of appPaths) {
|
|
220
|
-
if ((0, import_fs.existsSync)(appPath)) {
|
|
221
|
-
let content = (0, import_fs.readFileSync)(appPath, "utf-8");
|
|
222
|
-
if (!content.includes("EasyFunnelProvider")) {
|
|
223
|
-
const importLine = `import { EasyFunnelProvider } from '@easyfunnel/react'
|
|
224
|
-
`;
|
|
225
|
-
content = importLine + content;
|
|
226
|
-
(0, import_fs.writeFileSync)(appPath, content);
|
|
227
|
-
filesModified.push(appPath.replace(project_root + "/", ""));
|
|
228
|
-
}
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
310
|
+
);
|
|
311
|
+
steps.push(`[done] Wrapped {children} with <EasyFunnelProvider> in ${relPath}`);
|
|
312
|
+
} else {
|
|
313
|
+
steps.push(`[done] Added EasyFunnelProvider import to ${relPath} (manual wrapping may be needed)`);
|
|
231
314
|
}
|
|
315
|
+
(0, import_fs.writeFileSync)(fullPath, content);
|
|
316
|
+
filesModified.push({ file: relPath, action: "Added provider wrapper" });
|
|
317
|
+
providerWrapped = true;
|
|
318
|
+
break;
|
|
232
319
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
text: `SDK setup complete!
|
|
320
|
+
if (!providerWrapped) {
|
|
321
|
+
steps.push(`[skip] Could not find layout file to add provider. Searched: ${config.layoutPaths.join(", ")}`);
|
|
322
|
+
}
|
|
323
|
+
let output = `SDK SETUP COMPLETE
|
|
238
324
|
|
|
325
|
+
`;
|
|
326
|
+
output += `What I did:
|
|
327
|
+
`;
|
|
328
|
+
for (const step of steps) {
|
|
329
|
+
output += ` ${step}
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
if (filesModified.length > 0) {
|
|
333
|
+
output += `
|
|
239
334
|
Files modified:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
335
|
+
`;
|
|
336
|
+
for (const f of filesModified) {
|
|
337
|
+
output += ` ${f.file.padEnd(20)} ${f.action}
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
output += `
|
|
342
|
+
IMPORTANT: Restart your dev server for the env var to take effect.
|
|
343
|
+
`;
|
|
344
|
+
output += `
|
|
345
|
+
Next: After restarting, I'll verify everything works with a test event.`;
|
|
346
|
+
return {
|
|
347
|
+
content: [{ type: "text", text: output }]
|
|
247
348
|
};
|
|
248
349
|
}
|
|
249
350
|
|
|
@@ -486,6 +587,9 @@ Apply this change to ${file} at line ${line}.`
|
|
|
486
587
|
}
|
|
487
588
|
|
|
488
589
|
// src/tools/create-funnel.ts
|
|
590
|
+
function humanizeEventName(name) {
|
|
591
|
+
return name.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
592
|
+
}
|
|
489
593
|
var createFunnelDefinition = {
|
|
490
594
|
name: "create_funnel",
|
|
491
595
|
description: "Create a conversion funnel from a sequence of events",
|
|
@@ -500,9 +604,9 @@ var createFunnelDefinition = {
|
|
|
500
604
|
type: "object",
|
|
501
605
|
properties: {
|
|
502
606
|
event_name: { type: "string" },
|
|
503
|
-
label: { type: "string" }
|
|
607
|
+
label: { type: "string", description: "Display label (auto-generated from event name if omitted)" }
|
|
504
608
|
},
|
|
505
|
-
required: ["event_name"
|
|
609
|
+
required: ["event_name"]
|
|
506
610
|
},
|
|
507
611
|
description: "Ordered funnel steps"
|
|
508
612
|
}
|
|
@@ -511,12 +615,16 @@ var createFunnelDefinition = {
|
|
|
511
615
|
}
|
|
512
616
|
};
|
|
513
617
|
async function createFunnel(client2, args) {
|
|
618
|
+
const stepsWithLabels = args.steps.map((s) => ({
|
|
619
|
+
event_name: s.event_name,
|
|
620
|
+
label: s.label || humanizeEventName(s.event_name)
|
|
621
|
+
}));
|
|
514
622
|
const funnel = await client2.createFunnel(
|
|
515
623
|
args.project_id,
|
|
516
624
|
args.name,
|
|
517
|
-
|
|
625
|
+
stepsWithLabels
|
|
518
626
|
);
|
|
519
|
-
const stepsDisplay =
|
|
627
|
+
const stepsDisplay = stepsWithLabels.map((s, i) => ` ${i + 1}. ${s.label} (${s.event_name})`).join("\n");
|
|
520
628
|
return {
|
|
521
629
|
content: [
|
|
522
630
|
{
|
|
@@ -591,14 +699,14 @@ Median time to convert: ${data.median_time_to_convert}`;
|
|
|
591
699
|
// src/tools/query-events.ts
|
|
592
700
|
var queryEventsDefinition = {
|
|
593
701
|
name: "query_events",
|
|
594
|
-
description: "Query
|
|
702
|
+
description: "Query event data for a project \u2014 counts, recent events, breakdowns, section engagement, or traffic sources",
|
|
595
703
|
inputSchema: {
|
|
596
704
|
type: "object",
|
|
597
705
|
properties: {
|
|
598
706
|
project_id: { type: "string", description: "Project ID" },
|
|
599
707
|
query_type: {
|
|
600
708
|
type: "string",
|
|
601
|
-
enum: ["count", "recent", "breakdown"],
|
|
709
|
+
enum: ["count", "recent", "breakdown", "section_engagement", "traffic_sources"],
|
|
602
710
|
description: "Type of query"
|
|
603
711
|
},
|
|
604
712
|
event_name: {
|
|
@@ -612,8 +720,12 @@ var queryEventsDefinition = {
|
|
|
612
720
|
},
|
|
613
721
|
group_by: {
|
|
614
722
|
type: "string",
|
|
615
|
-
enum: ["event_name", "url", "browser", "os"],
|
|
616
|
-
description:
|
|
723
|
+
enum: ["event_name", "url", "browser", "os", "property"],
|
|
724
|
+
description: 'Group by field (for breakdown query). Use "property" with property_name for JSONB grouping.'
|
|
725
|
+
},
|
|
726
|
+
property_name: {
|
|
727
|
+
type: "string",
|
|
728
|
+
description: 'Property name to group by when group_by is "property" (e.g. "source", "medium", "section")'
|
|
617
729
|
},
|
|
618
730
|
limit: {
|
|
619
731
|
type: "number",
|
|
@@ -645,6 +757,25 @@ async function queryEvents(client2, args) {
|
|
|
645
757
|
output += "\n";
|
|
646
758
|
}
|
|
647
759
|
if (!data.events?.length) output += " No events found.\n";
|
|
760
|
+
} else if (args.query_type === "section_engagement") {
|
|
761
|
+
output = `Section engagement (${args.time_range || "7d"}):
|
|
762
|
+
|
|
763
|
+
`;
|
|
764
|
+
for (const section of data.sections || []) {
|
|
765
|
+
const dwellSec = (section.avg_dwell_ms / 1e3).toFixed(1);
|
|
766
|
+
output += ` ${section.section}: ${section.views} views, ${section.unique_sessions} sessions, ${dwellSec}s avg dwell
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
if (!data.sections?.length) output += " No section data found.\n";
|
|
770
|
+
} else if (args.query_type === "traffic_sources") {
|
|
771
|
+
output = `Traffic sources (${args.time_range || "7d"}):
|
|
772
|
+
|
|
773
|
+
`;
|
|
774
|
+
for (const source of data.sources || []) {
|
|
775
|
+
output += ` ${source.source} (${source.medium}): ${source.sessions} sessions, ${source.events} events
|
|
776
|
+
`;
|
|
777
|
+
}
|
|
778
|
+
if (!data.sources?.length) output += " No traffic data found.\n";
|
|
648
779
|
} else {
|
|
649
780
|
output = `Event breakdown (${args.time_range || "7d"}):
|
|
650
781
|
|
|
@@ -662,6 +793,1135 @@ async function queryEvents(client2, args) {
|
|
|
662
793
|
};
|
|
663
794
|
}
|
|
664
795
|
|
|
796
|
+
// src/tools/delete-funnel.ts
|
|
797
|
+
var deleteFunnelDefinition = {
|
|
798
|
+
name: "delete_funnel",
|
|
799
|
+
description: "Delete a funnel from the server and get cleanup instructions for removing related tracking code from the codebase",
|
|
800
|
+
inputSchema: {
|
|
801
|
+
type: "object",
|
|
802
|
+
properties: {
|
|
803
|
+
project_id: { type: "string", description: "Project ID" },
|
|
804
|
+
funnel_id: { type: "string", description: "Funnel ID to delete" },
|
|
805
|
+
funnel_slug: {
|
|
806
|
+
type: "string",
|
|
807
|
+
description: 'Funnel slug from easyfunnel.config.ts (e.g. "trial-to-paid") for cleanup instructions'
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
required: ["project_id", "funnel_id"]
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
async function deleteFunnel(client2, args) {
|
|
814
|
+
await client2.deleteFunnel(args.project_id, args.funnel_id);
|
|
815
|
+
let output = `Funnel deleted from server.
|
|
816
|
+
`;
|
|
817
|
+
if (args.funnel_slug) {
|
|
818
|
+
output += `
|
|
819
|
+
To clean up your code:
|
|
820
|
+
`;
|
|
821
|
+
output += `1. Remove the '${args.funnel_slug}' entry from easyfunnel.config.ts (both rules and funnels sections)
|
|
822
|
+
`;
|
|
823
|
+
output += `2. Remove lines with // @ef:${args.funnel_slug} from your codebase:
|
|
824
|
+
`;
|
|
825
|
+
output += ` grep -rn "@ef:${args.funnel_slug}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx"
|
|
826
|
+
`;
|
|
827
|
+
output += `3. Remove any tracking rules whose events only belong to this funnel
|
|
828
|
+
`;
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
content: [{ type: "text", text: output }]
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/tools/update-funnel.ts
|
|
836
|
+
var updateFunnelDefinition = {
|
|
837
|
+
name: "update_funnel",
|
|
838
|
+
description: "Update a funnel name and/or steps",
|
|
839
|
+
inputSchema: {
|
|
840
|
+
type: "object",
|
|
841
|
+
properties: {
|
|
842
|
+
project_id: { type: "string", description: "Project ID" },
|
|
843
|
+
funnel_id: { type: "string", description: "Funnel ID to update" },
|
|
844
|
+
name: { type: "string", description: "New funnel name (optional)" },
|
|
845
|
+
steps: {
|
|
846
|
+
type: "array",
|
|
847
|
+
items: {
|
|
848
|
+
type: "object",
|
|
849
|
+
properties: {
|
|
850
|
+
event_name: { type: "string" },
|
|
851
|
+
label: { type: "string" }
|
|
852
|
+
},
|
|
853
|
+
required: ["event_name", "label"]
|
|
854
|
+
},
|
|
855
|
+
description: "New ordered funnel steps (optional)"
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
required: ["project_id", "funnel_id"]
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
async function updateFunnel(client2, args) {
|
|
862
|
+
const updates = {};
|
|
863
|
+
if (args.name) updates.name = args.name;
|
|
864
|
+
if (args.steps) updates.steps = args.steps;
|
|
865
|
+
const funnel = await client2.updateFunnel(args.project_id, args.funnel_id, updates);
|
|
866
|
+
const stepsDisplay = funnel.steps.sort((a, b) => a.order - b.order).map((s, i) => ` ${i + 1}. ${s.label} (${s.event_name})`).join("\n");
|
|
867
|
+
let output = `Funnel "${funnel.name}" updated!
|
|
868
|
+
|
|
869
|
+
ID: ${funnel.id}
|
|
870
|
+
Steps:
|
|
871
|
+
${stepsDisplay}
|
|
872
|
+
`;
|
|
873
|
+
if (args.steps) {
|
|
874
|
+
output += `
|
|
875
|
+
Remember to update easyfunnel.config.ts to match the new steps.`;
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
content: [{ type: "text", text: output }]
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/tools/scan-codebase.ts
|
|
883
|
+
var import_fs5 = require("fs");
|
|
884
|
+
|
|
885
|
+
// src/lib/scanners.ts
|
|
886
|
+
var import_fs4 = require("fs");
|
|
887
|
+
var import_path3 = require("path");
|
|
888
|
+
function walkDir2(dir, extensions, results = [], maxFiles = 500) {
|
|
889
|
+
if (results.length >= maxFiles) return results;
|
|
890
|
+
try {
|
|
891
|
+
const entries = (0, import_fs4.readdirSync)(dir);
|
|
892
|
+
for (const entry of entries) {
|
|
893
|
+
if (results.length >= maxFiles) break;
|
|
894
|
+
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === ".next" || entry === "build" || entry === "coverage" || entry === "__pycache__") {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const fullPath = (0, import_path3.join)(dir, entry);
|
|
898
|
+
try {
|
|
899
|
+
const stat = (0, import_fs4.statSync)(fullPath);
|
|
900
|
+
if (stat.isDirectory()) {
|
|
901
|
+
walkDir2(fullPath, extensions, results, maxFiles);
|
|
902
|
+
} else if (extensions.some((ext) => entry.endsWith(ext))) {
|
|
903
|
+
results.push(fullPath);
|
|
904
|
+
}
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
return results;
|
|
911
|
+
}
|
|
912
|
+
function readFileSafe(path) {
|
|
913
|
+
try {
|
|
914
|
+
return (0, import_fs4.readFileSync)(path, "utf-8");
|
|
915
|
+
} catch {
|
|
916
|
+
return "";
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function detectFramework(dir) {
|
|
920
|
+
const pkgPath = (0, import_path3.join)(dir, "package.json");
|
|
921
|
+
let framework = "unknown";
|
|
922
|
+
let router = "unknown";
|
|
923
|
+
if ((0, import_fs4.existsSync)(pkgPath)) {
|
|
924
|
+
const pkg = JSON.parse(readFileSafe(pkgPath) || "{}");
|
|
925
|
+
const allDeps = {
|
|
926
|
+
...pkg.dependencies,
|
|
927
|
+
...pkg.devDependencies
|
|
928
|
+
};
|
|
929
|
+
if (allDeps["next"]) {
|
|
930
|
+
framework = "nextjs";
|
|
931
|
+
if ((0, import_fs4.existsSync)((0, import_path3.join)(dir, "app"))) {
|
|
932
|
+
router = "app-router";
|
|
933
|
+
} else if ((0, import_fs4.existsSync)((0, import_path3.join)(dir, "pages"))) {
|
|
934
|
+
router = "pages-router";
|
|
935
|
+
}
|
|
936
|
+
} else if (allDeps["nuxt"] || allDeps["nuxt3"]) {
|
|
937
|
+
framework = "nuxt";
|
|
938
|
+
router = "file-based";
|
|
939
|
+
} else if (allDeps["@sveltejs/kit"]) {
|
|
940
|
+
framework = "sveltekit";
|
|
941
|
+
router = "file-based";
|
|
942
|
+
} else if (allDeps["react-router"] || allDeps["react-router-dom"]) {
|
|
943
|
+
framework = "react-spa";
|
|
944
|
+
router = "react-router";
|
|
945
|
+
} else if (allDeps["vue-router"]) {
|
|
946
|
+
framework = "vue-spa";
|
|
947
|
+
router = "vue-router";
|
|
948
|
+
} else if (allDeps["react"]) {
|
|
949
|
+
framework = "react";
|
|
950
|
+
} else if (allDeps["vue"]) {
|
|
951
|
+
framework = "vue";
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return { framework, router };
|
|
955
|
+
}
|
|
956
|
+
function discoverRoutes(dir, framework, router) {
|
|
957
|
+
const routes = [];
|
|
958
|
+
if (framework === "nextjs" && router === "app-router") {
|
|
959
|
+
const appDir = (0, import_path3.join)(dir, "app");
|
|
960
|
+
if ((0, import_fs4.existsSync)(appDir)) {
|
|
961
|
+
const pageFiles = walkDir2(appDir, ["page.tsx", "page.jsx", "page.ts", "page.js"]);
|
|
962
|
+
for (const f of pageFiles) {
|
|
963
|
+
let route = "/" + (0, import_path3.relative)(appDir, f).replace(/\/page\.(tsx|jsx|ts|js)$/, "").replace(/\([\w-]+\)\//g, "");
|
|
964
|
+
if (route === "/") route = "/";
|
|
965
|
+
else route = route.replace(/\/$/, "");
|
|
966
|
+
routes.push(route);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
} else if (framework === "nextjs" && router === "pages-router") {
|
|
970
|
+
const pagesDir = (0, import_path3.join)(dir, "pages");
|
|
971
|
+
if ((0, import_fs4.existsSync)(pagesDir)) {
|
|
972
|
+
const pageFiles = walkDir2(pagesDir, [".tsx", ".jsx", ".ts", ".js"]);
|
|
973
|
+
for (const f of pageFiles) {
|
|
974
|
+
const name = (0, import_path3.basename)(f).replace((0, import_path3.extname)(f), "");
|
|
975
|
+
if (name.startsWith("_")) continue;
|
|
976
|
+
let route = "/" + (0, import_path3.relative)(pagesDir, f).replace((0, import_path3.extname)(f), "");
|
|
977
|
+
if (route === "/index") route = "/";
|
|
978
|
+
routes.push(route);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
const srcFiles = walkDir2(dir, [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte"], [], 200);
|
|
983
|
+
for (const f of srcFiles) {
|
|
984
|
+
const content = readFileSafe(f);
|
|
985
|
+
const pathMatches = content.matchAll(/path:\s*['"](\/?[a-z][\w/:-]*)['"]/gi);
|
|
986
|
+
for (const m of pathMatches) {
|
|
987
|
+
routes.push(m[1]);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return [...new Set(routes)].sort();
|
|
992
|
+
}
|
|
993
|
+
var detectors = {
|
|
994
|
+
auth(content, _file, relPath) {
|
|
995
|
+
const results = [];
|
|
996
|
+
const patterns = [
|
|
997
|
+
[/createClient|createBrowserClient|createServerClient|supabase\.auth/i, "Supabase Auth", "high"],
|
|
998
|
+
[/NextAuth|getServerSession|useSession/i, "NextAuth", "high"],
|
|
999
|
+
[/ClerkProvider|useUser|useAuth.*clerk/i, "Clerk Auth", "high"],
|
|
1000
|
+
[/firebase\.auth|getAuth|onAuthStateChanged/i, "Firebase Auth", "high"],
|
|
1001
|
+
[/auth0|useAuth0|Auth0Provider/i, "Auth0", "high"],
|
|
1002
|
+
[/signIn|signUp|sign_in|sign_up|login|register/i, "Auth flow", "medium"]
|
|
1003
|
+
];
|
|
1004
|
+
for (const [re, signal, conf] of patterns) {
|
|
1005
|
+
if (re.test(content)) {
|
|
1006
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1007
|
+
results.push({ category: "auth", signal, file: relPath, line, confidence: conf });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return results;
|
|
1011
|
+
},
|
|
1012
|
+
payment(content, _file, relPath) {
|
|
1013
|
+
const results = [];
|
|
1014
|
+
const patterns = [
|
|
1015
|
+
[/stripe|Stripe\(|loadStripe/i, "Stripe", "high"],
|
|
1016
|
+
[/dodo|DodoPayments|DODO_/i, "Dodo Payments", "high"],
|
|
1017
|
+
[/paddle|Paddle\.Setup/i, "Paddle", "high"],
|
|
1018
|
+
[/lemonsqueezy|lemon_squeezy/i, "LemonSqueezy", "high"],
|
|
1019
|
+
[/checkout|createCheckout|payment_intent/i, "Checkout flow", "medium"],
|
|
1020
|
+
[/subscription|plan|pricing/i, "Subscription logic", "medium"]
|
|
1021
|
+
];
|
|
1022
|
+
for (const [re, signal, conf] of patterns) {
|
|
1023
|
+
if (re.test(content)) {
|
|
1024
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1025
|
+
results.push({ category: "payment", signal, file: relPath, line, confidence: conf });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return results;
|
|
1029
|
+
},
|
|
1030
|
+
onboarding(content, _file, relPath) {
|
|
1031
|
+
const results = [];
|
|
1032
|
+
const patterns = [
|
|
1033
|
+
[/welcome|onboarding|getting.?started/i, "Welcome/Onboarding page", "high"],
|
|
1034
|
+
[/wizard|stepper|step\s*[=:]\s*\d/i, "Multi-step wizard", "medium"],
|
|
1035
|
+
[/profile.?form|setup.?profile|complete.?profile/i, "Profile setup", "medium"],
|
|
1036
|
+
[/tutorial|walkthrough|tour/i, "Tutorial flow", "medium"]
|
|
1037
|
+
];
|
|
1038
|
+
for (const [re, signal, conf] of patterns) {
|
|
1039
|
+
if (re.test(content)) {
|
|
1040
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1041
|
+
results.push({ category: "onboarding", signal, file: relPath, line, confidence: conf });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return results;
|
|
1045
|
+
},
|
|
1046
|
+
content(content, _file, relPath) {
|
|
1047
|
+
const results = [];
|
|
1048
|
+
const patterns = [
|
|
1049
|
+
[/video|player|YouTube|Vimeo|<video/i, "Video content", "medium"],
|
|
1050
|
+
[/blog|article|post|markdown|mdx/i, "Blog/Content", "medium"],
|
|
1051
|
+
[/IntersectionObserver|scroll.*observer|onScroll/i, "Scroll tracking", "medium"],
|
|
1052
|
+
[/demo|interactive|playground/i, "Interactive demo", "medium"]
|
|
1053
|
+
];
|
|
1054
|
+
for (const [re, signal, conf] of patterns) {
|
|
1055
|
+
if (re.test(content)) {
|
|
1056
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1057
|
+
results.push({ category: "content", signal, file: relPath, line, confidence: conf });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return results;
|
|
1061
|
+
},
|
|
1062
|
+
pricing(content, _file, relPath) {
|
|
1063
|
+
const results = [];
|
|
1064
|
+
const patterns = [
|
|
1065
|
+
[/monthly|annually|yearly|billing.?cycle/i, "Plan toggle", "high"],
|
|
1066
|
+
[/free|starter|pro|enterprise|indie|startup/i, "Plan names", "medium"],
|
|
1067
|
+
[/upgrade|downgrade|change.?plan/i, "Upgrade prompts", "high"],
|
|
1068
|
+
[/trial|free.?trial|trial.?end/i, "Trial indicator", "high"]
|
|
1069
|
+
];
|
|
1070
|
+
for (const [re, signal, conf] of patterns) {
|
|
1071
|
+
if (re.test(content)) {
|
|
1072
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1073
|
+
results.push({ category: "pricing", signal, file: relPath, line, confidence: conf });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return results;
|
|
1077
|
+
},
|
|
1078
|
+
ecommerce(content, _file, relPath) {
|
|
1079
|
+
const results = [];
|
|
1080
|
+
const patterns = [
|
|
1081
|
+
[/cart|addToCart|add.?to.?cart|shopping.?cart/i, "Cart functionality", "high"],
|
|
1082
|
+
[/product|catalog|inventory/i, "Product pages", "medium"],
|
|
1083
|
+
[/checkout|order.?summary|place.?order/i, "Checkout", "high"],
|
|
1084
|
+
[/wishlist|save.?for.?later|favorites/i, "Wishlist", "medium"],
|
|
1085
|
+
[/search|filter|sort.*products/i, "Product search", "medium"]
|
|
1086
|
+
];
|
|
1087
|
+
for (const [re, signal, conf] of patterns) {
|
|
1088
|
+
if (re.test(content)) {
|
|
1089
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1090
|
+
results.push({ category: "ecommerce", signal, file: relPath, line, confidence: conf });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return results;
|
|
1094
|
+
},
|
|
1095
|
+
forms(content, _file, relPath) {
|
|
1096
|
+
const results = [];
|
|
1097
|
+
const patterns = [
|
|
1098
|
+
[/contact.?form|contact.?us|get.?in.?touch/i, "Contact form", "high"],
|
|
1099
|
+
[/book.?demo|request.?demo|schedule/i, "Demo request", "high"],
|
|
1100
|
+
[/newsletter|subscribe|mailing.?list/i, "Newsletter signup", "medium"],
|
|
1101
|
+
[/gated|download.?form|whitepaper/i, "Gated content", "medium"],
|
|
1102
|
+
[/quiz|calculator|assessment/i, "Quiz/Calculator", "medium"],
|
|
1103
|
+
[/<form|onSubmit|handleSubmit/i, "Form element", "low"]
|
|
1104
|
+
];
|
|
1105
|
+
for (const [re, signal, conf] of patterns) {
|
|
1106
|
+
if (re.test(content)) {
|
|
1107
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1108
|
+
results.push({ category: "forms", signal, file: relPath, line, confidence: conf });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return results;
|
|
1112
|
+
},
|
|
1113
|
+
marketplace(content, _file, relPath) {
|
|
1114
|
+
const results = [];
|
|
1115
|
+
const patterns = [
|
|
1116
|
+
[/listing|createListing|my.?listings/i, "Listing management", "high"],
|
|
1117
|
+
[/seller|vendor|merchant/i, "Seller flows", "medium"],
|
|
1118
|
+
[/booking|reservation|appointment/i, "Booking system", "high"],
|
|
1119
|
+
[/review|rating|stars/i, "Review system", "medium"]
|
|
1120
|
+
];
|
|
1121
|
+
for (const [re, signal, conf] of patterns) {
|
|
1122
|
+
if (re.test(content)) {
|
|
1123
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1124
|
+
results.push({ category: "marketplace", signal, file: relPath, line, confidence: conf });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return results;
|
|
1128
|
+
},
|
|
1129
|
+
community(content, _file, relPath) {
|
|
1130
|
+
const results = [];
|
|
1131
|
+
const patterns = [
|
|
1132
|
+
[/createPost|newPost|post.?editor/i, "Post creation", "high"],
|
|
1133
|
+
[/comment|reply|thread/i, "Comments/Discussion", "medium"],
|
|
1134
|
+
[/invite|referral|share.?link/i, "Invite/Referral", "medium"],
|
|
1135
|
+
[/feed|timeline|activity/i, "Activity feed", "medium"]
|
|
1136
|
+
];
|
|
1137
|
+
for (const [re, signal, conf] of patterns) {
|
|
1138
|
+
if (re.test(content)) {
|
|
1139
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1140
|
+
results.push({ category: "community", signal, file: relPath, line, confidence: conf });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return results;
|
|
1144
|
+
},
|
|
1145
|
+
identity(content, _file, relPath) {
|
|
1146
|
+
const results = [];
|
|
1147
|
+
const patterns = [
|
|
1148
|
+
[/callback|auth\/callback|redirect.*auth/i, "Auth callback", "high"],
|
|
1149
|
+
[/login.?success|authenticated|session.?created/i, "Login success", "medium"],
|
|
1150
|
+
[/session.?restore|refresh.?token|token.?refresh/i, "Session restore", "medium"]
|
|
1151
|
+
];
|
|
1152
|
+
for (const [re, signal, conf] of patterns) {
|
|
1153
|
+
if (re.test(content)) {
|
|
1154
|
+
const line = content.split("\n").findIndex((l) => re.test(l)) + 1;
|
|
1155
|
+
results.push({ category: "identity", signal, file: relPath, line, confidence: conf });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return results;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
function runDetectors(dir) {
|
|
1162
|
+
const files = walkDir2(dir, [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte"]);
|
|
1163
|
+
const allDetections = [];
|
|
1164
|
+
for (const file of files) {
|
|
1165
|
+
const content = readFileSafe(file);
|
|
1166
|
+
if (!content) continue;
|
|
1167
|
+
const relPath = (0, import_path3.relative)(dir, file);
|
|
1168
|
+
for (const detector of Object.values(detectors)) {
|
|
1169
|
+
const results = detector(content, file, relPath);
|
|
1170
|
+
allDetections.push(...results);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return allDetections;
|
|
1174
|
+
}
|
|
1175
|
+
function classifyProduct(detections) {
|
|
1176
|
+
const scores = {
|
|
1177
|
+
saas: 0,
|
|
1178
|
+
ecommerce: 0,
|
|
1179
|
+
content: 0,
|
|
1180
|
+
leadgen: 0,
|
|
1181
|
+
marketplace: 0,
|
|
1182
|
+
community: 0,
|
|
1183
|
+
devtools: 0
|
|
1184
|
+
};
|
|
1185
|
+
for (const d of detections) {
|
|
1186
|
+
switch (d.category) {
|
|
1187
|
+
case "auth":
|
|
1188
|
+
scores.saas += 2;
|
|
1189
|
+
scores.devtools += 1;
|
|
1190
|
+
break;
|
|
1191
|
+
case "payment":
|
|
1192
|
+
scores.saas += 3;
|
|
1193
|
+
scores.ecommerce += 2;
|
|
1194
|
+
break;
|
|
1195
|
+
case "pricing":
|
|
1196
|
+
scores.saas += 3;
|
|
1197
|
+
break;
|
|
1198
|
+
case "onboarding":
|
|
1199
|
+
scores.saas += 2;
|
|
1200
|
+
scores.devtools += 1;
|
|
1201
|
+
break;
|
|
1202
|
+
case "ecommerce":
|
|
1203
|
+
scores.ecommerce += 4;
|
|
1204
|
+
break;
|
|
1205
|
+
case "content":
|
|
1206
|
+
scores.content += 3;
|
|
1207
|
+
break;
|
|
1208
|
+
case "forms":
|
|
1209
|
+
scores.leadgen += 2;
|
|
1210
|
+
break;
|
|
1211
|
+
case "marketplace":
|
|
1212
|
+
scores.marketplace += 4;
|
|
1213
|
+
break;
|
|
1214
|
+
case "community":
|
|
1215
|
+
scores.community += 3;
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
let best = "saas";
|
|
1220
|
+
let bestScore = 0;
|
|
1221
|
+
for (const [type, score] of Object.entries(scores)) {
|
|
1222
|
+
if (score > bestScore) {
|
|
1223
|
+
best = type;
|
|
1224
|
+
bestScore = score;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return { type: best, confidence: Math.min(bestScore / 10, 1) };
|
|
1228
|
+
}
|
|
1229
|
+
var funnelTemplates = {
|
|
1230
|
+
universal: [
|
|
1231
|
+
{
|
|
1232
|
+
slug: "visitor-to-signup",
|
|
1233
|
+
name: "Visitor \u2192 Signup",
|
|
1234
|
+
steps: [
|
|
1235
|
+
{ event_name: "page_view", label: "Landing Page" },
|
|
1236
|
+
{ event_name: "signup_cta_clicked", label: "CTA Clicked" },
|
|
1237
|
+
{ event_name: "signup_submitted", label: "Signup Submitted" },
|
|
1238
|
+
{ event_name: "signup_completed", label: "Signup Completed" }
|
|
1239
|
+
]
|
|
1240
|
+
}
|
|
1241
|
+
],
|
|
1242
|
+
saas: [
|
|
1243
|
+
{
|
|
1244
|
+
slug: "trial-to-paid",
|
|
1245
|
+
name: "Trial \u2192 Paid",
|
|
1246
|
+
steps: [
|
|
1247
|
+
{ event_name: "pricing_viewed", label: "Pricing Viewed" },
|
|
1248
|
+
{ event_name: "plan_selected", label: "Plan Selected" },
|
|
1249
|
+
{ event_name: "checkout_started", label: "Checkout Started" },
|
|
1250
|
+
{ event_name: "payment_completed", label: "Payment Completed" }
|
|
1251
|
+
]
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
slug: "onboarding-completion",
|
|
1255
|
+
name: "Onboarding Completion",
|
|
1256
|
+
steps: [
|
|
1257
|
+
{ event_name: "signup_completed", label: "Signed Up" },
|
|
1258
|
+
{ event_name: "onboarding_started", label: "Onboarding Started" },
|
|
1259
|
+
{ event_name: "onboarding_completed", label: "Onboarding Completed" }
|
|
1260
|
+
]
|
|
1261
|
+
}
|
|
1262
|
+
],
|
|
1263
|
+
ecommerce: [
|
|
1264
|
+
{
|
|
1265
|
+
slug: "browse-to-purchase",
|
|
1266
|
+
name: "Browse \u2192 Purchase",
|
|
1267
|
+
steps: [
|
|
1268
|
+
{ event_name: "product_viewed", label: "Product Viewed" },
|
|
1269
|
+
{ event_name: "add_to_cart", label: "Added to Cart" },
|
|
1270
|
+
{ event_name: "checkout_started", label: "Checkout Started" },
|
|
1271
|
+
{ event_name: "purchase_completed", label: "Purchase Completed" }
|
|
1272
|
+
]
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
slug: "cart-abandonment",
|
|
1276
|
+
name: "Cart Abandonment",
|
|
1277
|
+
steps: [
|
|
1278
|
+
{ event_name: "add_to_cart", label: "Added to Cart" },
|
|
1279
|
+
{ event_name: "cart_viewed", label: "Cart Viewed" },
|
|
1280
|
+
{ event_name: "checkout_started", label: "Checkout Started" },
|
|
1281
|
+
{ event_name: "purchase_completed", label: "Purchase Completed" }
|
|
1282
|
+
]
|
|
1283
|
+
}
|
|
1284
|
+
],
|
|
1285
|
+
content: [
|
|
1286
|
+
{
|
|
1287
|
+
slug: "content-to-signup",
|
|
1288
|
+
name: "Content \u2192 Signup",
|
|
1289
|
+
steps: [
|
|
1290
|
+
{ event_name: "article_opened", label: "Article Opened" },
|
|
1291
|
+
{ event_name: "scroll_25_pct", label: "Scrolled 25%" },
|
|
1292
|
+
{ event_name: "signup_cta_clicked", label: "CTA Clicked" },
|
|
1293
|
+
{ event_name: "signup_submitted", label: "Signup Submitted" }
|
|
1294
|
+
]
|
|
1295
|
+
}
|
|
1296
|
+
],
|
|
1297
|
+
leadgen: [
|
|
1298
|
+
{
|
|
1299
|
+
slug: "lead-capture",
|
|
1300
|
+
name: "Lead Capture",
|
|
1301
|
+
steps: [
|
|
1302
|
+
{ event_name: "page_view", label: "Landing Page" },
|
|
1303
|
+
{ event_name: "form_viewed", label: "Form Viewed" },
|
|
1304
|
+
{ event_name: "form_focused", label: "Form Focused" },
|
|
1305
|
+
{ event_name: "form_submitted", label: "Form Submitted" }
|
|
1306
|
+
]
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
slug: "demo-request",
|
|
1310
|
+
name: "Demo Request",
|
|
1311
|
+
steps: [
|
|
1312
|
+
{ event_name: "pricing_viewed", label: "Pricing Viewed" },
|
|
1313
|
+
{ event_name: "demo_requested", label: "Demo Requested" }
|
|
1314
|
+
]
|
|
1315
|
+
}
|
|
1316
|
+
],
|
|
1317
|
+
marketplace: [
|
|
1318
|
+
{
|
|
1319
|
+
slug: "buyer-journey",
|
|
1320
|
+
name: "Buyer Journey",
|
|
1321
|
+
steps: [
|
|
1322
|
+
{ event_name: "search_performed", label: "Search Performed" },
|
|
1323
|
+
{ event_name: "listing_viewed", label: "Listing Viewed" },
|
|
1324
|
+
{ event_name: "booking_completed", label: "Booking Completed" }
|
|
1325
|
+
]
|
|
1326
|
+
}
|
|
1327
|
+
],
|
|
1328
|
+
community: [
|
|
1329
|
+
{
|
|
1330
|
+
slug: "lurker-to-contributor",
|
|
1331
|
+
name: "Lurker \u2192 Contributor",
|
|
1332
|
+
steps: [
|
|
1333
|
+
{ event_name: "page_view", label: "Feed Viewed" },
|
|
1334
|
+
{ event_name: "post_created", label: "Post Created" },
|
|
1335
|
+
{ event_name: "comment_posted", label: "Comment Posted" }
|
|
1336
|
+
]
|
|
1337
|
+
}
|
|
1338
|
+
],
|
|
1339
|
+
devtools: [
|
|
1340
|
+
{
|
|
1341
|
+
slug: "docs-to-integration",
|
|
1342
|
+
name: "Docs \u2192 Integration",
|
|
1343
|
+
steps: [
|
|
1344
|
+
{ event_name: "page_view", label: "Docs Viewed" },
|
|
1345
|
+
{ event_name: "signup_completed", label: "Signed Up" },
|
|
1346
|
+
{ event_name: "onboarding_completed", label: "Integration Complete" }
|
|
1347
|
+
]
|
|
1348
|
+
}
|
|
1349
|
+
]
|
|
1350
|
+
};
|
|
1351
|
+
function selectFunnels(productType, detections, existingFunnels) {
|
|
1352
|
+
const existingNames = new Set(existingFunnels.map((f) => f.name?.toLowerCase()));
|
|
1353
|
+
const selected = [];
|
|
1354
|
+
const hasAuth = detections.some((d) => d.category === "auth" && d.confidence === "high");
|
|
1355
|
+
const hasPayment = detections.some((d) => d.category === "payment" && d.confidence === "high");
|
|
1356
|
+
if (hasAuth) {
|
|
1357
|
+
const signup = funnelTemplates.universal[0];
|
|
1358
|
+
if (!existingNames.has(signup.name.toLowerCase())) {
|
|
1359
|
+
selected.push(signup);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (hasPayment) {
|
|
1363
|
+
const revenueFunnels = [
|
|
1364
|
+
...funnelTemplates.saas || [],
|
|
1365
|
+
...funnelTemplates.ecommerce || []
|
|
1366
|
+
];
|
|
1367
|
+
const revFunnel = revenueFunnels.find((f) => !existingNames.has(f.name.toLowerCase()));
|
|
1368
|
+
if (revFunnel) selected.push(revFunnel);
|
|
1369
|
+
}
|
|
1370
|
+
const typeFunnels = funnelTemplates[productType.type] || [];
|
|
1371
|
+
for (const f of typeFunnels) {
|
|
1372
|
+
if (selected.length >= 6) break;
|
|
1373
|
+
if (!existingNames.has(f.name.toLowerCase()) && !selected.some((s) => s.slug === f.slug)) {
|
|
1374
|
+
selected.push(f);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return selected.slice(0, 6);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/tools/scan-codebase.ts
|
|
1381
|
+
var scanCodebaseDefinition = {
|
|
1382
|
+
name: "scan_codebase",
|
|
1383
|
+
description: "Scan a project directory and report what was found: framework, routes, product type, and detected patterns. Does NOT write any files or create funnels \u2014 just reports findings for the user to review.",
|
|
1384
|
+
inputSchema: {
|
|
1385
|
+
type: "object",
|
|
1386
|
+
properties: {
|
|
1387
|
+
directory: {
|
|
1388
|
+
type: "string",
|
|
1389
|
+
description: "Absolute path to the project root directory"
|
|
1390
|
+
},
|
|
1391
|
+
project_id: {
|
|
1392
|
+
type: "string",
|
|
1393
|
+
description: "EasyFunnel project ID to check existing funnels (optional)"
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
required: ["directory"]
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
async function scanCodebase(client2, args) {
|
|
1400
|
+
const dir = args.directory;
|
|
1401
|
+
if (!(0, import_fs5.existsSync)(dir)) {
|
|
1402
|
+
return {
|
|
1403
|
+
content: [
|
|
1404
|
+
{
|
|
1405
|
+
type: "text",
|
|
1406
|
+
text: `Error: Directory "${dir}" does not exist.`
|
|
1407
|
+
}
|
|
1408
|
+
]
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
const { framework, router } = detectFramework(dir);
|
|
1412
|
+
const routes = discoverRoutes(dir, framework, router);
|
|
1413
|
+
const detections = runDetectors(dir);
|
|
1414
|
+
let existingFunnelCount = 0;
|
|
1415
|
+
if (args.project_id) {
|
|
1416
|
+
try {
|
|
1417
|
+
const funnels = await client2.listFunnels(args.project_id);
|
|
1418
|
+
existingFunnelCount = funnels.length;
|
|
1419
|
+
} catch {
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const productType = classifyProduct(detections);
|
|
1423
|
+
const byCategory = {};
|
|
1424
|
+
for (const d of detections) {
|
|
1425
|
+
if (!byCategory[d.category]) byCategory[d.category] = [];
|
|
1426
|
+
byCategory[d.category].push(d);
|
|
1427
|
+
}
|
|
1428
|
+
const categories = Object.keys(byCategory);
|
|
1429
|
+
let report = `SCAN COMPLETE
|
|
1430
|
+
|
|
1431
|
+
`;
|
|
1432
|
+
report += `Your project at a glance:
|
|
1433
|
+
`;
|
|
1434
|
+
report += ` Framework: ${framework}${router !== "unknown" ? ` (${router})` : ""}
|
|
1435
|
+
`;
|
|
1436
|
+
report += ` Routes found: ${routes.length}
|
|
1437
|
+
`;
|
|
1438
|
+
report += ` Product type: ${productType.type} (${Math.round(productType.confidence * 100)}% confidence)
|
|
1439
|
+
|
|
1440
|
+
`;
|
|
1441
|
+
report += `What I detected (${detections.length} signals across ${categories.length} categories):
|
|
1442
|
+
`;
|
|
1443
|
+
for (const cat of categories) {
|
|
1444
|
+
const signals = byCategory[cat];
|
|
1445
|
+
const uniqueSignals = [...new Set(signals.map((s) => s.signal))];
|
|
1446
|
+
report += ` ${cat.charAt(0).toUpperCase() + cat.slice(1).padEnd(12)}: ${uniqueSignals.join(", ")}
|
|
1447
|
+
`;
|
|
1448
|
+
}
|
|
1449
|
+
if (args.project_id) {
|
|
1450
|
+
report += `
|
|
1451
|
+
Existing funnels on server: ${existingFunnelCount}
|
|
1452
|
+
`;
|
|
1453
|
+
}
|
|
1454
|
+
report += `
|
|
1455
|
+
Next: I can set up the SDK in your project, or suggest conversion funnels based on these findings.`;
|
|
1456
|
+
const scanData = {
|
|
1457
|
+
framework,
|
|
1458
|
+
router,
|
|
1459
|
+
routes,
|
|
1460
|
+
product_type: productType.type,
|
|
1461
|
+
product_confidence: productType.confidence,
|
|
1462
|
+
detection_categories: categories,
|
|
1463
|
+
detection_count: detections.length,
|
|
1464
|
+
detections: detections.map((d) => ({
|
|
1465
|
+
category: d.category,
|
|
1466
|
+
signal: d.signal,
|
|
1467
|
+
file: d.file,
|
|
1468
|
+
line: d.line,
|
|
1469
|
+
confidence: d.confidence
|
|
1470
|
+
}))
|
|
1471
|
+
};
|
|
1472
|
+
return {
|
|
1473
|
+
content: [
|
|
1474
|
+
{ type: "text", text: report },
|
|
1475
|
+
{ type: "text", text: "```json\n" + JSON.stringify(scanData, null, 2) + "\n```" }
|
|
1476
|
+
]
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/tools/validate-setup.ts
|
|
1481
|
+
var import_fs6 = require("fs");
|
|
1482
|
+
var import_path4 = require("path");
|
|
1483
|
+
var validateSetupDefinition = {
|
|
1484
|
+
name: "validate_setup",
|
|
1485
|
+
description: "Verify that the EasyFunnel SDK is correctly set up: checks env var, provider presence, sends a test event, and confirms it arrives in the database.",
|
|
1486
|
+
inputSchema: {
|
|
1487
|
+
type: "object",
|
|
1488
|
+
properties: {
|
|
1489
|
+
project_root: {
|
|
1490
|
+
type: "string",
|
|
1491
|
+
description: "Absolute path to the project root directory"
|
|
1492
|
+
},
|
|
1493
|
+
project_id: {
|
|
1494
|
+
type: "string",
|
|
1495
|
+
description: "EasyFunnel project ID to verify against"
|
|
1496
|
+
},
|
|
1497
|
+
project_api_key: {
|
|
1498
|
+
type: "string",
|
|
1499
|
+
description: "Project API key (ef_...) \u2014 if not provided, will be read from env file"
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
required: ["project_root", "project_id"]
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
function readEnvFile(projectRoot) {
|
|
1506
|
+
const envFiles = [
|
|
1507
|
+
{ file: ".env.local", varNames: ["NEXT_PUBLIC_EASYFUNNEL_KEY"] },
|
|
1508
|
+
{ file: ".env", varNames: ["VITE_EASYFUNNEL_KEY", "REACT_APP_EASYFUNNEL_KEY", "PUBLIC_EASYFUNNEL_KEY"] }
|
|
1509
|
+
];
|
|
1510
|
+
for (const { file, varNames } of envFiles) {
|
|
1511
|
+
const envPath = (0, import_path4.join)(projectRoot, file);
|
|
1512
|
+
if (!(0, import_fs6.existsSync)(envPath)) continue;
|
|
1513
|
+
const content = (0, import_fs6.readFileSync)(envPath, "utf-8");
|
|
1514
|
+
for (const varName of varNames) {
|
|
1515
|
+
const match = content.match(new RegExp(`^${varName}=(.+)$`, "m"));
|
|
1516
|
+
if (match) {
|
|
1517
|
+
return { file, varName, value: match[1].trim() };
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
function findProviderFile(projectRoot) {
|
|
1524
|
+
const candidates = [
|
|
1525
|
+
"app/layout.tsx",
|
|
1526
|
+
"app/layout.jsx",
|
|
1527
|
+
"src/app/layout.tsx",
|
|
1528
|
+
"src/app/layout.jsx",
|
|
1529
|
+
"src/App.tsx",
|
|
1530
|
+
"src/App.jsx",
|
|
1531
|
+
"src/main.tsx",
|
|
1532
|
+
"src/main.jsx"
|
|
1533
|
+
];
|
|
1534
|
+
for (const relPath of candidates) {
|
|
1535
|
+
const fullPath = (0, import_path4.join)(projectRoot, relPath);
|
|
1536
|
+
if ((0, import_fs6.existsSync)(fullPath)) {
|
|
1537
|
+
const content = (0, import_fs6.readFileSync)(fullPath, "utf-8");
|
|
1538
|
+
if (content.includes("EasyFunnelProvider")) {
|
|
1539
|
+
return relPath;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
async function validateSetup(client2, args) {
|
|
1546
|
+
const { project_root, project_id } = args;
|
|
1547
|
+
const checks = [];
|
|
1548
|
+
const envResult = readEnvFile(project_root);
|
|
1549
|
+
let apiKey2 = args.project_api_key || "";
|
|
1550
|
+
if (envResult) {
|
|
1551
|
+
apiKey2 = apiKey2 || envResult.value;
|
|
1552
|
+
if (envResult.value && envResult.value.startsWith("ef_")) {
|
|
1553
|
+
checks.push({
|
|
1554
|
+
name: `${envResult.file} contains ${envResult.varName}`,
|
|
1555
|
+
passed: true,
|
|
1556
|
+
detail: `${envResult.varName}=${envResult.value.slice(0, 8)}...`
|
|
1557
|
+
});
|
|
1558
|
+
} else if (envResult.value) {
|
|
1559
|
+
checks.push({
|
|
1560
|
+
name: `${envResult.file} contains ${envResult.varName}`,
|
|
1561
|
+
passed: false,
|
|
1562
|
+
detail: `Value doesn't start with "ef_". Got: ${envResult.value.slice(0, 20)}`
|
|
1563
|
+
});
|
|
1564
|
+
} else {
|
|
1565
|
+
checks.push({
|
|
1566
|
+
name: `${envResult.file} contains ${envResult.varName}`,
|
|
1567
|
+
passed: false,
|
|
1568
|
+
detail: `${envResult.varName} is empty`
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
} else {
|
|
1572
|
+
checks.push({
|
|
1573
|
+
name: "Env file contains API key",
|
|
1574
|
+
passed: false,
|
|
1575
|
+
detail: "No EasyFunnel API key found in .env.local or .env"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
const providerFile = findProviderFile(project_root);
|
|
1579
|
+
if (providerFile) {
|
|
1580
|
+
checks.push({
|
|
1581
|
+
name: `EasyFunnelProvider found in ${providerFile}`,
|
|
1582
|
+
passed: true,
|
|
1583
|
+
detail: "Provider is wrapping the app"
|
|
1584
|
+
});
|
|
1585
|
+
} else {
|
|
1586
|
+
checks.push({
|
|
1587
|
+
name: "EasyFunnelProvider found",
|
|
1588
|
+
passed: false,
|
|
1589
|
+
detail: "EasyFunnelProvider not found in any layout or app file"
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
if (!apiKey2 || !apiKey2.startsWith("ef_")) {
|
|
1593
|
+
checks.push({
|
|
1594
|
+
name: "Test event sent to /api/collect",
|
|
1595
|
+
passed: false,
|
|
1596
|
+
detail: "Cannot send test event without a valid API key (ef_...)"
|
|
1597
|
+
});
|
|
1598
|
+
} else {
|
|
1599
|
+
const sessionId = `test_${Date.now()}`;
|
|
1600
|
+
try {
|
|
1601
|
+
const result = await client2.sendTestEvent(apiKey2, sessionId);
|
|
1602
|
+
if (result.status === 202) {
|
|
1603
|
+
checks.push({
|
|
1604
|
+
name: "Test event sent to /api/collect",
|
|
1605
|
+
passed: true,
|
|
1606
|
+
detail: `HTTP ${result.status} \u2014 accepted`
|
|
1607
|
+
});
|
|
1608
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1609
|
+
try {
|
|
1610
|
+
const events = await client2.queryEvents(project_id, {
|
|
1611
|
+
query_type: "recent",
|
|
1612
|
+
event_name: "__ef_test",
|
|
1613
|
+
limit: 1
|
|
1614
|
+
});
|
|
1615
|
+
const found = Array.isArray(events) && events.length > 0;
|
|
1616
|
+
checks.push({
|
|
1617
|
+
name: "Test event confirmed in database",
|
|
1618
|
+
passed: found,
|
|
1619
|
+
detail: found ? "Event arrived and is stored" : "Event was accepted but not yet visible in database (may take a moment)"
|
|
1620
|
+
});
|
|
1621
|
+
} catch {
|
|
1622
|
+
checks.push({
|
|
1623
|
+
name: "Test event confirmed in database",
|
|
1624
|
+
passed: false,
|
|
1625
|
+
detail: "Could not query events to verify"
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
} else {
|
|
1629
|
+
const hint = result.status === 400 ? "Check your API key is correct." : result.status === 401 ? "API key is invalid." : result.status === 403 ? "Domain may not be whitelisted." : "";
|
|
1630
|
+
checks.push({
|
|
1631
|
+
name: "Test event sent to /api/collect",
|
|
1632
|
+
passed: false,
|
|
1633
|
+
detail: `HTTP ${result.status}. ${hint}`
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
checks.push({
|
|
1638
|
+
name: "Test event sent to /api/collect",
|
|
1639
|
+
passed: false,
|
|
1640
|
+
detail: `Network error: ${err.message}`
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const allPassed = checks.every((c) => c.passed);
|
|
1645
|
+
const failedChecks = checks.filter((c) => !c.passed);
|
|
1646
|
+
let output = "";
|
|
1647
|
+
if (allPassed) {
|
|
1648
|
+
output += `SETUP VERIFIED
|
|
1649
|
+
|
|
1650
|
+
All checks passed:
|
|
1651
|
+
`;
|
|
1652
|
+
} else {
|
|
1653
|
+
output += `SETUP CHECK FAILED
|
|
1654
|
+
|
|
1655
|
+
`;
|
|
1656
|
+
}
|
|
1657
|
+
for (const check of checks) {
|
|
1658
|
+
const icon = check.passed ? "[pass]" : "[FAIL]";
|
|
1659
|
+
output += ` ${icon} ${check.name}
|
|
1660
|
+
`;
|
|
1661
|
+
if (!check.passed) {
|
|
1662
|
+
output += ` ${check.detail}
|
|
1663
|
+
`;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (allPassed) {
|
|
1667
|
+
output += `
|
|
1668
|
+
Your tracking pipeline is working end-to-end.
|
|
1669
|
+
`;
|
|
1670
|
+
output += `
|
|
1671
|
+
Next: Let me suggest conversion funnels based on what I found in your codebase.`;
|
|
1672
|
+
} else {
|
|
1673
|
+
output += `
|
|
1674
|
+
`;
|
|
1675
|
+
for (const check of failedChecks) {
|
|
1676
|
+
if (check.name.includes("Env file") || check.name.includes("API key")) {
|
|
1677
|
+
const varName = envResult?.varName || "NEXT_PUBLIC_EASYFUNNEL_KEY";
|
|
1678
|
+
const envFile = envResult?.file || ".env.local";
|
|
1679
|
+
output += `To fix: Add your project API key to ${envFile}:
|
|
1680
|
+
`;
|
|
1681
|
+
output += ` ${varName}=ef_your_key_here
|
|
1682
|
+
`;
|
|
1683
|
+
output += `Then restart your dev server.
|
|
1684
|
+
|
|
1685
|
+
`;
|
|
1686
|
+
} else if (check.name.includes("Provider")) {
|
|
1687
|
+
output += `To fix: Wrap your app with <EasyFunnelProvider>. Run setup_sdk to do this automatically.
|
|
1688
|
+
|
|
1689
|
+
`;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
content: [{ type: "text", text: output }]
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/tools/recommend-funnels.ts
|
|
1699
|
+
var import_fs7 = require("fs");
|
|
1700
|
+
var recommendFunnelsDefinition = {
|
|
1701
|
+
name: "recommend_funnels",
|
|
1702
|
+
description: "Recommend conversion funnels based on product type and detected patterns. Presents choices with WHY explanations for the user to approve before creating.",
|
|
1703
|
+
inputSchema: {
|
|
1704
|
+
type: "object",
|
|
1705
|
+
properties: {
|
|
1706
|
+
project_id: {
|
|
1707
|
+
type: "string",
|
|
1708
|
+
description: "EasyFunnel project ID"
|
|
1709
|
+
},
|
|
1710
|
+
product_type: {
|
|
1711
|
+
type: "string",
|
|
1712
|
+
description: "Product type from scan_codebase (saas, ecommerce, content, leadgen, marketplace, community, devtools)"
|
|
1713
|
+
},
|
|
1714
|
+
detection_categories: {
|
|
1715
|
+
type: "array",
|
|
1716
|
+
items: { type: "string" },
|
|
1717
|
+
description: 'Detection categories from scan_codebase (e.g. ["auth", "payment", "pricing"])'
|
|
1718
|
+
},
|
|
1719
|
+
directory: {
|
|
1720
|
+
type: "string",
|
|
1721
|
+
description: "Project directory \u2014 if provided, will re-scan for fresh detections"
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
1724
|
+
required: ["project_id", "product_type", "detection_categories"]
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
var funnelExplanations = {
|
|
1728
|
+
"visitor-to-signup": "You have auth (login/signup pages). This tracks your top-of-funnel acquisition \u2014 how visitors become users.",
|
|
1729
|
+
"trial-to-paid": "You have payment integration. This is your revenue pipeline \u2014 how users go from browsing pricing to paying.",
|
|
1730
|
+
"onboarding-completion": "You have onboarding flows. This tracks whether new signups actually complete setup and start using the product.",
|
|
1731
|
+
"browse-to-purchase": "You have product pages and checkout. This tracks the full purchase journey from browsing to buying.",
|
|
1732
|
+
"cart-abandonment": "You have cart functionality. This reveals where shoppers drop off between adding items and completing purchase.",
|
|
1733
|
+
"content-to-signup": "You have content/blog pages. This tracks whether your content drives signups \u2014 the content marketing funnel.",
|
|
1734
|
+
"lead-capture": "You have forms. This tracks how visitors interact with your lead capture \u2014 from viewing to submitting.",
|
|
1735
|
+
"demo-request": "You have a pricing page. This tracks the high-intent path from pricing to requesting a demo.",
|
|
1736
|
+
"buyer-journey": "You have listings/search. This tracks the buyer journey from searching to completing a transaction.",
|
|
1737
|
+
"lurker-to-contributor": "You have community features. This tracks the lurker-to-contributor pipeline \u2014 the core community health metric.",
|
|
1738
|
+
"docs-to-integration": "You have developer docs. This tracks whether docs visitors actually integrate \u2014 the devtools growth metric."
|
|
1739
|
+
};
|
|
1740
|
+
async function recommendFunnels(client2, args) {
|
|
1741
|
+
const { project_id, product_type, detection_categories } = args;
|
|
1742
|
+
let existingFunnels = [];
|
|
1743
|
+
try {
|
|
1744
|
+
existingFunnels = await client2.listFunnels(project_id);
|
|
1745
|
+
} catch {
|
|
1746
|
+
}
|
|
1747
|
+
let detections = [];
|
|
1748
|
+
if (args.directory && (0, import_fs7.existsSync)(args.directory)) {
|
|
1749
|
+
detections = runDetectors(args.directory);
|
|
1750
|
+
} else {
|
|
1751
|
+
for (const cat of detection_categories) {
|
|
1752
|
+
detections.push({
|
|
1753
|
+
category: cat,
|
|
1754
|
+
signal: cat,
|
|
1755
|
+
file: "",
|
|
1756
|
+
line: 0,
|
|
1757
|
+
confidence: "high"
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
const productTypeObj = {
|
|
1762
|
+
type: product_type,
|
|
1763
|
+
confidence: 0.8
|
|
1764
|
+
};
|
|
1765
|
+
const recommended = selectFunnels(productTypeObj, detections, existingFunnels);
|
|
1766
|
+
if (recommended.length === 0) {
|
|
1767
|
+
return {
|
|
1768
|
+
content: [
|
|
1769
|
+
{
|
|
1770
|
+
type: "text",
|
|
1771
|
+
text: `RECOMMENDED FUNNELS
|
|
1772
|
+
|
|
1773
|
+
No new funnels to recommend \u2014 you already have ${existingFunnels.length} funnel(s) covering the key flows.
|
|
1774
|
+
|
|
1775
|
+
Tip: Use get_funnel_health to check how your existing funnels are performing.`
|
|
1776
|
+
}
|
|
1777
|
+
]
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
let output = `RECOMMENDED FUNNELS
|
|
1781
|
+
|
|
1782
|
+
`;
|
|
1783
|
+
output += `Based on your ${product_type} product`;
|
|
1784
|
+
if (detection_categories.length > 0) {
|
|
1785
|
+
output += ` with ${detection_categories.join(" + ")}`;
|
|
1786
|
+
}
|
|
1787
|
+
output += `:
|
|
1788
|
+
|
|
1789
|
+
`;
|
|
1790
|
+
for (let i = 0; i < recommended.length; i++) {
|
|
1791
|
+
const funnel = recommended[i];
|
|
1792
|
+
output += `${i + 1}. ${funnel.name}
|
|
1793
|
+
`;
|
|
1794
|
+
output += ` ${funnel.steps.map((s) => s.event_name).join(" \u2192 ")}
|
|
1795
|
+
`;
|
|
1796
|
+
const why = funnelExplanations[funnel.slug];
|
|
1797
|
+
if (why) {
|
|
1798
|
+
output += ` WHY: ${why}
|
|
1799
|
+
`;
|
|
1800
|
+
}
|
|
1801
|
+
output += `
|
|
1802
|
+
`;
|
|
1803
|
+
}
|
|
1804
|
+
output += `Which funnels should I create? (all / numbers like "1 and 2" / none)`;
|
|
1805
|
+
const funnelData = recommended.map((f) => ({
|
|
1806
|
+
name: f.name,
|
|
1807
|
+
steps: f.steps
|
|
1808
|
+
}));
|
|
1809
|
+
return {
|
|
1810
|
+
content: [
|
|
1811
|
+
{ type: "text", text: output },
|
|
1812
|
+
{
|
|
1813
|
+
type: "text",
|
|
1814
|
+
text: "```json\n" + JSON.stringify({ recommended_funnels: funnelData }, null, 2) + "\n```"
|
|
1815
|
+
}
|
|
1816
|
+
]
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/tools/create-funnels-batch.ts
|
|
1821
|
+
function humanizeEventName2(name) {
|
|
1822
|
+
return name.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1823
|
+
}
|
|
1824
|
+
var createFunnelsBatchDefinition = {
|
|
1825
|
+
name: "create_funnels_batch",
|
|
1826
|
+
description: "Create multiple funnels at once. Use after recommend_funnels to create the user-approved funnels.",
|
|
1827
|
+
inputSchema: {
|
|
1828
|
+
type: "object",
|
|
1829
|
+
properties: {
|
|
1830
|
+
project_id: {
|
|
1831
|
+
type: "string",
|
|
1832
|
+
description: "EasyFunnel project ID"
|
|
1833
|
+
},
|
|
1834
|
+
funnels: {
|
|
1835
|
+
type: "array",
|
|
1836
|
+
items: {
|
|
1837
|
+
type: "object",
|
|
1838
|
+
properties: {
|
|
1839
|
+
name: {
|
|
1840
|
+
type: "string",
|
|
1841
|
+
description: "Funnel name"
|
|
1842
|
+
},
|
|
1843
|
+
steps: {
|
|
1844
|
+
type: "array",
|
|
1845
|
+
items: {
|
|
1846
|
+
type: "object",
|
|
1847
|
+
properties: {
|
|
1848
|
+
event_name: { type: "string" },
|
|
1849
|
+
label: { type: "string" }
|
|
1850
|
+
},
|
|
1851
|
+
required: ["event_name"]
|
|
1852
|
+
},
|
|
1853
|
+
description: "Funnel steps in order"
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
required: ["name", "steps"]
|
|
1857
|
+
},
|
|
1858
|
+
description: "Array of funnels to create"
|
|
1859
|
+
}
|
|
1860
|
+
},
|
|
1861
|
+
required: ["project_id", "funnels"]
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
async function createFunnelsBatch(client2, args) {
|
|
1865
|
+
const { project_id, funnels } = args;
|
|
1866
|
+
if (!funnels || funnels.length === 0) {
|
|
1867
|
+
return {
|
|
1868
|
+
content: [
|
|
1869
|
+
{
|
|
1870
|
+
type: "text",
|
|
1871
|
+
text: "No funnels provided. Use recommend_funnels first to get suggestions."
|
|
1872
|
+
}
|
|
1873
|
+
]
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
const results = [];
|
|
1877
|
+
for (const funnel of funnels) {
|
|
1878
|
+
try {
|
|
1879
|
+
const stepsWithLabels = funnel.steps.map((s) => ({
|
|
1880
|
+
event_name: s.event_name,
|
|
1881
|
+
label: s.label || humanizeEventName2(s.event_name)
|
|
1882
|
+
}));
|
|
1883
|
+
await client2.createFunnel(project_id, funnel.name, stepsWithLabels);
|
|
1884
|
+
results.push({ name: funnel.name, success: true, stepCount: funnel.steps.length });
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
results.push({
|
|
1887
|
+
name: funnel.name,
|
|
1888
|
+
success: false,
|
|
1889
|
+
error: err.message,
|
|
1890
|
+
stepCount: funnel.steps.length
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const created = results.filter((r) => r.success);
|
|
1895
|
+
const failed = results.filter((r) => !r.success);
|
|
1896
|
+
let output = `FUNNELS CREATED
|
|
1897
|
+
|
|
1898
|
+
`;
|
|
1899
|
+
for (const r of results) {
|
|
1900
|
+
if (r.success) {
|
|
1901
|
+
output += ` [created] ${r.name} (${r.stepCount} steps)
|
|
1902
|
+
`;
|
|
1903
|
+
} else {
|
|
1904
|
+
output += ` [failed] ${r.name}: ${r.error}
|
|
1905
|
+
`;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
output += `
|
|
1909
|
+
`;
|
|
1910
|
+
if (created.length > 0) {
|
|
1911
|
+
output += `${created.length} funnel${created.length > 1 ? "s are" : " is"} now tracking. You'll see conversion data once events flow in.
|
|
1912
|
+
`;
|
|
1913
|
+
}
|
|
1914
|
+
if (failed.length > 0) {
|
|
1915
|
+
output += `${failed.length} funnel${failed.length > 1 ? "s" : ""} failed to create. Check the project ID and try again.
|
|
1916
|
+
`;
|
|
1917
|
+
}
|
|
1918
|
+
output += `
|
|
1919
|
+
Tip: Ask me "how is my signup funnel doing?" to check performance anytime.`;
|
|
1920
|
+
return {
|
|
1921
|
+
content: [{ type: "text", text: output }]
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
|
|
665
1925
|
// src/index.ts
|
|
666
1926
|
var apiKey = process.env.EASYFUNNEL_API_KEY;
|
|
667
1927
|
if (!apiKey) {
|
|
@@ -673,19 +1933,25 @@ if (!apiKey) {
|
|
|
673
1933
|
var baseUrl = process.env.EASYFUNNEL_API_URL || void 0;
|
|
674
1934
|
var client = new ApiClient(apiKey, baseUrl);
|
|
675
1935
|
var server = new import_server.Server(
|
|
676
|
-
{ name: "easyfunnel", version: "0.
|
|
1936
|
+
{ name: "easyfunnel", version: "0.2.0" },
|
|
677
1937
|
{ capabilities: { tools: {} } }
|
|
678
1938
|
);
|
|
679
1939
|
server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({
|
|
680
1940
|
tools: [
|
|
681
1941
|
listProjectsDefinition,
|
|
682
1942
|
createProjectDefinition,
|
|
1943
|
+
scanCodebaseDefinition,
|
|
683
1944
|
setupSdkDefinition,
|
|
1945
|
+
validateSetupDefinition,
|
|
1946
|
+
recommendFunnelsDefinition,
|
|
1947
|
+
createFunnelsBatchDefinition,
|
|
684
1948
|
scanForActionsDefinition,
|
|
685
1949
|
instrumentCodeDefinition,
|
|
686
1950
|
createFunnelDefinition,
|
|
687
1951
|
getFunnelHealthDefinition,
|
|
688
|
-
queryEventsDefinition
|
|
1952
|
+
queryEventsDefinition,
|
|
1953
|
+
deleteFunnelDefinition,
|
|
1954
|
+
updateFunnelDefinition
|
|
689
1955
|
]
|
|
690
1956
|
}));
|
|
691
1957
|
server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
@@ -695,8 +1961,16 @@ server.setRequestHandler(import_types.CallToolRequestSchema, async (request) =>
|
|
|
695
1961
|
return listProjects(client);
|
|
696
1962
|
case "create_project":
|
|
697
1963
|
return createProject(client, args);
|
|
1964
|
+
case "scan_codebase":
|
|
1965
|
+
return scanCodebase(client, args);
|
|
698
1966
|
case "setup_sdk":
|
|
699
1967
|
return setupSdk(args);
|
|
1968
|
+
case "validate_setup":
|
|
1969
|
+
return validateSetup(client, args);
|
|
1970
|
+
case "recommend_funnels":
|
|
1971
|
+
return recommendFunnels(client, args);
|
|
1972
|
+
case "create_funnels_batch":
|
|
1973
|
+
return createFunnelsBatch(client, args);
|
|
700
1974
|
case "scan_for_actions":
|
|
701
1975
|
return scanForActions(args);
|
|
702
1976
|
case "instrument_code":
|
|
@@ -707,6 +1981,10 @@ server.setRequestHandler(import_types.CallToolRequestSchema, async (request) =>
|
|
|
707
1981
|
return getFunnelHealth(client, args);
|
|
708
1982
|
case "query_events":
|
|
709
1983
|
return queryEvents(client, args);
|
|
1984
|
+
case "delete_funnel":
|
|
1985
|
+
return deleteFunnel(client, args);
|
|
1986
|
+
case "update_funnel":
|
|
1987
|
+
return updateFunnel(client, args);
|
|
710
1988
|
default:
|
|
711
1989
|
throw new Error(`Unknown tool: ${name}`);
|
|
712
1990
|
}
|