@elitedcs/ghl-mcp 3.47.0 → 3.48.1
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 +53 -0
- package/README.md +19 -17
- package/dist/index.js +325 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.48.1 — Docs patch: Firebase capture friction + live pricing copy
|
|
4
|
+
|
|
5
|
+
No behavior changes; text only.
|
|
6
|
+
|
|
7
|
+
- **Chrome "allow pasting" gotcha documented.** `auto_capture_firebase_script`'s steps now
|
|
8
|
+
tell buyers that when Chrome blocks the console paste, typing `allow pasting` in the
|
|
9
|
+
Console unblocks it. This was the single most common silent failure in the capture flow.
|
|
10
|
+
- **`request_license` copy corrected to the live offer.** Was "$97 one-time, no
|
|
11
|
+
subscription" pointing at the old elitedcs.com page; now "$97/mo, every sub-account you
|
|
12
|
+
manage" pointing at ghlcommand.com. The old copy misquoted the price to prospects
|
|
13
|
+
installing from npm.
|
|
14
|
+
- **README: one-paste capture is now the primary Workflow Builder path.** The manual
|
|
15
|
+
7-step IndexedDB walk is demoted to a collapsed fallback for locked-down browsers;
|
|
16
|
+
multi-tenant instructions reference the script instead of the manual steps.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## 3.48.0 — Blueprint: complete workflow-building (opportunities, branching, appointment timers, opt-in publish)
|
|
20
|
+
|
|
21
|
+
Completes the `apply_build_plan` executor so a Blueprint build produces workflows that
|
|
22
|
+
create and move real opportunities, branch on whether a contact has one, fire on
|
|
23
|
+
appointment-relative timers, and (opt-in) go live instead of staging as drafts. Every
|
|
24
|
+
item below was proven live on a throwaway account and read back before shipping.
|
|
25
|
+
|
|
26
|
+
- **Fixed: `create_opportunity` now actually creates an opportunity.** The executor was
|
|
27
|
+
emitting GHL's *update* node (`internal_update_opportunity`), which only updates an
|
|
28
|
+
existing opp and silently no-ops when none exists. It now emits the real
|
|
29
|
+
`internal_create_opportunity` node (pipeline hoisted to the attribute level, stage + name
|
|
30
|
+
in the custom input fields), so a new-lead workflow lands a deal in the pipeline as
|
|
31
|
+
intended.
|
|
32
|
+
- **New: `wait_appointment` action** — appointment-relative reminder timers (e.g. "24 hours
|
|
33
|
+
before the appointment"), emitting GHL's appointment-relative wait node. The workflow
|
|
34
|
+
builder's action-chain validation accepts the appointment-relative `appointmentStartAfter`
|
|
35
|
+
shape alongside the standard time-based `startAfter`.
|
|
36
|
+
- **New: optional `monetaryValue`** on `create_opportunity` / `update_opportunity` so a deal
|
|
37
|
+
carries its value.
|
|
38
|
+
- **New: `find_opportunity` multi-path branching.** The first branching logical action: it
|
|
39
|
+
loads a contact's latest opportunity in a pipeline, then routes to a `found` / `not-found`
|
|
40
|
+
branch (each with its own actions, including waits and sends). This is what makes a
|
|
41
|
+
move-the-opp step inside a branch work — the find loads the opp the move then acts on.
|
|
42
|
+
- **Fixed: backward opportunity moves (win-back / reactivation) silently no-op'd.** GHL's
|
|
43
|
+
"Move opportunity" defaults `allowBackward:false`, which lets a forward stage move through
|
|
44
|
+
but silently refuses a move to an earlier stage — breaking every reactivation build (whose
|
|
45
|
+
whole point is a backward move). The executor now emits `allowBackward:true` so the target
|
|
46
|
+
stage is honored in either direction; forward moves are unaffected.
|
|
47
|
+
- **New: opt-in `publishWorkflows`.** Workflows still build as DRAFT by default (the safe
|
|
48
|
+
default). Passing `publishWorkflows:true` publishes ungated, newly-built workflows live
|
|
49
|
+
(re-syncing their triggers); a workflow gated DRAFT by an unmet handoff is never
|
|
50
|
+
auto-published, and a publish failure never halts or rolls back the build (it surfaces a
|
|
51
|
+
"publish it yourself" note and the build still succeeds).
|
|
52
|
+
- **New: funnel page content template** (`templates/funnel-page-content-template.md`) —
|
|
53
|
+
brand-themeable CSS plus always-on A2P/10DLC opt-in, SEO, and AI-search (GEO) structure
|
|
54
|
+
for the manual funnel-page-content step.
|
|
55
|
+
|
|
3
56
|
## 3.47.0 — Blueprint Cap-0: cross-workflow exit chaining + native trigger builders
|
|
4
57
|
|
|
5
58
|
Two foundational executor fixes so `apply_build_plan` builds workflows that actually
|
package/README.md
CHANGED
|
@@ -154,11 +154,24 @@ Your Location ID (sub-account ID) is in the GHL URL when you'''re inside a sub-a
|
|
|
154
154
|
https://app.gohighlevel.com/v2/location/YOUR_LOCATION_ID/dashboard
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
## Enable Workflow Builder
|
|
157
|
+
## Enable the Workflow Builder
|
|
158
158
|
|
|
159
|
-
The builder + cloner + validator tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow) use GHL'
|
|
159
|
+
The builder + cloner + validator tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow) use GHL's internal API and require Firebase credentials. Without them, the other 163 tools work fine — you just won't have workflow/funnel/form/pipeline editing. This is the flagship feature: do this step.
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
**One-paste capture (the supported path):**
|
|
162
|
+
|
|
163
|
+
1. In Claude, ask: `Run auto_capture_firebase_script`. It returns a small browser-console script.
|
|
164
|
+
2. In Chrome, open a tab logged into your GHL account. Press **F12** (Windows) or **Cmd+Option+I** (Mac), click the **Console** tab, paste the script, press Enter. If Chrome blocks the paste, type `allow pasting` in the Console first, then paste again.
|
|
165
|
+
3. The script extracts the three Firebase values and copies them to your clipboard as JSON.
|
|
166
|
+
4. Back in Claude: `Run enable_workflow_builder` and paste the JSON into the `firebase_paste` field. (First-time setup? You can instead add `firebase_paste` as a fifth field on `setup_ghl_mcp`.)
|
|
167
|
+
5. Restart Claude. The Firebase-gated tools are live.
|
|
168
|
+
|
|
169
|
+
Screenshots and the same script behind a copy button: [elitedcs.com/ghl-mcp-firebase](https://elitedcs.com/ghl-mcp-firebase)
|
|
170
|
+
|
|
171
|
+
> Firebase refresh tokens can rotate. If workflow tools stop working after a few weeks (`health_check` shows Firebase auth: FAIL), re-run `auto_capture_firebase_script` for fresh values and pass them to `enable_workflow_builder` again.
|
|
172
|
+
|
|
173
|
+
<details>
|
|
174
|
+
<summary><strong>Manual fallback</strong> (locked-down browsers that block console pasting or clipboard access)</summary>
|
|
162
175
|
|
|
163
176
|
1. Open `app.gohighlevel.com` in Chrome (or any Chromium browser), log in.
|
|
164
177
|
2. Open DevTools (Cmd+Opt+I on Mac, F12 on Windows) → **Application** tab → **IndexedDB** in the left panel.
|
|
@@ -168,20 +181,9 @@ Grab the three values from your GHL browser session, then re-run `setup_ghl_mcp`
|
|
|
168
181
|
6. Inside that row's value object: `uid` is your **ghl_user_id**.
|
|
169
182
|
7. Inside `value.stsTokenManager.refreshToken` is your **ghl_firebase_refresh_token**.
|
|
170
183
|
|
|
171
|
-
Then in Claude
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
Re-run setup_ghl_mcp with workflow builder:
|
|
175
|
-
email: YOUR_PURCHASE_EMAIL
|
|
176
|
-
license_key: YOUR_LICENSE_KEY
|
|
177
|
-
ghl_api_key: YOUR_GHL_API_KEY
|
|
178
|
-
ghl_location_id: YOUR_LOCATION_ID
|
|
179
|
-
ghl_user_id: FROM_STEP_6
|
|
180
|
-
ghl_firebase_api_key: FROM_STEP_5
|
|
181
|
-
ghl_firebase_refresh_token: FROM_STEP_7
|
|
182
|
-
```
|
|
184
|
+
Then in Claude, run `enable_workflow_builder` (or re-run `setup_ghl_mcp` with your license fields) passing the three values individually as `ghl_user_id`, `ghl_firebase_api_key`, and `ghl_firebase_refresh_token`.
|
|
183
185
|
|
|
184
|
-
>
|
|
186
|
+
</details>
|
|
185
187
|
|
|
186
188
|
## Working in Clients' Accounts (Multi-Tenant)
|
|
187
189
|
|
|
@@ -198,7 +200,7 @@ To unlock full builder access across multiple clients from one install:
|
|
|
198
200
|
```
|
|
199
201
|
This auto-detects and stores the owning **companyId**.
|
|
200
202
|
|
|
201
|
-
2. **Capture the client's Firebase values** from a browser session logged into *their* account (same
|
|
203
|
+
2. **Capture the client's Firebase values** from a browser session logged into *their* account (run `auto_capture_firebase_script` the same way as above, but paste it into a tab logged into the client's GHL).
|
|
202
204
|
|
|
203
205
|
3. **Register the client's company Firebase:**
|
|
204
206
|
```
|
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.48.1",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
36
|
description: "GoHighLevel MCP Server for Claude. 220 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.",
|
|
37
37
|
main: "dist/index.js",
|
|
@@ -1237,7 +1237,9 @@ function validateActionChain(actions, existingIds) {
|
|
|
1237
1237
|
if (!Array.isArray(attr.tags) || !attr.tags.length) throw new Error(`Tag action "${action.name}" missing 'tags' array.`);
|
|
1238
1238
|
break;
|
|
1239
1239
|
case "wait":
|
|
1240
|
-
if (!attr.startAfter
|
|
1240
|
+
if (!attr.startAfter && !attr.appointmentStartAfter) {
|
|
1241
|
+
throw new Error(`Wait action "${action.name}" missing required 'startAfter' (or 'appointmentStartAfter' for an appointment-relative wait) in attributes.`);
|
|
1242
|
+
}
|
|
1241
1243
|
break;
|
|
1242
1244
|
case "internal_update_opportunity": {
|
|
1243
1245
|
const isRoundTripped = hasId(action) && (existingIds ? existingIds.has(action.id) : true);
|
|
@@ -1288,6 +1290,31 @@ function validateActionChain(actions, existingIds) {
|
|
|
1288
1290
|
if (!sibling || sibling.length !== 1) throw new Error(`If/else branch "${action.name}" must have exactly one sibling id.`);
|
|
1289
1291
|
}
|
|
1290
1292
|
}
|
|
1293
|
+
for (const action of actions) {
|
|
1294
|
+
if (action.type !== "find_opportunity") continue;
|
|
1295
|
+
if (existingIds && hasId(action) && existingIds.has(action.id)) continue;
|
|
1296
|
+
if (!action.id) throw new Error(`find_opportunity "${action.name}" missing id.`);
|
|
1297
|
+
const next = getStringArray(action.next);
|
|
1298
|
+
if (!next || next.length !== 2) {
|
|
1299
|
+
throw new Error(`find_opportunity "${action.name}" must have next as exactly [foundTransitionId, notFoundTransitionId].`);
|
|
1300
|
+
}
|
|
1301
|
+
for (const tid of next) {
|
|
1302
|
+
const t = byId.get(tid);
|
|
1303
|
+
if (!t || t.type !== "transition") {
|
|
1304
|
+
throw new Error(`find_opportunity "${action.name}" next must reference its two transition nodes.`);
|
|
1305
|
+
}
|
|
1306
|
+
if (t.parent !== action.id || t.parentKey !== action.id) {
|
|
1307
|
+
throw new Error(`Transition "${t.name}" of find_opportunity "${action.name}" must have parent and parentKey set to the find node id.`);
|
|
1308
|
+
}
|
|
1309
|
+
const firstChildId = typeof t.next === "string" ? t.next : void 0;
|
|
1310
|
+
if (firstChildId) {
|
|
1311
|
+
const c = byId.get(firstChildId);
|
|
1312
|
+
if (!c || c.parent !== tid || c.parentKey !== tid) {
|
|
1313
|
+
throw new Error(`First child of transition "${t.name}" must have parent and parentKey set to the transition id.`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1291
1318
|
}
|
|
1292
1319
|
var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
1293
1320
|
// Active Firebase auth — swapped when operating in another company's GHL.
|
|
@@ -1891,7 +1918,12 @@ ${errorBody}`
|
|
|
1891
1918
|
});
|
|
1892
1919
|
for (let i = 0; i < linked.length; i++) {
|
|
1893
1920
|
const action = linked[i];
|
|
1894
|
-
if (action.
|
|
1921
|
+
if (action.parent || action.nodeType) continue;
|
|
1922
|
+
if (action.cat === "multi-path") {
|
|
1923
|
+
if (i > 0 && !action.parentKey) action.parentKey = linked[i - 1].id;
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (action.cat) continue;
|
|
1895
1927
|
if (i > 0 && !action.parentKey) {
|
|
1896
1928
|
action.parentKey = linked[i - 1].id;
|
|
1897
1929
|
}
|
|
@@ -7235,7 +7267,7 @@ function registerFirebaseCaptureScriptTool(server2) {
|
|
|
7235
7267
|
"",
|
|
7236
7268
|
"**Step 2.** Open DevTools \u2014 press `F12` on Windows / Linux, or `Cmd+Option+I` on Mac. Click the **Console** tab.",
|
|
7237
7269
|
"",
|
|
7238
|
-
"**Step 3.** Copy the script below, paste it into the Console, and press Enter:",
|
|
7270
|
+
"**Step 3.** Copy the script below, paste it into the Console, and press Enter. If Chrome blocks the paste with a warning, type `allow pasting` in the Console first, then paste again:",
|
|
7239
7271
|
"",
|
|
7240
7272
|
"```javascript",
|
|
7241
7273
|
FIREBASE_CAPTURE_SCRIPT,
|
|
@@ -7262,13 +7294,13 @@ function registerFirebaseCaptureScriptTool(server2) {
|
|
|
7262
7294
|
function registerLeadCaptureTool(server2) {
|
|
7263
7295
|
server2.tool(
|
|
7264
7296
|
"request_license",
|
|
7265
|
-
"Get a GHL Command license. Use this if you installed from npm but don't have a license yet (or setup_ghl_mcp says your license is missing/invalid). GHL Command is $97
|
|
7297
|
+
"Get a GHL Command license. Use this if you installed from npm but don't have a license yet (or setup_ghl_mcp says your license is missing/invalid). GHL Command is $97/mo \u2014 every sub-account you manage, 3-machine activation, includes the only programmatic GHL workflow builder. Leave your email and we'll send the purchase link + setup help; the tool also returns where to buy right now.",
|
|
7266
7298
|
{
|
|
7267
7299
|
email: import_zod38.z.string().email().describe("Your email \u2014 where to send the purchase link and setup help."),
|
|
7268
7300
|
name: import_zod38.z.string().optional().describe("Your name (optional).")
|
|
7269
7301
|
},
|
|
7270
7302
|
async (args) => {
|
|
7271
|
-
const buyUrl = "https://
|
|
7303
|
+
const buyUrl = "https://ghlcommand.com";
|
|
7272
7304
|
try {
|
|
7273
7305
|
const res = await fetch(CAPTURE_API, {
|
|
7274
7306
|
method: "POST",
|
|
@@ -7283,7 +7315,7 @@ function registerLeadCaptureTool(server2) {
|
|
|
7283
7315
|
text: [
|
|
7284
7316
|
data.message || "You're on the list.",
|
|
7285
7317
|
"",
|
|
7286
|
-
"GHL Command is $97
|
|
7318
|
+
"GHL Command is $97/mo: every sub-account you manage, 3 machines, including the only programmatic GHL workflow builder. Cancel anytime.",
|
|
7287
7319
|
"",
|
|
7288
7320
|
`Buy now: ${url}`,
|
|
7289
7321
|
"Your license key arrives by email within a couple minutes. Then run setup_ghl_mcp with your email + key."
|
|
@@ -7294,7 +7326,7 @@ function registerLeadCaptureTool(server2) {
|
|
|
7294
7326
|
return {
|
|
7295
7327
|
content: [{
|
|
7296
7328
|
type: "text",
|
|
7297
|
-
text: `Buy your GHL Command license ($97
|
|
7329
|
+
text: `Buy your GHL Command license ($97/mo) at ${buyUrl} \u2014 your key arrives by email within a couple minutes, then run setup_ghl_mcp with your email + key.`
|
|
7298
7330
|
}]
|
|
7299
7331
|
};
|
|
7300
7332
|
}
|
|
@@ -11116,12 +11148,25 @@ var smsAssetSchema = import_zod53.z.object({
|
|
|
11116
11148
|
mergeTags: import_zod53.z.array(import_zod53.z.string()).optional()
|
|
11117
11149
|
});
|
|
11118
11150
|
var waitUnit = import_zod53.z.enum(["minutes", "hours", "days"]);
|
|
11119
|
-
var
|
|
11151
|
+
var branchActionOptions = [
|
|
11120
11152
|
import_zod53.z.object({ type: import_zod53.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
|
|
11121
11153
|
import_zod53.z.object({ type: import_zod53.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
|
|
11122
11154
|
import_zod53.z.object({ type: import_zod53.z.literal("send_email"), emailRef: nsRef("email") }),
|
|
11123
11155
|
import_zod53.z.object({ type: import_zod53.z.literal("send_sms"), smsRef: nsRef("sms") }),
|
|
11124
11156
|
import_zod53.z.object({ type: import_zod53.z.literal("wait"), value: import_zod53.z.number().positive(), unit: waitUnit }),
|
|
11157
|
+
// Appointment-relative wait ("wait until N BEFORE the appointment").
|
|
11158
|
+
// Only works when the workflow has an appointment in context (i.e. an
|
|
11159
|
+
// `appointment` trigger) — enforced by validateBuildPlan. Expands to GHL's
|
|
11160
|
+
// appointment-wait node shape (attributes.type "appointment" +
|
|
11161
|
+
// appointmentStartAfter, captured live from Lux Bio). Integer units only
|
|
11162
|
+
// (GHL stores whole minutes). Only "before" is emitted today — that's the
|
|
11163
|
+
// shape we captured + proved; "after" (post-appointment follow-up) is
|
|
11164
|
+
// deferred until its shape is captured from a real workflow.
|
|
11165
|
+
import_zod53.z.object({
|
|
11166
|
+
type: import_zod53.z.literal("wait_appointment"),
|
|
11167
|
+
value: import_zod53.z.number().int().positive(),
|
|
11168
|
+
unit: waitUnit
|
|
11169
|
+
}),
|
|
11125
11170
|
import_zod53.z.object({
|
|
11126
11171
|
type: import_zod53.z.literal("internal_notification"),
|
|
11127
11172
|
to: import_zod53.z.string(),
|
|
@@ -11147,12 +11192,22 @@ var actionSchema = import_zod53.z.discriminatedUnion("type", [
|
|
|
11147
11192
|
type: import_zod53.z.literal("create_opportunity"),
|
|
11148
11193
|
pipelineRef: nsRef("pipeline"),
|
|
11149
11194
|
stageRef: nsRef("stage"),
|
|
11150
|
-
|
|
11195
|
+
// Opportunity name (merge fields allowed). Defaults to the contact's name.
|
|
11196
|
+
// Required by GHL's create node; without it the create silently no-ops.
|
|
11197
|
+
name: import_zod53.z.string().optional(),
|
|
11198
|
+
// Opportunity monetary value (the deal/sale dollar amount). A string so it
|
|
11199
|
+
// can be a literal ("2500") OR a merge field ("{{contact.package_value}}").
|
|
11200
|
+
// Optional — omitted → GHL leaves the value unset. Shape captured from Lux
|
|
11201
|
+
// Bio "14. Package Sale". Lux models the lifecycle by pipeline STAGE, not GHL
|
|
11202
|
+
// won/lost status, so a "won" opp = move to the closing stage WITH this value.
|
|
11203
|
+
value: import_zod53.z.string().optional()
|
|
11151
11204
|
}),
|
|
11152
11205
|
import_zod53.z.object({
|
|
11153
11206
|
type: import_zod53.z.literal("update_opportunity"),
|
|
11154
11207
|
pipelineRef: nsRef("pipeline"),
|
|
11155
|
-
stageRef: nsRef("stage")
|
|
11208
|
+
stageRef: nsRef("stage"),
|
|
11209
|
+
// Opportunity monetary value (see create_opportunity.value). Optional.
|
|
11210
|
+
value: import_zod53.z.string().optional()
|
|
11156
11211
|
}),
|
|
11157
11212
|
import_zod53.z.object({
|
|
11158
11213
|
type: import_zod53.z.literal("goal_event"),
|
|
@@ -11160,7 +11215,15 @@ var actionSchema = import_zod53.z.discriminatedUnion("type", [
|
|
|
11160
11215
|
// GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
|
|
11161
11216
|
action: import_zod53.z.enum(["exit", "continue", "wait"]).optional()
|
|
11162
11217
|
})
|
|
11163
|
-
]
|
|
11218
|
+
];
|
|
11219
|
+
var branchActionSchema = import_zod53.z.discriminatedUnion("type", branchActionOptions);
|
|
11220
|
+
var findOpportunitySchema = import_zod53.z.object({
|
|
11221
|
+
type: import_zod53.z.literal("find_opportunity"),
|
|
11222
|
+
pipelineRef: nsRef("pipeline"),
|
|
11223
|
+
found: import_zod53.z.array(branchActionSchema).default([]),
|
|
11224
|
+
notFound: import_zod53.z.array(branchActionSchema).default([])
|
|
11225
|
+
});
|
|
11226
|
+
var actionSchema = import_zod53.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
|
|
11164
11227
|
var APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
|
|
11165
11228
|
var triggerSchema = import_zod53.z.object({
|
|
11166
11229
|
type: import_zod53.z.string(),
|
|
@@ -11293,8 +11356,7 @@ function checkRefIntegrity(plan, defined) {
|
|
|
11293
11356
|
check(t.pipelineRef, "pipeline", `workflows[${w.ref}].trigger.pipelineRef`);
|
|
11294
11357
|
check(t.stageRef, "stage", `workflows[${w.ref}].trigger.stageRef`);
|
|
11295
11358
|
}
|
|
11296
|
-
|
|
11297
|
-
const where = `workflows[${w.ref}].actions[${i}](${a.type})`;
|
|
11359
|
+
const checkActionRefs = (a, where) => {
|
|
11298
11360
|
if ("tagRef" in a) check(a.tagRef, "tag", where);
|
|
11299
11361
|
if ("emailRef" in a) check(a.emailRef, "email", where);
|
|
11300
11362
|
if ("smsRef" in a) check(a.smsRef, "sms", where);
|
|
@@ -11302,6 +11364,17 @@ function checkRefIntegrity(plan, defined) {
|
|
|
11302
11364
|
if ("pipelineRef" in a) check(a.pipelineRef, "pipeline", where);
|
|
11303
11365
|
if ("stageRef" in a) check(a.stageRef, "stage", where);
|
|
11304
11366
|
if ("workflowRef" in a) check(a.workflowRef, "workflow", where);
|
|
11367
|
+
};
|
|
11368
|
+
w.actions.forEach((a, i) => {
|
|
11369
|
+
const where = `workflows[${w.ref}].actions[${i}](${a.type})`;
|
|
11370
|
+
checkActionRefs(a, where);
|
|
11371
|
+
if (a.type === "find_opportunity") {
|
|
11372
|
+
if (i !== w.actions.length - 1) {
|
|
11373
|
+
errors.push(`${where}: find_opportunity must be the last action in the workflow (its Found/Not-Found branches do not rejoin a linear tail).`);
|
|
11374
|
+
}
|
|
11375
|
+
a.found.forEach((c, ci) => checkActionRefs(c, `${where}.found[${ci}](${c.type})`));
|
|
11376
|
+
a.notFound.forEach((c, ci) => checkActionRefs(c, `${where}.notFound[${ci}](${c.type})`));
|
|
11377
|
+
}
|
|
11305
11378
|
});
|
|
11306
11379
|
}
|
|
11307
11380
|
for (const h of plan.handoffs ?? []) {
|
|
@@ -11433,6 +11506,32 @@ function validateBuildPlan(input) {
|
|
|
11433
11506
|
);
|
|
11434
11507
|
}
|
|
11435
11508
|
}
|
|
11509
|
+
for (const wf of plan.workflows ?? []) {
|
|
11510
|
+
const hasApptWait = wf.actions.some((a) => a.type === "wait_appointment");
|
|
11511
|
+
if (hasApptWait && wf.trigger?.type !== "appointment") {
|
|
11512
|
+
warnings.push(
|
|
11513
|
+
`workflow "${wf.ref}" uses a wait_appointment ("before appointment" timer) but its trigger is ${wf.trigger?.type ? `"${wf.trigger.type}"` : "not set to an appointment trigger"} \u2014 that timer needs an appointment in context (an "appointment" trigger), or GHL silently mis-times it`
|
|
11514
|
+
);
|
|
11515
|
+
}
|
|
11516
|
+
}
|
|
11517
|
+
const MAX_NODES = 40;
|
|
11518
|
+
for (const wf of plan.workflows ?? []) {
|
|
11519
|
+
let nodeCount = 0;
|
|
11520
|
+
let hasBranch = false;
|
|
11521
|
+
for (const a of wf.actions) {
|
|
11522
|
+
if (a.type === "find_opportunity") {
|
|
11523
|
+
hasBranch = true;
|
|
11524
|
+
nodeCount += 3 + a.found.length + a.notFound.length;
|
|
11525
|
+
} else {
|
|
11526
|
+
nodeCount += 1;
|
|
11527
|
+
}
|
|
11528
|
+
}
|
|
11529
|
+
if (hasBranch && nodeCount > MAX_NODES) {
|
|
11530
|
+
allErrors.push(
|
|
11531
|
+
`workflow "${wf.ref}" expands to ${nodeCount} nodes (> ${MAX_NODES}); a workflow with a find_opportunity branch cannot be auto-split \u2014 move actions into separate workflows.`
|
|
11532
|
+
);
|
|
11533
|
+
}
|
|
11534
|
+
}
|
|
11436
11535
|
return {
|
|
11437
11536
|
valid: allErrors.length === 0,
|
|
11438
11537
|
errors: allErrors,
|
|
@@ -11526,6 +11625,15 @@ function htmlWrap(text) {
|
|
|
11526
11625
|
if (/^\s*<[a-z]/i.test(t)) return t;
|
|
11527
11626
|
return `<p style="margin:0px;">${t}</p>`;
|
|
11528
11627
|
}
|
|
11628
|
+
function monetaryValueField(value) {
|
|
11629
|
+
return {
|
|
11630
|
+
__customInputs__: { value: "numerical" },
|
|
11631
|
+
filterField: "monetaryValue",
|
|
11632
|
+
valueFieldType: "custom-input",
|
|
11633
|
+
value,
|
|
11634
|
+
dataType: "NUMERICAL"
|
|
11635
|
+
};
|
|
11636
|
+
}
|
|
11529
11637
|
function resolveId(ref, idMap) {
|
|
11530
11638
|
const real = idMap.get(ref);
|
|
11531
11639
|
return real ?? PENDING(ref);
|
|
@@ -11608,6 +11716,41 @@ function expandAction(action, idx, idMap) {
|
|
|
11608
11716
|
}
|
|
11609
11717
|
};
|
|
11610
11718
|
}
|
|
11719
|
+
case "wait_appointment": {
|
|
11720
|
+
const totalMinutes = action.value * (action.unit === "days" ? 1440 : action.unit === "hours" ? 60 : 1);
|
|
11721
|
+
const distributed = {
|
|
11722
|
+
months: 0,
|
|
11723
|
+
days: Math.floor(totalMinutes / 1440),
|
|
11724
|
+
hours: Math.floor(totalMinutes % 1440 / 60),
|
|
11725
|
+
minutes: totalMinutes % 60
|
|
11726
|
+
};
|
|
11727
|
+
const label = `Wait until ${action.value} ${action.unit} before appointment`;
|
|
11728
|
+
return {
|
|
11729
|
+
kind: "expanded",
|
|
11730
|
+
pendingRefs: [],
|
|
11731
|
+
native: {
|
|
11732
|
+
type: "wait",
|
|
11733
|
+
name: label,
|
|
11734
|
+
attributes: {
|
|
11735
|
+
type: "appointment",
|
|
11736
|
+
name: label,
|
|
11737
|
+
cat: "",
|
|
11738
|
+
isHybridAction: true,
|
|
11739
|
+
hybridActionType: "wait",
|
|
11740
|
+
convertToMultipath: false,
|
|
11741
|
+
transitions: [],
|
|
11742
|
+
appointmentStartAfter: {
|
|
11743
|
+
// Only "before" is emitted — the captured/proven shape. See plan.ts.
|
|
11744
|
+
when: "before",
|
|
11745
|
+
type: "minutes",
|
|
11746
|
+
value: totalMinutes,
|
|
11747
|
+
distributed
|
|
11748
|
+
},
|
|
11749
|
+
appointmentCondition: "skip"
|
|
11750
|
+
}
|
|
11751
|
+
}
|
|
11752
|
+
};
|
|
11753
|
+
}
|
|
11611
11754
|
case "internal_notification": {
|
|
11612
11755
|
const looksLikeUserId = /^[A-Za-z0-9]{17,}$/.test(action.to);
|
|
11613
11756
|
return {
|
|
@@ -11698,7 +11841,40 @@ function expandAction(action, idx, idMap) {
|
|
|
11698
11841
|
}
|
|
11699
11842
|
};
|
|
11700
11843
|
}
|
|
11701
|
-
case "create_opportunity":
|
|
11844
|
+
case "create_opportunity": {
|
|
11845
|
+
const pipelineId = resolveId(action.pipelineRef, idMap);
|
|
11846
|
+
const stageId = resolveId(action.stageRef, idMap);
|
|
11847
|
+
const pendingRefs = [];
|
|
11848
|
+
if (isPending(pipelineId)) pendingRefs.push(action.pipelineRef);
|
|
11849
|
+
if (isPending(stageId)) pendingRefs.push(action.stageRef);
|
|
11850
|
+
const pName = idx.pipelineName.get(action.pipelineRef) ?? action.pipelineRef;
|
|
11851
|
+
const sName = idx.stageName.get(action.stageRef) ?? action.stageRef;
|
|
11852
|
+
const oppName = action.name ?? "{{contact.first_name}} {{contact.last_name}}";
|
|
11853
|
+
return {
|
|
11854
|
+
kind: "expanded",
|
|
11855
|
+
pendingRefs,
|
|
11856
|
+
native: {
|
|
11857
|
+
type: "internal_create_opportunity",
|
|
11858
|
+
name: `Create opportunity: ${pName} / ${sName}`,
|
|
11859
|
+
workflowsActionType: "INTERNAL",
|
|
11860
|
+
attributes: {
|
|
11861
|
+
type: "internal_create_opportunity",
|
|
11862
|
+
pipelineId,
|
|
11863
|
+
// The save-time normalizer only scaffolds internal_update_opportunity, so this
|
|
11864
|
+
// create node carries GHL's full shape itself (attribute-level + per-field
|
|
11865
|
+
// __customInputs__), matching the captured live node exactly.
|
|
11866
|
+
__customInputs__: {},
|
|
11867
|
+
__customInputFields__: [
|
|
11868
|
+
{ __customInputs__: {}, filterField: "pipelineStageId", value: stageId, valueFieldType: "select", dataType: "SINGLE_OPTIONS" },
|
|
11869
|
+
{ __customInputs__: {}, filterField: "name", value: oppName, valueFieldType: "string", dataType: "TEXT" },
|
|
11870
|
+
// Optional opportunity dollar value (Lux "14. Package Sale" shape). Only
|
|
11871
|
+
// emitted when the plan supplies a value; omitted → GHL leaves it unset.
|
|
11872
|
+
...action.value ? [monetaryValueField(action.value)] : []
|
|
11873
|
+
]
|
|
11874
|
+
}
|
|
11875
|
+
}
|
|
11876
|
+
};
|
|
11877
|
+
}
|
|
11702
11878
|
case "update_opportunity": {
|
|
11703
11879
|
const pipelineId = resolveId(action.pipelineRef, idMap);
|
|
11704
11880
|
const stageId = resolveId(action.stageRef, idMap);
|
|
@@ -11707,26 +11883,119 @@ function expandAction(action, idx, idMap) {
|
|
|
11707
11883
|
if (isPending(stageId)) pendingRefs.push(action.stageRef);
|
|
11708
11884
|
const pName = idx.pipelineName.get(action.pipelineRef) ?? action.pipelineRef;
|
|
11709
11885
|
const sName = idx.stageName.get(action.stageRef) ?? action.stageRef;
|
|
11710
|
-
const verb = action.type === "create_opportunity" ? "Create opportunity" : "Move opportunity";
|
|
11711
11886
|
return {
|
|
11712
11887
|
kind: "expanded",
|
|
11713
11888
|
pendingRefs,
|
|
11714
11889
|
native: {
|
|
11715
11890
|
type: "internal_update_opportunity",
|
|
11716
|
-
name:
|
|
11717
|
-
// NODE-level discriminator (the normalizer also forces this; included so
|
|
11718
|
-
// the expanded action is correct even outside the save path).
|
|
11891
|
+
name: `Move opportunity: ${pName} / ${sName}`,
|
|
11719
11892
|
workflowsActionType: "INTERNAL",
|
|
11720
11893
|
attributes: {
|
|
11721
11894
|
type: "internal_update_opportunity",
|
|
11895
|
+
// Honor the operator's explicit target stage in EITHER direction. GHL's
|
|
11896
|
+
// "Move opportunity" action defaults allowBackward:false, which SILENTLY
|
|
11897
|
+
// refuses to move an opp to an EARLIER pipeline stage (lower position) —
|
|
11898
|
+
// breaking win-back / reactivation moves whose whole point is to move a
|
|
11899
|
+
// deal BACKWARD (e.g. Cancelled → Active). The Blueprint intent of an
|
|
11900
|
+
// update_opportunity action is "put this opp in this stage" regardless of
|
|
11901
|
+
// direction, so allow backward moves. (Live-caught on Cap-3 wf7 2026-06-24:
|
|
11902
|
+
// the identical node moved a deal FORWARD fine in wf6 but silently no-op'd
|
|
11903
|
+
// moving it BACKWARD here.) The save-time normalizer preserves an explicit
|
|
11904
|
+
// boolean, so this is honored end-to-end.
|
|
11905
|
+
allowBackward: true,
|
|
11722
11906
|
__customInputFields__: [
|
|
11723
11907
|
{ filterField: "pipelineId", value: pipelineId, valueFieldType: "select", dataType: "SINGLE_OPTIONS" },
|
|
11724
|
-
{ filterField: "pipelineStageId", value: stageId, valueFieldType: "select", dataType: "SINGLE_OPTIONS" }
|
|
11908
|
+
{ filterField: "pipelineStageId", value: stageId, valueFieldType: "select", dataType: "SINGLE_OPTIONS" },
|
|
11909
|
+
// Optional opportunity dollar value (Lux "14. Package Sale" shape). Only
|
|
11910
|
+
// emitted when the plan supplies a value; omitted → GHL leaves it unchanged.
|
|
11911
|
+
...action.value ? [monetaryValueField(action.value)] : []
|
|
11725
11912
|
]
|
|
11726
11913
|
}
|
|
11727
11914
|
}
|
|
11728
11915
|
};
|
|
11729
11916
|
}
|
|
11917
|
+
case "find_opportunity": {
|
|
11918
|
+
const pipelineId = resolveId(action.pipelineRef, idMap);
|
|
11919
|
+
const pendingRefs = [];
|
|
11920
|
+
if (isPending(pipelineId)) pendingRefs.push(action.pipelineRef);
|
|
11921
|
+
for (const child of [...action.found, ...action.notFound]) {
|
|
11922
|
+
const probe = expandAction(child, idx, idMap);
|
|
11923
|
+
if (probe.kind === "needs_content")
|
|
11924
|
+
return { kind: "needs_content", logicalType: `find_opportunity \u2192 ${child.type}`, reason: probe.reason };
|
|
11925
|
+
if (probe.kind === "manual")
|
|
11926
|
+
return { kind: "manual", logicalType: `find_opportunity \u2192 ${child.type}`, reason: probe.reason };
|
|
11927
|
+
}
|
|
11928
|
+
const findId = crypto.randomUUID();
|
|
11929
|
+
const foundTId = crypto.randomUUID();
|
|
11930
|
+
const notFoundTId = crypto.randomUUID();
|
|
11931
|
+
const wireBranch = (children, transitionId) => {
|
|
11932
|
+
const out = [];
|
|
11933
|
+
children.forEach((child, ci) => {
|
|
11934
|
+
const exp = expandAction(child, idx, idMap);
|
|
11935
|
+
if (exp.kind !== "expanded") return;
|
|
11936
|
+
exp.pendingRefs.forEach((r) => pendingRefs.push(r));
|
|
11937
|
+
const node = exp.native;
|
|
11938
|
+
node.id = crypto.randomUUID();
|
|
11939
|
+
node.parent = transitionId;
|
|
11940
|
+
node.parentKey = ci === 0 ? transitionId : out[ci - 1].id;
|
|
11941
|
+
node.order = ci;
|
|
11942
|
+
delete node.next;
|
|
11943
|
+
out.push(node);
|
|
11944
|
+
});
|
|
11945
|
+
for (let k = 0; k < out.length - 1; k++) out[k].next = out[k + 1].id;
|
|
11946
|
+
return out;
|
|
11947
|
+
};
|
|
11948
|
+
const foundNodes = wireBranch(action.found, foundTId);
|
|
11949
|
+
const notFoundNodes = wireBranch(action.notFound, notFoundTId);
|
|
11950
|
+
const makeTransition = (id, name, firstChildId) => ({
|
|
11951
|
+
id,
|
|
11952
|
+
type: "transition",
|
|
11953
|
+
name,
|
|
11954
|
+
cat: "transition",
|
|
11955
|
+
parent: findId,
|
|
11956
|
+
parentKey: findId,
|
|
11957
|
+
attributes: {},
|
|
11958
|
+
...firstChildId ? { next: firstChildId } : {}
|
|
11959
|
+
});
|
|
11960
|
+
const pName = idx.pipelineName.get(action.pipelineRef) ?? action.pipelineRef;
|
|
11961
|
+
const findNode = {
|
|
11962
|
+
id: findId,
|
|
11963
|
+
type: "find_opportunity",
|
|
11964
|
+
name: `Find opportunity: ${pName}`,
|
|
11965
|
+
cat: "multi-path",
|
|
11966
|
+
workflowsActionType: "INTERNAL",
|
|
11967
|
+
next: [foundTId, notFoundTId],
|
|
11968
|
+
attributes: {
|
|
11969
|
+
sorting: "latest",
|
|
11970
|
+
type: "find_opportunity",
|
|
11971
|
+
// Filter shape is DISTINCT from the update/create node: the field name is
|
|
11972
|
+
// `pipeline_id` (underscore), the operator lives in `value`, the id in
|
|
11973
|
+
// `secondValue` (captured from Lux "01"/"02").
|
|
11974
|
+
__customInputFields__: [
|
|
11975
|
+
{ __customInputs__: {}, filterField: "pipeline_id", value: "eq", secondValue: pipelineId }
|
|
11976
|
+
],
|
|
11977
|
+
__customInputs__: {},
|
|
11978
|
+
cat: "multi-path",
|
|
11979
|
+
convertToMultipath: true,
|
|
11980
|
+
transitions: [
|
|
11981
|
+
{ id: foundTId, name: "Opportunity Found", fields: [], meta: { __branchKey__: "predefined_Opportunity Found" }, conditionType: "pre-defined" },
|
|
11982
|
+
{ id: notFoundTId, name: "Opportunity Not Found", fields: [], meta: { __branchKey__: "predefined_Opportunity Not Found" }, conditionType: "pre-defined" }
|
|
11983
|
+
],
|
|
11984
|
+
__name__: "Find Opportunity"
|
|
11985
|
+
}
|
|
11986
|
+
};
|
|
11987
|
+
return {
|
|
11988
|
+
kind: "expanded",
|
|
11989
|
+
pendingRefs,
|
|
11990
|
+
native: findNode,
|
|
11991
|
+
extraNodes: [
|
|
11992
|
+
makeTransition(foundTId, "Opportunity Found", foundNodes[0]?.id),
|
|
11993
|
+
...foundNodes,
|
|
11994
|
+
makeTransition(notFoundTId, "Opportunity Not Found", notFoundNodes[0]?.id),
|
|
11995
|
+
...notFoundNodes
|
|
11996
|
+
]
|
|
11997
|
+
};
|
|
11998
|
+
}
|
|
11730
11999
|
case "goal_event": {
|
|
11731
12000
|
return {
|
|
11732
12001
|
kind: "expanded",
|
|
@@ -11838,16 +12107,30 @@ function expandWorkflow(workflow, idx, idMap, gatedBy) {
|
|
|
11838
12107
|
const manual = [];
|
|
11839
12108
|
const needsContent = [];
|
|
11840
12109
|
const pendingRefs = /* @__PURE__ */ new Set();
|
|
12110
|
+
let branchEmitted = false;
|
|
11841
12111
|
workflow.actions.forEach((a, i) => {
|
|
12112
|
+
if (branchEmitted) {
|
|
12113
|
+
manual.push({ index: i, logicalType: a.type, reason: "action follows a find_opportunity branch (which must be the last action) \u2014 not emitted to avoid an orphaned node" });
|
|
12114
|
+
return;
|
|
12115
|
+
}
|
|
11842
12116
|
const exp = expandAction(a, idx, idMap);
|
|
11843
12117
|
if (exp.kind === "expanded") {
|
|
11844
12118
|
nativeActions.push(exp.native);
|
|
11845
12119
|
exp.pendingRefs.forEach((r) => pendingRefs.add(r));
|
|
12120
|
+
if (exp.extraNodes && exp.extraNodes.length) {
|
|
12121
|
+
const baseOrder = nativeActions.length - 1;
|
|
12122
|
+
exp.native.order = baseOrder;
|
|
12123
|
+
for (const n of exp.extraNodes) {
|
|
12124
|
+
if (n.cat === "transition") n.order = baseOrder + 1;
|
|
12125
|
+
nativeActions.push(n);
|
|
12126
|
+
}
|
|
12127
|
+
}
|
|
11846
12128
|
} else if (exp.kind === "manual") {
|
|
11847
12129
|
manual.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
|
|
11848
12130
|
} else {
|
|
11849
12131
|
needsContent.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
|
|
11850
12132
|
}
|
|
12133
|
+
if (a.type === "find_opportunity") branchEmitted = true;
|
|
11851
12134
|
});
|
|
11852
12135
|
const splitInto = Math.max(1, Math.ceil(nativeActions.length / MAX_ACTIONS_PER_WORKFLOW));
|
|
11853
12136
|
let nativeTrigger;
|
|
@@ -12137,11 +12420,13 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
12137
12420
|
const idMap = {};
|
|
12138
12421
|
const built = [];
|
|
12139
12422
|
const manual = [];
|
|
12423
|
+
const published = [];
|
|
12140
12424
|
const halt = (atRef, type, reason) => ({
|
|
12141
12425
|
ok: false,
|
|
12142
12426
|
idMap,
|
|
12143
12427
|
built,
|
|
12144
12428
|
manual,
|
|
12429
|
+
published,
|
|
12145
12430
|
halted: { atRef, type, reason },
|
|
12146
12431
|
deferred: deferredSections(plan)
|
|
12147
12432
|
});
|
|
@@ -12522,11 +12807,19 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
12522
12807
|
}
|
|
12523
12808
|
savedWfIds.add(wfId);
|
|
12524
12809
|
built.push({ ref: wf.ref, type: "workflow", name: wf.name, status: "created", realId: wfId });
|
|
12810
|
+
if (opts.publishWorkflows && !opts.gatedWorkflowRefs?.has(wf.ref)) {
|
|
12811
|
+
try {
|
|
12812
|
+
await deps.publishWorkflow(wfId);
|
|
12813
|
+
published.push(wf.ref);
|
|
12814
|
+
} catch (e) {
|
|
12815
|
+
manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: `[${wf.name}] built successfully but auto-publish failed (${msg(e)}) \u2014 publish it manually in GHL.` });
|
|
12816
|
+
}
|
|
12817
|
+
}
|
|
12525
12818
|
if (expansion.triggerManual) manual.push({ ref: wf.ref, type: "workflow-trigger", name: wf.name, reason: `[${wf.name}] ${expansion.triggerManual}` });
|
|
12526
12819
|
for (const nc of expansion.needsContent) manual.push({ ref: wf.ref, type: "workflow-action", name: wf.name, reason: `[${wf.name}] ${nc.reason}` });
|
|
12527
12820
|
for (const m of expansion.manual) manual.push({ ref: wf.ref, type: "workflow-action", name: wf.name, reason: `[${wf.name}] ${m.reason}` });
|
|
12528
12821
|
}
|
|
12529
|
-
return { ok: true, idMap, built, manual, deferred: deferredSections(plan) };
|
|
12822
|
+
return { ok: true, idMap, built, manual, published, deferred: deferredSections(plan) };
|
|
12530
12823
|
}
|
|
12531
12824
|
async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
|
|
12532
12825
|
if (objects.length === 0) return null;
|
|
@@ -12940,6 +13233,9 @@ ${text2.slice(0, 300)}`);
|
|
|
12940
13233
|
},
|
|
12941
13234
|
deleteWorkflow: async (workflowId) => {
|
|
12942
13235
|
await builderClient.deleteWorkflow(workflowId);
|
|
13236
|
+
},
|
|
13237
|
+
publishWorkflow: async (workflowId) => {
|
|
13238
|
+
await builderClient.publishWorkflow(workflowId);
|
|
12943
13239
|
}
|
|
12944
13240
|
};
|
|
12945
13241
|
}
|
|
@@ -13072,7 +13368,11 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13072
13368
|
}
|
|
13073
13369
|
}
|
|
13074
13370
|
const deps = makeExecuteDeps(client, builderClient, activeLocation);
|
|
13075
|
-
const
|
|
13371
|
+
const gatedWorkflowRefs = new Set(result.workflows.filter((w) => w.gatedBy.length > 0).map((w) => w.ref));
|
|
13372
|
+
const exec = await executeBackbone(typedPlan, deps, {
|
|
13373
|
+
publishWorkflows: publishWorkflows ?? false,
|
|
13374
|
+
gatedWorkflowRefs
|
|
13375
|
+
});
|
|
13076
13376
|
const externalWiring = buildExternalWiring(typedPlan, exec.idMap, activeLocation);
|
|
13077
13377
|
const execManualLines = exec.manual.map((m) => `[${m.type}] ${m.reason}`);
|
|
13078
13378
|
const manualLines = result.workflows.flatMap((w) => [
|
|
@@ -13091,12 +13391,14 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13091
13391
|
built: exec.built,
|
|
13092
13392
|
manual: exec.manual,
|
|
13093
13393
|
idMap: exec.idMap,
|
|
13394
|
+
published: exec.published,
|
|
13395
|
+
publishNote: exec.published.length ? `Auto-published ${exec.published.length} workflow(s) live (you opted in). Any gated workflow stays DRAFT until its handoff is met.` : exec.built.some((b) => b.type === "workflow" && b.status === "created") ? "Workflows were built as DRAFT (the safe default). Review them in GHL and publish, or re-run with publishWorkflows:true to auto-publish ungated ones." : "No workflows were built in this run (none in the plan, or all already existed and were left untouched).",
|
|
13094
13396
|
externalWiring,
|
|
13095
13397
|
externalWiringNote: externalWiring ? "For each target:\"external\" funnel: plug these into your self-hosted site form + lead bridge (templates/external-funnel/). custom formFields carry the VERIFIED GHL field id your form's `custom` object must send (never name-guess keys); add a triggerTag to start the speed-to-lead workflow; bookingUrl is the GHL calendar widget for the CTA. Anything in `unresolved` wasn't built yet \u2014 re-run after it is." : void 0,
|
|
13096
13398
|
deferred: exec.deferred,
|
|
13097
13399
|
deferredNote: 'execute builds the WHOLE plan live: CRM backbone (pipelines, custom fields, tags, custom values), calendars, forms, funnels (GHL-built = funnel + named steps), and workflows (DRAFT, with all steps incl. opportunity create/move). Remaining manual steps are surfaced per item: staff-requiring calendars in multi-user accounts, GHL funnel page content/design, EXTERNAL funnels (target:"external" \u2014 the user builds + hosts the site themselves and wires it back; not built here), workflow triggers other than contact_tag, and publishing the DRAFT workflows.',
|
|
13098
13400
|
nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
|
|
13099
|
-
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.manual.length ? ` ${exec.manual.length} item(s) need a manual step (see nextManualSteps).` : ""}${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.`
|
|
13401
|
+
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.published.length ? ` Published ${exec.published.length} workflow(s) live.` : exec.built.some((b) => b.type === "workflow" && b.status === "created") ? " Workflows are DRAFT (opt in with publishWorkflows to auto-publish)." : ""}${exec.manual.length ? ` ${exec.manual.length} item(s) need a manual step (see nextManualSteps).` : ""}${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.`
|
|
13100
13402
|
});
|
|
13101
13403
|
}
|
|
13102
13404
|
const collisions = result.items.filter((i) => i.status === "existing");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.48.1",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
5
|
"description": "GoHighLevel MCP Server for Claude. 220 tools — 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.",
|
|
6
6
|
"main": "dist/index.js",
|