@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.
- package/dist/index.js +2398 -32
- 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.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
|
-
|
|
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
|
|
6540
|
-
return { ok: false, error: `Could not reach license server: ${
|
|
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
|
|
6579
|
-
return { ok: false, error: `Could not reach GHL: ${
|
|
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
|
|
6596
|
-
return { ok: false, error: `Could not reach Firebase: ${
|
|
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
|
|
7420
|
-
results.errors.push(`${contactId}: ${
|
|
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(
|
|
9860
|
-
process.stderr.write(
|
|
12225
|
+
function errLine(msg2) {
|
|
12226
|
+
process.stderr.write(msg2 + "\n");
|
|
9861
12227
|
}
|
|
9862
12228
|
function preflightWritable() {
|
|
9863
12229
|
try {
|