@easyfunnel/mcp 0.1.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +1360 -82
  2. package/package.json +13 -4
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7
7
  var import_types = require("@modelcontextprotocol/sdk/types.js");
8
8
 
9
9
  // src/api-client.ts
10
- var DEFAULT_BASE_URL = "https://easyfunnel.so";
10
+ var DEFAULT_BASE_URL = "https://easyfunnel.co";
11
11
  var ApiClient = class {
12
12
  constructor(apiKey2, baseUrl2) {
13
13
  this.apiKey = apiKey2;
@@ -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 easyfunnel SDK and add the provider to a Next.js/React project",
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", "react", "html"],
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
- const filesModified = [];
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: `For HTML projects, add this script tag to your <head>:
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
- <script defer data-api-key="${project_api_key}" src="https://easyfunnel.so/sdk.js"></script>
235
+ This will automatically track page views and clicks on elements with data-ef-track attributes.
161
236
 
162
- This will automatically track page views and clicks on elements with data-ef-track attributes.`
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
- } catch (e) {
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
- filesModified.push("package.json");
174
- const envPath = (0, import_path.join)(project_root, ".env.local");
175
- const envLine = `NEXT_PUBLIC_EASYFUNNEL_KEY=${project_api_key}`;
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("NEXT_PUBLIC_EASYFUNNEL_KEY")) {
179
- (0, import_fs.writeFileSync)(envPath, content + "\n" + envLine + "\n");
180
- filesModified.push(".env.local");
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
- filesModified.push(".env.local");
185
- }
186
- if (framework === "nextjs") {
187
- const layoutPaths = [
188
- (0, import_path.join)(project_root, "app", "layout.tsx"),
189
- (0, import_path.join)(project_root, "app", "layout.jsx"),
190
- (0, import_path.join)(project_root, "src", "app", "layout.tsx"),
191
- (0, import_path.join)(project_root, "src", "app", "layout.jsx")
192
- ];
193
- for (const layoutPath of layoutPaths) {
194
- if ((0, import_fs.existsSync)(layoutPath)) {
195
- let content = (0, import_fs.readFileSync)(layoutPath, "utf-8");
196
- if (!content.includes("EasyFunnelProvider")) {
197
- const importLine = `import { EasyFunnelProvider } from '@easyfunnel/react'
198
- `;
199
- content = importLine + content;
200
- content = content.replace(
201
- /(\{children\})/,
202
- `<EasyFunnelProvider apiKey={process.env.NEXT_PUBLIC_EASYFUNNEL_KEY!}>
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
- (0, import_fs.writeFileSync)(layoutPath, content);
207
- filesModified.push(layoutPath.replace(project_root + "/", ""));
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
- return {
234
- content: [
235
- {
236
- type: "text",
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
- ${filesModified.map((f) => ` - ${f}`).join("\n")}
241
-
242
- The EasyFunnel provider is now wrapping your app. Page views and click tracking are automatic.
243
-
244
- Next: I can scan your codebase for interactive elements to add tracking to.`
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", "label"]
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
- args.steps
625
+ stepsWithLabels
518
626
  );
519
- const stepsDisplay = args.steps.map((s, i) => ` ${i + 1}. ${s.label} (${s.event_name})`).join("\n");
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 raw event data for a project \u2014 counts, recent events, or filtered lists",
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: "Group by field (for breakdown query)"
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,30 +793,1165 @@ 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) {
668
1928
  console.error(
669
- "EASYFUNNEL_API_KEY environment variable is required. Get your account API key from https://easyfunnel.so/dashboard/settings"
1929
+ "EASYFUNNEL_API_KEY environment variable is required. Get your account API key from https://easyfunnel.co/dashboard/settings"
670
1930
  );
671
1931
  process.exit(1);
672
1932
  }
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.1.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
  }