@elitedcs/ghl-mcp 3.35.0 → 3.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2398 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@elitedcs/ghl-mcp",
34
- version: "3.35.0",
34
+ version: "3.37.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
36
  description: "GoHighLevel MCP Server for Claude. 218 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
37
37
  main: "dist/index.js",
@@ -6106,30 +6106,31 @@ function buildUpdateFormPath(formId, locationId2) {
6106
6106
  function buildUpdateFormBody(name, formData) {
6107
6107
  return { name, formData };
6108
6108
  }
6109
+ async function formApiRequest(client, method, path7, body) {
6110
+ const headers = await client.buildHeaders();
6111
+ const url = `https://backend.leadconnectorhq.com/forms${path7}`;
6112
+ const options = { method, headers };
6113
+ if (body && (method === "POST" || method === "PUT")) {
6114
+ options.body = JSON.stringify(body);
6115
+ }
6116
+ const response = await fetch(url, options);
6117
+ if (!response.ok) {
6118
+ const text2 = await response.text();
6119
+ throw new Error(`Form API Error ${response.status}: ${method} ${path7}
6120
+ ${text2}`);
6121
+ }
6122
+ const text = await response.text();
6123
+ if (!text) return {};
6124
+ try {
6125
+ return JSON.parse(text);
6126
+ } catch {
6127
+ return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
6128
+ }
6129
+ }
6109
6130
  function registerFormBuilderTools(server2, builderClient, publicClient) {
6110
6131
  const client = builderClient;
6111
6132
  if (!client) return;
6112
- async function formRequest(method, path7, body) {
6113
- const headers = await client.buildHeaders();
6114
- const url = `https://backend.leadconnectorhq.com/forms${path7}`;
6115
- const options = { method, headers };
6116
- if (body && (method === "POST" || method === "PUT")) {
6117
- options.body = JSON.stringify(body);
6118
- }
6119
- const response = await fetch(url, options);
6120
- if (!response.ok) {
6121
- const text2 = await response.text();
6122
- throw new Error(`Form API Error ${response.status}: ${method} ${path7}
6123
- ${text2}`);
6124
- }
6125
- const text = await response.text();
6126
- if (!text) return {};
6127
- try {
6128
- return JSON.parse(text);
6129
- } catch {
6130
- return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
6131
- }
6132
- }
6133
+ const formRequest = (method, path7, body) => formApiRequest(client, method, path7, body);
6133
6134
  server2.tool(
6134
6135
  "get_form_full",
6135
6136
  "Get a form with full builder data: all fields (labels, types, IDs, validation), conditional logic, auto-responder config, email notification settings, styling, and version history. This is the internal API \u2014 it returns everything the form builder UI shows.",
@@ -6536,8 +6537,8 @@ async function validateLicense(email, licenseKey) {
6536
6537
  signal: AbortSignal.timeout(1e4)
6537
6538
  });
6538
6539
  } catch (err) {
6539
- const msg = err instanceof Error ? err.message : String(err);
6540
- return { ok: false, error: `Could not reach license server: ${msg}`, reason: "unreachable" };
6540
+ const msg2 = err instanceof Error ? err.message : String(err);
6541
+ return { ok: false, error: `Could not reach license server: ${msg2}`, reason: "unreachable" };
6541
6542
  }
6542
6543
  if (res.status >= 500) {
6543
6544
  return { ok: false, error: `License server returned HTTP ${res.status}`, reason: "unreachable" };
@@ -6575,8 +6576,8 @@ async function validateGhl(apiKey2, locationId2) {
6575
6576
  const name = data?.location?.name || data?.name || "Unknown";
6576
6577
  return { ok: true, locationName: name };
6577
6578
  } catch (err) {
6578
- const msg = err instanceof Error ? err.message : String(err);
6579
- return { ok: false, error: `Could not reach GHL: ${msg}` };
6579
+ const msg2 = err instanceof Error ? err.message : String(err);
6580
+ return { ok: false, error: `Could not reach GHL: ${msg2}` };
6580
6581
  }
6581
6582
  }
6582
6583
  async function validateFirebase(firebaseKey, refreshToken) {
@@ -6592,8 +6593,8 @@ async function validateFirebase(firebaseKey, refreshToken) {
6592
6593
  const claims = data.id_token ? decodeFirebaseClaims(data.id_token) : {};
6593
6594
  return { ok: true, companyId: claims.companyId, userId: claims.userId };
6594
6595
  } catch (err) {
6595
- const msg = err instanceof Error ? err.message : String(err);
6596
- return { ok: false, error: `Could not reach Firebase: ${msg}` };
6596
+ const msg2 = err instanceof Error ? err.message : String(err);
6597
+ return { ok: false, error: `Could not reach Firebase: ${msg2}` };
6597
6598
  }
6598
6599
  }
6599
6600
  function registerSetupTool(server2) {
@@ -7416,8 +7417,8 @@ function registerBulkOperationTools(server2, client) {
7416
7417
  results.success++;
7417
7418
  } catch (error) {
7418
7419
  results.failed++;
7419
- const msg = error instanceof Error ? error.message : String(error);
7420
- results.errors.push(`${contactId}: ${msg}`);
7420
+ const msg2 = error instanceof Error ? error.message : String(error);
7421
+ results.errors.push(`${contactId}: ${msg2}`);
7421
7422
  }
7422
7423
  await delay(200);
7423
7424
  }
@@ -9617,6 +9618,2368 @@ function registerAccountHealthTools(server2, client) {
9617
9618
  );
9618
9619
  }
9619
9620
 
9621
+ // src/tools/intake-to-build.ts
9622
+ var import_zod53 = require("zod");
9623
+
9624
+ // src/intake-to-build/question-set.ts
9625
+ var QUESTION_SET_VERSION = "0.1";
9626
+ var INTAKE_FIELD_NAME_PREFIX = "Intake: ";
9627
+ function deriveFieldKey(fieldName) {
9628
+ const slug2 = fieldName.toLowerCase().replace(/[^a-z0-9 ]+/g, "").replace(/ /g, "_");
9629
+ return `contact.${slug2}`;
9630
+ }
9631
+ function intakeFieldName(label) {
9632
+ return `${INTAKE_FIELD_NAME_PREFIX}${label}`;
9633
+ }
9634
+ function expectedFieldKey(q) {
9635
+ return deriveFieldKey(intakeFieldName(q.label));
9636
+ }
9637
+ var YES_NO = ["Yes", "No"];
9638
+ var BUSINESS_TYPES = [
9639
+ "Med spa",
9640
+ "Clinic / practice",
9641
+ "Coach / consultant",
9642
+ "Ecommerce",
9643
+ "Local service",
9644
+ "Agency",
9645
+ "Other"
9646
+ ];
9647
+ var TEAM_SIZES = ["Just me", "2-5", "6+"];
9648
+ var MONTHLY_LEAD_VOLUMES = ["Under 100", "100-500", "500-1000", "1000+"];
9649
+ var PRIMARY_GOALS = [
9650
+ "Book appointments",
9651
+ "Capture + nurture leads",
9652
+ "Direct sales",
9653
+ "Re-engage past clients",
9654
+ "Other"
9655
+ ];
9656
+ var FOLLOW_UP_STYLES = ["High-touch / multi-step", "Light", "Single confirmation"];
9657
+ var A2P_STATUSES = ["Not started", "In progress", "Approved", "Not needed"];
9658
+ var PAYMENT_PROCESSORS = [
9659
+ "Stripe connected",
9660
+ "Stripe not connected",
9661
+ "Other",
9662
+ "None"
9663
+ ];
9664
+ var SOCIAL_CHANNELS = [
9665
+ "Instagram",
9666
+ "Facebook",
9667
+ "TikTok",
9668
+ "LinkedIn",
9669
+ "YouTube",
9670
+ "Google Business",
9671
+ "Other"
9672
+ ];
9673
+ var INTAKE_QUESTIONS = [
9674
+ // ── Submitter identity (standard contact fields; not in the Brief) ──
9675
+ {
9676
+ key: "first_name",
9677
+ label: "First Name",
9678
+ section: "contact",
9679
+ sectionLabel: "Your contact info",
9680
+ required: true,
9681
+ field: { kind: "standard", tag: "first_name" },
9682
+ placeholder: "First name",
9683
+ briefKind: "identity"
9684
+ },
9685
+ {
9686
+ key: "last_name",
9687
+ label: "Last Name",
9688
+ section: "contact",
9689
+ sectionLabel: "Your contact info",
9690
+ required: false,
9691
+ field: { kind: "standard", tag: "last_name" },
9692
+ placeholder: "Last name",
9693
+ briefKind: "identity"
9694
+ },
9695
+ {
9696
+ key: "email",
9697
+ label: "Email",
9698
+ section: "contact",
9699
+ sectionLabel: "Your contact info",
9700
+ required: true,
9701
+ field: { kind: "standard", tag: "email" },
9702
+ placeholder: "you@business.com",
9703
+ briefKind: "identity"
9704
+ },
9705
+ {
9706
+ key: "phone",
9707
+ label: "Phone",
9708
+ section: "contact",
9709
+ sectionLabel: "Your contact info",
9710
+ required: false,
9711
+ field: { kind: "standard", tag: "phone" },
9712
+ placeholder: "Phone",
9713
+ briefKind: "identity"
9714
+ },
9715
+ // ── Section A: Business basics ──
9716
+ {
9717
+ key: "business_name",
9718
+ label: "Business Name",
9719
+ section: "business",
9720
+ sectionLabel: "Business basics",
9721
+ required: true,
9722
+ field: { kind: "custom", dataType: "TEXT" },
9723
+ placeholder: "Your business name",
9724
+ briefPath: "business.name",
9725
+ briefKind: "string"
9726
+ },
9727
+ {
9728
+ key: "business_type",
9729
+ label: "Business Type",
9730
+ section: "business",
9731
+ sectionLabel: "Business basics",
9732
+ required: true,
9733
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9734
+ options: BUSINESS_TYPES,
9735
+ briefPath: "business.type",
9736
+ briefKind: "string"
9737
+ },
9738
+ {
9739
+ key: "website",
9740
+ label: "Website",
9741
+ section: "business",
9742
+ sectionLabel: "Business basics",
9743
+ required: false,
9744
+ field: { kind: "custom", dataType: "TEXT" },
9745
+ placeholder: "https://",
9746
+ briefPath: "business.website",
9747
+ briefKind: "string"
9748
+ },
9749
+ {
9750
+ key: "primary_location",
9751
+ label: "Primary Location",
9752
+ section: "business",
9753
+ sectionLabel: "Business basics",
9754
+ required: false,
9755
+ field: { kind: "custom", dataType: "TEXT" },
9756
+ placeholder: "City, State / Country",
9757
+ briefPath: "business.location",
9758
+ briefKind: "string"
9759
+ },
9760
+ {
9761
+ key: "timezone",
9762
+ label: "Timezone",
9763
+ section: "business",
9764
+ sectionLabel: "Business basics",
9765
+ required: false,
9766
+ field: { kind: "custom", dataType: "TEXT" },
9767
+ placeholder: "e.g. America/Phoenix",
9768
+ briefPath: "business.timezone",
9769
+ briefKind: "string"
9770
+ },
9771
+ // A6–A8: ratified additions (atlas 2026-06-15, Jerry decision #1). All optional,
9772
+ // additive under business.* (schemaVersion stays 0.1). fieldKeys live-captured on
9773
+ // FXaoz 2026-06-16: contact.intake_team_size / _monthly_lead_volume / _business_hours.
9774
+ {
9775
+ key: "team_size",
9776
+ label: "Team Size",
9777
+ section: "business",
9778
+ sectionLabel: "Business basics",
9779
+ required: false,
9780
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9781
+ options: TEAM_SIZES,
9782
+ briefPath: "business.teamSize",
9783
+ briefKind: "string"
9784
+ },
9785
+ {
9786
+ key: "monthly_lead_volume",
9787
+ label: "Monthly Lead Volume",
9788
+ section: "business",
9789
+ sectionLabel: "Business basics",
9790
+ required: false,
9791
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9792
+ options: MONTHLY_LEAD_VOLUMES,
9793
+ briefPath: "business.monthlyLeadVolume",
9794
+ briefKind: "string"
9795
+ },
9796
+ {
9797
+ key: "business_hours",
9798
+ label: "Business Hours",
9799
+ section: "business",
9800
+ sectionLabel: "Business basics",
9801
+ required: false,
9802
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
9803
+ placeholder: "e.g. Mon-Fri 9-5; Sat 10-1",
9804
+ briefPath: "business.hours",
9805
+ briefKind: "string"
9806
+ },
9807
+ // ── Section B: Offer and pricing ──
9808
+ {
9809
+ key: "core_offer",
9810
+ label: "Core Offer",
9811
+ section: "offer",
9812
+ sectionLabel: "Offer and pricing",
9813
+ required: true,
9814
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
9815
+ placeholder: "What you sell, in one or two sentences",
9816
+ briefPath: "offer.summary",
9817
+ briefKind: "string"
9818
+ },
9819
+ {
9820
+ key: "price_points",
9821
+ label: "Price Points",
9822
+ section: "offer",
9823
+ sectionLabel: "Offer and pricing",
9824
+ required: false,
9825
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
9826
+ placeholder: "Main offers + prices (one per line, e.g. Consult - $19)",
9827
+ briefPath: "offer.pricePoints",
9828
+ briefKind: "pricePoints"
9829
+ },
9830
+ {
9831
+ key: "lead_magnet",
9832
+ label: "Lead Magnet",
9833
+ section: "offer",
9834
+ sectionLabel: "Offer and pricing",
9835
+ required: false,
9836
+ field: { kind: "custom", dataType: "TEXT" },
9837
+ placeholder: "Free thing you give to capture leads, if any",
9838
+ briefPath: "offer.leadMagnet",
9839
+ briefKind: "string"
9840
+ },
9841
+ {
9842
+ key: "avg_deal_value",
9843
+ label: "Average Deal Value",
9844
+ section: "offer",
9845
+ sectionLabel: "Offer and pricing",
9846
+ required: false,
9847
+ field: { kind: "custom", dataType: "TEXT" },
9848
+ placeholder: "e.g. $350",
9849
+ briefPath: "offer.avgDealValue",
9850
+ briefKind: "string"
9851
+ },
9852
+ // ── Section C: Audience ──
9853
+ {
9854
+ key: "ideal_customer",
9855
+ label: "Ideal Customer",
9856
+ section: "audience",
9857
+ sectionLabel: "Audience",
9858
+ required: true,
9859
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
9860
+ placeholder: "Who you serve",
9861
+ briefPath: "audience.ideal",
9862
+ briefKind: "string"
9863
+ },
9864
+ {
9865
+ key: "top_pain_points",
9866
+ label: "Top Pain Points",
9867
+ section: "audience",
9868
+ sectionLabel: "Audience",
9869
+ required: false,
9870
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
9871
+ placeholder: "The problems they come to you with (one per line)",
9872
+ briefPath: "audience.painPoints",
9873
+ briefKind: "list"
9874
+ },
9875
+ {
9876
+ key: "objections",
9877
+ label: "Objections",
9878
+ section: "audience",
9879
+ sectionLabel: "Audience",
9880
+ required: false,
9881
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
9882
+ placeholder: "Why prospects hesitate (one per line)",
9883
+ briefPath: "audience.objections",
9884
+ briefKind: "list"
9885
+ },
9886
+ // ── Section D: Goal and sales process ──
9887
+ {
9888
+ key: "primary_goal",
9889
+ label: "Primary Goal",
9890
+ section: "goal",
9891
+ sectionLabel: "Goal and sales process",
9892
+ required: true,
9893
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9894
+ options: PRIMARY_GOALS,
9895
+ briefPath: "goal.primary",
9896
+ briefKind: "string"
9897
+ },
9898
+ {
9899
+ key: "sales_stages",
9900
+ label: "Sales Stages",
9901
+ section: "goal",
9902
+ sectionLabel: "Goal and sales process",
9903
+ required: false,
9904
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
9905
+ placeholder: "Steps a lead moves through from new to won (one per line)",
9906
+ briefPath: "goal.salesStages",
9907
+ briefKind: "list"
9908
+ },
9909
+ {
9910
+ key: "booking_needed",
9911
+ label: "Do you need appointment booking?",
9912
+ section: "goal",
9913
+ sectionLabel: "Goal and sales process",
9914
+ required: false,
9915
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9916
+ options: YES_NO,
9917
+ briefPath: "goal.bookingNeeded",
9918
+ briefKind: "boolean"
9919
+ },
9920
+ {
9921
+ key: "follow_up_style",
9922
+ label: "Follow-up Style",
9923
+ section: "goal",
9924
+ sectionLabel: "Goal and sales process",
9925
+ required: false,
9926
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9927
+ options: FOLLOW_UP_STYLES,
9928
+ briefPath: "goal.followUpStyle",
9929
+ briefKind: "string"
9930
+ },
9931
+ // ── Section E: Channels and tech ──
9932
+ {
9933
+ key: "email_ready",
9934
+ label: "Is your sending email / domain set up?",
9935
+ section: "channels",
9936
+ sectionLabel: "Channels and tech",
9937
+ required: false,
9938
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9939
+ options: YES_NO,
9940
+ briefPath: "channels.email",
9941
+ briefKind: "boolean"
9942
+ },
9943
+ {
9944
+ key: "sms_desired",
9945
+ label: "Do you want to send SMS?",
9946
+ section: "channels",
9947
+ sectionLabel: "Channels and tech",
9948
+ required: false,
9949
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9950
+ options: YES_NO,
9951
+ briefPath: "channels.sms",
9952
+ briefKind: "boolean"
9953
+ },
9954
+ {
9955
+ key: "a2p_status",
9956
+ label: "A2P registration status",
9957
+ section: "channels",
9958
+ sectionLabel: "Channels and tech",
9959
+ required: false,
9960
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9961
+ options: A2P_STATUSES,
9962
+ briefPath: "channels.a2pStatus",
9963
+ briefKind: "string"
9964
+ },
9965
+ {
9966
+ key: "payment_processor",
9967
+ label: "Payment processor",
9968
+ section: "channels",
9969
+ sectionLabel: "Channels and tech",
9970
+ required: false,
9971
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9972
+ options: PAYMENT_PROCESSORS,
9973
+ briefPath: "channels.payment",
9974
+ briefKind: "string"
9975
+ },
9976
+ {
9977
+ key: "calendar_connected",
9978
+ label: "Is a booking calendar already connected?",
9979
+ section: "channels",
9980
+ sectionLabel: "Channels and tech",
9981
+ required: false,
9982
+ field: { kind: "custom", dataType: "SINGLE_OPTIONS" },
9983
+ options: YES_NO,
9984
+ briefPath: "channels.calendarConnected",
9985
+ briefKind: "boolean"
9986
+ },
9987
+ {
9988
+ key: "social_channels",
9989
+ label: "Which social platforms do you use?",
9990
+ section: "channels",
9991
+ sectionLabel: "Channels and tech",
9992
+ required: false,
9993
+ field: { kind: "custom", dataType: "MULTIPLE_OPTIONS" },
9994
+ options: SOCIAL_CHANNELS,
9995
+ briefPath: "channels.social",
9996
+ briefKind: "multiselect"
9997
+ },
9998
+ // ── Section F: Assets on hand ──
9999
+ {
10000
+ key: "existing_pipeline",
10001
+ label: "Existing pipeline?",
10002
+ section: "assets",
10003
+ sectionLabel: "Assets on hand",
10004
+ required: false,
10005
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
10006
+ placeholder: "Yes/No + describe",
10007
+ briefPath: "assets.existingPipeline",
10008
+ briefKind: "string"
10009
+ },
10010
+ {
10011
+ key: "existing_workflows",
10012
+ label: "Existing workflows (do not clobber)",
10013
+ section: "assets",
10014
+ sectionLabel: "Assets on hand",
10015
+ required: false,
10016
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
10017
+ placeholder: "Anything already built we should leave alone",
10018
+ briefPath: "assets.existingWorkflows",
10019
+ briefKind: "string"
10020
+ },
10021
+ {
10022
+ key: "brand_assets",
10023
+ label: "Brand Assets",
10024
+ section: "assets",
10025
+ sectionLabel: "Assets on hand",
10026
+ required: false,
10027
+ field: { kind: "custom", dataType: "TEXT" },
10028
+ placeholder: "Logo / colors / domain available",
10029
+ briefPath: "assets.brand",
10030
+ briefKind: "string"
10031
+ },
10032
+ {
10033
+ key: "anything_else",
10034
+ label: "Anything else?",
10035
+ section: "assets",
10036
+ sectionLabel: "Assets on hand",
10037
+ required: false,
10038
+ field: { kind: "custom", dataType: "LARGE_TEXT" },
10039
+ placeholder: "Anything else we should know",
10040
+ briefPath: "assets.notes",
10041
+ briefKind: "string"
10042
+ }
10043
+ ];
10044
+ var INTAKE_FORM_NAME = "GHL Command Blueprint \u2014 Client Intake";
10045
+ function customQuestions() {
10046
+ return INTAKE_QUESTIONS.filter((q) => q.field.kind === "custom");
10047
+ }
10048
+
10049
+ // src/intake-to-build/form-template.ts
10050
+ function slug(s) {
10051
+ return s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("_");
10052
+ }
10053
+ function formFieldType(dataType) {
10054
+ switch (dataType) {
10055
+ case "TEXT":
10056
+ return "text";
10057
+ case "LARGE_TEXT":
10058
+ return "large_text";
10059
+ case "NUMERICAL":
10060
+ return "numerical";
10061
+ case "PHONE":
10062
+ return "phone";
10063
+ case "MONETORY":
10064
+ return "monetary";
10065
+ case "CHECKBOX":
10066
+ return "checkbox";
10067
+ case "SINGLE_OPTIONS":
10068
+ return "single_options";
10069
+ case "MULTIPLE_OPTIONS":
10070
+ return "multiple_options";
10071
+ case "DATE":
10072
+ return "date";
10073
+ case "FLOAT":
10074
+ return "float";
10075
+ default:
10076
+ return "text";
10077
+ }
10078
+ }
10079
+ var NO_BORDER = { border: 0, color: "FFFFFF", radius: 0, type: "none" };
10080
+ var NO_SHADOW = { blur: 0, color: "FFFFFF", horizontal: 0, spread: 0, vertical: 0 };
10081
+ function buildHeaderField(title, index) {
10082
+ return {
10083
+ active: false,
10084
+ align: "left",
10085
+ bgColor: "FFFFFF00",
10086
+ border: { ...NO_BORDER },
10087
+ hiddenFieldQueryKey: `header_${index}`,
10088
+ label: `<h1 style="line-height: 1;padding-left: 0px!important;margin: 0;color: #000000;font-family: Roboto;font-weight: 400;"><span style="font-size: 18px; color: rgb(0, 0, 0)">${title}</span></h1>`,
10089
+ padding: { bottom: 0, left: 0, right: 0, top: 0 },
10090
+ placeholder: "header",
10091
+ shadow: { ...NO_SHADOW },
10092
+ standard: true,
10093
+ tag: "header",
10094
+ type: "h1",
10095
+ typeLabel: "Text",
10096
+ weight: 400
10097
+ };
10098
+ }
10099
+ function buildStandardFormField(q) {
10100
+ if (q.field.kind !== "standard") throw new Error(`buildStandardFormField: ${q.key} is not standard`);
10101
+ const tag = q.field.tag;
10102
+ const isEmail = tag === "email";
10103
+ const isPhone = tag === "phone";
10104
+ const base = {
10105
+ fieldWidthPercentage: 100,
10106
+ hiddenFieldQueryKey: tag,
10107
+ label: q.label,
10108
+ placeholder: q.placeholder ?? q.label,
10109
+ required: q.required,
10110
+ standard: true,
10111
+ tag,
10112
+ type: isEmail ? "email" : "text",
10113
+ typeLabel: isEmail ? "Email" : isPhone ? "Phone" : "Text"
10114
+ };
10115
+ if (isPhone) base.enableCountryPicker = false;
10116
+ return base;
10117
+ }
10118
+ function buildCustomFormField(q, record, locationId2, position) {
10119
+ const type = formFieldType(record.dataType);
10120
+ const field = {
10121
+ Id: record.id,
10122
+ id: record.id,
10123
+ tag: record.id,
10124
+ active: false,
10125
+ allowCustomOption: false,
10126
+ customFieldLabel: q.label,
10127
+ dataType: record.dataType,
10128
+ dateAdded: record.dateAdded ?? "",
10129
+ description: "",
10130
+ documentType: "field",
10131
+ edit: false,
10132
+ fieldKey: record.fieldKey,
10133
+ fieldWidthPercentage: 100,
10134
+ fieldsCount: 0,
10135
+ hiddenFieldQueryKey: slug(record.name),
10136
+ label: q.label,
10137
+ locationId: locationId2,
10138
+ model: record.model ?? "contact",
10139
+ name: record.name,
10140
+ parentId: record.parentId ?? "",
10141
+ placeholder: q.placeholder ?? "",
10142
+ position,
10143
+ required: q.required,
10144
+ showInForms: true,
10145
+ standard: false,
10146
+ type
10147
+ };
10148
+ const options = record.picklistOptions ?? q.options ?? void 0;
10149
+ if (type === "single_options" || type === "multiple_options" || type === "checkbox") {
10150
+ field.picklistOptions = options ? [...options] : [];
10151
+ }
10152
+ if (type === "multiple_options") {
10153
+ field.calculatedOptions = (options ?? []).map((label) => ({ calculatedValue: "", label }));
10154
+ field.category = "choiceElements";
10155
+ field.typeLabel = "Multi Dropdown";
10156
+ }
10157
+ if (type === "phone") field.enableCountryPicker = false;
10158
+ return field;
10159
+ }
10160
+ function buildSubmitButton(label = "Submit") {
10161
+ return {
10162
+ active: false,
10163
+ align: "center",
10164
+ bgColor: "101828FF",
10165
+ border: 0,
10166
+ borderColor: "101828FF",
10167
+ borderRadius: 8,
10168
+ borderType: "none",
10169
+ color: "FFFFFFFF",
10170
+ fieldWidthPercentage: 100,
10171
+ fontFamily: "Inter",
10172
+ fontSize: 14,
10173
+ fullwidth: true,
10174
+ hiddenFieldQueryKey: "button",
10175
+ label: `<p style="padding-left: 0px!important;"><strong>${label}</strong></p>`,
10176
+ padding: { bottom: 12, left: 20, right: 20, top: 12 },
10177
+ placeholder: "Button",
10178
+ radius: 8,
10179
+ shadow: { blur: 2, color: "00000014", horizontal: 0, spread: 0, vertical: 1 },
10180
+ standard: true,
10181
+ subTextColor: "FFFFFFFF",
10182
+ subTextFontFamily: "Inter",
10183
+ subTextFontSize: 12,
10184
+ subTextWeight: 400,
10185
+ submitSubText: "",
10186
+ tag: "button",
10187
+ type: "submit",
10188
+ weight: 500
10189
+ };
10190
+ }
10191
+ function buildIntakeFormData(opts) {
10192
+ const questions = opts.questions ?? INTAKE_QUESTIONS;
10193
+ const fields = [];
10194
+ let lastSection = "";
10195
+ let headerIndex = 0;
10196
+ let position = 0;
10197
+ for (const q of questions) {
10198
+ if (q.section !== lastSection) {
10199
+ fields.push(buildHeaderField(q.sectionLabel, headerIndex++));
10200
+ lastSection = q.section;
10201
+ }
10202
+ if (q.field.kind === "standard") {
10203
+ fields.push(buildStandardFormField(q));
10204
+ } else {
10205
+ const record = opts.resolved.get(q.key);
10206
+ if (!record) continue;
10207
+ fields.push(buildCustomFormField(q, record, opts.locationId, position += 50));
10208
+ }
10209
+ }
10210
+ fields.push(buildSubmitButton("Submit"));
10211
+ return {
10212
+ autoResponder: false,
10213
+ emailNotifications: false,
10214
+ form: {
10215
+ fields,
10216
+ formLabelVisible: true,
10217
+ formAction: {
10218
+ actionType: "2",
10219
+ headerImageSrc: "",
10220
+ mobileHeaderImageSrc: "",
10221
+ redirectUrl: "",
10222
+ thankyouText: "<p style='text-align:center;margin:0;'>Thanks! Your intake has been received.</p>"
10223
+ }
10224
+ }
10225
+ };
10226
+ }
10227
+
10228
+ // src/intake-to-build/brief.ts
10229
+ var import_zod51 = require("zod");
10230
+ var BRIEF_SCHEMA_VERSION = "0.1";
10231
+ var PRESETS = [
10232
+ "generic",
10233
+ "med_spa",
10234
+ "clinic_launch_a2p",
10235
+ "coach",
10236
+ "ecom",
10237
+ "agency"
10238
+ ];
10239
+ var presetSchema = import_zod51.z.enum(PRESETS);
10240
+ var BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
10241
+ var briefSourceSchema = import_zod51.z.enum(BRIEF_SOURCES);
10242
+ var pricePointSchema = import_zod51.z.object({
10243
+ name: import_zod51.z.string(),
10244
+ price: import_zod51.z.string()
10245
+ });
10246
+ var briefSchema = import_zod51.z.object({
10247
+ schemaVersion: import_zod51.z.string(),
10248
+ briefId: import_zod51.z.string(),
10249
+ preset: presetSchema,
10250
+ briefSource: briefSourceSchema,
10251
+ /** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
10252
+ extended: import_zod51.z.record(import_zod51.z.unknown()).optional(),
10253
+ business: import_zod51.z.object({
10254
+ name: import_zod51.z.string(),
10255
+ type: import_zod51.z.string().optional(),
10256
+ website: import_zod51.z.string().optional(),
10257
+ location: import_zod51.z.string().optional(),
10258
+ timezone: import_zod51.z.string().optional(),
10259
+ // Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
10260
+ // the same tolerance reason as business.type (don't reject valid briefs).
10261
+ teamSize: import_zod51.z.string().optional(),
10262
+ monthlyLeadVolume: import_zod51.z.string().optional(),
10263
+ hours: import_zod51.z.string().optional()
10264
+ }).passthrough(),
10265
+ offer: import_zod51.z.object({
10266
+ summary: import_zod51.z.string().optional(),
10267
+ // Parsed best-effort; tolerate a raw string when parsing was not possible.
10268
+ pricePoints: import_zod51.z.union([import_zod51.z.array(pricePointSchema), import_zod51.z.string()]).optional(),
10269
+ leadMagnet: import_zod51.z.string().optional(),
10270
+ avgDealValue: import_zod51.z.string().optional()
10271
+ }).passthrough().optional(),
10272
+ audience: import_zod51.z.object({
10273
+ ideal: import_zod51.z.string().optional(),
10274
+ painPoints: import_zod51.z.array(import_zod51.z.string()).optional(),
10275
+ objections: import_zod51.z.array(import_zod51.z.string()).optional()
10276
+ }).passthrough().optional(),
10277
+ goal: import_zod51.z.object({
10278
+ // Kept as string: the form option labels are the canonical values, but
10279
+ // the contract sample shortens some (e.g. "high-touch"). See §7 note.
10280
+ primary: import_zod51.z.string(),
10281
+ salesStages: import_zod51.z.array(import_zod51.z.string()).optional(),
10282
+ bookingNeeded: import_zod51.z.boolean().optional(),
10283
+ followUpStyle: import_zod51.z.string().optional()
10284
+ }).passthrough(),
10285
+ channels: import_zod51.z.object({
10286
+ email: import_zod51.z.boolean().optional(),
10287
+ sms: import_zod51.z.boolean().optional(),
10288
+ a2pStatus: import_zod51.z.string().optional(),
10289
+ payment: import_zod51.z.string().optional(),
10290
+ calendarConnected: import_zod51.z.boolean().optional(),
10291
+ social: import_zod51.z.array(import_zod51.z.string()).optional()
10292
+ }).passthrough().optional(),
10293
+ assets: import_zod51.z.object({
10294
+ existingPipeline: import_zod51.z.string().optional(),
10295
+ existingWorkflows: import_zod51.z.string().optional(),
10296
+ brand: import_zod51.z.string().optional(),
10297
+ notes: import_zod51.z.string().optional()
10298
+ }).passthrough().optional(),
10299
+ flags: import_zod51.z.array(import_zod51.z.string()).optional()
10300
+ }).strict();
10301
+ function validateBrief(input) {
10302
+ const parsed = briefSchema.safeParse(input);
10303
+ if (parsed.success) {
10304
+ return { valid: true, errors: [], brief: parsed.data };
10305
+ }
10306
+ return {
10307
+ valid: false,
10308
+ errors: parsed.error.issues.map(
10309
+ (i) => `${i.path.join(".") || "(root)"}: ${i.message}`
10310
+ )
10311
+ };
10312
+ }
10313
+
10314
+ // src/intake-to-build/normalizer.ts
10315
+ function firstString(v) {
10316
+ if (Array.isArray(v)) return v.length ? String(v[0]).trim() : void 0;
10317
+ if (typeof v === "string") {
10318
+ const t = v.trim();
10319
+ return t.length ? t : void 0;
10320
+ }
10321
+ return void 0;
10322
+ }
10323
+ function splitList(text) {
10324
+ return text.split(/\r?\n|;|•/).flatMap((line) => line.includes(",") && !/\d,\d/.test(line) ? line.split(",") : [line]).map((s) => s.replace(/^[\s\-*\d.)]+/, "").trim()).filter(Boolean);
10325
+ }
10326
+ function parsePricePoints(text) {
10327
+ const out = [];
10328
+ for (const line of text.split(/\r?\n/)) {
10329
+ const t = line.trim();
10330
+ if (!t) continue;
10331
+ const m = t.match(/^(.*?)[\s]*[-—:|]\s*\$?\s*([\d,]+(?:\.\d+)?)/);
10332
+ if (m) {
10333
+ out.push({ name: m[1].trim(), price: m[2].replace(/,/g, "") });
10334
+ } else {
10335
+ const priceOnly = t.match(/\$?\s*([\d,]+(?:\.\d+)?)\s*$/);
10336
+ out.push({
10337
+ name: priceOnly ? t.slice(0, priceOnly.index).trim() || t : t,
10338
+ price: priceOnly ? priceOnly[1].replace(/,/g, "") : ""
10339
+ });
10340
+ }
10341
+ }
10342
+ return out;
10343
+ }
10344
+ function coerceBoolean(v) {
10345
+ const s = firstString(v);
10346
+ if (s === void 0) return void 0;
10347
+ if (/^(yes|true|y|1)$/i.test(s)) return true;
10348
+ if (/^(no|false|n|0)$/i.test(s)) return false;
10349
+ return void 0;
10350
+ }
10351
+ function coerceValue(raw, kind) {
10352
+ switch (kind) {
10353
+ case "string":
10354
+ return firstString(raw);
10355
+ case "boolean":
10356
+ return coerceBoolean(raw);
10357
+ case "list": {
10358
+ const s = firstString(raw);
10359
+ return s ? splitList(s) : void 0;
10360
+ }
10361
+ case "pricePoints": {
10362
+ const s = firstString(raw);
10363
+ return s ? parsePricePoints(s) : void 0;
10364
+ }
10365
+ case "multiselect": {
10366
+ if (Array.isArray(raw)) {
10367
+ const arr = raw.map((x) => String(x).trim()).filter(Boolean);
10368
+ return arr.length ? arr : void 0;
10369
+ }
10370
+ const s = firstString(raw);
10371
+ return s ? [s] : void 0;
10372
+ }
10373
+ case "identity":
10374
+ return void 0;
10375
+ }
10376
+ }
10377
+ function presetForBusinessType(type) {
10378
+ switch ((type ?? "").toLowerCase()) {
10379
+ case "med spa":
10380
+ return "med_spa";
10381
+ case "coach / consultant":
10382
+ return "coach";
10383
+ case "ecommerce":
10384
+ return "ecom";
10385
+ case "agency":
10386
+ return "agency";
10387
+ default:
10388
+ return "generic";
10389
+ }
10390
+ }
10391
+ function setPath(target, path7, value) {
10392
+ if (value === void 0) return;
10393
+ const parts = path7.split(".");
10394
+ let node = target;
10395
+ for (let i = 0; i < parts.length - 1; i++) {
10396
+ const k = parts[i];
10397
+ if (typeof node[k] !== "object" || node[k] === null) node[k] = {};
10398
+ node = node[k];
10399
+ }
10400
+ node[parts[parts.length - 1]] = value;
10401
+ }
10402
+ function deriveFlags(brief) {
10403
+ const flags = [];
10404
+ const channels = brief.channels ?? {};
10405
+ const goal = brief.goal ?? {};
10406
+ const a2p = typeof channels.a2pStatus === "string" ? channels.a2pStatus.toLowerCase() : "";
10407
+ if (channels.sms === true && a2p !== "approved" && a2p !== "not needed") {
10408
+ flags.push("needs_a2p");
10409
+ }
10410
+ if (channels.payment === "Stripe not connected") flags.push("stripe_not_connected");
10411
+ if (goal.bookingNeeded === true && channels.calendarConnected !== true) {
10412
+ flags.push("calendar_oauth_needed");
10413
+ }
10414
+ if (channels.email === false) flags.push("email_not_ready");
10415
+ return flags;
10416
+ }
10417
+ function buildFieldMapFromFormFields(fields, questions = INTAKE_QUESTIONS) {
10418
+ const byFieldKey = /* @__PURE__ */ new Map();
10419
+ for (const f of fields) {
10420
+ const fieldKey = typeof f.fieldKey === "string" ? f.fieldKey : void 0;
10421
+ const id = typeof f.id === "string" ? f.id : typeof f.tag === "string" ? f.tag : void 0;
10422
+ if (fieldKey && id) byFieldKey.set(fieldKey.toLowerCase(), id);
10423
+ }
10424
+ const map = {};
10425
+ for (const q of questions) {
10426
+ if (q.field.kind !== "custom") continue;
10427
+ const id = byFieldKey.get(expectedFieldKey(q).toLowerCase());
10428
+ if (id) map[q.key] = id;
10429
+ }
10430
+ return map;
10431
+ }
10432
+ function normalizeSubmissionToBrief(opts) {
10433
+ const questions = opts.questions ?? INTAKE_QUESTIONS;
10434
+ const brief = {
10435
+ schemaVersion: BRIEF_SCHEMA_VERSION,
10436
+ briefId: opts.briefId,
10437
+ briefSource: "intake_form",
10438
+ business: {},
10439
+ goal: {}
10440
+ };
10441
+ for (const q of questions) {
10442
+ if (!q.briefPath || q.briefKind === "identity") continue;
10443
+ const rawKey = q.field.kind === "standard" ? q.field.tag : opts.fieldMap[q.key];
10444
+ const raw = rawKey ? opts.others[rawKey] : void 0;
10445
+ const value = coerceValue(raw, q.briefKind);
10446
+ setPath(brief, q.briefPath, value);
10447
+ }
10448
+ const businessType = typeof brief.business?.type === "string" ? brief.business.type : void 0;
10449
+ brief.preset = opts.preset ?? presetForBusinessType(businessType);
10450
+ brief.flags = deriveFlags(brief);
10451
+ return brief;
10452
+ }
10453
+
10454
+ // src/intake-to-build/plan.ts
10455
+ var import_zod52 = require("zod");
10456
+ var REF_NAMESPACES = [
10457
+ "pipeline",
10458
+ "stage",
10459
+ "field",
10460
+ "tag",
10461
+ "workflow",
10462
+ "form",
10463
+ "funnel",
10464
+ "page",
10465
+ "calendar",
10466
+ "email",
10467
+ "sms",
10468
+ "cv",
10469
+ "handoff"
10470
+ ];
10471
+ var REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
10472
+ var refSchema = import_zod52.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
10473
+ function nsRef(ns) {
10474
+ return import_zod52.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
10475
+ }
10476
+ function refNamespace(ref) {
10477
+ return ref.split(".")[0];
10478
+ }
10479
+ var stageSchema = import_zod52.z.object({
10480
+ ref: nsRef("stage"),
10481
+ name: import_zod52.z.string(),
10482
+ position: import_zod52.z.number().int().nonnegative()
10483
+ });
10484
+ var pipelineSchema = import_zod52.z.object({
10485
+ ref: nsRef("pipeline"),
10486
+ name: import_zod52.z.string(),
10487
+ stages: import_zod52.z.array(stageSchema).min(1)
10488
+ });
10489
+ var GHL_FIELD_DATATYPES = [
10490
+ "TEXT",
10491
+ "LARGE_TEXT",
10492
+ "NUMERICAL",
10493
+ "PHONE",
10494
+ "MONETORY",
10495
+ "CHECKBOX",
10496
+ "SINGLE_OPTIONS",
10497
+ "MULTIPLE_OPTIONS",
10498
+ "FLOAT",
10499
+ "DATE",
10500
+ "TEXTBOX_LIST",
10501
+ "FILE_UPLOAD",
10502
+ "SIGNATURE"
10503
+ ];
10504
+ var customFieldSchema = import_zod52.z.object({
10505
+ ref: nsRef("field"),
10506
+ name: import_zod52.z.string(),
10507
+ dataType: import_zod52.z.enum(GHL_FIELD_DATATYPES),
10508
+ model: import_zod52.z.enum(["contact", "opportunity"]).optional(),
10509
+ options: import_zod52.z.array(import_zod52.z.string()).optional()
10510
+ });
10511
+ var tagSchema = import_zod52.z.object({
10512
+ ref: nsRef("tag"),
10513
+ name: import_zod52.z.string()
10514
+ });
10515
+ var customValueSchema = import_zod52.z.object({
10516
+ ref: nsRef("cv"),
10517
+ name: import_zod52.z.string(),
10518
+ value: import_zod52.z.string().optional(),
10519
+ filledBy: refSchema.optional()
10520
+ });
10521
+ var CALENDAR_TYPES = [
10522
+ "round_robin",
10523
+ "event",
10524
+ "class_booking",
10525
+ "collective",
10526
+ "service_booking"
10527
+ ];
10528
+ var openHoursBlockSchema = import_zod52.z.object({
10529
+ daysOfTheWeek: import_zod52.z.array(import_zod52.z.number().int().min(0).max(6)),
10530
+ hours: import_zod52.z.array(
10531
+ import_zod52.z.object({
10532
+ openHour: import_zod52.z.number().int().min(0).max(23),
10533
+ openMinute: import_zod52.z.number().int().min(0).max(59),
10534
+ closeHour: import_zod52.z.number().int().min(0).max(23),
10535
+ closeMinute: import_zod52.z.number().int().min(0).max(59)
10536
+ })
10537
+ )
10538
+ });
10539
+ var calendarSchema = import_zod52.z.object({
10540
+ ref: nsRef("calendar"),
10541
+ name: import_zod52.z.string(),
10542
+ calendarType: import_zod52.z.enum(CALENDAR_TYPES),
10543
+ openHours: import_zod52.z.array(openHoursBlockSchema).optional(),
10544
+ availabilityType: import_zod52.z.number().int().optional(),
10545
+ requiresStaff: import_zod52.z.boolean().optional()
10546
+ });
10547
+ var formFieldSchema = import_zod52.z.discriminatedUnion("type", [
10548
+ import_zod52.z.object({
10549
+ type: import_zod52.z.literal("standard"),
10550
+ key: import_zod52.z.string(),
10551
+ required: import_zod52.z.boolean().optional()
10552
+ }),
10553
+ import_zod52.z.object({
10554
+ type: import_zod52.z.literal("custom"),
10555
+ fieldRef: nsRef("field"),
10556
+ required: import_zod52.z.boolean().optional()
10557
+ })
10558
+ ]);
10559
+ var formSchema = import_zod52.z.object({
10560
+ ref: nsRef("form"),
10561
+ name: import_zod52.z.string(),
10562
+ fields: import_zod52.z.array(formFieldSchema)
10563
+ });
10564
+ var pageSchema = import_zod52.z.object({
10565
+ ref: nsRef("page"),
10566
+ name: import_zod52.z.string(),
10567
+ role: import_zod52.z.string().optional(),
10568
+ outline: import_zod52.z.string().optional(),
10569
+ formRef: nsRef("form").optional(),
10570
+ calendarRef: nsRef("calendar").optional()
10571
+ });
10572
+ var funnelSchema = import_zod52.z.object({
10573
+ ref: nsRef("funnel"),
10574
+ name: import_zod52.z.string(),
10575
+ pages: import_zod52.z.array(pageSchema)
10576
+ });
10577
+ var emailAssetSchema = import_zod52.z.object({
10578
+ ref: nsRef("email"),
10579
+ name: import_zod52.z.string(),
10580
+ subject: import_zod52.z.string().optional(),
10581
+ bodyOutline: import_zod52.z.string().optional(),
10582
+ body: import_zod52.z.string().optional(),
10583
+ mergeTags: import_zod52.z.array(import_zod52.z.string()).optional()
10584
+ });
10585
+ var smsAssetSchema = import_zod52.z.object({
10586
+ ref: nsRef("sms"),
10587
+ name: import_zod52.z.string(),
10588
+ bodyOutline: import_zod52.z.string().optional(),
10589
+ body: import_zod52.z.string().optional(),
10590
+ mergeTags: import_zod52.z.array(import_zod52.z.string()).optional()
10591
+ });
10592
+ var waitUnit = import_zod52.z.enum(["minutes", "hours", "days"]);
10593
+ var actionSchema = import_zod52.z.discriminatedUnion("type", [
10594
+ import_zod52.z.object({ type: import_zod52.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
10595
+ import_zod52.z.object({ type: import_zod52.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
10596
+ import_zod52.z.object({ type: import_zod52.z.literal("send_email"), emailRef: nsRef("email") }),
10597
+ import_zod52.z.object({ type: import_zod52.z.literal("send_sms"), smsRef: nsRef("sms") }),
10598
+ import_zod52.z.object({ type: import_zod52.z.literal("wait"), value: import_zod52.z.number().positive(), unit: waitUnit }),
10599
+ import_zod52.z.object({
10600
+ type: import_zod52.z.literal("internal_notification"),
10601
+ to: import_zod52.z.string(),
10602
+ title: import_zod52.z.string(),
10603
+ body: import_zod52.z.string()
10604
+ }),
10605
+ import_zod52.z.object({
10606
+ type: import_zod52.z.literal("update_contact_field"),
10607
+ fieldRef: nsRef("field"),
10608
+ value: import_zod52.z.string()
10609
+ }),
10610
+ import_zod52.z.object({ type: import_zod52.z.literal("add_notes"), body: import_zod52.z.string() }),
10611
+ import_zod52.z.object({
10612
+ type: import_zod52.z.literal("task_notification"),
10613
+ title: import_zod52.z.string(),
10614
+ body: import_zod52.z.string().optional(),
10615
+ dueDate: import_zod52.z.string().optional(),
10616
+ assignedTo: import_zod52.z.string().optional()
10617
+ }),
10618
+ import_zod52.z.object({ type: import_zod52.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
10619
+ import_zod52.z.object({ type: import_zod52.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
10620
+ import_zod52.z.object({
10621
+ type: import_zod52.z.literal("create_opportunity"),
10622
+ pipelineRef: nsRef("pipeline"),
10623
+ stageRef: nsRef("stage"),
10624
+ status: import_zod52.z.string().optional()
10625
+ }),
10626
+ import_zod52.z.object({
10627
+ type: import_zod52.z.literal("update_opportunity"),
10628
+ pipelineRef: nsRef("pipeline"),
10629
+ stageRef: nsRef("stage")
10630
+ }),
10631
+ import_zod52.z.object({
10632
+ type: import_zod52.z.literal("goal_event"),
10633
+ goalCondition: import_zod52.z.string(),
10634
+ // GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
10635
+ action: import_zod52.z.enum(["exit", "continue", "wait"]).optional()
10636
+ })
10637
+ ]);
10638
+ var triggerSchema = import_zod52.z.object({
10639
+ type: import_zod52.z.string(),
10640
+ formRef: nsRef("form").optional(),
10641
+ tagRef: nsRef("tag").optional(),
10642
+ calendarRef: nsRef("calendar").optional(),
10643
+ pipelineRef: nsRef("pipeline").optional(),
10644
+ stageRef: nsRef("stage").optional()
10645
+ });
10646
+ var workflowSchema = import_zod52.z.object({
10647
+ ref: nsRef("workflow"),
10648
+ name: import_zod52.z.string(),
10649
+ trigger: triggerSchema.optional(),
10650
+ stopOnResponse: import_zod52.z.boolean().optional(),
10651
+ actions: import_zod52.z.array(actionSchema).max(40)
10652
+ // house rule: <=40 actions/workflow
10653
+ });
10654
+ var handoffSchema = import_zod52.z.object({
10655
+ ref: nsRef("handoff"),
10656
+ owner: import_zod52.z.enum(["JERRY-UI", "JERRY-EXT", "SASHA"]),
10657
+ title: import_zod52.z.string(),
10658
+ trigger: import_zod52.z.string().optional(),
10659
+ instruction: import_zod52.z.string(),
10660
+ produces: refSchema.nullable().optional(),
10661
+ successCheck: import_zod52.z.string(),
10662
+ blocks: import_zod52.z.array(import_zod52.z.string()).optional()
10663
+ });
10664
+ var buildPlanSchema = import_zod52.z.object({
10665
+ schemaVersion: import_zod52.z.string(),
10666
+ planId: import_zod52.z.string(),
10667
+ briefId: import_zod52.z.string(),
10668
+ preset: import_zod52.z.string(),
10669
+ summary: import_zod52.z.string().optional(),
10670
+ pipelines: import_zod52.z.array(pipelineSchema).optional(),
10671
+ customFields: import_zod52.z.array(customFieldSchema).optional(),
10672
+ tags: import_zod52.z.array(tagSchema).optional(),
10673
+ customValues: import_zod52.z.array(customValueSchema).optional(),
10674
+ calendars: import_zod52.z.array(calendarSchema).optional(),
10675
+ forms: import_zod52.z.array(formSchema).optional(),
10676
+ funnels: import_zod52.z.array(funnelSchema).optional(),
10677
+ emails: import_zod52.z.array(emailAssetSchema).optional(),
10678
+ sms: import_zod52.z.array(smsAssetSchema).optional(),
10679
+ workflows: import_zod52.z.array(workflowSchema).optional(),
10680
+ handoffs: import_zod52.z.array(handoffSchema).optional(),
10681
+ buildOrder: import_zod52.z.array(import_zod52.z.string()).optional(),
10682
+ idMap: import_zod52.z.record(import_zod52.z.string()).optional()
10683
+ }).strict();
10684
+ function collectDefinedRefs(plan) {
10685
+ const refs = /* @__PURE__ */ new Map();
10686
+ const duplicates = [];
10687
+ const add = (ref) => {
10688
+ if (refs.has(ref)) duplicates.push(ref);
10689
+ else refs.set(ref, refNamespace(ref));
10690
+ };
10691
+ for (const p of plan.pipelines ?? []) {
10692
+ add(p.ref);
10693
+ for (const s of p.stages) add(s.ref);
10694
+ }
10695
+ for (const f of plan.customFields ?? []) add(f.ref);
10696
+ for (const t of plan.tags ?? []) add(t.ref);
10697
+ for (const cv of plan.customValues ?? []) add(cv.ref);
10698
+ for (const c of plan.calendars ?? []) add(c.ref);
10699
+ for (const fm of plan.forms ?? []) add(fm.ref);
10700
+ for (const fn of plan.funnels ?? []) {
10701
+ add(fn.ref);
10702
+ for (const pg of fn.pages) add(pg.ref);
10703
+ }
10704
+ for (const e of plan.emails ?? []) add(e.ref);
10705
+ for (const s of plan.sms ?? []) add(s.ref);
10706
+ for (const w of plan.workflows ?? []) add(w.ref);
10707
+ for (const h of plan.handoffs ?? []) add(h.ref);
10708
+ return { refs, duplicates };
10709
+ }
10710
+ function checkRefIntegrity(plan, defined) {
10711
+ const errors = [];
10712
+ let scanned = 0;
10713
+ const check = (ref, expectNs, where) => {
10714
+ if (ref === null || ref === void 0) return;
10715
+ scanned++;
10716
+ if (!defined.has(ref)) {
10717
+ errors.push(`${where}: ref "${ref}" does not resolve to any defined ${expectNs}`);
10718
+ return;
10719
+ }
10720
+ const ns = defined.get(ref);
10721
+ if (expectNs !== "any" && ns !== expectNs) {
10722
+ errors.push(`${where}: ref "${ref}" is a ${ns}, expected a ${expectNs}`);
10723
+ }
10724
+ };
10725
+ const checkMaybeWildcard = (ref, where) => {
10726
+ scanned++;
10727
+ if (ref.endsWith(".*")) {
10728
+ const ns = ref.slice(0, -2);
10729
+ const known = REF_NAMESPACES;
10730
+ if (!known.includes(ns)) {
10731
+ errors.push(`${where}: "${ref}" is not a valid <namespace>.* wildcard`);
10732
+ }
10733
+ return;
10734
+ }
10735
+ if (!defined.has(ref)) {
10736
+ errors.push(`${where}: ref "${ref}" does not resolve to any defined object`);
10737
+ }
10738
+ };
10739
+ for (const cv of plan.customValues ?? []) {
10740
+ if (cv.filledBy) check(cv.filledBy, "any", `customValues[${cv.ref}].filledBy`);
10741
+ }
10742
+ for (const fm of plan.forms ?? []) {
10743
+ for (const fl of fm.fields) {
10744
+ if (fl.type === "custom") check(fl.fieldRef, "field", `forms[${fm.ref}].fields`);
10745
+ }
10746
+ }
10747
+ for (const fn of plan.funnels ?? []) {
10748
+ for (const pg of fn.pages) {
10749
+ check(pg.formRef, "form", `funnels[${fn.ref}].pages[${pg.ref}].formRef`);
10750
+ check(pg.calendarRef, "calendar", `funnels[${fn.ref}].pages[${pg.ref}].calendarRef`);
10751
+ }
10752
+ }
10753
+ for (const w of plan.workflows ?? []) {
10754
+ if (w.trigger) {
10755
+ const t = w.trigger;
10756
+ check(t.formRef, "form", `workflows[${w.ref}].trigger.formRef`);
10757
+ check(t.tagRef, "tag", `workflows[${w.ref}].trigger.tagRef`);
10758
+ check(t.calendarRef, "calendar", `workflows[${w.ref}].trigger.calendarRef`);
10759
+ check(t.pipelineRef, "pipeline", `workflows[${w.ref}].trigger.pipelineRef`);
10760
+ check(t.stageRef, "stage", `workflows[${w.ref}].trigger.stageRef`);
10761
+ }
10762
+ w.actions.forEach((a, i) => {
10763
+ const where = `workflows[${w.ref}].actions[${i}](${a.type})`;
10764
+ if ("tagRef" in a) check(a.tagRef, "tag", where);
10765
+ if ("emailRef" in a) check(a.emailRef, "email", where);
10766
+ if ("smsRef" in a) check(a.smsRef, "sms", where);
10767
+ if ("fieldRef" in a) check(a.fieldRef, "field", where);
10768
+ if ("pipelineRef" in a) check(a.pipelineRef, "pipeline", where);
10769
+ if ("stageRef" in a) check(a.stageRef, "stage", where);
10770
+ if ("workflowRef" in a) check(a.workflowRef, "workflow", where);
10771
+ });
10772
+ }
10773
+ for (const h of plan.handoffs ?? []) {
10774
+ if (h.produces) check(h.produces, "any", `handoffs[${h.ref}].produces`);
10775
+ for (const b of h.blocks ?? []) checkMaybeWildcard(b, `handoffs[${h.ref}].blocks`);
10776
+ }
10777
+ for (const b of plan.buildOrder ?? []) checkMaybeWildcard(b, "buildOrder");
10778
+ return { errors, scanned };
10779
+ }
10780
+ function validateBuildPlan(input) {
10781
+ const parsed = buildPlanSchema.safeParse(input);
10782
+ if (!parsed.success) {
10783
+ return {
10784
+ valid: false,
10785
+ errors: parsed.error.issues.map(
10786
+ (i) => `${i.path.join(".") || "(root)"}: ${i.message}`
10787
+ ),
10788
+ warnings: [],
10789
+ referencesScanned: 0
10790
+ };
10791
+ }
10792
+ const plan = parsed.data;
10793
+ const { refs, duplicates } = collectDefinedRefs(plan);
10794
+ const { errors, scanned } = checkRefIntegrity(plan, refs);
10795
+ const dupErrors = duplicates.map((d) => `duplicate ref "${d}" \u2014 refs must be unique`);
10796
+ const allErrors = [...dupErrors, ...errors];
10797
+ for (const p of plan.pipelines ?? []) {
10798
+ const seen = /* @__PURE__ */ new Set();
10799
+ for (const s of p.stages) {
10800
+ const key = s.name.trim().toLowerCase();
10801
+ if (seen.has(key)) {
10802
+ allErrors.push(
10803
+ `pipeline "${p.ref}" has duplicate stage name "${s.name}" \u2014 stage names must be unique within a pipeline so each stage ref resolves to a single real id`
10804
+ );
10805
+ }
10806
+ seen.add(key);
10807
+ }
10808
+ }
10809
+ const warnings = [];
10810
+ for (const p of plan.pipelines ?? []) {
10811
+ const positions = p.stages.map((s) => s.position).sort((a, b) => a - b);
10812
+ const expected = positions.every((pos, idx) => pos === idx);
10813
+ if (!expected) {
10814
+ warnings.push(`pipeline "${p.ref}" stage positions are not a clean 0..${p.stages.length - 1} sequence`);
10815
+ }
10816
+ }
10817
+ const CHOICE_DATATYPES = /* @__PURE__ */ new Set(["SINGLE_OPTIONS", "MULTIPLE_OPTIONS", "CHECKBOX"]);
10818
+ for (const f of plan.customFields ?? []) {
10819
+ if (CHOICE_DATATYPES.has(f.dataType) && !(f.options && f.options.length > 0)) {
10820
+ warnings.push(
10821
+ `customFields "${f.ref}" is a ${f.dataType} but declares no options \u2014 the executor cannot create a usable choice field without them`
10822
+ );
10823
+ }
10824
+ }
10825
+ return {
10826
+ valid: allErrors.length === 0,
10827
+ errors: allErrors,
10828
+ warnings,
10829
+ plan,
10830
+ referencesScanned: scanned
10831
+ };
10832
+ }
10833
+
10834
+ // src/intake-to-build/executor.ts
10835
+ var EXECUTION_ORDER = [
10836
+ "pipelines",
10837
+ "customFields",
10838
+ "tags",
10839
+ "customValues",
10840
+ "calendars",
10841
+ "forms",
10842
+ "funnels",
10843
+ "emails",
10844
+ "sms",
10845
+ "workflows"
10846
+ ];
10847
+ var WAIT_UNIT_MAP = {
10848
+ minutes: "minutes",
10849
+ hours: "hour",
10850
+ days: "day"
10851
+ };
10852
+ var PENDING = (ref) => `__PENDING__:${ref}`;
10853
+ var isPending = (v) => v.startsWith("__PENDING__:");
10854
+ var norm = (s) => s.trim().toLowerCase();
10855
+ function buildRefIndex(plan) {
10856
+ const idx = {
10857
+ tagName: /* @__PURE__ */ new Map(),
10858
+ fieldName: /* @__PURE__ */ new Map(),
10859
+ fieldType: /* @__PURE__ */ new Map(),
10860
+ email: /* @__PURE__ */ new Map(),
10861
+ sms: /* @__PURE__ */ new Map(),
10862
+ workflowName: /* @__PURE__ */ new Map(),
10863
+ pipelineName: /* @__PURE__ */ new Map(),
10864
+ stageName: /* @__PURE__ */ new Map()
10865
+ };
10866
+ for (const t of plan.tags ?? []) idx.tagName.set(t.ref, t.name);
10867
+ for (const f of plan.customFields ?? []) {
10868
+ idx.fieldName.set(f.ref, f.name);
10869
+ idx.fieldType.set(f.ref, f.dataType);
10870
+ }
10871
+ for (const e of plan.emails ?? []) idx.email.set(e.ref, { subject: e.subject, body: e.body, bodyOutline: e.bodyOutline, name: e.name });
10872
+ for (const s of plan.sms ?? []) idx.sms.set(s.ref, { body: s.body, bodyOutline: s.bodyOutline, name: s.name });
10873
+ for (const w of plan.workflows ?? []) idx.workflowName.set(w.ref, w.name);
10874
+ for (const p of plan.pipelines ?? []) {
10875
+ idx.pipelineName.set(p.ref, p.name);
10876
+ for (const st of p.stages) idx.stageName.set(st.ref, st.name);
10877
+ }
10878
+ return idx;
10879
+ }
10880
+ function htmlWrap(text) {
10881
+ const t = text.trim();
10882
+ if (/^\s*<[a-z]/i.test(t)) return t;
10883
+ return `<p style="margin:0px;">${t}</p>`;
10884
+ }
10885
+ function resolveId(ref, idMap) {
10886
+ const real = idMap.get(ref);
10887
+ return real ?? PENDING(ref);
10888
+ }
10889
+ function expandAction(action, idx, idMap) {
10890
+ switch (action.type) {
10891
+ case "add_contact_tag":
10892
+ case "remove_contact_tag": {
10893
+ const name = idx.tagName.get(action.tagRef) ?? action.tagRef;
10894
+ return {
10895
+ kind: "expanded",
10896
+ pendingRefs: [],
10897
+ native: {
10898
+ type: action.type,
10899
+ name: action.type === "add_contact_tag" ? `Add tag: ${name}` : `Remove tag: ${name}`,
10900
+ attributes: { tags: [name] }
10901
+ }
10902
+ };
10903
+ }
10904
+ case "send_email": {
10905
+ const e = idx.email.get(action.emailRef);
10906
+ if (!e) return { kind: "manual", logicalType: action.type, reason: `email ref ${action.emailRef} not found in plan` };
10907
+ if (!e.body) {
10908
+ return {
10909
+ kind: "needs_content",
10910
+ logicalType: action.type,
10911
+ reason: `email "${action.emailRef}" has only an outline (no send-ready body) \u2014 supply copy before this email step can be built`
10912
+ };
10913
+ }
10914
+ return {
10915
+ kind: "expanded",
10916
+ pendingRefs: [],
10917
+ native: {
10918
+ type: "email",
10919
+ name: `Email: ${e.name}`,
10920
+ attributes: {
10921
+ subject: e.subject ?? e.name,
10922
+ html: htmlWrap(e.body),
10923
+ trackingOptions: { hasTrackingLinks: false, hasUtmTracking: false, hasTags: false }
10924
+ }
10925
+ }
10926
+ };
10927
+ }
10928
+ case "send_sms": {
10929
+ const s = idx.sms.get(action.smsRef);
10930
+ if (!s) return { kind: "manual", logicalType: action.type, reason: `sms ref ${action.smsRef} not found in plan` };
10931
+ if (!s.body) {
10932
+ return {
10933
+ kind: "needs_content",
10934
+ logicalType: action.type,
10935
+ reason: `sms "${action.smsRef}" has only an outline (no send-ready body) \u2014 supply copy before this SMS step can be built`
10936
+ };
10937
+ }
10938
+ return {
10939
+ kind: "expanded",
10940
+ pendingRefs: [],
10941
+ native: {
10942
+ type: "sms",
10943
+ name: `SMS: ${s.name}`,
10944
+ attributes: { body: s.body, attachments: [] }
10945
+ }
10946
+ };
10947
+ }
10948
+ case "wait": {
10949
+ return {
10950
+ kind: "expanded",
10951
+ pendingRefs: [],
10952
+ native: {
10953
+ type: "wait",
10954
+ name: "Wait",
10955
+ attributes: {
10956
+ type: "time",
10957
+ startAfter: { type: WAIT_UNIT_MAP[action.unit], value: action.value, when: "after" },
10958
+ name: "Wait",
10959
+ isHybridAction: true,
10960
+ hybridActionType: "wait",
10961
+ convertToMultipath: false,
10962
+ transitions: []
10963
+ }
10964
+ }
10965
+ };
10966
+ }
10967
+ case "internal_notification": {
10968
+ const looksLikeUserId = /^[A-Za-z0-9]{17,}$/.test(action.to);
10969
+ return {
10970
+ kind: "expanded",
10971
+ pendingRefs: [],
10972
+ native: {
10973
+ type: "internal_notification",
10974
+ name: `Notify: ${action.title}`,
10975
+ attributes: {
10976
+ type: "notification",
10977
+ notification: {
10978
+ body: action.body,
10979
+ title: action.title,
10980
+ userType: "user",
10981
+ redirectPage: "contact",
10982
+ type: "send_notification",
10983
+ selectedUser: looksLikeUserId ? action.to : ""
10984
+ }
10985
+ }
10986
+ }
10987
+ };
10988
+ }
10989
+ case "update_contact_field": {
10990
+ const fieldId = resolveId(action.fieldRef, idMap);
10991
+ const title = idx.fieldName.get(action.fieldRef) ?? action.fieldRef;
10992
+ return {
10993
+ kind: "expanded",
10994
+ pendingRefs: isPending(fieldId) ? [action.fieldRef] : [],
10995
+ native: {
10996
+ type: "update_contact_field",
10997
+ name: `Update field: ${title}`,
10998
+ attributes: {
10999
+ type: "update_contact_field",
11000
+ actionType: "update_field_data",
11001
+ fields: [{ field: fieldId, value: action.value, title, type: "text", date: "" }]
11002
+ }
11003
+ }
11004
+ };
11005
+ }
11006
+ case "add_notes": {
11007
+ return {
11008
+ kind: "expanded",
11009
+ pendingRefs: [],
11010
+ native: { type: "add_notes", name: "Add note", attributes: { type: "add_notes", html: htmlWrap(action.body) } }
11011
+ };
11012
+ }
11013
+ case "task_notification": {
11014
+ return {
11015
+ kind: "expanded",
11016
+ pendingRefs: [],
11017
+ native: {
11018
+ type: "task-notification",
11019
+ name: `Task: ${action.title}`,
11020
+ attributes: {
11021
+ assignedTo: action.assignedTo ?? "",
11022
+ title: action.title,
11023
+ dueDate: action.dueDate ?? "1",
11024
+ body: action.body ?? "",
11025
+ type: "task-notification",
11026
+ __customInputs__: {}
11027
+ }
11028
+ }
11029
+ };
11030
+ }
11031
+ case "remove_from_workflow":
11032
+ case "add_to_workflow": {
11033
+ const wfId = resolveId(action.workflowRef, idMap);
11034
+ const wfName = idx.workflowName.get(action.workflowRef) ?? action.workflowRef;
11035
+ const pending = isPending(wfId) ? [action.workflowRef] : [];
11036
+ if (action.type === "remove_from_workflow") {
11037
+ return {
11038
+ kind: "expanded",
11039
+ pendingRefs: pending,
11040
+ native: {
11041
+ type: "remove_from_workflow",
11042
+ name: `Remove from: ${wfName}`,
11043
+ attributes: { workflowId: wfId, workflowName: wfName, type: "remove_from_workflow", workflow_id: [wfId] }
11044
+ }
11045
+ };
11046
+ }
11047
+ return {
11048
+ kind: "expanded",
11049
+ pendingRefs: pending,
11050
+ native: {
11051
+ type: "add_to_workflow",
11052
+ name: `Add to: ${wfName}`,
11053
+ attributes: { workflowId: wfId, workflowName: wfName, type: "add_to_workflow", workflow_id: [wfId] }
11054
+ }
11055
+ };
11056
+ }
11057
+ case "create_opportunity":
11058
+ case "update_opportunity": {
11059
+ const pName = idx.pipelineName.get(action.pipelineRef) ?? action.pipelineRef;
11060
+ const sName = idx.stageName.get(action.stageRef) ?? action.stageRef;
11061
+ const verb = action.type === "create_opportunity" ? "Create" : "Move";
11062
+ return {
11063
+ kind: "manual",
11064
+ logicalType: action.type,
11065
+ reason: `${verb} opportunity \u2192 "${pName}" / stage "${sName}": GHL rejects a synthesized opportunity node ("corrupted type") and silently kills the whole workflow save. Add this step by hand in the GHL workflow builder (Add action \u2192 Create/Update Opportunity), or round-trip an existing node.`
11066
+ };
11067
+ }
11068
+ case "goal_event": {
11069
+ return {
11070
+ kind: "expanded",
11071
+ pendingRefs: [],
11072
+ native: {
11073
+ type: "workflow_goal",
11074
+ name: "Goal",
11075
+ attributes: {
11076
+ op: "or",
11077
+ segments: [{ op: "or", conditions: [{ goal_condition: action.goalCondition, id: "" }] }],
11078
+ type: "workflow_goal",
11079
+ action: action.action ?? "exit"
11080
+ }
11081
+ }
11082
+ };
11083
+ }
11084
+ default: {
11085
+ const _exhaustive = action;
11086
+ return { kind: "manual", logicalType: _exhaustive.type, reason: "unrecognized logical action type" };
11087
+ }
11088
+ }
11089
+ }
11090
+ var MAX_ACTIONS_PER_WORKFLOW = 40;
11091
+ function expandWorkflow(workflow, idx, idMap, gatedBy) {
11092
+ const nativeActions = [];
11093
+ const manual = [];
11094
+ const needsContent = [];
11095
+ const pendingRefs = /* @__PURE__ */ new Set();
11096
+ workflow.actions.forEach((a, i) => {
11097
+ const exp = expandAction(a, idx, idMap);
11098
+ if (exp.kind === "expanded") {
11099
+ nativeActions.push(exp.native);
11100
+ exp.pendingRefs.forEach((r) => pendingRefs.add(r));
11101
+ } else if (exp.kind === "manual") {
11102
+ manual.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
11103
+ } else {
11104
+ needsContent.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
11105
+ }
11106
+ });
11107
+ const splitInto = Math.max(1, Math.ceil(nativeActions.length / MAX_ACTIONS_PER_WORKFLOW));
11108
+ return {
11109
+ ref: workflow.ref,
11110
+ name: workflow.name,
11111
+ nativeActions,
11112
+ manual,
11113
+ needsContent,
11114
+ pendingRefs: [...pendingRefs],
11115
+ splitInto,
11116
+ gatedBy
11117
+ };
11118
+ }
11119
+ function scanSection(section2, planObjects, existing) {
11120
+ const byName = /* @__PURE__ */ new Map();
11121
+ for (const e of existing ?? []) byName.set(norm(e.name), e);
11122
+ return planObjects.map((o) => {
11123
+ const hit = byName.get(norm(o.name));
11124
+ return hit ? { ref: o.ref, type: section2, name: o.name, status: "existing", existingId: hit.id } : { ref: o.ref, type: section2, name: o.name, status: "would_create" };
11125
+ });
11126
+ }
11127
+ function computeWorkflowGates(plan, metHandoffs) {
11128
+ const gates = /* @__PURE__ */ new Map();
11129
+ const addGate = (wfRef, handoffRef) => {
11130
+ const cur = gates.get(wfRef) ?? [];
11131
+ if (!cur.includes(handoffRef)) cur.push(handoffRef);
11132
+ gates.set(wfRef, cur);
11133
+ };
11134
+ const workflowRefs = (plan.workflows ?? []).map((w) => w.ref);
11135
+ for (const h of plan.handoffs ?? []) {
11136
+ if (metHandoffs.has(h.ref)) continue;
11137
+ for (const block of h.blocks ?? []) {
11138
+ if (block.endsWith(".*")) {
11139
+ if (block === "workflow.*") for (const wf of workflowRefs) addGate(wf, h.ref);
11140
+ } else if (refNamespace(block) === "workflow") {
11141
+ addGate(block, h.ref);
11142
+ }
11143
+ }
11144
+ }
11145
+ return gates;
11146
+ }
11147
+ var SECTION_OBJECTS = {
11148
+ pipelines: (p) => (p.pipelines ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11149
+ customFields: (p) => (p.customFields ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11150
+ tags: (p) => (p.tags ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11151
+ customValues: (p) => (p.customValues ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11152
+ calendars: (p) => (p.calendars ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11153
+ forms: (p) => (p.forms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11154
+ funnels: (p) => (p.funnels ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11155
+ emails: (p) => (p.emails ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11156
+ sms: (p) => (p.sms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11157
+ workflows: (p) => (p.workflows ?? []).map((x) => ({ ref: x.ref, name: x.name }))
11158
+ };
11159
+ var SECTION_EXISTING = {
11160
+ pipelines: "pipelines",
11161
+ customFields: "customFields",
11162
+ tags: "tags",
11163
+ customValues: "customValues",
11164
+ calendars: "calendars",
11165
+ forms: "forms",
11166
+ funnels: "funnels",
11167
+ emails: null,
11168
+ // emails/sms live inside workflows; no standalone clobber scan
11169
+ sms: null,
11170
+ workflows: "workflows"
11171
+ };
11172
+ function resolvePlan(plan, existing, opts = {}) {
11173
+ const idx = buildRefIndex(plan);
11174
+ const metHandoffs = new Set(opts.metHandoffs ?? []);
11175
+ const seededIdMap = new Map(Object.entries(opts.idMap ?? {}));
11176
+ const items = [];
11177
+ const idMap = new Map(seededIdMap);
11178
+ for (const section2 of EXECUTION_ORDER) {
11179
+ const objs = SECTION_OBJECTS[section2](plan);
11180
+ if (objs.length === 0) continue;
11181
+ const existingKey = SECTION_EXISTING[section2];
11182
+ const existingList = existingKey ? existing[existingKey] : void 0;
11183
+ const scanned = scanSection(section2, objs, existingList);
11184
+ for (const it of scanned) {
11185
+ items.push(it);
11186
+ if (it.status === "existing" && it.existingId) idMap.set(it.ref, it.existingId);
11187
+ else if (!idMap.has(it.ref)) idMap.set(it.ref, PENDING(it.ref));
11188
+ }
11189
+ if (section2 === "pipelines") {
11190
+ for (const p of plan.pipelines ?? []) {
11191
+ for (const st of p.stages) if (!idMap.has(st.ref)) idMap.set(st.ref, PENDING(st.ref));
11192
+ }
11193
+ }
11194
+ }
11195
+ const gates = computeWorkflowGates(plan, metHandoffs);
11196
+ const workflows = (plan.workflows ?? []).map((w) => expandWorkflow(w, idx, idMap, gates.get(w.ref) ?? []));
11197
+ const handoffs = (plan.handoffs ?? []).map((h) => ({
11198
+ ref: h.ref,
11199
+ owner: h.owner,
11200
+ title: h.title,
11201
+ instruction: h.instruction,
11202
+ successCheck: h.successCheck,
11203
+ met: metHandoffs.has(h.ref),
11204
+ blocks: h.blocks ?? []
11205
+ }));
11206
+ const summary = {
11207
+ wouldCreate: items.filter((i) => i.status === "would_create").length,
11208
+ existing: items.filter((i) => i.status === "existing").length,
11209
+ workflowsTotal: workflows.length,
11210
+ workflowsGated: workflows.filter((w) => w.gatedBy.length > 0).length,
11211
+ actionsExpanded: workflows.reduce((n, w) => n + w.nativeActions.length, 0),
11212
+ actionsManual: workflows.reduce((n, w) => n + w.manual.length, 0),
11213
+ actionsNeedContent: workflows.reduce((n, w) => n + w.needsContent.length, 0)
11214
+ };
11215
+ return { items, idMap: Object.fromEntries(idMap), workflows, handoffs, summary };
11216
+ }
11217
+ function renderReport(plan, result, ctx) {
11218
+ const L = [];
11219
+ const { summary } = result;
11220
+ L.push(`Blueprint build ${ctx.mode === "dry_run" ? "PREVIEW (dry run \u2014 no changes written)" : "REPORT"}`);
11221
+ L.push(`Account: ${ctx.locationName} (${ctx.locationId})`);
11222
+ L.push(`Plan: ${plan.planId} \xB7 preset: ${plan.preset}`);
11223
+ L.push("");
11224
+ L.push(
11225
+ `Objects: ${summary.wouldCreate} to create, ${summary.existing} already exist (skipped, never modified).`
11226
+ );
11227
+ L.push(
11228
+ `Workflows: ${summary.workflowsTotal} (${summary.workflowsGated} gated DRAFT by an unmet handoff). Actions: ${summary.actionsExpanded} auto-built, ${summary.actionsManual} need a manual GHL-UI step, ${summary.actionsNeedContent} need send-ready copy.`
11229
+ );
11230
+ L.push("");
11231
+ L.push("\u2500\u2500 Blueprint builds automatically \u2500\u2500");
11232
+ for (const section2 of EXECUTION_ORDER) {
11233
+ const secItems = result.items.filter((i) => i.type === section2);
11234
+ if (secItems.length === 0) continue;
11235
+ for (const it of secItems) {
11236
+ const mark = it.status === "existing" ? "skip (exists)" : ctx.mode === "dry_run" ? "would create" : "create";
11237
+ L.push(` [${section2}] ${it.name} \u2014 ${mark}${it.existingId ? ` \u2192 ${it.existingId}` : ""}`);
11238
+ }
11239
+ }
11240
+ for (const w of result.workflows) {
11241
+ const gate = w.gatedBy.length ? ` \u2014 DRAFT, gated by ${w.gatedBy.join(", ")}` : ctx.publishWorkflows ? " \u2014 publish" : " \u2014 DRAFT";
11242
+ const split = w.splitInto > 1 ? ` (splits into ${w.splitInto} chained workflows, >${MAX_ACTIONS_PER_WORKFLOW} actions)` : "";
11243
+ L.push(` [workflow] ${w.name}: ${w.nativeActions.length} actions${split}${gate}`);
11244
+ }
11245
+ L.push("");
11246
+ L.push("\u2500\u2500 You must do these by hand (in order) \u2500\u2500");
11247
+ let any = false;
11248
+ for (const w of result.workflows) {
11249
+ for (const m of w.manual) {
11250
+ any = true;
11251
+ L.push(` \u2022 [${w.name}] ${m.reason}`);
11252
+ }
11253
+ for (const nc of w.needsContent) {
11254
+ any = true;
11255
+ L.push(` \u2022 [${w.name}] ${nc.reason}`);
11256
+ }
11257
+ }
11258
+ for (const h of result.handoffs) {
11259
+ if (h.met) continue;
11260
+ any = true;
11261
+ L.push(` \u2022 [${h.owner}] ${h.title}: ${h.instruction} (done when: ${h.successCheck})`);
11262
+ }
11263
+ if (!any) L.push(" (nothing \u2014 everything in this plan is auto-buildable)");
11264
+ return L.join("\n");
11265
+ }
11266
+
11267
+ // src/intake-to-build/execute.ts
11268
+ var norm2 = (s) => s.trim().toLowerCase();
11269
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11270
+ async function executeBackbone(plan, deps, opts = {}) {
11271
+ const retries = opts.verifyRetries ?? 4;
11272
+ const backoff = opts.verifyBackoffMs ?? 500;
11273
+ const idMap = {};
11274
+ const built = [];
11275
+ const halt = (atRef, type, reason) => ({
11276
+ ok: false,
11277
+ idMap,
11278
+ built,
11279
+ halted: { atRef, type, reason },
11280
+ deferred: deferredSections(plan)
11281
+ });
11282
+ async function pollForNew(read, name, beforeIds) {
11283
+ for (let attempt = 1; attempt <= retries; attempt++) {
11284
+ const fresh = (await read()).filter((o) => norm2(o.name) === norm2(name) && !beforeIds.has(o.id));
11285
+ if (fresh.length === 1) return fresh[0];
11286
+ if (fresh.length > 1) return void 0;
11287
+ if (attempt < retries) await sleep(backoff * attempt);
11288
+ }
11289
+ return void 0;
11290
+ }
11291
+ for (const p of plan.pipelines ?? []) {
11292
+ let pipelines;
11293
+ try {
11294
+ pipelines = await deps.listPipelines();
11295
+ } catch (e) {
11296
+ return halt(p.ref, "pipeline", `could not read existing pipelines: ${msg(e)}`);
11297
+ }
11298
+ const matches = pipelines.filter((x) => norm2(x.name) === norm2(p.name));
11299
+ if (matches.length > 1) {
11300
+ return halt(p.ref, "pipeline", `${matches.length} existing pipelines are named "${p.name}" \u2014 ambiguous, cannot safely bind ${p.ref}. Resolve the duplicate in GHL or rename, then re-run.`);
11301
+ }
11302
+ let pipeline;
11303
+ let created;
11304
+ if (matches.length === 1) {
11305
+ pipeline = matches[0];
11306
+ created = false;
11307
+ } else {
11308
+ const beforeIds = new Set(pipelines.map((x) => x.id));
11309
+ try {
11310
+ await deps.createPipeline(p.name, p.stages.map((s) => ({ name: s.name, position: s.position })));
11311
+ } catch (e) {
11312
+ return halt(p.ref, "pipeline", `create failed: ${msg(e)}`);
11313
+ }
11314
+ const verified = await pollForNew(() => deps.listPipelines(), p.name, beforeIds);
11315
+ if (!verified) return halt(p.ref, "pipeline", "created but could not verify a single new pipeline by read-back (none, or an ambiguous duplicate, appeared)");
11316
+ pipeline = verified;
11317
+ created = true;
11318
+ }
11319
+ idMap[p.ref] = pipeline.id;
11320
+ built.push({ ref: p.ref, type: "pipeline", name: p.name, status: created ? "created" : "existing", realId: pipeline.id });
11321
+ for (const st of p.stages) {
11322
+ const stageMatches = pipeline.stages.filter((es) => norm2(es.name) === norm2(st.name));
11323
+ if (stageMatches.length === 0) {
11324
+ return halt(st.ref, "stage", `pipeline "${p.name}" has no stage named "${st.name}" after build \u2014 cannot resolve ${st.ref}`);
11325
+ }
11326
+ if (stageMatches.length > 1) {
11327
+ return halt(st.ref, "stage", `pipeline "${p.name}" has ${stageMatches.length} stages named "${st.name}" \u2014 cannot resolve ${st.ref} to a single id`);
11328
+ }
11329
+ idMap[st.ref] = stageMatches[0].id;
11330
+ built.push({ ref: st.ref, type: "stage", name: st.name, status: created ? "created" : "existing", realId: stageMatches[0].id });
11331
+ }
11332
+ }
11333
+ const fieldHalt = await buildSimple(
11334
+ plan.customFields ?? [],
11335
+ "field",
11336
+ () => deps.listCustomFields(),
11337
+ (f) => deps.createCustomField({ name: f.name, dataType: f.dataType, model: f.model, options: f.options }),
11338
+ (f) => f.name,
11339
+ pollForNew,
11340
+ idMap,
11341
+ built
11342
+ );
11343
+ if (fieldHalt) return halt(fieldHalt.ref, "field", fieldHalt.reason);
11344
+ const tagHalt = await buildSimple(
11345
+ plan.tags ?? [],
11346
+ "tag",
11347
+ () => deps.listTags(),
11348
+ (t) => deps.createTag(t.name),
11349
+ (t) => t.name,
11350
+ pollForNew,
11351
+ idMap,
11352
+ built
11353
+ );
11354
+ if (tagHalt) return halt(tagHalt.ref, "tag", tagHalt.reason);
11355
+ const cvHalt = await buildSimple(
11356
+ plan.customValues ?? [],
11357
+ "cv",
11358
+ () => deps.listCustomValues(),
11359
+ (cv) => deps.createCustomValue(cv.name, cv.value ?? ""),
11360
+ (cv) => cv.name,
11361
+ pollForNew,
11362
+ idMap,
11363
+ built
11364
+ );
11365
+ if (cvHalt) return halt(cvHalt.ref, "cv", cvHalt.reason);
11366
+ return { ok: true, idMap, built, deferred: deferredSections(plan) };
11367
+ }
11368
+ async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
11369
+ if (objects.length === 0) return null;
11370
+ for (const obj of objects) {
11371
+ const name = nameOf(obj);
11372
+ let existing;
11373
+ try {
11374
+ existing = await list();
11375
+ } catch (e) {
11376
+ return { ref: obj.ref, reason: `could not read existing ${type}s: ${msg(e)}` };
11377
+ }
11378
+ const matches = existing.filter((o) => norm2(o.name) === norm2(name));
11379
+ if (matches.length > 1) {
11380
+ return { ref: obj.ref, reason: `${matches.length} existing ${type}s are named "${name}" \u2014 ambiguous, cannot safely bind ${obj.ref}. Resolve the duplicate in GHL, then re-run.` };
11381
+ }
11382
+ if (matches.length === 1) {
11383
+ idMap[obj.ref] = matches[0].id;
11384
+ built.push({ ref: obj.ref, type, name, status: "existing", realId: matches[0].id });
11385
+ continue;
11386
+ }
11387
+ const beforeIds = new Set(existing.map((o) => o.id));
11388
+ try {
11389
+ await create(obj);
11390
+ } catch (e) {
11391
+ return { ref: obj.ref, reason: `create failed: ${msg(e)}` };
11392
+ }
11393
+ const verified = await pollForNew(list, name, beforeIds);
11394
+ if (!verified) return { ref: obj.ref, reason: `created but could not verify a single new ${type} by read-back (none, or an ambiguous duplicate, appeared)` };
11395
+ idMap[obj.ref] = verified.id;
11396
+ built.push({ ref: obj.ref, type, name, status: "created", realId: verified.id });
11397
+ }
11398
+ return null;
11399
+ }
11400
+ function deferredSections(plan) {
11401
+ const out = [];
11402
+ if (plan.calendars?.length) out.push({ section: "calendars", count: plan.calendars.length });
11403
+ if (plan.forms?.length) out.push({ section: "forms", count: plan.forms.length });
11404
+ if (plan.funnels?.length) out.push({ section: "funnels", count: plan.funnels.length });
11405
+ if (plan.workflows?.length) out.push({ section: "workflows", count: plan.workflows.length });
11406
+ return out;
11407
+ }
11408
+ function msg(e) {
11409
+ return e instanceof Error ? e.message : String(e);
11410
+ }
11411
+
11412
+ // src/tools/intake-to-build.ts
11413
+ var customFieldItemSchema = import_zod53.z.object({
11414
+ id: import_zod53.z.string(),
11415
+ name: import_zod53.z.string(),
11416
+ fieldKey: import_zod53.z.string(),
11417
+ dataType: import_zod53.z.string(),
11418
+ model: import_zod53.z.string().optional(),
11419
+ parentId: import_zod53.z.string().optional(),
11420
+ position: import_zod53.z.number().optional(),
11421
+ dateAdded: import_zod53.z.string().optional(),
11422
+ picklistOptions: import_zod53.z.array(import_zod53.z.string()).optional()
11423
+ }).passthrough();
11424
+ function parseCustomFields(raw) {
11425
+ const obj = raw && typeof raw === "object" ? raw : {};
11426
+ const list = Array.isArray(obj.customFields) ? obj.customFields : [];
11427
+ const out = [];
11428
+ for (const item of list) {
11429
+ const parsed = customFieldItemSchema.safeParse(item);
11430
+ if (parsed.success) out.push(parsed.data);
11431
+ }
11432
+ return out;
11433
+ }
11434
+ function extractFormId(result) {
11435
+ if (!result || typeof result !== "object") return void 0;
11436
+ const r = result;
11437
+ if (typeof r.id === "string") return r.id;
11438
+ if (typeof r._id === "string") return r._id;
11439
+ const form = r.form;
11440
+ if (form && typeof form === "object") {
11441
+ const f = form;
11442
+ if (typeof f._id === "string") return f._id;
11443
+ if (typeof f.id === "string") return f.id;
11444
+ }
11445
+ return void 0;
11446
+ }
11447
+ function countFormFields(formFull) {
11448
+ const r = formFull && typeof formFull === "object" ? formFull : {};
11449
+ const form = r.form && typeof r.form === "object" ? r.form : {};
11450
+ const fd = form.formData && typeof form.formData === "object" ? form.formData : {};
11451
+ const inner = fd.form && typeof fd.form === "object" ? fd.form : {};
11452
+ return Array.isArray(inner.fields) ? inner.fields.length : 0;
11453
+ }
11454
+ function extractFormFields(formFull) {
11455
+ const r = formFull && typeof formFull === "object" ? formFull : {};
11456
+ const form = r.form && typeof r.form === "object" ? r.form : {};
11457
+ const fd = form.formData && typeof form.formData === "object" ? form.formData : {};
11458
+ const inner = fd.form && typeof fd.form === "object" ? fd.form : {};
11459
+ const fields = Array.isArray(inner.fields) ? inner.fields : [];
11460
+ return fields.filter((f) => !!f && typeof f === "object");
11461
+ }
11462
+ function findRecordForQuestion(q, records) {
11463
+ const wantKey = expectedFieldKey(q).toLowerCase();
11464
+ const byKey = records.find((r) => r.fieldKey.toLowerCase() === wantKey);
11465
+ if (byKey) return byKey;
11466
+ const wantName = intakeFieldName(q.label).toLowerCase();
11467
+ return records.find((r) => r.name.toLowerCase() === wantName);
11468
+ }
11469
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
11470
+ function isFormNotYetPropagated(error) {
11471
+ const msg2 = error instanceof Error ? error.message : String(error);
11472
+ return /does not exist or is deleted/i.test(msg2);
11473
+ }
11474
+ function pickObjects(raw, keys) {
11475
+ const root = raw && typeof raw === "object" ? raw : {};
11476
+ let list = Array.isArray(raw) ? raw : void 0;
11477
+ if (!list) {
11478
+ for (const k of keys) {
11479
+ if (Array.isArray(root[k])) {
11480
+ list = root[k];
11481
+ break;
11482
+ }
11483
+ }
11484
+ }
11485
+ if (!Array.isArray(list)) return [];
11486
+ const out = [];
11487
+ for (const item of list) {
11488
+ if (!item || typeof item !== "object") continue;
11489
+ const o = item;
11490
+ const id = typeof o.id === "string" ? o.id : typeof o._id === "string" ? o._id : void 0;
11491
+ const name = typeof o.name === "string" ? o.name : void 0;
11492
+ if (id && name) out.push({ id, name });
11493
+ }
11494
+ return out;
11495
+ }
11496
+ async function scanExistingAssets(client, locationId2) {
11497
+ const warnings = [];
11498
+ const assets = {};
11499
+ const read = async (label, fn, into) => {
11500
+ try {
11501
+ assets[into] = await fn();
11502
+ } catch (e) {
11503
+ warnings.push(`could not scan ${label}: ${e instanceof Error ? e.message : String(e)}`);
11504
+ }
11505
+ };
11506
+ await read("pipelines", async () => pickObjects(await client.get("/opportunities/pipelines", { params: { locationId: locationId2 } }), ["pipelines"]), "pipelines");
11507
+ await read("custom fields", async () => pickObjects(await client.get(`/locations/${locationId2}/customFields`), ["customFields"]), "customFields");
11508
+ await read("tags", async () => pickObjects(await client.get(`/locations/${locationId2}/tags`), ["tags"]), "tags");
11509
+ await read("custom values", async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]), "customValues");
11510
+ await read("calendars", async () => pickObjects(await client.get("/calendars/", { params: { locationId: locationId2 } }), ["calendars"]), "calendars");
11511
+ await read("forms", async () => pickObjects(await client.get("/forms/", { params: { locationId: locationId2, limit: 100 } }), ["forms"]), "forms");
11512
+ await read("funnels", async () => pickObjects(await client.get("/funnels/funnel/list", { params: { locationId: locationId2, limit: 100 } }), ["funnels"]), "funnels");
11513
+ await read("workflows", async () => pickObjects(await client.get("/workflows/", { params: { locationId: locationId2 } }), ["workflows"]), "workflows");
11514
+ return { assets, warnings };
11515
+ }
11516
+ function pickPipelines(raw) {
11517
+ const root = raw && typeof raw === "object" ? raw : {};
11518
+ const list = Array.isArray(root.pipelines) ? root.pipelines : Array.isArray(raw) ? raw : [];
11519
+ const out = [];
11520
+ for (const item of list) {
11521
+ if (!item || typeof item !== "object") continue;
11522
+ const p = item;
11523
+ const id = typeof p.id === "string" ? p.id : typeof p._id === "string" ? p._id : void 0;
11524
+ const name = typeof p.name === "string" ? p.name : void 0;
11525
+ if (!id || !name) continue;
11526
+ const stagesRaw = Array.isArray(p.stages) ? p.stages : [];
11527
+ const stages = stagesRaw.filter((s) => !!s && typeof s === "object").map((s) => ({
11528
+ id: typeof s.id === "string" ? s.id : typeof s._id === "string" ? s._id : "",
11529
+ name: typeof s.name === "string" ? s.name : "",
11530
+ position: typeof s.position === "number" ? s.position : void 0
11531
+ })).filter((s) => s.id && s.name);
11532
+ out.push({ id, name, stages });
11533
+ }
11534
+ return out;
11535
+ }
11536
+ function makeExecuteDeps(client, builderClient, locationId2) {
11537
+ const pipelineApi = async (method, path7, body) => {
11538
+ const headers = await builderClient.buildHeaders();
11539
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path7}`;
11540
+ const options = { method, headers };
11541
+ if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
11542
+ const response = await fetch(url, options);
11543
+ if (!response.ok) {
11544
+ const text2 = await response.text();
11545
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path7}
11546
+ ${text2.slice(0, 300)}`);
11547
+ }
11548
+ const text = await response.text();
11549
+ if (!text) return {};
11550
+ try {
11551
+ return JSON.parse(text);
11552
+ } catch {
11553
+ return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
11554
+ }
11555
+ };
11556
+ return {
11557
+ listPipelines: async () => pickPipelines(await pipelineApi("GET", `?locationId=${locationId2}`)),
11558
+ createPipeline: async (name, stages) => {
11559
+ await pipelineApi("POST", "", {
11560
+ name,
11561
+ stages: stages.map((s) => ({ name: s.name, position: s.position, showInFunnel: true, showInPieChart: true })),
11562
+ locationId: locationId2,
11563
+ showInFunnel: true,
11564
+ showInPieChart: true
11565
+ });
11566
+ },
11567
+ // noRetry on every CREATE: these POSTs are not idempotent, so an auto-retry
11568
+ // after a lost response (429/5xx/network) would DUPLICATE the object in the
11569
+ // live account. With noRetry, a failed create simply fails → the executor's
11570
+ // verify-after catches it and halts; the idempotent re-run then binds the
11571
+ // one real object instead of stacking a second.
11572
+ listCustomFields: async () => pickObjects(await client.get(`/locations/${locationId2}/customFields`), ["customFields"]),
11573
+ createCustomField: async (f) => {
11574
+ const body = { name: f.name, dataType: f.dataType, model: f.model ?? "contact" };
11575
+ if (f.options && f.options.length) body.options = f.options;
11576
+ await client.post(`/locations/${locationId2}/customFields`, { body, noRetry: true });
11577
+ },
11578
+ listTags: async () => pickObjects(await client.get(`/locations/${locationId2}/tags`), ["tags"]),
11579
+ createTag: async (name) => {
11580
+ await client.post(`/locations/${locationId2}/tags`, { body: { name }, noRetry: true });
11581
+ },
11582
+ listCustomValues: async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]),
11583
+ createCustomValue: async (name, value) => {
11584
+ await client.post(`/locations/${locationId2}/customValues`, { body: { name, value }, noRetry: true });
11585
+ }
11586
+ };
11587
+ }
11588
+ async function readLocationName(client, locationId2) {
11589
+ try {
11590
+ const raw = await client.get(`/locations/${locationId2}`);
11591
+ const r = raw && typeof raw === "object" ? raw : {};
11592
+ const loc = r.location && typeof r.location === "object" ? r.location : r;
11593
+ const name = loc.name;
11594
+ return typeof name === "string" && name ? name : locationId2;
11595
+ } catch {
11596
+ return locationId2;
11597
+ }
11598
+ }
11599
+ function registerIntakeToBuildTools(server2, client, builderClient) {
11600
+ safeTool(
11601
+ server2,
11602
+ "get_intake_question_set",
11603
+ "Return the canonical Intake-to-Build question set (the questions the installed intake form asks) plus each question's GHL field mapping and the Brief field it feeds. Read-only. Use this to review or render the intake before installing it.",
11604
+ {},
11605
+ async () => ({
11606
+ questionSetVersion: QUESTION_SET_VERSION,
11607
+ formName: INTAKE_FORM_NAME,
11608
+ count: INTAKE_QUESTIONS.length,
11609
+ questions: INTAKE_QUESTIONS.map((q) => ({
11610
+ key: q.key,
11611
+ label: q.label,
11612
+ section: q.sectionLabel,
11613
+ required: q.required,
11614
+ fieldKind: q.field.kind,
11615
+ dataType: q.field.kind === "custom" ? q.field.dataType : void 0,
11616
+ standardTag: q.field.kind === "standard" ? q.field.tag : void 0,
11617
+ expectedFieldKey: q.field.kind === "custom" ? expectedFieldKey(q) : void 0,
11618
+ options: q.options,
11619
+ briefPath: q.briefPath
11620
+ }))
11621
+ })
11622
+ );
11623
+ safeTool(
11624
+ server2,
11625
+ "validate_brief",
11626
+ "Validate an Intake-to-Build Brief object against the \xA74 schema. Returns {valid, errors}. Use to check a normalized brief before handing it to the plan-generation skill.",
11627
+ {
11628
+ brief: import_zod53.z.record(import_zod53.z.unknown()).describe("The Brief object to validate.")
11629
+ },
11630
+ async ({ brief }) => validateBrief(brief)
11631
+ );
11632
+ safeTool(
11633
+ server2,
11634
+ "validate_build_plan",
11635
+ "Validate an Intake-to-Build Build Plan against the \xA75 schema AND check ref-integrity: every symbolic ref (pipeline.*, stage.*, tag.*, ...) must resolve to a defined object of the right type, and refs must be unique. Dead references are reported as errors here, before any build runs \u2014 the structural guard against 'an invalid ID silently kills downstream actions'. Returns {valid, errors, warnings, referencesScanned}.",
11636
+ {
11637
+ plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The Build Plan object to validate.")
11638
+ },
11639
+ async ({ plan }) => validateBuildPlan(plan)
11640
+ );
11641
+ server2.tool(
11642
+ "apply_build_plan",
11643
+ `Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow's logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values): never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op). Calendars/forms/funnels/workflows are surfaced as manual next steps, not auto-built yet. Always confirms the active location and validates the plan before any write.`,
11644
+ {
11645
+ plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
11646
+ mode: import_zod53.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
11647
+ locationId: import_zod53.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
11648
+ metHandoffs: import_zod53.z.array(import_zod53.z.string()).optional().describe('Handoff refs the operator has already satisfied (e.g. ["handoff.a2p"]) \u2014 lifts their gate so dependent workflows are not held DRAFT.'),
11649
+ publishWorkflows: import_zod53.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
11650
+ onConflict: import_zod53.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
11651
+ },
11652
+ async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
11653
+ try {
11654
+ const resolvedMode = mode ?? "dry_run";
11655
+ const activeLocation = client.resolveLocationId();
11656
+ if (locationId2 && locationId2 !== activeLocation) {
11657
+ return jsonResponse({
11658
+ ok: false,
11659
+ error: `Location mismatch: active sub-account is ${activeLocation} but you passed ${locationId2}. Run switch_location to ${locationId2} and confirm with get_current_location first, then re-run. (Blueprint never switches accounts underneath you.)`
11660
+ });
11661
+ }
11662
+ const validation = validateBuildPlan(plan);
11663
+ if (!validation.valid || !validation.plan) {
11664
+ return jsonResponse({
11665
+ ok: false,
11666
+ phase: "validate",
11667
+ error: "Build plan failed \xA75 validation \u2014 fix these before building.",
11668
+ errors: validation.errors,
11669
+ warnings: validation.warnings
11670
+ });
11671
+ }
11672
+ const typedPlan = validation.plan;
11673
+ const locationName = await readLocationName(client, activeLocation);
11674
+ const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation);
11675
+ const result = resolvePlan(typedPlan, assets, { metHandoffs });
11676
+ if (resolvedMode === "execute") {
11677
+ if (!builderClient) {
11678
+ return jsonResponse({
11679
+ ok: false,
11680
+ phase: "execute",
11681
+ error: "execute needs the workflow-builder (Firebase) client to create pipelines, and it is not configured on this install. Run the setup wizard to enable the builder, then retry. dry_run works without it."
11682
+ });
11683
+ }
11684
+ if (builderClient.locationId && builderClient.locationId !== activeLocation) {
11685
+ return jsonResponse({
11686
+ ok: false,
11687
+ phase: "execute",
11688
+ error: `Location mismatch: the public API is on ${activeLocation} but the workflow-builder client is on ${builderClient.locationId}. Restart Claude or run switch_location so both point at the same sub-account before building (registry-staleness guard). No writes were made.`
11689
+ });
11690
+ }
11691
+ try {
11692
+ await builderClient.buildHeaders();
11693
+ } catch (e) {
11694
+ return jsonResponse({ ok: false, phase: "execute", error: `Could not authenticate the workflow-builder for ${activeLocation} (${e instanceof Error ? e.message : String(e)}). No writes were made.` });
11695
+ }
11696
+ const tokenCompany = builderClient.getTokenCompanyId();
11697
+ const intendedCompany = builderClient.getIntendedCompanyId();
11698
+ if (tokenCompany && intendedCompany && tokenCompany !== intendedCompany) {
11699
+ return jsonResponse({
11700
+ ok: false,
11701
+ phase: "execute",
11702
+ error: `Firebase binding mismatch: the workflow-builder authenticates as company ${tokenCompany} but the active location ${activeLocation} is owned by company ${intendedCompany}. Restart Claude or re-run switch_location to ${activeLocation} so the Firebase session rebinds before building. No writes were made.`
11703
+ });
11704
+ }
11705
+ if (onConflict === "abort") {
11706
+ const collisions2 = result.items.filter((i) => i.status === "existing");
11707
+ if (collisions2.length > 0) {
11708
+ return jsonResponse({
11709
+ ok: false,
11710
+ phase: "execute",
11711
+ error: `onConflict=abort: ${collisions2.length} object(s) in the plan already exist in this account \u2014 aborting before any write. Re-run with onConflict:"skip" to bind to the existing ones, or resolve them first.`,
11712
+ collisions: collisions2.map((c) => ({ ref: c.ref, name: c.name, existingId: c.existingId }))
11713
+ });
11714
+ }
11715
+ }
11716
+ const deps = makeExecuteDeps(client, builderClient, activeLocation);
11717
+ const exec = await executeBackbone(typedPlan, deps);
11718
+ const manualLines = result.workflows.flatMap((w) => [
11719
+ ...w.manual.map((m) => `[${w.name}] ${m.reason}`),
11720
+ ...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
11721
+ ]);
11722
+ const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}`);
11723
+ return jsonResponse({
11724
+ ok: exec.ok,
11725
+ mode: "execute",
11726
+ locationId: activeLocation,
11727
+ locationName,
11728
+ planId: typedPlan.planId,
11729
+ scanWarnings,
11730
+ halted: exec.halted,
11731
+ built: exec.built,
11732
+ idMap: exec.idMap,
11733
+ deferred: exec.deferred,
11734
+ deferredNote: "v1 execute builds the CRM backbone (pipelines, custom fields, tags, custom values) live. These object types are planned but NOT auto-built yet \u2014 create them via the GHL UI or the dedicated tools, in this order: calendars \u2192 forms \u2192 funnels \u2192 workflows.",
11735
+ nextManualSteps: [...manualLines, ...handoffLines],
11736
+ summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing").length} existing. ${exec.deferred.length ? "Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
11737
+ });
11738
+ }
11739
+ const collisions = result.items.filter((i) => i.status === "existing");
11740
+ const aborted = onConflict === "abort" && collisions.length > 0;
11741
+ const report = renderReport(typedPlan, result, {
11742
+ mode: "dry_run",
11743
+ locationName,
11744
+ locationId: activeLocation,
11745
+ publishWorkflows: publishWorkflows ?? false
11746
+ });
11747
+ return jsonResponse({
11748
+ ok: true,
11749
+ mode: "dry_run",
11750
+ locationId: activeLocation,
11751
+ locationName,
11752
+ planId: typedPlan.planId,
11753
+ validation: { valid: true, warnings: validation.warnings, referencesScanned: validation.referencesScanned },
11754
+ scanWarnings,
11755
+ onConflict: onConflict ?? "skip",
11756
+ aborted: aborted ? { reason: `${collisions.length} same-named object(s) already exist and onConflict=abort`, collisions: collisions.map((c) => c.ref) } : void 0,
11757
+ summary: result.summary,
11758
+ items: result.items,
11759
+ idMap: result.idMap,
11760
+ workflows: result.workflows.map((w) => ({
11761
+ ref: w.ref,
11762
+ name: w.name,
11763
+ autoActions: w.nativeActions.length,
11764
+ manualSteps: w.manual,
11765
+ needsContent: w.needsContent,
11766
+ pendingRefs: w.pendingRefs,
11767
+ splitInto: w.splitInto,
11768
+ gatedBy: w.gatedBy,
11769
+ draft: w.gatedBy.length > 0 || !(publishWorkflows ?? false)
11770
+ })),
11771
+ handoffs: result.handoffs,
11772
+ report,
11773
+ next: 'Review the report. When it looks right, re-run with mode:"execute" to build the CRM backbone live (pipelines, fields, tags, custom values). Calendars/forms/funnels/workflows are listed as manual next steps.'
11774
+ });
11775
+ } catch (error) {
11776
+ return errorResponse(error);
11777
+ }
11778
+ }
11779
+ );
11780
+ if (!builderClient) return;
11781
+ const bc = builderClient;
11782
+ async function resolveCustomFields(locationId2, dryRun) {
11783
+ const cqs = customQuestions();
11784
+ const existing = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
11785
+ const created = [];
11786
+ const reused = [];
11787
+ for (const q of cqs) {
11788
+ const found = findRecordForQuestion(q, existing);
11789
+ if (found) {
11790
+ reused.push(q.label);
11791
+ } else {
11792
+ created.push(q.label);
11793
+ if (!dryRun) {
11794
+ const body = {
11795
+ name: intakeFieldName(q.label),
11796
+ dataType: q.field.kind === "custom" ? q.field.dataType : "TEXT",
11797
+ model: "contact"
11798
+ };
11799
+ if (q.options && q.field.kind === "custom") {
11800
+ const dt = q.field.dataType;
11801
+ if (dt === "SINGLE_OPTIONS" || dt === "MULTIPLE_OPTIONS" || dt === "CHECKBOX") {
11802
+ body.options = [...q.options];
11803
+ }
11804
+ }
11805
+ await client.post(`/locations/${locationId2}/customFields`, { body });
11806
+ }
11807
+ }
11808
+ }
11809
+ const resolved = /* @__PURE__ */ new Map();
11810
+ if (!dryRun) {
11811
+ let missing;
11812
+ for (let attempt = 1; attempt <= 6; attempt++) {
11813
+ const after = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
11814
+ resolved.clear();
11815
+ missing = void 0;
11816
+ for (const q of cqs) {
11817
+ const rec = findRecordForQuestion(q, after);
11818
+ if (!rec) {
11819
+ missing = q;
11820
+ break;
11821
+ }
11822
+ resolved.set(q.key, rec);
11823
+ }
11824
+ if (!missing) break;
11825
+ if (attempt < 6) await sleep2(700 * attempt);
11826
+ }
11827
+ if (missing) {
11828
+ throw new Error(
11829
+ `Could not resolve custom field for question "${missing.key}" after creation (expected fieldKey ${expectedFieldKey(missing)}). Aborting before building the form.`
11830
+ );
11831
+ }
11832
+ }
11833
+ return { resolved, created, reused };
11834
+ }
11835
+ server2.tool(
11836
+ "install_intake_form",
11837
+ "Install the canonical Intake-to-Build client-intake form into the CURRENT GHL location (whatever get_current_location returns). Account-agnostic, zero hardcoded IDs. Creates any missing intake custom fields (idempotent \u2014 reused on re-run), then builds the form with the proven GHL form-builder field shapes and verifies it. Returns {formId, fieldMap} \u2014 keep fieldMap; normalize_submission_to_brief uses it. Pass dryRun:true to preview what would be created without writing. Pass formId to update an existing intake form in place instead of creating a new one.",
11838
+ {
11839
+ dryRun: import_zod53.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
11840
+ formId: import_zod53.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
11841
+ formName: import_zod53.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
11842
+ },
11843
+ async ({ dryRun, formId, formName }) => {
11844
+ try {
11845
+ const locationId2 = client.resolveLocationId();
11846
+ if (bc.locationId && bc.locationId !== locationId2) {
11847
+ throw new Error(
11848
+ `Location mismatch: the public API is on ${locationId2} but the form-builder client is on ${bc.locationId}. Restart Claude or run switch_location so both point at the same sub-account before installing (registry-staleness guard).`
11849
+ );
11850
+ }
11851
+ const name = formName ?? INTAKE_FORM_NAME;
11852
+ const { resolved, created, reused } = await resolveCustomFields(locationId2, dryRun ?? false);
11853
+ if (dryRun) {
11854
+ return jsonResponse({
11855
+ dryRun: true,
11856
+ locationId: locationId2,
11857
+ formName: name,
11858
+ questionSetVersion: QUESTION_SET_VERSION,
11859
+ standardFields: INTAKE_QUESTIONS.filter((q) => q.field.kind === "standard").map((q) => q.label),
11860
+ customFieldsToCreate: created,
11861
+ customFieldsToReuse: reused,
11862
+ note: "No changes written. Re-run without dryRun to install."
11863
+ });
11864
+ }
11865
+ const formData = buildIntakeFormData({ resolved, locationId: locationId2 });
11866
+ let resolvedFormId = formId;
11867
+ const justCreated = !resolvedFormId;
11868
+ if (!resolvedFormId) {
11869
+ const createResult = await formApiRequest(bc, "POST", `/?locationId=${locationId2}`, {
11870
+ name,
11871
+ locationId: locationId2,
11872
+ formData: { form: { fields: [], formLabelVisible: true } }
11873
+ });
11874
+ resolvedFormId = extractFormId(createResult);
11875
+ if (!resolvedFormId) {
11876
+ throw new Error(
11877
+ `create_form succeeded but no form id was found in the response: ${JSON.stringify(createResult).slice(0, 300)}`
11878
+ );
11879
+ }
11880
+ }
11881
+ const maxSaveAttempts = justCreated ? 6 : 1;
11882
+ for (let attempt = 1; ; attempt++) {
11883
+ try {
11884
+ await formApiRequest(bc, "POST", `/${resolvedFormId}?locationId=${locationId2}`, {
11885
+ name,
11886
+ formData
11887
+ });
11888
+ break;
11889
+ } catch (saveErr) {
11890
+ if (justCreated && isFormNotYetPropagated(saveErr) && attempt < maxSaveAttempts) {
11891
+ await sleep2(700 * attempt);
11892
+ continue;
11893
+ }
11894
+ throw saveErr;
11895
+ }
11896
+ }
11897
+ const expectedCount = INTAKE_QUESTIONS.length + 1 + 7;
11898
+ let persistedCount = 0;
11899
+ for (let attempt = 1; attempt <= 6; attempt++) {
11900
+ const verify = await formApiRequest(bc, "GET", `/${resolvedFormId}?locationId=${locationId2}`);
11901
+ persistedCount = countFormFields(verify);
11902
+ if (persistedCount > 0) break;
11903
+ if (attempt < 6) await sleep2(700 * attempt);
11904
+ }
11905
+ const fieldMap = {};
11906
+ for (const [key, rec] of resolved) fieldMap[key] = rec.id;
11907
+ return jsonResponse({
11908
+ ok: true,
11909
+ locationId: locationId2,
11910
+ formId: resolvedFormId,
11911
+ formName: name,
11912
+ questionSetVersion: QUESTION_SET_VERSION,
11913
+ customFieldsCreated: created,
11914
+ customFieldsReused: reused,
11915
+ fieldsPersisted: persistedCount,
11916
+ fieldsExpectedApprox: expectedCount,
11917
+ fieldMap,
11918
+ next: "Drive traffic to the form, then run normalize_submission_to_brief with this formId (and fieldMap) once a submission lands."
11919
+ });
11920
+ } catch (error) {
11921
+ return errorResponse(error);
11922
+ }
11923
+ }
11924
+ );
11925
+ server2.tool(
11926
+ "normalize_submission_to_brief",
11927
+ 'Read an intake form submission and normalize it into a \xA74 Brief (briefSource:"intake_form"). Pass the formId; by default the most recent submission is used (or pass submissionId). The intakeKey->customFieldId map is taken from fieldMap if provided (the install_intake_form output, most robust), otherwise reconstructed from the live form. Returns {brief, validation, submissionId} \u2014 validation flags any missing required fields (e.g. an incomplete submission).',
11928
+ {
11929
+ formId: import_zod53.z.string().describe("The intake form ID (from install_intake_form)."),
11930
+ submissionId: import_zod53.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
11931
+ fieldMap: import_zod53.z.record(import_zod53.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
11932
+ preset: import_zod53.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
11933
+ },
11934
+ async ({ formId, submissionId, fieldMap, preset }) => {
11935
+ try {
11936
+ const locationId2 = client.resolveLocationId();
11937
+ const subsRaw = await formApiRequest(
11938
+ bc,
11939
+ "GET",
11940
+ `/submissions?locationId=${locationId2}&formId=${formId}&limit=20`
11941
+ );
11942
+ const subsObj = subsRaw && typeof subsRaw === "object" ? subsRaw : {};
11943
+ const submissions = Array.isArray(subsObj.submissions) ? subsObj.submissions : [];
11944
+ if (submissions.length === 0) {
11945
+ throw new Error(`No submissions found for form ${formId}.`);
11946
+ }
11947
+ const pick = submissionId ? submissions.find(
11948
+ (s) => s && typeof s === "object" && s.id === submissionId
11949
+ ) : submissions[0];
11950
+ if (!pick || typeof pick !== "object") {
11951
+ throw new Error(`Submission ${submissionId ?? "(latest)"} not found for form ${formId}.`);
11952
+ }
11953
+ const sub = pick;
11954
+ const others = sub.others && typeof sub.others === "object" ? sub.others : {};
11955
+ const briefId = typeof sub.id === "string" && sub.id || typeof sub.contactId === "string" && sub.contactId || formId;
11956
+ let resolvedMap = fieldMap;
11957
+ if (!resolvedMap) {
11958
+ const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
11959
+ resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
11960
+ }
11961
+ const presetSchema2 = import_zod53.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
11962
+ const presetParsed = presetSchema2.safeParse(preset);
11963
+ const brief = normalizeSubmissionToBrief({
11964
+ others,
11965
+ fieldMap: resolvedMap,
11966
+ briefId,
11967
+ preset: presetParsed.success ? presetParsed.data : void 0
11968
+ });
11969
+ const validation = validateBrief(brief);
11970
+ return jsonResponse({
11971
+ submissionId: typeof sub.id === "string" ? sub.id : void 0,
11972
+ fieldMapKeys: Object.keys(resolvedMap).length,
11973
+ brief,
11974
+ validation
11975
+ });
11976
+ } catch (error) {
11977
+ return errorResponse(error);
11978
+ }
11979
+ }
11980
+ );
11981
+ }
11982
+
9620
11983
  // src/tools/index.ts
9621
11984
  var publicApiTools = [
9622
11985
  [registerContactTools, "contacts"],
@@ -9668,10 +12031,12 @@ var DIAGNOSTICS_MODULE = "diagnostics";
9668
12031
  var LOCATION_SWITCHER_MODULE = "location-switcher";
9669
12032
  var SNAPSHOTS_MODULE = "snapshots";
9670
12033
  var FORM_BUILDER_MODULE = "form-builder";
12034
+ var INTAKE_TO_BUILD_MODULE = "intake-to-build";
9671
12035
  var KNOWN_MODULES = /* @__PURE__ */ new Set([
9672
12036
  ...publicApiTools.map(([, label]) => label),
9673
12037
  ...internalApiTools.map(([, label]) => label),
9674
12038
  FORM_BUILDER_MODULE,
12039
+ INTAKE_TO_BUILD_MODULE,
9675
12040
  VALIDATORS_MODULE,
9676
12041
  DIAGNOSTICS_MODULE,
9677
12042
  LOCATION_SWITCHER_MODULE,
@@ -9690,6 +12055,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
9690
12055
  register(wrap(moduleName), builderClient);
9691
12056
  }
9692
12057
  registerFormBuilderTools(wrap(FORM_BUILDER_MODULE), builderClient, client);
12058
+ registerIntakeToBuildTools(wrap(INTAKE_TO_BUILD_MODULE), client, builderClient);
9693
12059
  registerValidatorTools(wrap(VALIDATORS_MODULE), client, builderClient);
9694
12060
  registerDiagnosticTools(
9695
12061
  wrap(DIAGNOSTICS_MODULE),
@@ -9856,8 +12222,8 @@ Subcommands:
9856
12222
 
9857
12223
  Exit codes: 0 ok, 2 usage, 3 validation failed, 4 filesystem write failed.
9858
12224
  Seed while the MCP server is stopped, or restart it afterwards.`;
9859
- function errLine(msg) {
9860
- process.stderr.write(msg + "\n");
12225
+ function errLine(msg2) {
12226
+ process.stderr.write(msg2 + "\n");
9861
12227
  }
9862
12228
  function preflightWritable() {
9863
12229
  try {