@elitedcs/ghl-mcp 3.35.0 → 3.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1533 -22
- 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.
|
|
34
|
+
version: "3.36.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
|
-
|
|
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.",
|
|
@@ -9617,6 +9618,1513 @@ 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
|
+
action: import_zod52.z.string().optional()
|
|
10635
|
+
})
|
|
10636
|
+
]);
|
|
10637
|
+
var triggerSchema = import_zod52.z.object({
|
|
10638
|
+
type: import_zod52.z.string(),
|
|
10639
|
+
formRef: nsRef("form").optional(),
|
|
10640
|
+
tagRef: nsRef("tag").optional(),
|
|
10641
|
+
calendarRef: nsRef("calendar").optional(),
|
|
10642
|
+
pipelineRef: nsRef("pipeline").optional(),
|
|
10643
|
+
stageRef: nsRef("stage").optional()
|
|
10644
|
+
});
|
|
10645
|
+
var workflowSchema = import_zod52.z.object({
|
|
10646
|
+
ref: nsRef("workflow"),
|
|
10647
|
+
name: import_zod52.z.string(),
|
|
10648
|
+
trigger: triggerSchema.optional(),
|
|
10649
|
+
stopOnResponse: import_zod52.z.boolean().optional(),
|
|
10650
|
+
actions: import_zod52.z.array(actionSchema).max(40)
|
|
10651
|
+
// house rule: <=40 actions/workflow
|
|
10652
|
+
});
|
|
10653
|
+
var handoffSchema = import_zod52.z.object({
|
|
10654
|
+
ref: nsRef("handoff"),
|
|
10655
|
+
owner: import_zod52.z.enum(["JERRY-UI", "JERRY-EXT", "SASHA"]),
|
|
10656
|
+
title: import_zod52.z.string(),
|
|
10657
|
+
trigger: import_zod52.z.string().optional(),
|
|
10658
|
+
instruction: import_zod52.z.string(),
|
|
10659
|
+
produces: refSchema.nullable().optional(),
|
|
10660
|
+
successCheck: import_zod52.z.string(),
|
|
10661
|
+
blocks: import_zod52.z.array(import_zod52.z.string()).optional()
|
|
10662
|
+
});
|
|
10663
|
+
var buildPlanSchema = import_zod52.z.object({
|
|
10664
|
+
schemaVersion: import_zod52.z.string(),
|
|
10665
|
+
planId: import_zod52.z.string(),
|
|
10666
|
+
briefId: import_zod52.z.string(),
|
|
10667
|
+
preset: import_zod52.z.string(),
|
|
10668
|
+
summary: import_zod52.z.string().optional(),
|
|
10669
|
+
pipelines: import_zod52.z.array(pipelineSchema).optional(),
|
|
10670
|
+
customFields: import_zod52.z.array(customFieldSchema).optional(),
|
|
10671
|
+
tags: import_zod52.z.array(tagSchema).optional(),
|
|
10672
|
+
customValues: import_zod52.z.array(customValueSchema).optional(),
|
|
10673
|
+
calendars: import_zod52.z.array(calendarSchema).optional(),
|
|
10674
|
+
forms: import_zod52.z.array(formSchema).optional(),
|
|
10675
|
+
funnels: import_zod52.z.array(funnelSchema).optional(),
|
|
10676
|
+
emails: import_zod52.z.array(emailAssetSchema).optional(),
|
|
10677
|
+
sms: import_zod52.z.array(smsAssetSchema).optional(),
|
|
10678
|
+
workflows: import_zod52.z.array(workflowSchema).optional(),
|
|
10679
|
+
handoffs: import_zod52.z.array(handoffSchema).optional(),
|
|
10680
|
+
buildOrder: import_zod52.z.array(import_zod52.z.string()).optional(),
|
|
10681
|
+
idMap: import_zod52.z.record(import_zod52.z.string()).optional()
|
|
10682
|
+
}).strict();
|
|
10683
|
+
function collectDefinedRefs(plan) {
|
|
10684
|
+
const refs = /* @__PURE__ */ new Map();
|
|
10685
|
+
const duplicates = [];
|
|
10686
|
+
const add = (ref) => {
|
|
10687
|
+
if (refs.has(ref)) duplicates.push(ref);
|
|
10688
|
+
else refs.set(ref, refNamespace(ref));
|
|
10689
|
+
};
|
|
10690
|
+
for (const p of plan.pipelines ?? []) {
|
|
10691
|
+
add(p.ref);
|
|
10692
|
+
for (const s of p.stages) add(s.ref);
|
|
10693
|
+
}
|
|
10694
|
+
for (const f of plan.customFields ?? []) add(f.ref);
|
|
10695
|
+
for (const t of plan.tags ?? []) add(t.ref);
|
|
10696
|
+
for (const cv of plan.customValues ?? []) add(cv.ref);
|
|
10697
|
+
for (const c of plan.calendars ?? []) add(c.ref);
|
|
10698
|
+
for (const fm of plan.forms ?? []) add(fm.ref);
|
|
10699
|
+
for (const fn of plan.funnels ?? []) {
|
|
10700
|
+
add(fn.ref);
|
|
10701
|
+
for (const pg of fn.pages) add(pg.ref);
|
|
10702
|
+
}
|
|
10703
|
+
for (const e of plan.emails ?? []) add(e.ref);
|
|
10704
|
+
for (const s of plan.sms ?? []) add(s.ref);
|
|
10705
|
+
for (const w of plan.workflows ?? []) add(w.ref);
|
|
10706
|
+
for (const h of plan.handoffs ?? []) add(h.ref);
|
|
10707
|
+
return { refs, duplicates };
|
|
10708
|
+
}
|
|
10709
|
+
function checkRefIntegrity(plan, defined) {
|
|
10710
|
+
const errors = [];
|
|
10711
|
+
let scanned = 0;
|
|
10712
|
+
const check = (ref, expectNs, where) => {
|
|
10713
|
+
if (ref === null || ref === void 0) return;
|
|
10714
|
+
scanned++;
|
|
10715
|
+
if (!defined.has(ref)) {
|
|
10716
|
+
errors.push(`${where}: ref "${ref}" does not resolve to any defined ${expectNs}`);
|
|
10717
|
+
return;
|
|
10718
|
+
}
|
|
10719
|
+
const ns = defined.get(ref);
|
|
10720
|
+
if (expectNs !== "any" && ns !== expectNs) {
|
|
10721
|
+
errors.push(`${where}: ref "${ref}" is a ${ns}, expected a ${expectNs}`);
|
|
10722
|
+
}
|
|
10723
|
+
};
|
|
10724
|
+
const checkMaybeWildcard = (ref, where) => {
|
|
10725
|
+
scanned++;
|
|
10726
|
+
if (ref.endsWith(".*")) {
|
|
10727
|
+
const ns = ref.slice(0, -2);
|
|
10728
|
+
const known = REF_NAMESPACES;
|
|
10729
|
+
if (!known.includes(ns)) {
|
|
10730
|
+
errors.push(`${where}: "${ref}" is not a valid <namespace>.* wildcard`);
|
|
10731
|
+
}
|
|
10732
|
+
return;
|
|
10733
|
+
}
|
|
10734
|
+
if (!defined.has(ref)) {
|
|
10735
|
+
errors.push(`${where}: ref "${ref}" does not resolve to any defined object`);
|
|
10736
|
+
}
|
|
10737
|
+
};
|
|
10738
|
+
for (const cv of plan.customValues ?? []) {
|
|
10739
|
+
if (cv.filledBy) check(cv.filledBy, "any", `customValues[${cv.ref}].filledBy`);
|
|
10740
|
+
}
|
|
10741
|
+
for (const fm of plan.forms ?? []) {
|
|
10742
|
+
for (const fl of fm.fields) {
|
|
10743
|
+
if (fl.type === "custom") check(fl.fieldRef, "field", `forms[${fm.ref}].fields`);
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
for (const fn of plan.funnels ?? []) {
|
|
10747
|
+
for (const pg of fn.pages) {
|
|
10748
|
+
check(pg.formRef, "form", `funnels[${fn.ref}].pages[${pg.ref}].formRef`);
|
|
10749
|
+
check(pg.calendarRef, "calendar", `funnels[${fn.ref}].pages[${pg.ref}].calendarRef`);
|
|
10750
|
+
}
|
|
10751
|
+
}
|
|
10752
|
+
for (const w of plan.workflows ?? []) {
|
|
10753
|
+
if (w.trigger) {
|
|
10754
|
+
const t = w.trigger;
|
|
10755
|
+
check(t.formRef, "form", `workflows[${w.ref}].trigger.formRef`);
|
|
10756
|
+
check(t.tagRef, "tag", `workflows[${w.ref}].trigger.tagRef`);
|
|
10757
|
+
check(t.calendarRef, "calendar", `workflows[${w.ref}].trigger.calendarRef`);
|
|
10758
|
+
check(t.pipelineRef, "pipeline", `workflows[${w.ref}].trigger.pipelineRef`);
|
|
10759
|
+
check(t.stageRef, "stage", `workflows[${w.ref}].trigger.stageRef`);
|
|
10760
|
+
}
|
|
10761
|
+
w.actions.forEach((a, i) => {
|
|
10762
|
+
const where = `workflows[${w.ref}].actions[${i}](${a.type})`;
|
|
10763
|
+
if ("tagRef" in a) check(a.tagRef, "tag", where);
|
|
10764
|
+
if ("emailRef" in a) check(a.emailRef, "email", where);
|
|
10765
|
+
if ("smsRef" in a) check(a.smsRef, "sms", where);
|
|
10766
|
+
if ("fieldRef" in a) check(a.fieldRef, "field", where);
|
|
10767
|
+
if ("pipelineRef" in a) check(a.pipelineRef, "pipeline", where);
|
|
10768
|
+
if ("stageRef" in a) check(a.stageRef, "stage", where);
|
|
10769
|
+
if ("workflowRef" in a) check(a.workflowRef, "workflow", where);
|
|
10770
|
+
});
|
|
10771
|
+
}
|
|
10772
|
+
for (const h of plan.handoffs ?? []) {
|
|
10773
|
+
if (h.produces) check(h.produces, "any", `handoffs[${h.ref}].produces`);
|
|
10774
|
+
for (const b of h.blocks ?? []) checkMaybeWildcard(b, `handoffs[${h.ref}].blocks`);
|
|
10775
|
+
}
|
|
10776
|
+
for (const b of plan.buildOrder ?? []) checkMaybeWildcard(b, "buildOrder");
|
|
10777
|
+
return { errors, scanned };
|
|
10778
|
+
}
|
|
10779
|
+
function validateBuildPlan(input) {
|
|
10780
|
+
const parsed = buildPlanSchema.safeParse(input);
|
|
10781
|
+
if (!parsed.success) {
|
|
10782
|
+
return {
|
|
10783
|
+
valid: false,
|
|
10784
|
+
errors: parsed.error.issues.map(
|
|
10785
|
+
(i) => `${i.path.join(".") || "(root)"}: ${i.message}`
|
|
10786
|
+
),
|
|
10787
|
+
warnings: [],
|
|
10788
|
+
referencesScanned: 0
|
|
10789
|
+
};
|
|
10790
|
+
}
|
|
10791
|
+
const plan = parsed.data;
|
|
10792
|
+
const { refs, duplicates } = collectDefinedRefs(plan);
|
|
10793
|
+
const { errors, scanned } = checkRefIntegrity(plan, refs);
|
|
10794
|
+
const dupErrors = duplicates.map((d) => `duplicate ref "${d}" \u2014 refs must be unique`);
|
|
10795
|
+
const allErrors = [...dupErrors, ...errors];
|
|
10796
|
+
const warnings = [];
|
|
10797
|
+
for (const p of plan.pipelines ?? []) {
|
|
10798
|
+
const positions = p.stages.map((s) => s.position).sort((a, b) => a - b);
|
|
10799
|
+
const expected = positions.every((pos, idx) => pos === idx);
|
|
10800
|
+
if (!expected) {
|
|
10801
|
+
warnings.push(`pipeline "${p.ref}" stage positions are not a clean 0..${p.stages.length - 1} sequence`);
|
|
10802
|
+
}
|
|
10803
|
+
}
|
|
10804
|
+
const CHOICE_DATATYPES = /* @__PURE__ */ new Set(["SINGLE_OPTIONS", "MULTIPLE_OPTIONS", "CHECKBOX"]);
|
|
10805
|
+
for (const f of plan.customFields ?? []) {
|
|
10806
|
+
if (CHOICE_DATATYPES.has(f.dataType) && !(f.options && f.options.length > 0)) {
|
|
10807
|
+
warnings.push(
|
|
10808
|
+
`customFields "${f.ref}" is a ${f.dataType} but declares no options \u2014 the executor cannot create a usable choice field without them`
|
|
10809
|
+
);
|
|
10810
|
+
}
|
|
10811
|
+
}
|
|
10812
|
+
return {
|
|
10813
|
+
valid: allErrors.length === 0,
|
|
10814
|
+
errors: allErrors,
|
|
10815
|
+
warnings,
|
|
10816
|
+
plan,
|
|
10817
|
+
referencesScanned: scanned
|
|
10818
|
+
};
|
|
10819
|
+
}
|
|
10820
|
+
|
|
10821
|
+
// src/tools/intake-to-build.ts
|
|
10822
|
+
var customFieldItemSchema = import_zod53.z.object({
|
|
10823
|
+
id: import_zod53.z.string(),
|
|
10824
|
+
name: import_zod53.z.string(),
|
|
10825
|
+
fieldKey: import_zod53.z.string(),
|
|
10826
|
+
dataType: import_zod53.z.string(),
|
|
10827
|
+
model: import_zod53.z.string().optional(),
|
|
10828
|
+
parentId: import_zod53.z.string().optional(),
|
|
10829
|
+
position: import_zod53.z.number().optional(),
|
|
10830
|
+
dateAdded: import_zod53.z.string().optional(),
|
|
10831
|
+
picklistOptions: import_zod53.z.array(import_zod53.z.string()).optional()
|
|
10832
|
+
}).passthrough();
|
|
10833
|
+
function parseCustomFields(raw) {
|
|
10834
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
10835
|
+
const list = Array.isArray(obj.customFields) ? obj.customFields : [];
|
|
10836
|
+
const out = [];
|
|
10837
|
+
for (const item of list) {
|
|
10838
|
+
const parsed = customFieldItemSchema.safeParse(item);
|
|
10839
|
+
if (parsed.success) out.push(parsed.data);
|
|
10840
|
+
}
|
|
10841
|
+
return out;
|
|
10842
|
+
}
|
|
10843
|
+
function extractFormId(result) {
|
|
10844
|
+
if (!result || typeof result !== "object") return void 0;
|
|
10845
|
+
const r = result;
|
|
10846
|
+
if (typeof r.id === "string") return r.id;
|
|
10847
|
+
if (typeof r._id === "string") return r._id;
|
|
10848
|
+
const form = r.form;
|
|
10849
|
+
if (form && typeof form === "object") {
|
|
10850
|
+
const f = form;
|
|
10851
|
+
if (typeof f._id === "string") return f._id;
|
|
10852
|
+
if (typeof f.id === "string") return f.id;
|
|
10853
|
+
}
|
|
10854
|
+
return void 0;
|
|
10855
|
+
}
|
|
10856
|
+
function countFormFields(formFull) {
|
|
10857
|
+
const r = formFull && typeof formFull === "object" ? formFull : {};
|
|
10858
|
+
const form = r.form && typeof r.form === "object" ? r.form : {};
|
|
10859
|
+
const fd = form.formData && typeof form.formData === "object" ? form.formData : {};
|
|
10860
|
+
const inner = fd.form && typeof fd.form === "object" ? fd.form : {};
|
|
10861
|
+
return Array.isArray(inner.fields) ? inner.fields.length : 0;
|
|
10862
|
+
}
|
|
10863
|
+
function extractFormFields(formFull) {
|
|
10864
|
+
const r = formFull && typeof formFull === "object" ? formFull : {};
|
|
10865
|
+
const form = r.form && typeof r.form === "object" ? r.form : {};
|
|
10866
|
+
const fd = form.formData && typeof form.formData === "object" ? form.formData : {};
|
|
10867
|
+
const inner = fd.form && typeof fd.form === "object" ? fd.form : {};
|
|
10868
|
+
const fields = Array.isArray(inner.fields) ? inner.fields : [];
|
|
10869
|
+
return fields.filter((f) => !!f && typeof f === "object");
|
|
10870
|
+
}
|
|
10871
|
+
function findRecordForQuestion(q, records) {
|
|
10872
|
+
const wantKey = expectedFieldKey(q).toLowerCase();
|
|
10873
|
+
const byKey = records.find((r) => r.fieldKey.toLowerCase() === wantKey);
|
|
10874
|
+
if (byKey) return byKey;
|
|
10875
|
+
const wantName = intakeFieldName(q.label).toLowerCase();
|
|
10876
|
+
return records.find((r) => r.name.toLowerCase() === wantName);
|
|
10877
|
+
}
|
|
10878
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10879
|
+
function isFormNotYetPropagated(error) {
|
|
10880
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
10881
|
+
return /does not exist or is deleted/i.test(msg);
|
|
10882
|
+
}
|
|
10883
|
+
function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
10884
|
+
safeTool(
|
|
10885
|
+
server2,
|
|
10886
|
+
"get_intake_question_set",
|
|
10887
|
+
"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.",
|
|
10888
|
+
{},
|
|
10889
|
+
async () => ({
|
|
10890
|
+
questionSetVersion: QUESTION_SET_VERSION,
|
|
10891
|
+
formName: INTAKE_FORM_NAME,
|
|
10892
|
+
count: INTAKE_QUESTIONS.length,
|
|
10893
|
+
questions: INTAKE_QUESTIONS.map((q) => ({
|
|
10894
|
+
key: q.key,
|
|
10895
|
+
label: q.label,
|
|
10896
|
+
section: q.sectionLabel,
|
|
10897
|
+
required: q.required,
|
|
10898
|
+
fieldKind: q.field.kind,
|
|
10899
|
+
dataType: q.field.kind === "custom" ? q.field.dataType : void 0,
|
|
10900
|
+
standardTag: q.field.kind === "standard" ? q.field.tag : void 0,
|
|
10901
|
+
expectedFieldKey: q.field.kind === "custom" ? expectedFieldKey(q) : void 0,
|
|
10902
|
+
options: q.options,
|
|
10903
|
+
briefPath: q.briefPath
|
|
10904
|
+
}))
|
|
10905
|
+
})
|
|
10906
|
+
);
|
|
10907
|
+
safeTool(
|
|
10908
|
+
server2,
|
|
10909
|
+
"validate_brief",
|
|
10910
|
+
"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.",
|
|
10911
|
+
{
|
|
10912
|
+
brief: import_zod53.z.record(import_zod53.z.unknown()).describe("The Brief object to validate.")
|
|
10913
|
+
},
|
|
10914
|
+
async ({ brief }) => validateBrief(brief)
|
|
10915
|
+
);
|
|
10916
|
+
safeTool(
|
|
10917
|
+
server2,
|
|
10918
|
+
"validate_build_plan",
|
|
10919
|
+
"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}.",
|
|
10920
|
+
{
|
|
10921
|
+
plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The Build Plan object to validate.")
|
|
10922
|
+
},
|
|
10923
|
+
async ({ plan }) => validateBuildPlan(plan)
|
|
10924
|
+
);
|
|
10925
|
+
if (!builderClient) return;
|
|
10926
|
+
const bc = builderClient;
|
|
10927
|
+
async function resolveCustomFields(locationId2, dryRun) {
|
|
10928
|
+
const cqs = customQuestions();
|
|
10929
|
+
const existing = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
|
|
10930
|
+
const created = [];
|
|
10931
|
+
const reused = [];
|
|
10932
|
+
for (const q of cqs) {
|
|
10933
|
+
const found = findRecordForQuestion(q, existing);
|
|
10934
|
+
if (found) {
|
|
10935
|
+
reused.push(q.label);
|
|
10936
|
+
} else {
|
|
10937
|
+
created.push(q.label);
|
|
10938
|
+
if (!dryRun) {
|
|
10939
|
+
const body = {
|
|
10940
|
+
name: intakeFieldName(q.label),
|
|
10941
|
+
dataType: q.field.kind === "custom" ? q.field.dataType : "TEXT",
|
|
10942
|
+
model: "contact"
|
|
10943
|
+
};
|
|
10944
|
+
if (q.options && q.field.kind === "custom") {
|
|
10945
|
+
const dt = q.field.dataType;
|
|
10946
|
+
if (dt === "SINGLE_OPTIONS" || dt === "MULTIPLE_OPTIONS" || dt === "CHECKBOX") {
|
|
10947
|
+
body.options = [...q.options];
|
|
10948
|
+
}
|
|
10949
|
+
}
|
|
10950
|
+
await client.post(`/locations/${locationId2}/customFields`, { body });
|
|
10951
|
+
}
|
|
10952
|
+
}
|
|
10953
|
+
}
|
|
10954
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
10955
|
+
if (!dryRun) {
|
|
10956
|
+
let missing;
|
|
10957
|
+
for (let attempt = 1; attempt <= 6; attempt++) {
|
|
10958
|
+
const after = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
|
|
10959
|
+
resolved.clear();
|
|
10960
|
+
missing = void 0;
|
|
10961
|
+
for (const q of cqs) {
|
|
10962
|
+
const rec = findRecordForQuestion(q, after);
|
|
10963
|
+
if (!rec) {
|
|
10964
|
+
missing = q;
|
|
10965
|
+
break;
|
|
10966
|
+
}
|
|
10967
|
+
resolved.set(q.key, rec);
|
|
10968
|
+
}
|
|
10969
|
+
if (!missing) break;
|
|
10970
|
+
if (attempt < 6) await sleep(700 * attempt);
|
|
10971
|
+
}
|
|
10972
|
+
if (missing) {
|
|
10973
|
+
throw new Error(
|
|
10974
|
+
`Could not resolve custom field for question "${missing.key}" after creation (expected fieldKey ${expectedFieldKey(missing)}). Aborting before building the form.`
|
|
10975
|
+
);
|
|
10976
|
+
}
|
|
10977
|
+
}
|
|
10978
|
+
return { resolved, created, reused };
|
|
10979
|
+
}
|
|
10980
|
+
server2.tool(
|
|
10981
|
+
"install_intake_form",
|
|
10982
|
+
"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.",
|
|
10983
|
+
{
|
|
10984
|
+
dryRun: import_zod53.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
|
|
10985
|
+
formId: import_zod53.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
|
|
10986
|
+
formName: import_zod53.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
|
|
10987
|
+
},
|
|
10988
|
+
async ({ dryRun, formId, formName }) => {
|
|
10989
|
+
try {
|
|
10990
|
+
const locationId2 = client.resolveLocationId();
|
|
10991
|
+
if (bc.locationId && bc.locationId !== locationId2) {
|
|
10992
|
+
throw new Error(
|
|
10993
|
+
`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).`
|
|
10994
|
+
);
|
|
10995
|
+
}
|
|
10996
|
+
const name = formName ?? INTAKE_FORM_NAME;
|
|
10997
|
+
const { resolved, created, reused } = await resolveCustomFields(locationId2, dryRun ?? false);
|
|
10998
|
+
if (dryRun) {
|
|
10999
|
+
return jsonResponse({
|
|
11000
|
+
dryRun: true,
|
|
11001
|
+
locationId: locationId2,
|
|
11002
|
+
formName: name,
|
|
11003
|
+
questionSetVersion: QUESTION_SET_VERSION,
|
|
11004
|
+
standardFields: INTAKE_QUESTIONS.filter((q) => q.field.kind === "standard").map((q) => q.label),
|
|
11005
|
+
customFieldsToCreate: created,
|
|
11006
|
+
customFieldsToReuse: reused,
|
|
11007
|
+
note: "No changes written. Re-run without dryRun to install."
|
|
11008
|
+
});
|
|
11009
|
+
}
|
|
11010
|
+
const formData = buildIntakeFormData({ resolved, locationId: locationId2 });
|
|
11011
|
+
let resolvedFormId = formId;
|
|
11012
|
+
const justCreated = !resolvedFormId;
|
|
11013
|
+
if (!resolvedFormId) {
|
|
11014
|
+
const createResult = await formApiRequest(bc, "POST", `/?locationId=${locationId2}`, {
|
|
11015
|
+
name,
|
|
11016
|
+
locationId: locationId2,
|
|
11017
|
+
formData: { form: { fields: [], formLabelVisible: true } }
|
|
11018
|
+
});
|
|
11019
|
+
resolvedFormId = extractFormId(createResult);
|
|
11020
|
+
if (!resolvedFormId) {
|
|
11021
|
+
throw new Error(
|
|
11022
|
+
`create_form succeeded but no form id was found in the response: ${JSON.stringify(createResult).slice(0, 300)}`
|
|
11023
|
+
);
|
|
11024
|
+
}
|
|
11025
|
+
}
|
|
11026
|
+
const maxSaveAttempts = justCreated ? 6 : 1;
|
|
11027
|
+
for (let attempt = 1; ; attempt++) {
|
|
11028
|
+
try {
|
|
11029
|
+
await formApiRequest(bc, "POST", `/${resolvedFormId}?locationId=${locationId2}`, {
|
|
11030
|
+
name,
|
|
11031
|
+
formData
|
|
11032
|
+
});
|
|
11033
|
+
break;
|
|
11034
|
+
} catch (saveErr) {
|
|
11035
|
+
if (justCreated && isFormNotYetPropagated(saveErr) && attempt < maxSaveAttempts) {
|
|
11036
|
+
await sleep(700 * attempt);
|
|
11037
|
+
continue;
|
|
11038
|
+
}
|
|
11039
|
+
throw saveErr;
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
11042
|
+
const expectedCount = INTAKE_QUESTIONS.length + 1 + 7;
|
|
11043
|
+
let persistedCount = 0;
|
|
11044
|
+
for (let attempt = 1; attempt <= 6; attempt++) {
|
|
11045
|
+
const verify = await formApiRequest(bc, "GET", `/${resolvedFormId}?locationId=${locationId2}`);
|
|
11046
|
+
persistedCount = countFormFields(verify);
|
|
11047
|
+
if (persistedCount > 0) break;
|
|
11048
|
+
if (attempt < 6) await sleep(700 * attempt);
|
|
11049
|
+
}
|
|
11050
|
+
const fieldMap = {};
|
|
11051
|
+
for (const [key, rec] of resolved) fieldMap[key] = rec.id;
|
|
11052
|
+
return jsonResponse({
|
|
11053
|
+
ok: true,
|
|
11054
|
+
locationId: locationId2,
|
|
11055
|
+
formId: resolvedFormId,
|
|
11056
|
+
formName: name,
|
|
11057
|
+
questionSetVersion: QUESTION_SET_VERSION,
|
|
11058
|
+
customFieldsCreated: created,
|
|
11059
|
+
customFieldsReused: reused,
|
|
11060
|
+
fieldsPersisted: persistedCount,
|
|
11061
|
+
fieldsExpectedApprox: expectedCount,
|
|
11062
|
+
fieldMap,
|
|
11063
|
+
next: "Drive traffic to the form, then run normalize_submission_to_brief with this formId (and fieldMap) once a submission lands."
|
|
11064
|
+
});
|
|
11065
|
+
} catch (error) {
|
|
11066
|
+
return errorResponse(error);
|
|
11067
|
+
}
|
|
11068
|
+
}
|
|
11069
|
+
);
|
|
11070
|
+
server2.tool(
|
|
11071
|
+
"normalize_submission_to_brief",
|
|
11072
|
+
'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).',
|
|
11073
|
+
{
|
|
11074
|
+
formId: import_zod53.z.string().describe("The intake form ID (from install_intake_form)."),
|
|
11075
|
+
submissionId: import_zod53.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
|
|
11076
|
+
fieldMap: import_zod53.z.record(import_zod53.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
|
|
11077
|
+
preset: import_zod53.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
|
|
11078
|
+
},
|
|
11079
|
+
async ({ formId, submissionId, fieldMap, preset }) => {
|
|
11080
|
+
try {
|
|
11081
|
+
const locationId2 = client.resolveLocationId();
|
|
11082
|
+
const subsRaw = await formApiRequest(
|
|
11083
|
+
bc,
|
|
11084
|
+
"GET",
|
|
11085
|
+
`/submissions?locationId=${locationId2}&formId=${formId}&limit=20`
|
|
11086
|
+
);
|
|
11087
|
+
const subsObj = subsRaw && typeof subsRaw === "object" ? subsRaw : {};
|
|
11088
|
+
const submissions = Array.isArray(subsObj.submissions) ? subsObj.submissions : [];
|
|
11089
|
+
if (submissions.length === 0) {
|
|
11090
|
+
throw new Error(`No submissions found for form ${formId}.`);
|
|
11091
|
+
}
|
|
11092
|
+
const pick = submissionId ? submissions.find(
|
|
11093
|
+
(s) => s && typeof s === "object" && s.id === submissionId
|
|
11094
|
+
) : submissions[0];
|
|
11095
|
+
if (!pick || typeof pick !== "object") {
|
|
11096
|
+
throw new Error(`Submission ${submissionId ?? "(latest)"} not found for form ${formId}.`);
|
|
11097
|
+
}
|
|
11098
|
+
const sub = pick;
|
|
11099
|
+
const others = sub.others && typeof sub.others === "object" ? sub.others : {};
|
|
11100
|
+
const briefId = typeof sub.id === "string" && sub.id || typeof sub.contactId === "string" && sub.contactId || formId;
|
|
11101
|
+
let resolvedMap = fieldMap;
|
|
11102
|
+
if (!resolvedMap) {
|
|
11103
|
+
const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
|
|
11104
|
+
resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
|
|
11105
|
+
}
|
|
11106
|
+
const presetSchema2 = import_zod53.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
|
|
11107
|
+
const presetParsed = presetSchema2.safeParse(preset);
|
|
11108
|
+
const brief = normalizeSubmissionToBrief({
|
|
11109
|
+
others,
|
|
11110
|
+
fieldMap: resolvedMap,
|
|
11111
|
+
briefId,
|
|
11112
|
+
preset: presetParsed.success ? presetParsed.data : void 0
|
|
11113
|
+
});
|
|
11114
|
+
const validation = validateBrief(brief);
|
|
11115
|
+
return jsonResponse({
|
|
11116
|
+
submissionId: typeof sub.id === "string" ? sub.id : void 0,
|
|
11117
|
+
fieldMapKeys: Object.keys(resolvedMap).length,
|
|
11118
|
+
brief,
|
|
11119
|
+
validation
|
|
11120
|
+
});
|
|
11121
|
+
} catch (error) {
|
|
11122
|
+
return errorResponse(error);
|
|
11123
|
+
}
|
|
11124
|
+
}
|
|
11125
|
+
);
|
|
11126
|
+
}
|
|
11127
|
+
|
|
9620
11128
|
// src/tools/index.ts
|
|
9621
11129
|
var publicApiTools = [
|
|
9622
11130
|
[registerContactTools, "contacts"],
|
|
@@ -9668,10 +11176,12 @@ var DIAGNOSTICS_MODULE = "diagnostics";
|
|
|
9668
11176
|
var LOCATION_SWITCHER_MODULE = "location-switcher";
|
|
9669
11177
|
var SNAPSHOTS_MODULE = "snapshots";
|
|
9670
11178
|
var FORM_BUILDER_MODULE = "form-builder";
|
|
11179
|
+
var INTAKE_TO_BUILD_MODULE = "intake-to-build";
|
|
9671
11180
|
var KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
9672
11181
|
...publicApiTools.map(([, label]) => label),
|
|
9673
11182
|
...internalApiTools.map(([, label]) => label),
|
|
9674
11183
|
FORM_BUILDER_MODULE,
|
|
11184
|
+
INTAKE_TO_BUILD_MODULE,
|
|
9675
11185
|
VALIDATORS_MODULE,
|
|
9676
11186
|
DIAGNOSTICS_MODULE,
|
|
9677
11187
|
LOCATION_SWITCHER_MODULE,
|
|
@@ -9690,6 +11200,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
9690
11200
|
register(wrap(moduleName), builderClient);
|
|
9691
11201
|
}
|
|
9692
11202
|
registerFormBuilderTools(wrap(FORM_BUILDER_MODULE), builderClient, client);
|
|
11203
|
+
registerIntakeToBuildTools(wrap(INTAKE_TO_BUILD_MODULE), client, builderClient);
|
|
9693
11204
|
registerValidatorTools(wrap(VALIDATORS_MODULE), client, builderClient);
|
|
9694
11205
|
registerDiagnosticTools(
|
|
9695
11206
|
wrap(DIAGNOSTICS_MODULE),
|