@elitedcs/ghl-mcp 3.78.0 → 3.79.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/CHANGELOG.md +79 -0
- package/README.md +1 -1
- package/dist/index.js +308 -255
- package/guide/guide.html +664 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10713,6 +10713,15 @@ async function templateFacets(api, product = "funnels") {
|
|
|
10713
10713
|
const names = (list2) => (list2 ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
|
|
10714
10714
|
return { categories: names(out.categories), tags: names(out.tags), features: names(out.features) };
|
|
10715
10715
|
}
|
|
10716
|
+
function installedFunnelId(loaded) {
|
|
10717
|
+
const r = loaded;
|
|
10718
|
+
if (!r || typeof r !== "object") return void 0;
|
|
10719
|
+
if (r.status !== "ok") return void 0;
|
|
10720
|
+
if (r.data?.status !== "completed") return void 0;
|
|
10721
|
+
const t = r.data?.target;
|
|
10722
|
+
if (!t || t.asset !== "funnels") return void 0;
|
|
10723
|
+
return typeof t.assetId === "string" && GHL_ID.test(t.assetId) ? t.assetId : void 0;
|
|
10724
|
+
}
|
|
10716
10725
|
async function installTemplate(api, templateId) {
|
|
10717
10726
|
if (!/^[A-Za-z0-9]{12,40}$/.test(templateId)) throw new Error(`Not a template id: ${JSON.stringify(templateId)}`);
|
|
10718
10727
|
return call(api, "/templates/template/load", {
|
|
@@ -10737,12 +10746,13 @@ function rankForBrand(rows, brand) {
|
|
|
10737
10746
|
return { ...t, why: parts.join("; ") };
|
|
10738
10747
|
});
|
|
10739
10748
|
}
|
|
10740
|
-
var SVC;
|
|
10749
|
+
var SVC, GHL_ID;
|
|
10741
10750
|
var init_ghl_templates = __esm({
|
|
10742
10751
|
"src/command-os/design/ghl-templates.ts"() {
|
|
10743
10752
|
"use strict";
|
|
10744
10753
|
init_color();
|
|
10745
10754
|
SVC = "https://services.leadconnectorhq.com";
|
|
10755
|
+
GHL_ID = /^[A-Za-z0-9]{15,40}$/;
|
|
10746
10756
|
}
|
|
10747
10757
|
});
|
|
10748
10758
|
|
|
@@ -13550,7 +13560,7 @@ function installedFunnelTracker(before) {
|
|
|
13550
13560
|
};
|
|
13551
13561
|
}
|
|
13552
13562
|
function removalHoldBack(opts) {
|
|
13553
|
-
if (opts.truncated) return `this sub-account
|
|
13563
|
+
if (opts.truncated && !opts.identifiedFromResponse) return `this sub-account's funnels could not all be read back before the install, so "which funnel is new" could not be established with certainty`;
|
|
13554
13564
|
if (opts.multiPageStepNames.length > 0) return `${opts.multiPageStepNames.length} step(s) carry more than one page (${opts.multiPageStepNames.join(", ")}) and only the first of each was rebuilt`;
|
|
13555
13565
|
return null;
|
|
13556
13566
|
}
|
|
@@ -13563,12 +13573,23 @@ function registerPageStudioTools(server2, builderClient) {
|
|
|
13563
13573
|
locationId: () => client.locationId
|
|
13564
13574
|
});
|
|
13565
13575
|
const FUNNEL_PAGE = 100;
|
|
13576
|
+
const FUNNEL_PAGE_CAP = 20;
|
|
13566
13577
|
async function funnelSummary() {
|
|
13567
13578
|
const out = /* @__PURE__ */ new Map();
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13579
|
+
let truncated = false;
|
|
13580
|
+
for (let page = 0; page < FUNNEL_PAGE_CAP; page++) {
|
|
13581
|
+
const r = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}&offset=${page * FUNNEL_PAGE}`);
|
|
13582
|
+
const rows = r.funnels ?? [];
|
|
13583
|
+
const fresh = rows.filter((f) => !out.has(f._id));
|
|
13584
|
+
if (page > 0 && fresh.length === 0 && rows.length > 0) {
|
|
13585
|
+
truncated = true;
|
|
13586
|
+
break;
|
|
13587
|
+
}
|
|
13588
|
+
for (const f of rows) out.set(f._id, `${f.name} (${(f.steps ?? []).length} steps)`);
|
|
13589
|
+
if (rows.length < FUNNEL_PAGE) return { map: out, truncated: false };
|
|
13590
|
+
if (page === FUNNEL_PAGE_CAP - 1) truncated = true;
|
|
13591
|
+
}
|
|
13592
|
+
return { map: out, truncated };
|
|
13572
13593
|
}
|
|
13573
13594
|
async function funnelRequest(method, path36, body) {
|
|
13574
13595
|
const headers = await client.buildHeaders();
|
|
@@ -13723,23 +13744,34 @@ ${await response.text()}`);
|
|
|
13723
13744
|
templateId: import_zod42.z.string().describe("From find_ghl_template."),
|
|
13724
13745
|
brandSlug: import_zod42.z.string().optional().describe("A stored brand to repaint the template in. Omit to install it in the designer's own colours."),
|
|
13725
13746
|
confirm: import_zod42.z.literal("INSTALL").describe("Type INSTALL. This writes into the live sub-account you are switched to."),
|
|
13726
|
-
keepOriginals: import_zod42.z.boolean().optional().describe("With brandSlug: leave the template's original unbranded steps beside the branded ones.
|
|
13747
|
+
keepOriginals: import_zod42.z.boolean().optional().describe("With brandSlug: leave the template's original unbranded steps beside the branded ones. Normally they are removed once every branded page has been read back and measured. They are KEPT automatically, whatever you pass, if GoHighLevel did not name the funnel it created in its install response and the identification had to fall back to comparing the account before and after.")
|
|
13727
13748
|
},
|
|
13728
13749
|
async ({ templateId, brandSlug, keepOriginals }) => {
|
|
13729
13750
|
const brand = brandSlug ? houseLooks().find((x) => x.slug === brandSlug) ?? readBrand(slugify(brandSlug)) : null;
|
|
13730
13751
|
const inventory = await funnelSummary();
|
|
13731
13752
|
const before = inventory.map;
|
|
13732
|
-
await installTemplate(templateApi(), templateId);
|
|
13753
|
+
const named = installedFunnelId(await installTemplate(templateApi(), templateId));
|
|
13754
|
+
const namedFunnelId = named && !before.has(named) ? named : void 0;
|
|
13755
|
+
const namedButNotNew = Boolean(named) && named !== namedFunnelId;
|
|
13733
13756
|
let funnel;
|
|
13734
13757
|
let ambiguous = [];
|
|
13735
13758
|
const tracker = installedFunnelTracker(before);
|
|
13736
13759
|
for (let tries = 0; tries < 8; tries++) {
|
|
13737
13760
|
const list2 = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`);
|
|
13738
|
-
const
|
|
13739
|
-
if (
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13761
|
+
const rows = list2.funnels ?? [];
|
|
13762
|
+
if (namedFunnelId) {
|
|
13763
|
+
const mine = rows.find((f) => f._id === namedFunnelId);
|
|
13764
|
+
if (mine && (mine.steps ?? []).length > 0) {
|
|
13765
|
+
funnel = mine;
|
|
13766
|
+
break;
|
|
13767
|
+
}
|
|
13768
|
+
} else {
|
|
13769
|
+
const seen = tracker.observe(rows);
|
|
13770
|
+
if (seen.settled) {
|
|
13771
|
+
if ("funnel" in seen) funnel = seen.funnel;
|
|
13772
|
+
else ambiguous = seen.ambiguous;
|
|
13773
|
+
break;
|
|
13774
|
+
}
|
|
13743
13775
|
}
|
|
13744
13776
|
await new Promise((r) => setTimeout(r, 1500));
|
|
13745
13777
|
}
|
|
@@ -13839,9 +13871,10 @@ ${await response.text()}`);
|
|
|
13839
13871
|
}
|
|
13840
13872
|
}
|
|
13841
13873
|
let removed = 0;
|
|
13842
|
-
const
|
|
13874
|
+
const certain = Boolean(namedFunnelId) && !inventory.truncated;
|
|
13875
|
+
const removeOriginals = certain ? keepOriginals !== true : keepOriginals === false;
|
|
13843
13876
|
const complete = problems.length === 0 && done.length === originals.length && done.length > 0;
|
|
13844
|
-
const holdBack = removalHoldBack({ truncated: inventory.truncated, funnelPageSize: FUNNEL_PAGE, multiPageStepNames: multiPage.map((m) => m.name) });
|
|
13877
|
+
const holdBack = removalHoldBack({ truncated: inventory.truncated, funnelPageSize: FUNNEL_PAGE, multiPageStepNames: multiPage.map((m) => m.name), identifiedFromResponse: certain });
|
|
13845
13878
|
if (complete && removeOriginals && holdBack === null) {
|
|
13846
13879
|
for (const d of done) {
|
|
13847
13880
|
try {
|
|
@@ -13860,6 +13893,7 @@ ${await response.text()}`);
|
|
|
13860
13893
|
pages: done,
|
|
13861
13894
|
originalsRemoved: complete && removeOriginals ? removed : 0,
|
|
13862
13895
|
...problems.length > 0 && { problems, kept: "Every original step was LEFT IN PLACE because something failed. Nothing was deleted." },
|
|
13896
|
+
...namedButNotNew && { identification: "GoHighLevel named a funnel that this account ALREADY had before the install. That name was discarded and the account was compared instead, so the funnel above was identified the cautious way and originals are only removed if you asked for it explicitly. Worth reporting: it means the install response no longer means what it did." },
|
|
13863
13897
|
...holdBack !== null && { kept: `Every original step was LEFT IN PLACE on purpose: ${holdBack}. The branded pages are there beside them \u2014 remove the originals by hand once you have looked.` },
|
|
13864
13898
|
warnings: rethemeEnvelope({}, brand, palette).warnings,
|
|
13865
13899
|
next: done.length ? `Grade one before the client sees it: verify_design on ${String(done[0].preview)} with medium "site" and placement "ghl". GoHighLevel's own templates are designed but frequently fail contrast and tap-target rules.` : "Nothing was branded.",
|
|
@@ -31555,12 +31589,23 @@ RESULT: {"ok":true,"summary":"<one sentence: who the client is and what they wan
|
|
|
31555
31589
|
"mcp__ghl__get_pipelines",
|
|
31556
31590
|
"mcp__ghl__list_workflows_full",
|
|
31557
31591
|
// Finding 17: the recipient of a staff notification comes from THIS account's user list or stays empty.
|
|
31558
|
-
"mcp__ghl__get_users"
|
|
31592
|
+
"mcp__ghl__get_users",
|
|
31593
|
+
// Finding 22 (2026-09-03 proof run): an intake filled in the cockpit lands as "Intake:" custom
|
|
31594
|
+
// fields on a contact tagged blueprint-intake, NOT as a form submission — GHL's submissions
|
|
31595
|
+
// endpoint returns nothing for API-created forms (client-intake.ts:175). Stage 1 already checks
|
|
31596
|
+
// both paths; stage 3 could not, because it lacked these three reads, so the cockpit's own
|
|
31597
|
+
// intake panel produced a valid Brief and then a dead stop here.
|
|
31598
|
+
"mcp__ghl__search_contacts",
|
|
31599
|
+
"mcp__ghl__get_contact",
|
|
31600
|
+
"mcp__ghl__get_custom_fields"
|
|
31559
31601
|
],
|
|
31560
31602
|
prompt: (locationId2, locationName, ctx) => ctx?.savedPlan ? buildStageReusePrompt(locationId2, locationName, ctx.savedPlan) : `You are running ONE unattended step of a GHL Command Blueprint build. Use ONLY the ghl MCP tools.
|
|
31561
31603
|
THE PLAN IS COMPOSED IN CODE, NOT BY YOU (plan and build were split 2026-08-26 after a run spent 18 of its 25 minutes writing 108 workflow actions by hand). You never author workflows, actions, emails or texts unless step 6's fallback fires.
|
|
31562
31604
|
1. switch_location to ${locationId2} (${locationName}); verify with get_current_location and stop if it is not that location.
|
|
31563
|
-
2. Rebuild the Brief
|
|
31605
|
+
2. Rebuild the Brief. The answers arrive one of TWO ways and you must check BOTH:
|
|
31606
|
+
(a) a real form submission \u2014 get_forms then get_form_submissions_full, then normalize_submission_to_brief; or
|
|
31607
|
+
(b) an intake completed in the cockpit \u2014 search_contacts for the tag "blueprint-intake", read that contact with get_contact, and assemble the Brief directly from its "Intake: <question>" custom fields (get_custom_fields maps ids to names). apply_build_plan accepts a Brief assembled this way.
|
|
31608
|
+
Only if BOTH come up empty, stop and report that there are no intake answers yet \u2014 never tell the operator to fill in the form when path (b) was never checked.
|
|
31564
31609
|
3. Call apply_build_plan with fromPreset:true, preset:"${presetForIndustry(ctx?.industry)}", brief:<the Brief from step 2>, mode:"dry_run". The server expands the industry preset with the client's staff, calendars (appointment lengths included), pipeline stages, offer and notification rules in under a second, validates it, and reports what would be built.
|
|
31565
31610
|
4. Read the dry-run report. Its composed.copySlots list the message templates; their copy is already filled from the brief and send-ready. ONLY where the brief gives client-specific material the preset copy misses (the client's signature line, a named offer or price), pass copy:[{ref:"email_template.<slug>", subject, html}] / [{ref:"sms_template.<slug>", body}] for JUST those templates on the next call. Never restructure; never rewrite every message.
|
|
31566
31611
|
5. Call apply_build_plan again with the SAME fromPreset:true, preset and brief (plus your copy overrides, if any) and mode:"execute". Never delete or recreate anything that already exists.
|
|
@@ -33906,6 +33951,240 @@ var init_publish_report = __esm({
|
|
|
33906
33951
|
}
|
|
33907
33952
|
});
|
|
33908
33953
|
|
|
33954
|
+
// src/client-intake.ts
|
|
33955
|
+
function publicFormUrl(formId) {
|
|
33956
|
+
return `https://api.leadconnectorhq.com/widget/form/${formId}`;
|
|
33957
|
+
}
|
|
33958
|
+
function urlCannotCarry(value) {
|
|
33959
|
+
if (Array.isArray(value)) return value.length > 1 ? "several answers" : value[0] && /\+/.test(value[0]) ? "contains a plus sign" : null;
|
|
33960
|
+
return /\+/.test(value) ? "contains a plus sign" : null;
|
|
33961
|
+
}
|
|
33962
|
+
function prefillFormUrl(formUrl, queryKeys, answers, contact = {}, altQueryKeys = {}) {
|
|
33963
|
+
const p = new URLSearchParams();
|
|
33964
|
+
if (contact.firstName) p.set("first_name", contact.firstName);
|
|
33965
|
+
if (contact.lastName) p.set("last_name", contact.lastName);
|
|
33966
|
+
if (contact.email) p.set("email", contact.email);
|
|
33967
|
+
if (contact.phone) p.set("phone", contact.phone);
|
|
33968
|
+
const leftovers = [];
|
|
33969
|
+
for (const [label, raw] of Object.entries(answers)) {
|
|
33970
|
+
const values = (Array.isArray(raw) ? raw : [raw]).map((v) => String(v ?? "").trim()).filter(Boolean);
|
|
33971
|
+
if (!values.length) continue;
|
|
33972
|
+
const key = queryKeys[label];
|
|
33973
|
+
if (!key) {
|
|
33974
|
+
leftovers.push({ label, values, reason: "no field for this question" });
|
|
33975
|
+
continue;
|
|
33976
|
+
}
|
|
33977
|
+
const reason = urlCannotCarry(Array.isArray(raw) ? values : values[0]);
|
|
33978
|
+
if (reason) {
|
|
33979
|
+
leftovers.push({ label, values, reason });
|
|
33980
|
+
continue;
|
|
33981
|
+
}
|
|
33982
|
+
p.set(key, values[0]);
|
|
33983
|
+
const alt = altQueryKeys[label];
|
|
33984
|
+
if (alt && alt !== key) p.set(alt, values[0]);
|
|
33985
|
+
}
|
|
33986
|
+
const qs = p.toString();
|
|
33987
|
+
return { url: qs ? `${formUrl}?${qs}` : formUrl, leftovers };
|
|
33988
|
+
}
|
|
33989
|
+
async function readIntakeSnapshot(client, locationId2, sentMarker) {
|
|
33990
|
+
const empty = { status: "not_installed", fields: {}, queryKeys: {}, submissions: 0 };
|
|
33991
|
+
try {
|
|
33992
|
+
const forms = await client.get("/forms/", { params: { locationId: locationId2, limit: 100 } });
|
|
33993
|
+
const form = (forms.forms ?? []).find((f) => f.name === INTAKE_FORM_NAME2);
|
|
33994
|
+
if (!form) return empty;
|
|
33995
|
+
const cf = await client.get("/locations/" + locationId2 + "/customFields");
|
|
33996
|
+
const fields = {};
|
|
33997
|
+
const queryKeys = {};
|
|
33998
|
+
const altQueryKeys = {};
|
|
33999
|
+
const multiFields = {};
|
|
34000
|
+
for (const f of cf.customFields ?? []) {
|
|
34001
|
+
if (!f.name?.startsWith(INTAKE_FIELD_NAME_PREFIX)) continue;
|
|
34002
|
+
const label = f.name.slice(INTAKE_FIELD_NAME_PREFIX.length);
|
|
34003
|
+
fields[label] = f.id;
|
|
34004
|
+
queryKeys[label] = formQueryKey(f.name);
|
|
34005
|
+
if (f.dataType === "MULTIPLE_OPTIONS") multiFields[label] = true;
|
|
34006
|
+
if (f.fieldKey) {
|
|
34007
|
+
const alt = f.fieldKey.replace(/^contact\./, "");
|
|
34008
|
+
if (alt !== queryKeys[label]) altQueryKeys[label] = alt;
|
|
34009
|
+
}
|
|
34010
|
+
}
|
|
34011
|
+
let submissions = 0;
|
|
34012
|
+
let answeredAt;
|
|
34013
|
+
let channel;
|
|
34014
|
+
let contactId;
|
|
34015
|
+
try {
|
|
34016
|
+
const subs = await client.get("/forms/submissions", { params: { locationId: locationId2, formId: form.id, limit: 5 } });
|
|
34017
|
+
if ((subs.submissions ?? []).length) {
|
|
34018
|
+
submissions = subs.submissions.length;
|
|
34019
|
+
answeredAt = subs.submissions[0]?.createdAt;
|
|
34020
|
+
channel = "form";
|
|
34021
|
+
}
|
|
34022
|
+
} catch {
|
|
34023
|
+
}
|
|
34024
|
+
if (!submissions) {
|
|
34025
|
+
try {
|
|
34026
|
+
const seen = /* @__PURE__ */ new Map();
|
|
34027
|
+
for (const q2 of [INTAKE_CONTACT_TAG, INTAKE_FORM_NAME2]) {
|
|
34028
|
+
const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q2, limit: 20 } });
|
|
34029
|
+
for (const c of found.contacts ?? []) seen.set(c.id, c);
|
|
34030
|
+
}
|
|
34031
|
+
const isFormSubmission = (c) => c.source === INTAKE_FORM_NAME2 || (c.attributions ?? []).some((a) => a.mediumId === form.id || a.medium === "form");
|
|
34032
|
+
const all = [...seen.values()];
|
|
34033
|
+
const hit = all.find(isFormSubmission) ?? all.find((c) => (c.tags ?? []).includes(INTAKE_CONTACT_TAG));
|
|
34034
|
+
if (hit) {
|
|
34035
|
+
submissions = 1;
|
|
34036
|
+
answeredAt = hit.dateUpdated;
|
|
34037
|
+
contactId = hit.id;
|
|
34038
|
+
channel = isFormSubmission(hit) ? "form" : "cockpit";
|
|
34039
|
+
}
|
|
34040
|
+
} catch {
|
|
34041
|
+
}
|
|
34042
|
+
}
|
|
34043
|
+
const status = submissions > 0 ? "received" : sentMarker ? "sent" : "installed";
|
|
34044
|
+
return { status, formId: form.id, formUrl: publicFormUrl(form.id), fields, queryKeys, altQueryKeys, multiFields, submissions, answeredAt, channel, contactId };
|
|
34045
|
+
} catch {
|
|
34046
|
+
return empty;
|
|
34047
|
+
}
|
|
34048
|
+
}
|
|
34049
|
+
async function submitIntakeAnswers(client, locationId2, snapshot, answers, contact) {
|
|
34050
|
+
const customFields = [];
|
|
34051
|
+
const unmapped = [];
|
|
34052
|
+
for (const [label, raw] of Object.entries(answers)) {
|
|
34053
|
+
const values = (Array.isArray(raw) ? raw : [raw]).map((v) => String(v ?? "").trim()).filter(Boolean);
|
|
34054
|
+
if (!values.length) continue;
|
|
34055
|
+
const id = snapshot.fields[label];
|
|
34056
|
+
if (id) customFields.push({ id, value: snapshot.multiFields?.[label] ? values : values[0] });
|
|
34057
|
+
else unmapped.push(label);
|
|
34058
|
+
}
|
|
34059
|
+
try {
|
|
34060
|
+
const body = {
|
|
34061
|
+
locationId: locationId2,
|
|
34062
|
+
firstName: contact.firstName || "Intake",
|
|
34063
|
+
lastName: contact.lastName || "Submission",
|
|
34064
|
+
...contact.email && { email: contact.email },
|
|
34065
|
+
...contact.phone && { phone: contact.phone },
|
|
34066
|
+
tags: [INTAKE_CONTACT_TAG],
|
|
34067
|
+
customFields
|
|
34068
|
+
};
|
|
34069
|
+
const res = await client.post("/contacts/upsert", { body });
|
|
34070
|
+
return { ok: true, contactId: res.contact?.id, unmapped };
|
|
34071
|
+
} catch (e) {
|
|
34072
|
+
return { ok: false, unmapped, error: e instanceof Error ? e.message : String(e) };
|
|
34073
|
+
}
|
|
34074
|
+
}
|
|
34075
|
+
async function emailIntakeLink(client, locationId2, to, formUrl, agencyName) {
|
|
34076
|
+
try {
|
|
34077
|
+
const found = await client.get("/contacts/", { params: { locationId: locationId2, query: to.email, limit: 5 } });
|
|
34078
|
+
let contactId = (found.contacts ?? []).find((c) => c.email?.toLowerCase() === to.email.toLowerCase())?.id;
|
|
34079
|
+
if (!contactId) {
|
|
34080
|
+
const made = await client.post("/contacts/", {
|
|
34081
|
+
body: { locationId: locationId2, email: to.email, ...to.firstName && { firstName: to.firstName }, tags: ["blueprint-intake-invited"] }
|
|
34082
|
+
});
|
|
34083
|
+
contactId = made.contact?.id;
|
|
34084
|
+
}
|
|
34085
|
+
if (!contactId) throw new Error("could not create a contact to email");
|
|
34086
|
+
const html2 = [
|
|
34087
|
+
`<p>Hi ${to.firstName || "there"},</p>`,
|
|
34088
|
+
`<p>Before we build out your account, we need the details only you can give us. It takes about ten minutes, and you can stop and come back to it.</p>`,
|
|
34089
|
+
`<p><a href="${formUrl}"><strong>Open your onboarding questionnaire</strong></a></p>`,
|
|
34090
|
+
`<p>Answer what you know. Anything you're unsure about, leave it and we'll cover it on our next call.</p>`,
|
|
34091
|
+
`<p>${agencyName}</p>`
|
|
34092
|
+
].join("");
|
|
34093
|
+
await client.post("/conversations/messages", {
|
|
34094
|
+
body: { type: "Email", contactId, emailTo: to.email, subject: "Your onboarding questionnaire", html: html2 }
|
|
34095
|
+
});
|
|
34096
|
+
return { ok: true };
|
|
34097
|
+
} catch (e) {
|
|
34098
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
34099
|
+
}
|
|
34100
|
+
}
|
|
34101
|
+
function recorderAllowPatterns(mcpListOutput) {
|
|
34102
|
+
const out = [];
|
|
34103
|
+
for (const line3 of mcpListOutput.split("\n")) {
|
|
34104
|
+
const m = /^\s*(.+?):\s+https?:\/\//.exec(line3);
|
|
34105
|
+
if (!m) continue;
|
|
34106
|
+
const name = m[1].trim();
|
|
34107
|
+
if (!/✔|Connected/i.test(line3)) continue;
|
|
34108
|
+
if (!RECORDER_HINTS.some((h) => name.toLowerCase().includes(h))) continue;
|
|
34109
|
+
out.push(`mcp__${name.replace(/[^A-Za-z0-9]+/g, "_")}__*`);
|
|
34110
|
+
}
|
|
34111
|
+
return [...new Set(out)];
|
|
34112
|
+
}
|
|
34113
|
+
function prefillTools(source, recorderPatterns = []) {
|
|
34114
|
+
const deny = "Bash,Write,Edit,MultiEdit,NotebookEdit,Task";
|
|
34115
|
+
if (source.kind === "text") return { allow: "", deny: deny + ",WebFetch,WebSearch,Read,Glob,Grep" };
|
|
34116
|
+
if (source.kind === "url") return { allow: "WebFetch", deny: deny + ",WebSearch,Read,Glob,Grep" };
|
|
34117
|
+
return { allow: recorderPatterns.join(","), deny: deny + ",Read,Glob,Grep" };
|
|
34118
|
+
}
|
|
34119
|
+
function sourcePreamble(source) {
|
|
34120
|
+
if (source.kind === "url") {
|
|
34121
|
+
return `Fetch this and use it as the material: ${source.url}
|
|
34122
|
+
If it needs a login or returns nothing usable, say so \u2014 do not guess from the URL alone.`;
|
|
34123
|
+
}
|
|
34124
|
+
if (source.kind === "recorder") {
|
|
34125
|
+
return `Find the recording/transcript the operator means using their connected recorder tools, then use its transcript as the material.
|
|
34126
|
+
Operator's request: "${source.instruction}"
|
|
34127
|
+
If you cannot find exactly one clear match, stop and say which recordings you did find instead of guessing.`;
|
|
34128
|
+
}
|
|
34129
|
+
return "";
|
|
34130
|
+
}
|
|
34131
|
+
function prefillPrompt(labels, transcript) {
|
|
34132
|
+
return [
|
|
34133
|
+
"You are filling in a client-onboarding questionnaire from a discovery call.",
|
|
34134
|
+
"Answer ONLY from the material provided. Never invent a fact, a number, or a name.",
|
|
34135
|
+
'If the material does not clearly answer a question, leave it out and list it under "unknown".',
|
|
34136
|
+
"Keep answers short and concrete \u2014 this feeds an automated build, not a report.",
|
|
34137
|
+
"",
|
|
34138
|
+
"QUESTIONS (use these labels verbatim as keys):",
|
|
34139
|
+
...labels.map((l) => `- ${l}`),
|
|
34140
|
+
"",
|
|
34141
|
+
"MATERIAL:",
|
|
34142
|
+
transcript.slice(0, 6e4),
|
|
34143
|
+
"",
|
|
34144
|
+
"Reply with ONE line of JSON and nothing else:",
|
|
34145
|
+
'RESULT: {"answers":{"<label>":"<answer>"},"unknown":["<label>"]}'
|
|
34146
|
+
].join("\n");
|
|
34147
|
+
}
|
|
34148
|
+
function prefillPromptForSource(labels, source) {
|
|
34149
|
+
const pre = sourcePreamble(source);
|
|
34150
|
+
const body = prefillPrompt(labels, source.kind === "text" ? source.text : "(see the material you fetched above)");
|
|
34151
|
+
return pre ? `${pre}
|
|
34152
|
+
|
|
34153
|
+
${body}` : body;
|
|
34154
|
+
}
|
|
34155
|
+
async function prefillFromSource(spawnClaude, labels, source, recorderPatterns = []) {
|
|
34156
|
+
const empty = source.kind === "text" && !source.text.trim() || source.kind === "url" && !source.url.trim() || source.kind === "recorder" && !source.instruction.trim();
|
|
34157
|
+
if (empty) return { ok: false, answers: {}, unknown: labels, error: "nothing to read" };
|
|
34158
|
+
return runPrefill(() => spawnClaude(prefillPromptForSource(labels, source), prefillTools(source, recorderPatterns)), labels);
|
|
34159
|
+
}
|
|
34160
|
+
async function runPrefill(call2, labels) {
|
|
34161
|
+
try {
|
|
34162
|
+
const out = await call2();
|
|
34163
|
+
const m = /RESULT:\s*(\{[\s\S]*\})/.exec(out);
|
|
34164
|
+
if (!m) return { ok: false, answers: {}, unknown: labels, error: "the model did not return a result line" };
|
|
34165
|
+
const parsed = JSON.parse(m[1]);
|
|
34166
|
+
const answers = {};
|
|
34167
|
+
for (const [k, v] of Object.entries(parsed.answers ?? {})) {
|
|
34168
|
+
if (labels.includes(k) && typeof v === "string" && v.trim()) answers[k] = v.trim();
|
|
34169
|
+
}
|
|
34170
|
+
const unknown = labels.filter((l) => !(l in answers));
|
|
34171
|
+
return { ok: true, answers, unknown };
|
|
34172
|
+
} catch (e) {
|
|
34173
|
+
return { ok: false, answers: {}, unknown: labels, error: e instanceof Error ? e.message : String(e) };
|
|
34174
|
+
}
|
|
34175
|
+
}
|
|
34176
|
+
var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, RECORDER_HINTS;
|
|
34177
|
+
var init_client_intake = __esm({
|
|
34178
|
+
"src/client-intake.ts"() {
|
|
34179
|
+
"use strict";
|
|
34180
|
+
init_question_set();
|
|
34181
|
+
init_form_template();
|
|
34182
|
+
INTAKE_FORM_NAME2 = "GHL Command Blueprint \u2014 Client Intake";
|
|
34183
|
+
INTAKE_CONTACT_TAG = "blueprint-intake";
|
|
34184
|
+
RECORDER_HINTS = ["plaud", "otter", "fireflies", "grain", "fathom", "tldv", "granola", "zoom", "read.ai", "readai", "supernormal", "avoma"];
|
|
34185
|
+
}
|
|
34186
|
+
});
|
|
34187
|
+
|
|
33909
34188
|
// src/intake-to-build/customization.ts
|
|
33910
34189
|
function industryPack(slug3) {
|
|
33911
34190
|
if (!slug3) return void 0;
|
|
@@ -37884,7 +38163,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
37884
38163
|
}
|
|
37885
38164
|
server2.tool(
|
|
37886
38165
|
"install_intake_form",
|
|
37887
|
-
`Install the 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. Pass industry (e.g. "clinic", "med-spa") to install the TAILORED set \u2014 base questions + that industry's pack + the agency's own overlay (added / removed questions), the same set the intake interview asks \u2014 so no cockpit answer is left without a form field; omit it for the base set. Returns {formId, fieldMap} \u2014 keep fieldMap
|
|
38166
|
+
`Install the 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. Pass industry (e.g. "clinic", "med-spa") to install the TAILORED set \u2014 base questions + that industry's pack + the agency's own overlay (added / removed questions), the same set the intake interview asks \u2014 so no cockpit answer is left without a form field; omit it for the base set. Returns {formId, formUrl, fieldMap} \u2014 formUrl is the public link to send the client; keep fieldMap. NOTE: the answers come back on a CONTACT tagged "blueprint-intake", not as a form submission (GoHighLevel reports no submissions for forms created through the API), so read the contact and assemble the Brief from its fields. Pass dryRun:true to preview what would be created without writing. Pass formId to update an existing intake form in place (e.g. to add an industry's questions to a form installed without one) instead of creating a new one.`,
|
|
37888
38167
|
{
|
|
37889
38168
|
dryRun: import_zod76.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
|
|
37890
38169
|
formId: import_zod76.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
|
|
@@ -37981,7 +38260,14 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
37981
38260
|
fieldsPersisted: persistedCount,
|
|
37982
38261
|
fieldsExpectedApprox: expectedCount,
|
|
37983
38262
|
fieldMap,
|
|
37984
|
-
|
|
38263
|
+
// The link the operator actually sends the client. It was missing for
|
|
38264
|
+
// six releases: the guide promised "gives you a link to send them",
|
|
38265
|
+
// the code already knew how to build one (client-intake.publicFormUrl), and
|
|
38266
|
+
// this response never handed it over — so the operator had to go and
|
|
38267
|
+
// find the form in the GoHighLevel UI, which is the one thing this
|
|
38268
|
+
// product exists to avoid. Found by running the guide (2026-09-04).
|
|
38269
|
+
formUrl: publicFormUrl(resolvedFormId),
|
|
38270
|
+
next: `Send the client ${publicFormUrl(resolvedFormId)}. When the answers are in, read them from the CONTACT tagged "${INTAKE_CONTACT_TAG}" (search_contacts for that tag, get_contact for its fields, get_custom_fields to map ids to question names) and pass the assembled Brief to apply_build_plan. Do NOT reach for normalize_submission_to_brief here: GoHighLevel's submissions endpoint returns nothing for a form created through the API, so it will report no submissions even after the client has filled this one in.`
|
|
37985
38271
|
});
|
|
37986
38272
|
} catch (error) {
|
|
37987
38273
|
return errorResponse(error);
|
|
@@ -37990,7 +38276,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
37990
38276
|
);
|
|
37991
38277
|
server2.tool(
|
|
37992
38278
|
"normalize_submission_to_brief",
|
|
37993
|
-
|
|
38279
|
+
`Read an intake form submission and normalize it into a \xA74 Brief (briefSource:"intake_form"). ONLY works for a form whose submissions GoHighLevel actually reports, which in practice means a form built in GHL's own editor \u2014 for an intake form installed by install_intake_form it will answer "No submissions found" even after the client has filled it in, and the answers should be read from the contact tagged "blueprint-intake" instead. 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).`,
|
|
37994
38280
|
{
|
|
37995
38281
|
formId: import_zod76.z.string().describe("The intake form ID (from install_intake_form)."),
|
|
37996
38282
|
submissionId: import_zod76.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
|
|
@@ -38057,6 +38343,7 @@ var init_intake_to_build = __esm({
|
|
|
38057
38343
|
init_user_provisioning();
|
|
38058
38344
|
init_plan_form();
|
|
38059
38345
|
init_publish_report();
|
|
38346
|
+
init_client_intake();
|
|
38060
38347
|
init_question_set();
|
|
38061
38348
|
init_customization();
|
|
38062
38349
|
init_intake_overlay();
|
|
@@ -38775,7 +39062,7 @@ var require_package = __commonJS({
|
|
|
38775
39062
|
"package.json"(exports2, module2) {
|
|
38776
39063
|
module2.exports = {
|
|
38777
39064
|
name: "@elitedcs/ghl-mcp",
|
|
38778
|
-
version: "3.
|
|
39065
|
+
version: "3.79.0",
|
|
38779
39066
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
38780
39067
|
description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
38781
39068
|
main: "dist/index.js",
|
|
@@ -39086,240 +39373,6 @@ var init_intake_contact = __esm({
|
|
|
39086
39373
|
}
|
|
39087
39374
|
});
|
|
39088
39375
|
|
|
39089
|
-
// src/client-intake.ts
|
|
39090
|
-
function publicFormUrl(formId) {
|
|
39091
|
-
return `https://api.leadconnectorhq.com/widget/form/${formId}`;
|
|
39092
|
-
}
|
|
39093
|
-
function urlCannotCarry(value) {
|
|
39094
|
-
if (Array.isArray(value)) return value.length > 1 ? "several answers" : value[0] && /\+/.test(value[0]) ? "contains a plus sign" : null;
|
|
39095
|
-
return /\+/.test(value) ? "contains a plus sign" : null;
|
|
39096
|
-
}
|
|
39097
|
-
function prefillFormUrl(formUrl, queryKeys, answers, contact = {}, altQueryKeys = {}) {
|
|
39098
|
-
const p = new URLSearchParams();
|
|
39099
|
-
if (contact.firstName) p.set("first_name", contact.firstName);
|
|
39100
|
-
if (contact.lastName) p.set("last_name", contact.lastName);
|
|
39101
|
-
if (contact.email) p.set("email", contact.email);
|
|
39102
|
-
if (contact.phone) p.set("phone", contact.phone);
|
|
39103
|
-
const leftovers = [];
|
|
39104
|
-
for (const [label, raw] of Object.entries(answers)) {
|
|
39105
|
-
const values = (Array.isArray(raw) ? raw : [raw]).map((v) => String(v ?? "").trim()).filter(Boolean);
|
|
39106
|
-
if (!values.length) continue;
|
|
39107
|
-
const key = queryKeys[label];
|
|
39108
|
-
if (!key) {
|
|
39109
|
-
leftovers.push({ label, values, reason: "no field for this question" });
|
|
39110
|
-
continue;
|
|
39111
|
-
}
|
|
39112
|
-
const reason = urlCannotCarry(Array.isArray(raw) ? values : values[0]);
|
|
39113
|
-
if (reason) {
|
|
39114
|
-
leftovers.push({ label, values, reason });
|
|
39115
|
-
continue;
|
|
39116
|
-
}
|
|
39117
|
-
p.set(key, values[0]);
|
|
39118
|
-
const alt = altQueryKeys[label];
|
|
39119
|
-
if (alt && alt !== key) p.set(alt, values[0]);
|
|
39120
|
-
}
|
|
39121
|
-
const qs = p.toString();
|
|
39122
|
-
return { url: qs ? `${formUrl}?${qs}` : formUrl, leftovers };
|
|
39123
|
-
}
|
|
39124
|
-
async function readIntakeSnapshot(client, locationId2, sentMarker) {
|
|
39125
|
-
const empty = { status: "not_installed", fields: {}, queryKeys: {}, submissions: 0 };
|
|
39126
|
-
try {
|
|
39127
|
-
const forms = await client.get("/forms/", { params: { locationId: locationId2, limit: 100 } });
|
|
39128
|
-
const form = (forms.forms ?? []).find((f) => f.name === INTAKE_FORM_NAME2);
|
|
39129
|
-
if (!form) return empty;
|
|
39130
|
-
const cf = await client.get("/locations/" + locationId2 + "/customFields");
|
|
39131
|
-
const fields = {};
|
|
39132
|
-
const queryKeys = {};
|
|
39133
|
-
const altQueryKeys = {};
|
|
39134
|
-
const multiFields = {};
|
|
39135
|
-
for (const f of cf.customFields ?? []) {
|
|
39136
|
-
if (!f.name?.startsWith(INTAKE_FIELD_NAME_PREFIX)) continue;
|
|
39137
|
-
const label = f.name.slice(INTAKE_FIELD_NAME_PREFIX.length);
|
|
39138
|
-
fields[label] = f.id;
|
|
39139
|
-
queryKeys[label] = formQueryKey(f.name);
|
|
39140
|
-
if (f.dataType === "MULTIPLE_OPTIONS") multiFields[label] = true;
|
|
39141
|
-
if (f.fieldKey) {
|
|
39142
|
-
const alt = f.fieldKey.replace(/^contact\./, "");
|
|
39143
|
-
if (alt !== queryKeys[label]) altQueryKeys[label] = alt;
|
|
39144
|
-
}
|
|
39145
|
-
}
|
|
39146
|
-
let submissions = 0;
|
|
39147
|
-
let answeredAt;
|
|
39148
|
-
let channel;
|
|
39149
|
-
let contactId;
|
|
39150
|
-
try {
|
|
39151
|
-
const subs = await client.get("/forms/submissions", { params: { locationId: locationId2, formId: form.id, limit: 5 } });
|
|
39152
|
-
if ((subs.submissions ?? []).length) {
|
|
39153
|
-
submissions = subs.submissions.length;
|
|
39154
|
-
answeredAt = subs.submissions[0]?.createdAt;
|
|
39155
|
-
channel = "form";
|
|
39156
|
-
}
|
|
39157
|
-
} catch {
|
|
39158
|
-
}
|
|
39159
|
-
if (!submissions) {
|
|
39160
|
-
try {
|
|
39161
|
-
const seen = /* @__PURE__ */ new Map();
|
|
39162
|
-
for (const q2 of [INTAKE_CONTACT_TAG, INTAKE_FORM_NAME2]) {
|
|
39163
|
-
const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q2, limit: 20 } });
|
|
39164
|
-
for (const c of found.contacts ?? []) seen.set(c.id, c);
|
|
39165
|
-
}
|
|
39166
|
-
const isFormSubmission = (c) => c.source === INTAKE_FORM_NAME2 || (c.attributions ?? []).some((a) => a.mediumId === form.id || a.medium === "form");
|
|
39167
|
-
const all = [...seen.values()];
|
|
39168
|
-
const hit = all.find(isFormSubmission) ?? all.find((c) => (c.tags ?? []).includes(INTAKE_CONTACT_TAG));
|
|
39169
|
-
if (hit) {
|
|
39170
|
-
submissions = 1;
|
|
39171
|
-
answeredAt = hit.dateUpdated;
|
|
39172
|
-
contactId = hit.id;
|
|
39173
|
-
channel = isFormSubmission(hit) ? "form" : "cockpit";
|
|
39174
|
-
}
|
|
39175
|
-
} catch {
|
|
39176
|
-
}
|
|
39177
|
-
}
|
|
39178
|
-
const status = submissions > 0 ? "received" : sentMarker ? "sent" : "installed";
|
|
39179
|
-
return { status, formId: form.id, formUrl: publicFormUrl(form.id), fields, queryKeys, altQueryKeys, multiFields, submissions, answeredAt, channel, contactId };
|
|
39180
|
-
} catch {
|
|
39181
|
-
return empty;
|
|
39182
|
-
}
|
|
39183
|
-
}
|
|
39184
|
-
async function submitIntakeAnswers(client, locationId2, snapshot, answers, contact) {
|
|
39185
|
-
const customFields = [];
|
|
39186
|
-
const unmapped = [];
|
|
39187
|
-
for (const [label, raw] of Object.entries(answers)) {
|
|
39188
|
-
const values = (Array.isArray(raw) ? raw : [raw]).map((v) => String(v ?? "").trim()).filter(Boolean);
|
|
39189
|
-
if (!values.length) continue;
|
|
39190
|
-
const id = snapshot.fields[label];
|
|
39191
|
-
if (id) customFields.push({ id, value: snapshot.multiFields?.[label] ? values : values[0] });
|
|
39192
|
-
else unmapped.push(label);
|
|
39193
|
-
}
|
|
39194
|
-
try {
|
|
39195
|
-
const body = {
|
|
39196
|
-
locationId: locationId2,
|
|
39197
|
-
firstName: contact.firstName || "Intake",
|
|
39198
|
-
lastName: contact.lastName || "Submission",
|
|
39199
|
-
...contact.email && { email: contact.email },
|
|
39200
|
-
...contact.phone && { phone: contact.phone },
|
|
39201
|
-
tags: [INTAKE_CONTACT_TAG],
|
|
39202
|
-
customFields
|
|
39203
|
-
};
|
|
39204
|
-
const res = await client.post("/contacts/upsert", { body });
|
|
39205
|
-
return { ok: true, contactId: res.contact?.id, unmapped };
|
|
39206
|
-
} catch (e) {
|
|
39207
|
-
return { ok: false, unmapped, error: e instanceof Error ? e.message : String(e) };
|
|
39208
|
-
}
|
|
39209
|
-
}
|
|
39210
|
-
async function emailIntakeLink(client, locationId2, to, formUrl, agencyName) {
|
|
39211
|
-
try {
|
|
39212
|
-
const found = await client.get("/contacts/", { params: { locationId: locationId2, query: to.email, limit: 5 } });
|
|
39213
|
-
let contactId = (found.contacts ?? []).find((c) => c.email?.toLowerCase() === to.email.toLowerCase())?.id;
|
|
39214
|
-
if (!contactId) {
|
|
39215
|
-
const made = await client.post("/contacts/", {
|
|
39216
|
-
body: { locationId: locationId2, email: to.email, ...to.firstName && { firstName: to.firstName }, tags: ["blueprint-intake-invited"] }
|
|
39217
|
-
});
|
|
39218
|
-
contactId = made.contact?.id;
|
|
39219
|
-
}
|
|
39220
|
-
if (!contactId) throw new Error("could not create a contact to email");
|
|
39221
|
-
const html2 = [
|
|
39222
|
-
`<p>Hi ${to.firstName || "there"},</p>`,
|
|
39223
|
-
`<p>Before we build out your account, we need the details only you can give us. It takes about ten minutes, and you can stop and come back to it.</p>`,
|
|
39224
|
-
`<p><a href="${formUrl}"><strong>Open your onboarding questionnaire</strong></a></p>`,
|
|
39225
|
-
`<p>Answer what you know. Anything you're unsure about, leave it and we'll cover it on our next call.</p>`,
|
|
39226
|
-
`<p>${agencyName}</p>`
|
|
39227
|
-
].join("");
|
|
39228
|
-
await client.post("/conversations/messages", {
|
|
39229
|
-
body: { type: "Email", contactId, emailTo: to.email, subject: "Your onboarding questionnaire", html: html2 }
|
|
39230
|
-
});
|
|
39231
|
-
return { ok: true };
|
|
39232
|
-
} catch (e) {
|
|
39233
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
39234
|
-
}
|
|
39235
|
-
}
|
|
39236
|
-
function recorderAllowPatterns(mcpListOutput) {
|
|
39237
|
-
const out = [];
|
|
39238
|
-
for (const line3 of mcpListOutput.split("\n")) {
|
|
39239
|
-
const m = /^\s*(.+?):\s+https?:\/\//.exec(line3);
|
|
39240
|
-
if (!m) continue;
|
|
39241
|
-
const name = m[1].trim();
|
|
39242
|
-
if (!/✔|Connected/i.test(line3)) continue;
|
|
39243
|
-
if (!RECORDER_HINTS.some((h) => name.toLowerCase().includes(h))) continue;
|
|
39244
|
-
out.push(`mcp__${name.replace(/[^A-Za-z0-9]+/g, "_")}__*`);
|
|
39245
|
-
}
|
|
39246
|
-
return [...new Set(out)];
|
|
39247
|
-
}
|
|
39248
|
-
function prefillTools(source, recorderPatterns = []) {
|
|
39249
|
-
const deny = "Bash,Write,Edit,MultiEdit,NotebookEdit,Task";
|
|
39250
|
-
if (source.kind === "text") return { allow: "", deny: deny + ",WebFetch,WebSearch,Read,Glob,Grep" };
|
|
39251
|
-
if (source.kind === "url") return { allow: "WebFetch", deny: deny + ",WebSearch,Read,Glob,Grep" };
|
|
39252
|
-
return { allow: recorderPatterns.join(","), deny: deny + ",Read,Glob,Grep" };
|
|
39253
|
-
}
|
|
39254
|
-
function sourcePreamble(source) {
|
|
39255
|
-
if (source.kind === "url") {
|
|
39256
|
-
return `Fetch this and use it as the material: ${source.url}
|
|
39257
|
-
If it needs a login or returns nothing usable, say so \u2014 do not guess from the URL alone.`;
|
|
39258
|
-
}
|
|
39259
|
-
if (source.kind === "recorder") {
|
|
39260
|
-
return `Find the recording/transcript the operator means using their connected recorder tools, then use its transcript as the material.
|
|
39261
|
-
Operator's request: "${source.instruction}"
|
|
39262
|
-
If you cannot find exactly one clear match, stop and say which recordings you did find instead of guessing.`;
|
|
39263
|
-
}
|
|
39264
|
-
return "";
|
|
39265
|
-
}
|
|
39266
|
-
function prefillPrompt(labels, transcript) {
|
|
39267
|
-
return [
|
|
39268
|
-
"You are filling in a client-onboarding questionnaire from a discovery call.",
|
|
39269
|
-
"Answer ONLY from the material provided. Never invent a fact, a number, or a name.",
|
|
39270
|
-
'If the material does not clearly answer a question, leave it out and list it under "unknown".',
|
|
39271
|
-
"Keep answers short and concrete \u2014 this feeds an automated build, not a report.",
|
|
39272
|
-
"",
|
|
39273
|
-
"QUESTIONS (use these labels verbatim as keys):",
|
|
39274
|
-
...labels.map((l) => `- ${l}`),
|
|
39275
|
-
"",
|
|
39276
|
-
"MATERIAL:",
|
|
39277
|
-
transcript.slice(0, 6e4),
|
|
39278
|
-
"",
|
|
39279
|
-
"Reply with ONE line of JSON and nothing else:",
|
|
39280
|
-
'RESULT: {"answers":{"<label>":"<answer>"},"unknown":["<label>"]}'
|
|
39281
|
-
].join("\n");
|
|
39282
|
-
}
|
|
39283
|
-
function prefillPromptForSource(labels, source) {
|
|
39284
|
-
const pre = sourcePreamble(source);
|
|
39285
|
-
const body = prefillPrompt(labels, source.kind === "text" ? source.text : "(see the material you fetched above)");
|
|
39286
|
-
return pre ? `${pre}
|
|
39287
|
-
|
|
39288
|
-
${body}` : body;
|
|
39289
|
-
}
|
|
39290
|
-
async function prefillFromSource(spawnClaude, labels, source, recorderPatterns = []) {
|
|
39291
|
-
const empty = source.kind === "text" && !source.text.trim() || source.kind === "url" && !source.url.trim() || source.kind === "recorder" && !source.instruction.trim();
|
|
39292
|
-
if (empty) return { ok: false, answers: {}, unknown: labels, error: "nothing to read" };
|
|
39293
|
-
return runPrefill(() => spawnClaude(prefillPromptForSource(labels, source), prefillTools(source, recorderPatterns)), labels);
|
|
39294
|
-
}
|
|
39295
|
-
async function runPrefill(call2, labels) {
|
|
39296
|
-
try {
|
|
39297
|
-
const out = await call2();
|
|
39298
|
-
const m = /RESULT:\s*(\{[\s\S]*\})/.exec(out);
|
|
39299
|
-
if (!m) return { ok: false, answers: {}, unknown: labels, error: "the model did not return a result line" };
|
|
39300
|
-
const parsed = JSON.parse(m[1]);
|
|
39301
|
-
const answers = {};
|
|
39302
|
-
for (const [k, v] of Object.entries(parsed.answers ?? {})) {
|
|
39303
|
-
if (labels.includes(k) && typeof v === "string" && v.trim()) answers[k] = v.trim();
|
|
39304
|
-
}
|
|
39305
|
-
const unknown = labels.filter((l) => !(l in answers));
|
|
39306
|
-
return { ok: true, answers, unknown };
|
|
39307
|
-
} catch (e) {
|
|
39308
|
-
return { ok: false, answers: {}, unknown: labels, error: e instanceof Error ? e.message : String(e) };
|
|
39309
|
-
}
|
|
39310
|
-
}
|
|
39311
|
-
var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, RECORDER_HINTS;
|
|
39312
|
-
var init_client_intake = __esm({
|
|
39313
|
-
"src/client-intake.ts"() {
|
|
39314
|
-
"use strict";
|
|
39315
|
-
init_question_set();
|
|
39316
|
-
init_form_template();
|
|
39317
|
-
INTAKE_FORM_NAME2 = "GHL Command Blueprint \u2014 Client Intake";
|
|
39318
|
-
INTAKE_CONTACT_TAG = "blueprint-intake";
|
|
39319
|
-
RECORDER_HINTS = ["plaud", "otter", "fireflies", "grain", "fathom", "tldv", "granola", "zoom", "read.ai", "readai", "supernormal", "avoma"];
|
|
39320
|
-
}
|
|
39321
|
-
});
|
|
39322
|
-
|
|
39323
39376
|
// src/agency-profile-page.ts
|
|
39324
39377
|
function agencyProfilePageHtml(p, clients = [], seatToken = "") {
|
|
39325
39378
|
const offers = (p.offers ?? []).map((o) => `${o.name}${o.priceFloor ? ` \u2014 ${o.priceFloor}${o.cadence === "monthly" ? "/mo" : ""}` : ""}`).join("\n");
|