@elitedcs/ghl-mcp 3.73.1 → 3.74.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 +31 -0
- package/dist/index.js +1235 -625
- package/guide/guide.html +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -114,8 +114,8 @@ var init_ghl_client = __esm({
|
|
|
114
114
|
Version: version || GHL_API_VERSION
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
buildUrl(
|
|
118
|
-
const url = new URL(
|
|
117
|
+
buildUrl(path27, params) {
|
|
118
|
+
const url = new URL(path27, GHL_BASE_URL);
|
|
119
119
|
if (params) {
|
|
120
120
|
for (const [key, value] of Object.entries(params)) {
|
|
121
121
|
if (value !== void 0 && value !== null) {
|
|
@@ -125,8 +125,8 @@ var init_ghl_client = __esm({
|
|
|
125
125
|
}
|
|
126
126
|
return url.toString();
|
|
127
127
|
}
|
|
128
|
-
async request(method,
|
|
129
|
-
const url = this.buildUrl(
|
|
128
|
+
async request(method, path27, options = {}, attempt = 0) {
|
|
129
|
+
const url = this.buildUrl(path27, options.params);
|
|
130
130
|
const headers = this.buildHeaders(options.version);
|
|
131
131
|
const fetchOptions = {
|
|
132
132
|
method,
|
|
@@ -144,14 +144,14 @@ var init_ghl_client = __esm({
|
|
|
144
144
|
} catch (error) {
|
|
145
145
|
clearTimeout(timeout);
|
|
146
146
|
if (error instanceof Error && error.name === "AbortError") {
|
|
147
|
-
throw new Error(`Request timeout (30s): ${method} ${
|
|
147
|
+
throw new Error(`Request timeout (30s): ${method} ${path27}`);
|
|
148
148
|
}
|
|
149
149
|
if (!options.noRetry && attempt < MAX_RETRIES) {
|
|
150
150
|
const delay5 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
|
|
151
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${
|
|
151
|
+
process.stderr.write(`[ghl-mcp] Network error on ${method} ${path27}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
|
|
152
152
|
`);
|
|
153
153
|
await new Promise((r) => setTimeout(r, delay5));
|
|
154
|
-
return this.request(method,
|
|
154
|
+
return this.request(method, path27, options, attempt + 1);
|
|
155
155
|
}
|
|
156
156
|
throw error;
|
|
157
157
|
} finally {
|
|
@@ -159,10 +159,10 @@ var init_ghl_client = __esm({
|
|
|
159
159
|
}
|
|
160
160
|
if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
|
|
161
161
|
const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
|
|
162
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${
|
|
162
|
+
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path27}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
|
|
163
163
|
`);
|
|
164
164
|
await new Promise((r) => setTimeout(r, delay5));
|
|
165
|
-
return this.request(method,
|
|
165
|
+
return this.request(method, path27, options, attempt + 1);
|
|
166
166
|
}
|
|
167
167
|
if (!response.ok) {
|
|
168
168
|
let errorBody = "";
|
|
@@ -171,7 +171,7 @@ var init_ghl_client = __esm({
|
|
|
171
171
|
} catch {
|
|
172
172
|
}
|
|
173
173
|
throw new Error(
|
|
174
|
-
`GHL API Error ${response.status} ${response.statusText}: ${method} ${
|
|
174
|
+
`GHL API Error ${response.status} ${response.statusText}: ${method} ${path27}
|
|
175
175
|
${errorBody}`
|
|
176
176
|
);
|
|
177
177
|
}
|
|
@@ -183,20 +183,20 @@ ${errorBody}`
|
|
|
183
183
|
return { message: text };
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
async get(
|
|
187
|
-
return this.request("GET",
|
|
186
|
+
async get(path27, options) {
|
|
187
|
+
return this.request("GET", path27, options);
|
|
188
188
|
}
|
|
189
|
-
async post(
|
|
190
|
-
return this.request("POST",
|
|
189
|
+
async post(path27, options) {
|
|
190
|
+
return this.request("POST", path27, options);
|
|
191
191
|
}
|
|
192
|
-
async put(
|
|
193
|
-
return this.request("PUT",
|
|
192
|
+
async put(path27, options) {
|
|
193
|
+
return this.request("PUT", path27, options);
|
|
194
194
|
}
|
|
195
|
-
async patch(
|
|
196
|
-
return this.request("PATCH",
|
|
195
|
+
async patch(path27, options) {
|
|
196
|
+
return this.request("PATCH", path27, options);
|
|
197
197
|
}
|
|
198
|
-
async delete(
|
|
199
|
-
return this.request("DELETE",
|
|
198
|
+
async delete(path27, options) {
|
|
199
|
+
return this.request("DELETE", path27, options);
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
202
|
* Helper: resolves locationId from args or falls back to default
|
|
@@ -2354,6 +2354,7 @@ var init_product_manifest = __esm({
|
|
|
2354
2354
|
"use strict";
|
|
2355
2355
|
COMMAND_OS_ONLY_TOOLS = /* @__PURE__ */ new Set([]);
|
|
2356
2356
|
COMMAND_OS_ONLY_MODULES = /* @__PURE__ */ new Set([
|
|
2357
|
+
"connectors",
|
|
2357
2358
|
"assessment",
|
|
2358
2359
|
"agency-profile",
|
|
2359
2360
|
"client-engagements",
|
|
@@ -2438,6 +2439,30 @@ var init_product_manifest = __esm({
|
|
|
2438
2439
|
does: "Writes the finished assessment as a complete page on your own machine, in your branding, ready to hand to the prospect. It loads nothing from anywhere, so it opens on any host in any browser; it is marked not-to-be-indexed and given a random filename, because it carries another business's revenue and customer numbers.",
|
|
2439
2440
|
why: "We never publish it. It is your prospect's data on your letterhead, so where it goes is yours to decide \u2014 and nothing in the $97 product produces a client-facing document at all.",
|
|
2440
2441
|
layer: "Layer 4 \u2014 Delivery"
|
|
2442
|
+
},
|
|
2443
|
+
{
|
|
2444
|
+
name: "connect_system",
|
|
2445
|
+
does: "Stores a connector's credentials on YOUR machine so Command OS can read that system. Written beside your GoHighLevel credentials, readable only by you, never sent anywhere and never returned by any tool.",
|
|
2446
|
+
why: "The $97 product manages one GoHighLevel account. Reading your Stripe to learn what each client pays is about the agency's own business, not a client's account.",
|
|
2447
|
+
layer: "Layer 2 \u2014 Data"
|
|
2448
|
+
},
|
|
2449
|
+
{
|
|
2450
|
+
name: "list_connected_systems",
|
|
2451
|
+
does: "Which systems Command OS can read on this machine, and when each was connected. Never shows a key.",
|
|
2452
|
+
why: "It reports on the agency's own stack rather than a client's account, and the $97 product has no concept of a system beyond the one GoHighLevel account it is pointed at.",
|
|
2453
|
+
layer: "Layer 2 \u2014 Data"
|
|
2454
|
+
},
|
|
2455
|
+
{
|
|
2456
|
+
name: "disconnect_system",
|
|
2457
|
+
does: "Removes a connector's credentials from this machine. Anything already written into your client records stays.",
|
|
2458
|
+
why: "It removes access to the agency's own billing system, which is a thing the $97 product never had access to in the first place.",
|
|
2459
|
+
layer: "Layer 2 \u2014 Data"
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
name: "sync_stripe",
|
|
2463
|
+
does: "Reads your Stripe and fills in what your client records do not know: what each client pays, when they renew, and who owes you money. Shows you every change before making any, and reports a customer it cannot match to one of your clients rather than guessing.",
|
|
2464
|
+
why: "This is the record your daily brief already reads and finds empty. Nothing in the $97 product reads your billing, and nothing in it knows which of your clients a payment belongs to.",
|
|
2465
|
+
layer: "Layer 2 \u2014 Data"
|
|
2441
2466
|
}
|
|
2442
2467
|
];
|
|
2443
2468
|
CATALOGUED_TOOLS = new Set(COMMAND_OS_CATALOGUE.map((t) => t.name));
|
|
@@ -6061,16 +6086,16 @@ function registerEmailTools(server2, client) {
|
|
|
6061
6086
|
function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
|
|
6062
6087
|
const client = builderClient;
|
|
6063
6088
|
if (!client) return;
|
|
6064
|
-
async function builderRequest(method,
|
|
6089
|
+
async function builderRequest(method, path27, body) {
|
|
6065
6090
|
const headers = await client.buildHeaders();
|
|
6066
|
-
const response = await fetch(`${EMAIL_BUILDER_BASE}${
|
|
6091
|
+
const response = await fetch(`${EMAIL_BUILDER_BASE}${path27}`, {
|
|
6067
6092
|
method,
|
|
6068
6093
|
headers,
|
|
6069
6094
|
body: body ? JSON.stringify(body) : void 0
|
|
6070
6095
|
});
|
|
6071
6096
|
if (!response.ok) {
|
|
6072
6097
|
const text2 = await response.text();
|
|
6073
|
-
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${
|
|
6098
|
+
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path27}
|
|
6074
6099
|
${text2}`);
|
|
6075
6100
|
}
|
|
6076
6101
|
const text = await response.text();
|
|
@@ -7616,23 +7641,23 @@ var init_workflow_builder = __esm({
|
|
|
7616
7641
|
function registerFunnelBuilderTools(server2, builderClient) {
|
|
7617
7642
|
const client = builderClient;
|
|
7618
7643
|
if (!client) return;
|
|
7619
|
-
async function internalGet(
|
|
7620
|
-
return client.request("GET",
|
|
7644
|
+
async function internalGet(path27) {
|
|
7645
|
+
return client.request("GET", path27);
|
|
7621
7646
|
}
|
|
7622
|
-
async function internalPost(
|
|
7623
|
-
return client.request("POST",
|
|
7647
|
+
async function internalPost(path27, body) {
|
|
7648
|
+
return client.request("POST", path27, body);
|
|
7624
7649
|
}
|
|
7625
|
-
async function internalPut(
|
|
7626
|
-
return client.request("PUT",
|
|
7650
|
+
async function internalPut(path27, body) {
|
|
7651
|
+
return client.request("PUT", path27, body);
|
|
7627
7652
|
}
|
|
7628
|
-
async function internalDelete(
|
|
7629
|
-
return client.request("DELETE",
|
|
7653
|
+
async function internalDelete(path27) {
|
|
7654
|
+
return client.request("DELETE", path27);
|
|
7630
7655
|
}
|
|
7631
|
-
async function funnelRequest(method,
|
|
7656
|
+
async function funnelRequest(method, path27, body) {
|
|
7632
7657
|
const headers = await client.buildHeaders();
|
|
7633
7658
|
headers.Origin = "https://app.gohighlevel.com";
|
|
7634
7659
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
7635
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
7660
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path27}`;
|
|
7636
7661
|
const options = { method, headers };
|
|
7637
7662
|
if (body && (method === "POST" || method === "PUT")) {
|
|
7638
7663
|
options.body = JSON.stringify(body);
|
|
@@ -7640,7 +7665,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
7640
7665
|
const response = await fetch(url, options);
|
|
7641
7666
|
if (!response.ok) {
|
|
7642
7667
|
const text2 = await response.text();
|
|
7643
|
-
throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
7668
|
+
throw new Error(`Funnel API Error ${response.status}: ${method} ${path27}
|
|
7644
7669
|
${text2}`);
|
|
7645
7670
|
}
|
|
7646
7671
|
const text = await response.text();
|
|
@@ -8624,12 +8649,12 @@ var init_website = __esm({
|
|
|
8624
8649
|
function registerPageStudioTools(server2, builderClient) {
|
|
8625
8650
|
const client = builderClient;
|
|
8626
8651
|
if (!client) return;
|
|
8627
|
-
async function funnelRequest(method,
|
|
8652
|
+
async function funnelRequest(method, path27) {
|
|
8628
8653
|
const headers = await client.buildHeaders();
|
|
8629
8654
|
headers.Origin = "https://app.gohighlevel.com";
|
|
8630
8655
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
8631
|
-
const response = await fetch(`https://backend.leadconnectorhq.com/funnels${
|
|
8632
|
-
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
8656
|
+
const response = await fetch(`https://backend.leadconnectorhq.com/funnels${path27}`, { method, headers });
|
|
8657
|
+
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path27}
|
|
8633
8658
|
${await response.text()}`);
|
|
8634
8659
|
const text = await response.text();
|
|
8635
8660
|
return text ? JSON.parse(text) : {};
|
|
@@ -9013,9 +9038,9 @@ function cleanFormDataForSave(formData) {
|
|
|
9013
9038
|
const cleaned = dropRedundantCaseCollidingKeys(formData);
|
|
9014
9039
|
return isJsonObject(cleaned) ? cleaned : formData;
|
|
9015
9040
|
}
|
|
9016
|
-
async function formApiRequest(client, method,
|
|
9041
|
+
async function formApiRequest(client, method, path27, body) {
|
|
9017
9042
|
const headers = await client.buildHeaders();
|
|
9018
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
9043
|
+
const url = `https://backend.leadconnectorhq.com/forms${path27}`;
|
|
9019
9044
|
const options = { method, headers };
|
|
9020
9045
|
if (body && (method === "POST" || method === "PUT")) {
|
|
9021
9046
|
options.body = JSON.stringify(body);
|
|
@@ -9023,7 +9048,7 @@ async function formApiRequest(client, method, path26, body) {
|
|
|
9023
9048
|
const response = await fetch(url, options);
|
|
9024
9049
|
if (!response.ok) {
|
|
9025
9050
|
const text2 = await response.text();
|
|
9026
|
-
throw new Error(`Form API Error ${response.status}: ${method} ${
|
|
9051
|
+
throw new Error(`Form API Error ${response.status}: ${method} ${path27}
|
|
9027
9052
|
${text2}`);
|
|
9028
9053
|
}
|
|
9029
9054
|
const text = await response.text();
|
|
@@ -9037,7 +9062,7 @@ ${text2}`);
|
|
|
9037
9062
|
function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
9038
9063
|
const client = builderClient;
|
|
9039
9064
|
if (!client) return;
|
|
9040
|
-
const formRequest = (method,
|
|
9065
|
+
const formRequest = (method, path27, body) => formApiRequest(client, method, path27, body);
|
|
9041
9066
|
server2.tool(
|
|
9042
9067
|
"get_form_full",
|
|
9043
9068
|
"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 what the form builder UI shows, with one exception: where GoHighLevel stores the same value under two spellings of one key that differ only in case (it writes custom fields with both `Id` and `id`), only the lowercase one is returned. No value is lost, and the result can be edited and passed straight back to update_form.",
|
|
@@ -9160,10 +9185,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
|
9160
9185
|
},
|
|
9161
9186
|
async ({ formId, limit, skip }) => {
|
|
9162
9187
|
try {
|
|
9163
|
-
let
|
|
9164
|
-
if (formId)
|
|
9165
|
-
if (skip)
|
|
9166
|
-
const result = await formRequest("GET",
|
|
9188
|
+
let path27 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
|
|
9189
|
+
if (formId) path27 += `&formId=${formId}`;
|
|
9190
|
+
if (skip) path27 += `&skip=${skip}`;
|
|
9191
|
+
const result = await formRequest("GET", path27);
|
|
9167
9192
|
return {
|
|
9168
9193
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9169
9194
|
};
|
|
@@ -9570,9 +9595,9 @@ var init_funnel_qa = __esm({
|
|
|
9570
9595
|
function registerPipelineBuilderTools(server2, builderClient) {
|
|
9571
9596
|
const client = builderClient;
|
|
9572
9597
|
if (!client) return;
|
|
9573
|
-
async function pipelineRequest(method,
|
|
9598
|
+
async function pipelineRequest(method, path27, body) {
|
|
9574
9599
|
const headers = await client.buildHeaders();
|
|
9575
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
9600
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path27}`;
|
|
9576
9601
|
const options = { method, headers };
|
|
9577
9602
|
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
9578
9603
|
options.body = JSON.stringify(body);
|
|
@@ -9580,7 +9605,7 @@ function registerPipelineBuilderTools(server2, builderClient) {
|
|
|
9580
9605
|
const response = await fetch(url, options);
|
|
9581
9606
|
if (!response.ok) {
|
|
9582
9607
|
const text2 = await response.text();
|
|
9583
|
-
throw new Error(`Pipeline API Error ${response.status}: ${method} ${
|
|
9608
|
+
throw new Error(`Pipeline API Error ${response.status}: ${method} ${path27}
|
|
9584
9609
|
${text2}`);
|
|
9585
9610
|
}
|
|
9586
9611
|
const text = await response.text();
|
|
@@ -12619,9 +12644,9 @@ function buildContactSmartListBody(args) {
|
|
|
12619
12644
|
function registerSmartListTools(server2, builderClient) {
|
|
12620
12645
|
const client = builderClient;
|
|
12621
12646
|
if (!client) return;
|
|
12622
|
-
async function smartListRequest(method,
|
|
12647
|
+
async function smartListRequest(method, path27, body) {
|
|
12623
12648
|
const headers = await client.buildHeaders();
|
|
12624
|
-
const url = `${SMARTLIST_BASE}${
|
|
12649
|
+
const url = `${SMARTLIST_BASE}${path27}`;
|
|
12625
12650
|
const options = { method, headers };
|
|
12626
12651
|
if (body && (method === "POST" || method === "PUT")) {
|
|
12627
12652
|
options.body = JSON.stringify(body);
|
|
@@ -12629,7 +12654,7 @@ function registerSmartListTools(server2, builderClient) {
|
|
|
12629
12654
|
const response = await fetch(url, options);
|
|
12630
12655
|
if (!response.ok) {
|
|
12631
12656
|
const text2 = await response.text();
|
|
12632
|
-
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${
|
|
12657
|
+
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path27}
|
|
12633
12658
|
${text2}`);
|
|
12634
12659
|
}
|
|
12635
12660
|
const text = await response.text();
|
|
@@ -12861,12 +12886,12 @@ var init_smart_lists = __esm({
|
|
|
12861
12886
|
function registerReputationTools(server2, builderClient) {
|
|
12862
12887
|
const client = builderClient;
|
|
12863
12888
|
if (!client) return;
|
|
12864
|
-
async function reputationRequest(method,
|
|
12889
|
+
async function reputationRequest(method, path27) {
|
|
12865
12890
|
const headers = await client.buildHeaders();
|
|
12866
|
-
const response = await fetch(`${REPUTATION_BASE}${
|
|
12891
|
+
const response = await fetch(`${REPUTATION_BASE}${path27}`, { method, headers });
|
|
12867
12892
|
if (!response.ok) {
|
|
12868
12893
|
const text2 = await response.text();
|
|
12869
|
-
throw new Error(`Reputation API Error ${response.status}: ${method} ${
|
|
12894
|
+
throw new Error(`Reputation API Error ${response.status}: ${method} ${path27}
|
|
12870
12895
|
${text2}`);
|
|
12871
12896
|
}
|
|
12872
12897
|
const text = await response.text();
|
|
@@ -13007,16 +13032,16 @@ var init_email_campaigns = __esm({
|
|
|
13007
13032
|
function registerMembershipTools(server2, builderClient) {
|
|
13008
13033
|
const client = builderClient;
|
|
13009
13034
|
if (!client) return;
|
|
13010
|
-
async function membershipRequest(
|
|
13035
|
+
async function membershipRequest(path27, method = "GET", body) {
|
|
13011
13036
|
const headers = await client.buildHeaders();
|
|
13012
|
-
const response = await fetch(`${MEMBERSHIP_BASE2}${
|
|
13037
|
+
const response = await fetch(`${MEMBERSHIP_BASE2}${path27}`, {
|
|
13013
13038
|
method,
|
|
13014
13039
|
headers,
|
|
13015
13040
|
body: body ? JSON.stringify(body) : void 0
|
|
13016
13041
|
});
|
|
13017
13042
|
if (!response.ok) {
|
|
13018
13043
|
const text2 = await response.text();
|
|
13019
|
-
throw new Error(`Membership API Error ${response.status}: ${method} ${
|
|
13044
|
+
throw new Error(`Membership API Error ${response.status}: ${method} ${path27}
|
|
13020
13045
|
${text2}`);
|
|
13021
13046
|
}
|
|
13022
13047
|
const text = await response.text();
|
|
@@ -14761,10 +14786,10 @@ async function confirm(run) {
|
|
|
14761
14786
|
async function runProbes(client, loc) {
|
|
14762
14787
|
return Promise.all(
|
|
14763
14788
|
PROBES.map((probe) => confirm(async () => {
|
|
14764
|
-
const
|
|
14789
|
+
const path27 = probe.path.replace("{loc}", loc);
|
|
14765
14790
|
const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
|
|
14766
14791
|
try {
|
|
14767
|
-
const raw = await client.get(
|
|
14792
|
+
const raw = await client.get(path27, { params, noRetry: true });
|
|
14768
14793
|
const count = countRows(raw, probe.listKeys);
|
|
14769
14794
|
return {
|
|
14770
14795
|
key: probe.key,
|
|
@@ -16443,10 +16468,414 @@ var init_assessment = __esm({
|
|
|
16443
16468
|
}
|
|
16444
16469
|
});
|
|
16445
16470
|
|
|
16471
|
+
// src/command-os/connectors/store.ts
|
|
16472
|
+
function connectorsPath() {
|
|
16473
|
+
return path9.join(appDataDir(), "connectors.json");
|
|
16474
|
+
}
|
|
16475
|
+
function readFile() {
|
|
16476
|
+
try {
|
|
16477
|
+
const raw = fs10.readFileSync(connectorsPath(), "utf-8");
|
|
16478
|
+
const parsed = JSON.parse(raw);
|
|
16479
|
+
if (!parsed || typeof parsed !== "object" || !parsed.connectors) return { v: 1, connectors: {} };
|
|
16480
|
+
return parsed;
|
|
16481
|
+
} catch {
|
|
16482
|
+
return { v: 1, connectors: {} };
|
|
16483
|
+
}
|
|
16484
|
+
}
|
|
16485
|
+
function writeFile(f) {
|
|
16486
|
+
ensureAppDataDir();
|
|
16487
|
+
const target = connectorsPath();
|
|
16488
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
16489
|
+
fs10.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", "utf-8");
|
|
16490
|
+
try {
|
|
16491
|
+
fs10.chmodSync(tmp, 384);
|
|
16492
|
+
} catch {
|
|
16493
|
+
}
|
|
16494
|
+
fs10.renameSync(tmp, target);
|
|
16495
|
+
try {
|
|
16496
|
+
fs10.chmodSync(target, 384);
|
|
16497
|
+
} catch {
|
|
16498
|
+
}
|
|
16499
|
+
}
|
|
16500
|
+
function connect(system, key, label, now = () => /* @__PURE__ */ new Date()) {
|
|
16501
|
+
const trimmed = String(key ?? "").trim();
|
|
16502
|
+
if (!trimmed) throw new Error("No key given. Nothing was stored.");
|
|
16503
|
+
const f = readFile();
|
|
16504
|
+
const secret = { system, key: trimmed, label, connectedAt: now().toISOString() };
|
|
16505
|
+
f.connectors[system] = secret;
|
|
16506
|
+
writeFile(f);
|
|
16507
|
+
return summaryOf(secret);
|
|
16508
|
+
}
|
|
16509
|
+
function disconnect(system) {
|
|
16510
|
+
const f = readFile();
|
|
16511
|
+
if (!f.connectors[system]) return false;
|
|
16512
|
+
delete f.connectors[system];
|
|
16513
|
+
writeFile(f);
|
|
16514
|
+
return true;
|
|
16515
|
+
}
|
|
16516
|
+
function summaryOf(s) {
|
|
16517
|
+
return { system: s.system, label: s.label, connectedAt: s.connectedAt, hint: hintOf(s.key) };
|
|
16518
|
+
}
|
|
16519
|
+
function listConnectors() {
|
|
16520
|
+
return Object.values(readFile().connectors).filter((s) => Boolean(s)).map(summaryOf);
|
|
16521
|
+
}
|
|
16522
|
+
function isConnected(system) {
|
|
16523
|
+
return Boolean(readFile().connectors[system]);
|
|
16524
|
+
}
|
|
16525
|
+
async function withKey(system, fn) {
|
|
16526
|
+
const s = readFile().connectors[system];
|
|
16527
|
+
if (!s) throw new Error(`${system} is not connected on this machine.`);
|
|
16528
|
+
return fn(s.key);
|
|
16529
|
+
}
|
|
16530
|
+
var fs10, path9, hintOf;
|
|
16531
|
+
var init_store2 = __esm({
|
|
16532
|
+
"src/command-os/connectors/store.ts"() {
|
|
16533
|
+
"use strict";
|
|
16534
|
+
fs10 = __toESM(require("node:fs"));
|
|
16535
|
+
path9 = __toESM(require("node:path"));
|
|
16536
|
+
init_credentials_store();
|
|
16537
|
+
hintOf = (key) => `\u2026${String(key).slice(-4)}`;
|
|
16538
|
+
}
|
|
16539
|
+
});
|
|
16540
|
+
|
|
16541
|
+
// src/command-os/connectors/stripe.ts
|
|
16542
|
+
function identity(kind, externalId) {
|
|
16543
|
+
return { system: SYSTEM, kind, externalId, tenantId: null };
|
|
16544
|
+
}
|
|
16545
|
+
function observe(id, field, value, observedAt, raw) {
|
|
16546
|
+
return { identity: id, field, value, source: SYSTEM, observedAt, raw };
|
|
16547
|
+
}
|
|
16548
|
+
function mapSubscription(sub, observedAt) {
|
|
16549
|
+
const id = identity("client", sub.customer);
|
|
16550
|
+
const price = sub.items?.data?.[0]?.price;
|
|
16551
|
+
const amount = fromMinor(price?.unit_amount ?? null);
|
|
16552
|
+
const o = [
|
|
16553
|
+
observe(id, "stripe_subscription_id", sub.id, observedAt, sub),
|
|
16554
|
+
observe(id, "subscription_status", sub.status, observedAt, sub)
|
|
16555
|
+
];
|
|
16556
|
+
if (amount !== null) o.push(observe(id, "pays_per_period", amount, observedAt, sub));
|
|
16557
|
+
if (price?.recurring?.interval) o.push(observe(id, "billing_interval", price.recurring.interval, observedAt, sub));
|
|
16558
|
+
const renews = iso(sub.current_period_end);
|
|
16559
|
+
if (renews) o.push(observe(id, "renews_at", renews, observedAt, sub));
|
|
16560
|
+
o.push(observe(id, "cancelling_at_period_end", sub.cancel_at_period_end === true, observedAt, sub));
|
|
16561
|
+
return { identities: [id], observations: o };
|
|
16562
|
+
}
|
|
16563
|
+
function mapInvoice(inv, observedAt) {
|
|
16564
|
+
const id = identity("payment", inv.id);
|
|
16565
|
+
const due = iso(inv.due_date ?? null);
|
|
16566
|
+
const o = [
|
|
16567
|
+
observe(id, "invoice_status", inv.status, observedAt, inv),
|
|
16568
|
+
observe(id, "stripe_customer_id", inv.customer, observedAt, inv)
|
|
16569
|
+
];
|
|
16570
|
+
const remaining = fromMinor(inv.amount_remaining ?? inv.amount_due ?? null);
|
|
16571
|
+
if (remaining !== null) o.push(observe(id, "amount_outstanding", remaining, observedAt, inv));
|
|
16572
|
+
if (due) o.push(observe(id, "due_at", due, observedAt, inv));
|
|
16573
|
+
if (inv.number) o.push(observe(id, "invoice_number", inv.number, observedAt, inv));
|
|
16574
|
+
if (inv.hosted_invoice_url) o.push(observe(id, "invoice_url", inv.hosted_invoice_url, observedAt, inv));
|
|
16575
|
+
return { identities: [id], observations: o };
|
|
16576
|
+
}
|
|
16577
|
+
function proposeClientLinks(customers, known, observedAt) {
|
|
16578
|
+
const byEmail = /* @__PURE__ */ new Map();
|
|
16579
|
+
for (const k of known) {
|
|
16580
|
+
for (const e of k.emails) {
|
|
16581
|
+
const key = e.trim().toLowerCase();
|
|
16582
|
+
if (!key) continue;
|
|
16583
|
+
byEmail.set(key, [...byEmail.get(key) ?? [], k]);
|
|
16584
|
+
}
|
|
16585
|
+
}
|
|
16586
|
+
const links = [];
|
|
16587
|
+
for (const c of customers) {
|
|
16588
|
+
const email = (c.email ?? "").trim().toLowerCase();
|
|
16589
|
+
if (!email) continue;
|
|
16590
|
+
const hits = byEmail.get(email) ?? [];
|
|
16591
|
+
if (hits.length !== 1) continue;
|
|
16592
|
+
const k = hits[0];
|
|
16593
|
+
links.push({
|
|
16594
|
+
a: identity("client", c.id),
|
|
16595
|
+
b: { system: "ghl", kind: "client", externalId: k.tenantId, tenantId: k.tenantId },
|
|
16596
|
+
confidence: "weak",
|
|
16597
|
+
reason: `Stripe billing email ${email} matches ${k.name}. Confirm before this counts.`,
|
|
16598
|
+
observedAt
|
|
16599
|
+
});
|
|
16600
|
+
}
|
|
16601
|
+
return links;
|
|
16602
|
+
}
|
|
16603
|
+
function outstanding(invoices, now = /* @__PURE__ */ new Date()) {
|
|
16604
|
+
return invoices.filter((i) => i.status === "open" || i.status === "uncollectible").map((i) => {
|
|
16605
|
+
const dueAt = iso(i.due_date ?? null);
|
|
16606
|
+
const days = dueAt ? Math.floor((now.getTime() - new Date(dueAt).getTime()) / 864e5) : null;
|
|
16607
|
+
return {
|
|
16608
|
+
invoiceId: i.id,
|
|
16609
|
+
number: i.number ?? null,
|
|
16610
|
+
customerId: i.customer,
|
|
16611
|
+
amount: fromMinor(i.amount_remaining ?? i.amount_due ?? null) ?? 0,
|
|
16612
|
+
dueAt,
|
|
16613
|
+
daysOverdue: days !== null && days > 0 ? days : null,
|
|
16614
|
+
url: i.hosted_invoice_url ?? null
|
|
16615
|
+
};
|
|
16616
|
+
}).sort((a, b) => (b.daysOverdue ?? -1) - (a.daysOverdue ?? -1));
|
|
16617
|
+
}
|
|
16618
|
+
function renewalsDue(subs, withinDays = 30, now = /* @__PURE__ */ new Date()) {
|
|
16619
|
+
const out = [];
|
|
16620
|
+
for (const s of subs) {
|
|
16621
|
+
if (s.status !== "active" && s.status !== "trialing" && s.status !== "past_due") continue;
|
|
16622
|
+
const renewsAt = iso(s.current_period_end);
|
|
16623
|
+
if (!renewsAt) continue;
|
|
16624
|
+
const days = Math.ceil((new Date(renewsAt).getTime() - now.getTime()) / 864e5);
|
|
16625
|
+
if (days > withinDays) continue;
|
|
16626
|
+
out.push({
|
|
16627
|
+
customerId: s.customer,
|
|
16628
|
+
renewsAt,
|
|
16629
|
+
days,
|
|
16630
|
+
cancelling: s.cancel_at_period_end === true,
|
|
16631
|
+
amount: fromMinor(s.items?.data?.[0]?.price?.unit_amount ?? null)
|
|
16632
|
+
});
|
|
16633
|
+
}
|
|
16634
|
+
return out.sort((a, b) => a.days - b.days);
|
|
16635
|
+
}
|
|
16636
|
+
async function health(fetcher, now = () => /* @__PURE__ */ new Date()) {
|
|
16637
|
+
const checkedAt = now().toISOString();
|
|
16638
|
+
try {
|
|
16639
|
+
const res = await fetcher("/v1/subscriptions", { limit: "1" });
|
|
16640
|
+
if (!res || !Array.isArray(res.data)) {
|
|
16641
|
+
return { system: SYSTEM, state: "INCONCLUSIVE", detail: "Stripe answered, but not with anything we recognised.", checkedAt };
|
|
16642
|
+
}
|
|
16643
|
+
return { system: SYSTEM, state: "WORKING", detail: "Reading subscriptions and invoices.", checkedAt };
|
|
16644
|
+
} catch (e) {
|
|
16645
|
+
const msg3 = String(e?.message ?? e);
|
|
16646
|
+
if (/401|403|permission|invalid api key/i.test(msg3)) {
|
|
16647
|
+
return { system: SYSTEM, state: "NO-ACCESS", detail: "Stripe refused the key. Check it allows reading subscriptions and invoices.", checkedAt };
|
|
16648
|
+
}
|
|
16649
|
+
if (/network|timeout|ENOTFOUND|ECONN/i.test(msg3)) {
|
|
16650
|
+
return { system: SYSTEM, state: "UNAVAILABLE", detail: "Could not reach Stripe. Nothing to fix on your side.", checkedAt };
|
|
16651
|
+
}
|
|
16652
|
+
return { system: SYSTEM, state: "BROKEN", detail: `Stripe read failed: ${msg3.slice(0, 120)}`, checkedAt };
|
|
16653
|
+
}
|
|
16654
|
+
}
|
|
16655
|
+
var SYSTEM, iso, fromMinor;
|
|
16656
|
+
var init_stripe = __esm({
|
|
16657
|
+
"src/command-os/connectors/stripe.ts"() {
|
|
16658
|
+
"use strict";
|
|
16659
|
+
SYSTEM = "stripe";
|
|
16660
|
+
iso = (unix) => typeof unix === "number" && isFinite(unix) ? new Date(unix * 1e3).toISOString() : null;
|
|
16661
|
+
fromMinor = (n) => typeof n === "number" && isFinite(n) ? Math.round(n) / 100 : null;
|
|
16662
|
+
}
|
|
16663
|
+
});
|
|
16664
|
+
|
|
16665
|
+
// src/command-os/connectors/spine.ts
|
|
16666
|
+
function identityKey(i) {
|
|
16667
|
+
return `${i.system}:${i.kind}:${i.externalId}`;
|
|
16668
|
+
}
|
|
16669
|
+
function linkIsLive(l) {
|
|
16670
|
+
return !l.retractedAt;
|
|
16671
|
+
}
|
|
16672
|
+
function resolveEntities(identities, links) {
|
|
16673
|
+
const parent = /* @__PURE__ */ new Map();
|
|
16674
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
16675
|
+
for (const i of identities) {
|
|
16676
|
+
parent.set(identityKey(i), identityKey(i));
|
|
16677
|
+
byKey.set(identityKey(i), i);
|
|
16678
|
+
}
|
|
16679
|
+
const find = (k) => {
|
|
16680
|
+
let cur = k;
|
|
16681
|
+
while (parent.get(cur) !== cur) {
|
|
16682
|
+
const up = parent.get(cur);
|
|
16683
|
+
if (up === void 0) return cur;
|
|
16684
|
+
parent.set(cur, parent.get(up) ?? up);
|
|
16685
|
+
cur = parent.get(cur);
|
|
16686
|
+
}
|
|
16687
|
+
return cur;
|
|
16688
|
+
};
|
|
16689
|
+
const union = (x, y) => {
|
|
16690
|
+
const rx = find(x), ry = find(y);
|
|
16691
|
+
if (rx !== ry) parent.set(rx, ry);
|
|
16692
|
+
};
|
|
16693
|
+
for (const l of links) {
|
|
16694
|
+
if (!linkIsLive(l)) continue;
|
|
16695
|
+
const ka = identityKey(l.a), kb = identityKey(l.b);
|
|
16696
|
+
if (!parent.has(ka) || !parent.has(kb)) continue;
|
|
16697
|
+
union(ka, kb);
|
|
16698
|
+
}
|
|
16699
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16700
|
+
for (const k of parent.keys()) {
|
|
16701
|
+
const root = find(k);
|
|
16702
|
+
const bucket = groups.get(root) ?? [];
|
|
16703
|
+
bucket.push(byKey.get(k));
|
|
16704
|
+
groups.set(root, bucket);
|
|
16705
|
+
}
|
|
16706
|
+
return [...groups.values()];
|
|
16707
|
+
}
|
|
16708
|
+
function resolveTenant(group, links) {
|
|
16709
|
+
const direct = group.filter((i) => i.tenantId !== null);
|
|
16710
|
+
const distinct = [...new Set(direct.map((i) => i.tenantId))];
|
|
16711
|
+
if (distinct.length === 1) {
|
|
16712
|
+
return { tenantId: distinct[0], because: `${direct[0].system} says so directly` };
|
|
16713
|
+
}
|
|
16714
|
+
if (distinct.length > 1) {
|
|
16715
|
+
const keys = new Set(group.map(identityKey));
|
|
16716
|
+
const touching = links.filter(linkIsLive).filter((l) => keys.has(identityKey(l.a)) || keys.has(identityKey(l.b)));
|
|
16717
|
+
let best = null;
|
|
16718
|
+
let tied = false;
|
|
16719
|
+
for (const l of touching) {
|
|
16720
|
+
const t = l.a.tenantId ?? l.b.tenantId;
|
|
16721
|
+
if (t === null || t === void 0) continue;
|
|
16722
|
+
if (!best) {
|
|
16723
|
+
best = l;
|
|
16724
|
+
tied = false;
|
|
16725
|
+
continue;
|
|
16726
|
+
}
|
|
16727
|
+
const r = CONFIDENCE_RANK[l.confidence] - CONFIDENCE_RANK[best.confidence];
|
|
16728
|
+
if (r > 0) {
|
|
16729
|
+
best = l;
|
|
16730
|
+
tied = false;
|
|
16731
|
+
} else if (r === 0 && (best.a.tenantId ?? best.b.tenantId) !== t) tied = true;
|
|
16732
|
+
}
|
|
16733
|
+
if (best && !tied) {
|
|
16734
|
+
const t = best.a.tenantId ?? best.b.tenantId;
|
|
16735
|
+
return { tenantId: t, because: `${best.confidence} link: ${best.reason}` };
|
|
16736
|
+
}
|
|
16737
|
+
return {
|
|
16738
|
+
tenantId: null,
|
|
16739
|
+
because: `systems disagree about which client this belongs to (${distinct.join(", ")}) and no link settles it`
|
|
16740
|
+
};
|
|
16741
|
+
}
|
|
16742
|
+
return {
|
|
16743
|
+
tenantId: null,
|
|
16744
|
+
because: "no system on this record knows which client it belongs to"
|
|
16745
|
+
};
|
|
16746
|
+
}
|
|
16747
|
+
function currentValue(observations, field, sourcePriority = []) {
|
|
16748
|
+
const hits = observations.filter((o) => o.field === field);
|
|
16749
|
+
if (!hits.length) return null;
|
|
16750
|
+
const rank = (s) => {
|
|
16751
|
+
const i = sourcePriority.indexOf(s);
|
|
16752
|
+
return i === -1 ? sourcePriority.length : i;
|
|
16753
|
+
};
|
|
16754
|
+
const best = hits.reduce((a, b) => {
|
|
16755
|
+
if (a.observedAt !== b.observedAt) return a.observedAt > b.observedAt ? a : b;
|
|
16756
|
+
return rank(a.source) <= rank(b.source) ? a : b;
|
|
16757
|
+
});
|
|
16758
|
+
return { value: best.value, source: best.source, observedAt: best.observedAt };
|
|
16759
|
+
}
|
|
16760
|
+
var CONFIDENCE_RANK;
|
|
16761
|
+
var init_spine = __esm({
|
|
16762
|
+
"src/command-os/connectors/spine.ts"() {
|
|
16763
|
+
"use strict";
|
|
16764
|
+
CONFIDENCE_RANK = { asserted: 3, strong: 2, weak: 1 };
|
|
16765
|
+
}
|
|
16766
|
+
});
|
|
16767
|
+
|
|
16768
|
+
// src/command-os/connectors/to-engagements.ts
|
|
16769
|
+
function toLocalDate(iso2) {
|
|
16770
|
+
if (typeof iso2 !== "string") return void 0;
|
|
16771
|
+
const d = new Date(iso2);
|
|
16772
|
+
if (Number.isNaN(d.getTime())) return void 0;
|
|
16773
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
16774
|
+
}
|
|
16775
|
+
function statusFrom(sub) {
|
|
16776
|
+
switch (sub) {
|
|
16777
|
+
case "active":
|
|
16778
|
+
case "trialing":
|
|
16779
|
+
return "active";
|
|
16780
|
+
case "past_due":
|
|
16781
|
+
case "unpaid":
|
|
16782
|
+
return "active";
|
|
16783
|
+
// still a client, and the money problem is its own line
|
|
16784
|
+
case "canceled":
|
|
16785
|
+
return "ended";
|
|
16786
|
+
default:
|
|
16787
|
+
return void 0;
|
|
16788
|
+
}
|
|
16789
|
+
}
|
|
16790
|
+
function bridgeToEngagements(identities, links, observations, existing = {}) {
|
|
16791
|
+
const trusted = links.filter((l) => linkIsLive(l) && TRUSTED.has(l.confidence));
|
|
16792
|
+
const awaitingConfirmation = links.filter((l) => linkIsLive(l) && !TRUSTED.has(l.confidence));
|
|
16793
|
+
const patch = {};
|
|
16794
|
+
const disagreements = [];
|
|
16795
|
+
const unattributed = [];
|
|
16796
|
+
for (const group of resolveEntities(identities, trusted)) {
|
|
16797
|
+
const stripeOnes = group.filter((i) => i.system === "stripe");
|
|
16798
|
+
if (!stripeOnes.length) continue;
|
|
16799
|
+
const { tenantId, because } = resolveTenant(group, trusted);
|
|
16800
|
+
if (!tenantId) {
|
|
16801
|
+
for (const i of stripeOnes) unattributed.push({ identity: i, because });
|
|
16802
|
+
continue;
|
|
16803
|
+
}
|
|
16804
|
+
const keys = new Set(group.map((i) => `${i.system}:${i.kind}:${i.externalId}`));
|
|
16805
|
+
const mine = observations.filter((o) => keys.has(`${o.identity.system}:${o.identity.kind}:${o.identity.externalId}`));
|
|
16806
|
+
const was = existing[tenantId] ?? {};
|
|
16807
|
+
const into = patch[tenantId] ?? {};
|
|
16808
|
+
const pays = currentValue(mine, "pays_per_period")?.value;
|
|
16809
|
+
if (typeof pays === "number") {
|
|
16810
|
+
if (isSet(was.price) && was.price !== pays) {
|
|
16811
|
+
disagreements.push({ tenantId, field: "price", yours: was.price, stripe: pays });
|
|
16812
|
+
} else if (!isSet(was.price)) {
|
|
16813
|
+
into.price = pays;
|
|
16814
|
+
}
|
|
16815
|
+
}
|
|
16816
|
+
const interval = currentValue(mine, "billing_interval")?.value;
|
|
16817
|
+
if (!isSet(was.cadence) && (interval === "month" || interval === "year")) {
|
|
16818
|
+
if (interval === "month") into.cadence = "monthly";
|
|
16819
|
+
}
|
|
16820
|
+
const renews = toLocalDate(currentValue(mine, "renews_at")?.value);
|
|
16821
|
+
if (renews) {
|
|
16822
|
+
if (isSet(was.renewsOn) && was.renewsOn !== renews) {
|
|
16823
|
+
disagreements.push({ tenantId, field: "renewsOn", yours: was.renewsOn, stripe: renews });
|
|
16824
|
+
} else if (!isSet(was.renewsOn)) {
|
|
16825
|
+
into.renewsOn = renews;
|
|
16826
|
+
}
|
|
16827
|
+
}
|
|
16828
|
+
const status = statusFrom(currentValue(mine, "subscription_status")?.value);
|
|
16829
|
+
if (status && !isSet(was.status)) into.status = status;
|
|
16830
|
+
if (Object.keys(into).length) patch[tenantId] = into;
|
|
16831
|
+
}
|
|
16832
|
+
return { patch, disagreements, unattributed, awaitingConfirmation };
|
|
16833
|
+
}
|
|
16834
|
+
function owedByClient(owing, identities, links, nameFor2) {
|
|
16835
|
+
const trusted = links.filter((l) => linkIsLive(l) && TRUSTED.has(l.confidence));
|
|
16836
|
+
const tenantOfCustomer = /* @__PURE__ */ new Map();
|
|
16837
|
+
for (const group of resolveEntities(identities, trusted)) {
|
|
16838
|
+
const { tenantId } = resolveTenant(group, trusted);
|
|
16839
|
+
if (!tenantId) continue;
|
|
16840
|
+
for (const i of group) {
|
|
16841
|
+
if (i.system === "stripe" && i.kind === "client") tenantOfCustomer.set(i.externalId, tenantId);
|
|
16842
|
+
}
|
|
16843
|
+
}
|
|
16844
|
+
const lines = [];
|
|
16845
|
+
let unattributedTotal = 0;
|
|
16846
|
+
let unattributedCount = 0;
|
|
16847
|
+
for (const o of owing) {
|
|
16848
|
+
const tenant = tenantOfCustomer.get(o.customerId);
|
|
16849
|
+
if (!tenant) {
|
|
16850
|
+
unattributedTotal += o.amount;
|
|
16851
|
+
unattributedCount += 1;
|
|
16852
|
+
continue;
|
|
16853
|
+
}
|
|
16854
|
+
lines.push({
|
|
16855
|
+
client: nameFor2(tenant),
|
|
16856
|
+
amount: o.amount,
|
|
16857
|
+
number: o.number,
|
|
16858
|
+
daysOverdue: o.daysOverdue,
|
|
16859
|
+
url: o.url
|
|
16860
|
+
});
|
|
16861
|
+
}
|
|
16862
|
+
lines.sort((a, b) => (b.daysOverdue ?? -1) - (a.daysOverdue ?? -1));
|
|
16863
|
+
return { lines, unattributedTotal, unattributedCount };
|
|
16864
|
+
}
|
|
16865
|
+
var TRUSTED, isSet;
|
|
16866
|
+
var init_to_engagements = __esm({
|
|
16867
|
+
"src/command-os/connectors/to-engagements.ts"() {
|
|
16868
|
+
"use strict";
|
|
16869
|
+
init_spine();
|
|
16870
|
+
TRUSTED = /* @__PURE__ */ new Set(["asserted", "strong"]);
|
|
16871
|
+
isSet = (v) => v !== void 0 && v !== null && v !== "";
|
|
16872
|
+
}
|
|
16873
|
+
});
|
|
16874
|
+
|
|
16446
16875
|
// src/client-engagements.ts
|
|
16447
16876
|
function clientEngagementsPath() {
|
|
16448
16877
|
const override = process.env[CLIENT_ENGAGEMENTS_ENV];
|
|
16449
|
-
return override && override.trim() ? override :
|
|
16878
|
+
return override && override.trim() ? override : path10.join(appDataDir(), "client-engagements.json");
|
|
16450
16879
|
}
|
|
16451
16880
|
function safeParse2(raw) {
|
|
16452
16881
|
try {
|
|
@@ -16490,7 +16919,7 @@ function sanitizeEngagements(raw) {
|
|
|
16490
16919
|
}
|
|
16491
16920
|
function readClientEngagements(file = clientEngagementsPath()) {
|
|
16492
16921
|
try {
|
|
16493
|
-
const raw = JSON.parse(
|
|
16922
|
+
const raw = JSON.parse(fs11.readFileSync(file, "utf8"));
|
|
16494
16923
|
if (raw && typeof raw === "object") return sanitizeEngagements(raw);
|
|
16495
16924
|
} catch {
|
|
16496
16925
|
}
|
|
@@ -16517,7 +16946,7 @@ function writeClientEngagement(locationId2, patch, file = clientEngagementsPath(
|
|
|
16517
16946
|
if (!id || FORBIDDEN_KEY.has(id)) throw new Error("A client id is required.");
|
|
16518
16947
|
const stamp = () => {
|
|
16519
16948
|
try {
|
|
16520
|
-
return
|
|
16949
|
+
return fs11.readFileSync(file, "utf8");
|
|
16521
16950
|
} catch {
|
|
16522
16951
|
return "";
|
|
16523
16952
|
}
|
|
@@ -16537,20 +16966,20 @@ function writeClientEngagement(locationId2, patch, file = clientEngagementsPath(
|
|
|
16537
16966
|
};
|
|
16538
16967
|
const persist = (next) => {
|
|
16539
16968
|
if (file === clientEngagementsPath() && !process.env[CLIENT_ENGAGEMENTS_ENV]) ensureAppDataDir();
|
|
16540
|
-
|
|
16969
|
+
fs11.mkdirSync(path10.dirname(file), { recursive: true });
|
|
16541
16970
|
const tmp = `${file}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
16542
16971
|
try {
|
|
16543
|
-
|
|
16972
|
+
fs11.writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
16544
16973
|
if (process.platform !== "win32") {
|
|
16545
16974
|
try {
|
|
16546
|
-
|
|
16975
|
+
fs11.chmodSync(tmp, 384);
|
|
16547
16976
|
} catch {
|
|
16548
16977
|
}
|
|
16549
16978
|
}
|
|
16550
|
-
|
|
16979
|
+
fs11.renameSync(tmp, file);
|
|
16551
16980
|
} catch (e) {
|
|
16552
16981
|
try {
|
|
16553
|
-
|
|
16982
|
+
fs11.unlinkSync(tmp);
|
|
16554
16983
|
} catch {
|
|
16555
16984
|
}
|
|
16556
16985
|
throw e;
|
|
@@ -16609,12 +17038,12 @@ function monthlyFloor(roster, profile) {
|
|
|
16609
17038
|
counted: priced.length
|
|
16610
17039
|
};
|
|
16611
17040
|
}
|
|
16612
|
-
var
|
|
17041
|
+
var fs11, path10, CLIENT_ENGAGEMENTS_ENV, ENGAGEMENT_STATUSES, EMPTY2, MAX_NOTES, MAX_SOP2, MAX_CLIENTS, FORBIDDEN_KEY;
|
|
16613
17042
|
var init_client_engagements = __esm({
|
|
16614
17043
|
"src/client-engagements.ts"() {
|
|
16615
17044
|
"use strict";
|
|
16616
|
-
|
|
16617
|
-
|
|
17045
|
+
fs11 = __toESM(require("fs"));
|
|
17046
|
+
path10 = __toESM(require("path"));
|
|
16618
17047
|
init_credentials_store();
|
|
16619
17048
|
init_field_sanitize();
|
|
16620
17049
|
CLIENT_ENGAGEMENTS_ENV = "GHL_MCP_CLIENT_ENGAGEMENTS";
|
|
@@ -16627,6 +17056,182 @@ var init_client_engagements = __esm({
|
|
|
16627
17056
|
}
|
|
16628
17057
|
});
|
|
16629
17058
|
|
|
17059
|
+
// src/tools/connectors.ts
|
|
17060
|
+
function stripeFetcher(key) {
|
|
17061
|
+
return async (path27, query) => {
|
|
17062
|
+
const url = new URL(`https://api.stripe.com${path27}`);
|
|
17063
|
+
for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
|
|
17064
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
|
|
17065
|
+
if (!res.ok) {
|
|
17066
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
17067
|
+
}
|
|
17068
|
+
return res.json();
|
|
17069
|
+
};
|
|
17070
|
+
}
|
|
17071
|
+
async function listAll(f, path27, extra = {}) {
|
|
17072
|
+
const out = [];
|
|
17073
|
+
let after;
|
|
17074
|
+
for (let page = 0; page < 20; page++) {
|
|
17075
|
+
const q2 = { limit: "100", ...extra };
|
|
17076
|
+
if (after) q2.starting_after = after;
|
|
17077
|
+
const res = await f(path27, q2);
|
|
17078
|
+
const data = res?.data ?? [];
|
|
17079
|
+
out.push(...data);
|
|
17080
|
+
if (!res?.has_more || !data.length) break;
|
|
17081
|
+
after = data[data.length - 1]?.id;
|
|
17082
|
+
if (!after) break;
|
|
17083
|
+
}
|
|
17084
|
+
return out;
|
|
17085
|
+
}
|
|
17086
|
+
function registerConnectorTools(server2) {
|
|
17087
|
+
safeTool(
|
|
17088
|
+
server2,
|
|
17089
|
+
"connect_system",
|
|
17090
|
+
"Store a connector's credentials on THIS machine so Command OS can read that system. The key is written next to your GoHighLevel credentials, readable only by you, and is never sent anywhere, never returned by any tool, and never written to a log. Today: Stripe, which answers who owes you money, what each client pays and when they renew.",
|
|
17091
|
+
{
|
|
17092
|
+
system: import_zod57.z.enum(["stripe"]).describe("The system to connect. Stripe is the one that is built."),
|
|
17093
|
+
key: import_zod57.z.string().min(8).describe("A Stripe RESTRICTED key with read access to subscriptions, invoices and customers. A restricted key is deliberate: nothing here writes, so nothing here needs a key that can."),
|
|
17094
|
+
label: import_zod57.z.string().optional().describe("Which account this is, so you can tell two apart later.")
|
|
17095
|
+
},
|
|
17096
|
+
async ({ system, key, label }) => {
|
|
17097
|
+
const s = connect(system, key, label);
|
|
17098
|
+
return {
|
|
17099
|
+
connected: s.system,
|
|
17100
|
+
label: s.label ?? null,
|
|
17101
|
+
keyEndsWith: s.hint,
|
|
17102
|
+
storedAt: "your own machine, alongside your GoHighLevel credentials",
|
|
17103
|
+
nextStep: "Run sync_stripe to see what it finds. It changes nothing until you tell it to."
|
|
17104
|
+
};
|
|
17105
|
+
}
|
|
17106
|
+
);
|
|
17107
|
+
safeTool(
|
|
17108
|
+
server2,
|
|
17109
|
+
"list_connected_systems",
|
|
17110
|
+
"Which systems Command OS can read on this machine, and when each was connected. Never shows a key.",
|
|
17111
|
+
{},
|
|
17112
|
+
async () => {
|
|
17113
|
+
const all = listConnectors();
|
|
17114
|
+
return {
|
|
17115
|
+
connected: all,
|
|
17116
|
+
note: all.length ? null : "Nothing is connected yet. The daily brief can only see GoHighLevel."
|
|
17117
|
+
};
|
|
17118
|
+
}
|
|
17119
|
+
);
|
|
17120
|
+
safeTool(
|
|
17121
|
+
server2,
|
|
17122
|
+
"disconnect_system",
|
|
17123
|
+
"Remove a connector's credentials from this machine. The data already written into your client records stays; only the ability to read that system again is removed.",
|
|
17124
|
+
{ system: import_zod57.z.enum(["stripe"]) },
|
|
17125
|
+
async ({ system }) => ({
|
|
17126
|
+
removed: disconnect(system),
|
|
17127
|
+
note: "Anything already written into your client records is untouched."
|
|
17128
|
+
})
|
|
17129
|
+
);
|
|
17130
|
+
safeTool(
|
|
17131
|
+
server2,
|
|
17132
|
+
"sync_stripe",
|
|
17133
|
+
"Read your Stripe and fill in what your client records do not know: what each client pays, when they renew, and who owes you money. DRY RUN BY DEFAULT \u2014 it shows you every change before making any. A client whose Stripe customer cannot be matched to one of your clients is reported, never guessed at.",
|
|
17134
|
+
{
|
|
17135
|
+
apply: import_zod57.z.boolean().optional().describe("Write the changes into your client records. Defaults to false: look first."),
|
|
17136
|
+
confirmLinks: import_zod57.z.array(import_zod57.z.object({
|
|
17137
|
+
stripeCustomerId: import_zod57.z.string(),
|
|
17138
|
+
clientKey: import_zod57.z.string().describe("The client in your records this Stripe customer is.")
|
|
17139
|
+
})).optional().describe("Matches you are confirming. An email match alone is never enough to move money against a client's name.")
|
|
17140
|
+
},
|
|
17141
|
+
async ({ apply, confirmLinks }) => {
|
|
17142
|
+
if (!isConnected("stripe")) {
|
|
17143
|
+
return { error: "Stripe is not connected on this machine. Run connect_system first." };
|
|
17144
|
+
}
|
|
17145
|
+
const observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17146
|
+
const record = readClientEngagements();
|
|
17147
|
+
const clients = record?.clients ?? {};
|
|
17148
|
+
const known = Object.entries(clients).map(([key, c]) => ({
|
|
17149
|
+
tenantId: key,
|
|
17150
|
+
name: c.clientName ?? key,
|
|
17151
|
+
emails: []
|
|
17152
|
+
// Layer 1 holds no billing email yet; matches come from confirmations below.
|
|
17153
|
+
}));
|
|
17154
|
+
const result = await withKey("stripe", async (key) => {
|
|
17155
|
+
const f = stripeFetcher(key);
|
|
17156
|
+
const h = await health(f);
|
|
17157
|
+
if (h.state !== "WORKING") return { health: h, subs: [], invoices: [], customers: [] };
|
|
17158
|
+
const [subs, invoices, customers] = await Promise.all([
|
|
17159
|
+
listAll(f, "/v1/subscriptions", { status: "all" }),
|
|
17160
|
+
listAll(f, "/v1/invoices"),
|
|
17161
|
+
listAll(f, "/v1/customers")
|
|
17162
|
+
]);
|
|
17163
|
+
return { health: h, subs, invoices, customers };
|
|
17164
|
+
});
|
|
17165
|
+
if (result.health.state !== "WORKING") {
|
|
17166
|
+
return { health: result.health, readNothing: true };
|
|
17167
|
+
}
|
|
17168
|
+
const identities = [];
|
|
17169
|
+
const observations = [];
|
|
17170
|
+
for (const s of result.subs) {
|
|
17171
|
+
const m = mapSubscription(s, observedAt);
|
|
17172
|
+
identities.push(...m.identities);
|
|
17173
|
+
observations.push(...m.observations);
|
|
17174
|
+
}
|
|
17175
|
+
for (const i of result.invoices) {
|
|
17176
|
+
const m = mapInvoice(i, observedAt);
|
|
17177
|
+
identities.push(...m.identities);
|
|
17178
|
+
observations.push(...m.observations);
|
|
17179
|
+
}
|
|
17180
|
+
for (const key of Object.keys(clients)) {
|
|
17181
|
+
identities.push({ system: "ghl", kind: "client", externalId: key, tenantId: key });
|
|
17182
|
+
}
|
|
17183
|
+
const links = (confirmLinks ?? []).map((c) => ({
|
|
17184
|
+
a: { system: "stripe", kind: "client", externalId: c.stripeCustomerId, tenantId: null },
|
|
17185
|
+
b: { system: "ghl", kind: "client", externalId: c.clientKey, tenantId: c.clientKey },
|
|
17186
|
+
confidence: "asserted",
|
|
17187
|
+
reason: "you confirmed this match",
|
|
17188
|
+
observedAt
|
|
17189
|
+
}));
|
|
17190
|
+
links.push(...proposeClientLinks(result.customers, known, observedAt));
|
|
17191
|
+
const bridged = bridgeToEngagements(identities, links, observations, clients);
|
|
17192
|
+
const owed = owedByClient(
|
|
17193
|
+
outstanding(result.invoices),
|
|
17194
|
+
identities,
|
|
17195
|
+
links,
|
|
17196
|
+
(t) => clients[t]?.clientName ?? t
|
|
17197
|
+
);
|
|
17198
|
+
const renewals = renewalsDue(result.subs);
|
|
17199
|
+
let written = 0;
|
|
17200
|
+
if (apply) {
|
|
17201
|
+
for (const [clientKey, p] of Object.entries(bridged.patch)) {
|
|
17202
|
+
writeClientEngagement(clientKey, p);
|
|
17203
|
+
written += 1;
|
|
17204
|
+
}
|
|
17205
|
+
}
|
|
17206
|
+
const unmatched = result.customers.filter((c) => !links.some((l) => l.a.externalId === c.id && l.confidence === "asserted")).map((c) => ({ stripeCustomerId: c.id, email: c.email ?? null, name: c.name ?? null }));
|
|
17207
|
+
return {
|
|
17208
|
+
health: result.health,
|
|
17209
|
+
read: { subscriptions: result.subs.length, invoices: result.invoices.length, customers: result.customers.length },
|
|
17210
|
+
wouldChange: bridged.patch,
|
|
17211
|
+
written: written > 0 ? `${written} client record(s) updated` : false,
|
|
17212
|
+
owed: owed.lines,
|
|
17213
|
+
owedYouCannotName: owed.unattributedCount ? { count: owed.unattributedCount, total: owed.unattributedTotal } : null,
|
|
17214
|
+
renewalsInside30Days: renewals,
|
|
17215
|
+
disagreements: bridged.disagreements,
|
|
17216
|
+
needsYourConfirmation: unmatched,
|
|
17217
|
+
note: written > 0 ? "Written. Tomorrow's brief will know what each of these clients pays and when they renew." : "Nothing was changed. Re-run with apply:true once the matches above are right."
|
|
17218
|
+
};
|
|
17219
|
+
}
|
|
17220
|
+
);
|
|
17221
|
+
}
|
|
17222
|
+
var import_zod57;
|
|
17223
|
+
var init_connectors = __esm({
|
|
17224
|
+
"src/tools/connectors.ts"() {
|
|
17225
|
+
"use strict";
|
|
17226
|
+
import_zod57 = require("zod");
|
|
17227
|
+
init_tool_helpers();
|
|
17228
|
+
init_store2();
|
|
17229
|
+
init_stripe();
|
|
17230
|
+
init_to_engagements();
|
|
17231
|
+
init_client_engagements();
|
|
17232
|
+
}
|
|
17233
|
+
});
|
|
17234
|
+
|
|
16630
17235
|
// src/client-week.ts
|
|
16631
17236
|
function daysUntil(then, now) {
|
|
16632
17237
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(then);
|
|
@@ -16873,17 +17478,17 @@ var init_stage_boundary = __esm({
|
|
|
16873
17478
|
// src/plan-guide.ts
|
|
16874
17479
|
function skillRoot() {
|
|
16875
17480
|
const candidates = [
|
|
16876
|
-
|
|
17481
|
+
path11.join(__dirname, "..", "skills", "blueprint"),
|
|
16877
17482
|
// dist/ → package root
|
|
16878
|
-
|
|
17483
|
+
path11.join(__dirname, "..", "..", "skills", "blueprint"),
|
|
16879
17484
|
// src/ in dev
|
|
16880
|
-
|
|
17485
|
+
path11.join(process.cwd(), "skills", "blueprint")
|
|
16881
17486
|
];
|
|
16882
|
-
return candidates.find((c) =>
|
|
17487
|
+
return candidates.find((c) => fs12.existsSync(path11.join(c, "references", "build-plan-schema.md"))) ?? null;
|
|
16883
17488
|
}
|
|
16884
17489
|
function readIf(p, max) {
|
|
16885
17490
|
try {
|
|
16886
|
-
const t =
|
|
17491
|
+
const t = fs12.readFileSync(p, "utf8");
|
|
16887
17492
|
return t.length > max ? t.slice(0, max) : t;
|
|
16888
17493
|
} catch {
|
|
16889
17494
|
return null;
|
|
@@ -16896,12 +17501,12 @@ function presetForIndustry(industry) {
|
|
|
16896
17501
|
function loadPlanGuide(industry) {
|
|
16897
17502
|
const root = skillRoot();
|
|
16898
17503
|
if (!root) return "";
|
|
16899
|
-
const schema = readIf(
|
|
17504
|
+
const schema = readIf(path11.join(root, "references", "build-plan-schema.md"), CAP_SCHEMA);
|
|
16900
17505
|
const presetName = presetForIndustry(industry);
|
|
16901
|
-
const preset = readIf(
|
|
17506
|
+
const preset = readIf(path11.join(root, "presets", `${presetName}.preset.json`), CAP_PRESET);
|
|
16902
17507
|
const exampleFile = EXAMPLE_FOR_PRESET[presetName] ?? "sample-build-plan.json";
|
|
16903
|
-
const example = readIf(
|
|
16904
|
-
const copyGuide = readIf(
|
|
17508
|
+
const example = readIf(path11.join(root, "examples", exampleFile), CAP_EXAMPLE);
|
|
17509
|
+
const copyGuide = readIf(path11.join(root, "references", "copy-guide.md"), CAP_COPY_GUIDE);
|
|
16905
17510
|
if (!schema) return "";
|
|
16906
17511
|
return `
|
|
16907
17512
|
PLAN GUIDE \u2014 everything you need to write the build plan is below. Do NOT try to read files; you cannot.
|
|
@@ -16919,12 +17524,12 @@ ${example}
|
|
|
16919
17524
|
` : "") + `=== End of plan guide ===
|
|
16920
17525
|
`;
|
|
16921
17526
|
}
|
|
16922
|
-
var
|
|
17527
|
+
var fs12, path11, PRESET_FOR_INDUSTRY, EXAMPLE_FOR_PRESET, COPY_INSTRUCTION, CALENDAR_INSTRUCTION, CAP_SCHEMA, CAP_PRESET, CAP_EXAMPLE, CAP_COPY_GUIDE;
|
|
16923
17528
|
var init_plan_guide = __esm({
|
|
16924
17529
|
"src/plan-guide.ts"() {
|
|
16925
17530
|
"use strict";
|
|
16926
|
-
|
|
16927
|
-
|
|
17531
|
+
fs12 = __toESM(require("fs"));
|
|
17532
|
+
path11 = __toESM(require("path"));
|
|
16928
17533
|
PRESET_FOR_INDUSTRY = {
|
|
16929
17534
|
"med-spa": "med-spa",
|
|
16930
17535
|
clinic: "clinic",
|
|
@@ -17480,7 +18085,7 @@ var init_question_set = __esm({
|
|
|
17480
18085
|
|
|
17481
18086
|
// src/intake-to-build/plan.ts
|
|
17482
18087
|
function nsRef(ns) {
|
|
17483
|
-
return
|
|
18088
|
+
return import_zod58.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
|
|
17484
18089
|
}
|
|
17485
18090
|
function refNamespace(ref) {
|
|
17486
18091
|
return ref.split(".")[0];
|
|
@@ -17890,11 +18495,11 @@ function validateBuildPlan(input) {
|
|
|
17890
18495
|
referencesScanned: scanned
|
|
17891
18496
|
};
|
|
17892
18497
|
}
|
|
17893
|
-
var
|
|
18498
|
+
var import_zod58, REF_NAMESPACES, REF_RE, refSchema, USER_PENDING_REF, userRefSchema, userRefOrPendingSchema, USER_ROLES, EMAIL_RE, userSchema, stageSchema, pipelineSchema, GHL_FIELD_DATATYPES, customFieldSchema, tagSchema, customValueSchema, CALENDAR_TYPES, TEAMLESS_CALENDAR_TYPES, SINGLE_STAFF_CALENDAR_TYPE, openHoursBlockSchema, CALENDAR_SLOT_UNITS, calendarSchema, formFieldSchema, formSchema, pageSchema, FUNNEL_TARGETS, FUNNEL_HOSTS, funnelSchema, emailAssetSchema, smsAssetSchema, emailTemplateSchema, smsTemplateSchema, templatesSchema, waitUnit, branchActionOptions, branchActionSchema, findOpportunitySchema, actionSchema, APPOINTMENT_STATUSES, CALL_STATUSES, NUMBER_VALIDATION_STATES, triggerSchema, workflowSchema, HANDOFF_OWNER_LEGACY, handoffSchema, buildPlanSchema, PLAN_ERROR_CODES, NURTURE_MIN_DAYS, WAIT_UNIT_DAYS, isNurtureName, isSpeedName, KNOWN_STANDARD_FORM_KEYS;
|
|
17894
18499
|
var init_plan = __esm({
|
|
17895
18500
|
"src/intake-to-build/plan.ts"() {
|
|
17896
18501
|
"use strict";
|
|
17897
|
-
|
|
18502
|
+
import_zod58 = require("zod");
|
|
17898
18503
|
REF_NAMESPACES = [
|
|
17899
18504
|
"pipeline",
|
|
17900
18505
|
"stage",
|
|
@@ -17915,29 +18520,29 @@ var init_plan = __esm({
|
|
|
17915
18520
|
"sms_template"
|
|
17916
18521
|
];
|
|
17917
18522
|
REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
|
|
17918
|
-
refSchema =
|
|
18523
|
+
refSchema = import_zod58.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
|
|
17919
18524
|
USER_PENDING_REF = "user.__pending__";
|
|
17920
18525
|
userRefSchema = nsRef("user");
|
|
17921
|
-
userRefOrPendingSchema =
|
|
18526
|
+
userRefOrPendingSchema = import_zod58.z.union([userRefSchema, import_zod58.z.literal(USER_PENDING_REF)]);
|
|
17922
18527
|
USER_ROLES = ["admin", "user"];
|
|
17923
18528
|
EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
17924
|
-
userSchema =
|
|
18529
|
+
userSchema = import_zod58.z.object({
|
|
17925
18530
|
ref: userRefSchema,
|
|
17926
|
-
firstName:
|
|
17927
|
-
lastName:
|
|
17928
|
-
email:
|
|
17929
|
-
role:
|
|
17930
|
-
phone:
|
|
18531
|
+
firstName: import_zod58.z.string().min(1),
|
|
18532
|
+
lastName: import_zod58.z.string().min(1),
|
|
18533
|
+
email: import_zod58.z.string().regex(EMAIL_RE, "must be an email address (GHL creates the login from it)"),
|
|
18534
|
+
role: import_zod58.z.enum(USER_ROLES),
|
|
18535
|
+
phone: import_zod58.z.string().optional()
|
|
17931
18536
|
});
|
|
17932
|
-
stageSchema =
|
|
18537
|
+
stageSchema = import_zod58.z.object({
|
|
17933
18538
|
ref: nsRef("stage"),
|
|
17934
|
-
name:
|
|
17935
|
-
position:
|
|
18539
|
+
name: import_zod58.z.string(),
|
|
18540
|
+
position: import_zod58.z.number().int().nonnegative()
|
|
17936
18541
|
});
|
|
17937
|
-
pipelineSchema =
|
|
18542
|
+
pipelineSchema = import_zod58.z.object({
|
|
17938
18543
|
ref: nsRef("pipeline"),
|
|
17939
|
-
name:
|
|
17940
|
-
stages:
|
|
18544
|
+
name: import_zod58.z.string(),
|
|
18545
|
+
stages: import_zod58.z.array(stageSchema).min(1)
|
|
17941
18546
|
});
|
|
17942
18547
|
GHL_FIELD_DATATYPES = [
|
|
17943
18548
|
"TEXT",
|
|
@@ -17954,21 +18559,21 @@ var init_plan = __esm({
|
|
|
17954
18559
|
"FILE_UPLOAD",
|
|
17955
18560
|
"SIGNATURE"
|
|
17956
18561
|
];
|
|
17957
|
-
customFieldSchema =
|
|
18562
|
+
customFieldSchema = import_zod58.z.object({
|
|
17958
18563
|
ref: nsRef("field"),
|
|
17959
|
-
name:
|
|
17960
|
-
dataType:
|
|
17961
|
-
model:
|
|
17962
|
-
options:
|
|
18564
|
+
name: import_zod58.z.string(),
|
|
18565
|
+
dataType: import_zod58.z.enum(GHL_FIELD_DATATYPES),
|
|
18566
|
+
model: import_zod58.z.enum(["contact", "opportunity"]).optional(),
|
|
18567
|
+
options: import_zod58.z.array(import_zod58.z.string()).optional()
|
|
17963
18568
|
});
|
|
17964
|
-
tagSchema =
|
|
18569
|
+
tagSchema = import_zod58.z.object({
|
|
17965
18570
|
ref: nsRef("tag"),
|
|
17966
|
-
name:
|
|
18571
|
+
name: import_zod58.z.string()
|
|
17967
18572
|
});
|
|
17968
|
-
customValueSchema =
|
|
18573
|
+
customValueSchema = import_zod58.z.object({
|
|
17969
18574
|
ref: nsRef("cv"),
|
|
17970
|
-
name:
|
|
17971
|
-
value:
|
|
18575
|
+
name: import_zod58.z.string(),
|
|
18576
|
+
value: import_zod58.z.string().optional(),
|
|
17972
18577
|
filledBy: refSchema.optional()
|
|
17973
18578
|
});
|
|
17974
18579
|
CALENDAR_TYPES = [
|
|
@@ -17980,128 +18585,128 @@ var init_plan = __esm({
|
|
|
17980
18585
|
];
|
|
17981
18586
|
TEAMLESS_CALENDAR_TYPES = /* @__PURE__ */ new Set(["event"]);
|
|
17982
18587
|
SINGLE_STAFF_CALENDAR_TYPE = "round_robin";
|
|
17983
|
-
openHoursBlockSchema =
|
|
17984
|
-
daysOfTheWeek:
|
|
17985
|
-
hours:
|
|
17986
|
-
|
|
17987
|
-
openHour:
|
|
17988
|
-
openMinute:
|
|
17989
|
-
closeHour:
|
|
17990
|
-
closeMinute:
|
|
18588
|
+
openHoursBlockSchema = import_zod58.z.object({
|
|
18589
|
+
daysOfTheWeek: import_zod58.z.array(import_zod58.z.number().int().min(0).max(6)),
|
|
18590
|
+
hours: import_zod58.z.array(
|
|
18591
|
+
import_zod58.z.object({
|
|
18592
|
+
openHour: import_zod58.z.number().int().min(0).max(23),
|
|
18593
|
+
openMinute: import_zod58.z.number().int().min(0).max(59),
|
|
18594
|
+
closeHour: import_zod58.z.number().int().min(0).max(23),
|
|
18595
|
+
closeMinute: import_zod58.z.number().int().min(0).max(59)
|
|
17991
18596
|
})
|
|
17992
18597
|
)
|
|
17993
18598
|
});
|
|
17994
18599
|
CALENDAR_SLOT_UNITS = ["mins", "hours"];
|
|
17995
|
-
calendarSchema =
|
|
18600
|
+
calendarSchema = import_zod58.z.object({
|
|
17996
18601
|
ref: nsRef("calendar"),
|
|
17997
|
-
name:
|
|
17998
|
-
calendarType:
|
|
17999
|
-
openHours:
|
|
18000
|
-
availabilityType:
|
|
18001
|
-
requiresStaff:
|
|
18602
|
+
name: import_zod58.z.string(),
|
|
18603
|
+
calendarType: import_zod58.z.enum(CALENDAR_TYPES),
|
|
18604
|
+
openHours: import_zod58.z.array(openHoursBlockSchema).optional(),
|
|
18605
|
+
availabilityType: import_zod58.z.number().int().optional(),
|
|
18606
|
+
requiresStaff: import_zod58.z.boolean().optional(),
|
|
18002
18607
|
/** Slot length (finding 25, 2026-08-26). GoHighLevel defaults to 30-minute
|
|
18003
18608
|
* slots when this is omitted — a brief that says "Discovery Call, 15
|
|
18004
18609
|
* minutes" must land here as slotDuration: 15. Unit defaults to "mins". */
|
|
18005
|
-
slotDuration:
|
|
18006
|
-
slotDurationUnit:
|
|
18610
|
+
slotDuration: import_zod58.z.number().int().positive().optional(),
|
|
18611
|
+
slotDurationUnit: import_zod58.z.enum(CALENDAR_SLOT_UNITS).optional(),
|
|
18007
18612
|
/** Minutes between slot start times (defaults to the slot duration in GHL). */
|
|
18008
|
-
slotInterval:
|
|
18613
|
+
slotInterval: import_zod58.z.number().int().positive().optional(),
|
|
18009
18614
|
/** Minutes of buffer after each appointment. */
|
|
18010
|
-
slotBuffer:
|
|
18615
|
+
slotBuffer: import_zod58.z.number().int().min(0).optional(),
|
|
18011
18616
|
/** v2: the plan users on this calendar (the executor assigns them after the
|
|
18012
18617
|
* users exist). Omitted → the executor's staff handoff applies as before. */
|
|
18013
|
-
teamMemberRefs:
|
|
18618
|
+
teamMemberRefs: import_zod58.z.array(userRefSchema).optional()
|
|
18014
18619
|
});
|
|
18015
|
-
formFieldSchema =
|
|
18016
|
-
|
|
18017
|
-
type:
|
|
18018
|
-
key:
|
|
18019
|
-
required:
|
|
18620
|
+
formFieldSchema = import_zod58.z.discriminatedUnion("type", [
|
|
18621
|
+
import_zod58.z.object({
|
|
18622
|
+
type: import_zod58.z.literal("standard"),
|
|
18623
|
+
key: import_zod58.z.string(),
|
|
18624
|
+
required: import_zod58.z.boolean().optional()
|
|
18020
18625
|
}),
|
|
18021
|
-
|
|
18022
|
-
type:
|
|
18626
|
+
import_zod58.z.object({
|
|
18627
|
+
type: import_zod58.z.literal("custom"),
|
|
18023
18628
|
fieldRef: nsRef("field"),
|
|
18024
|
-
required:
|
|
18629
|
+
required: import_zod58.z.boolean().optional()
|
|
18025
18630
|
})
|
|
18026
18631
|
]);
|
|
18027
|
-
formSchema =
|
|
18632
|
+
formSchema = import_zod58.z.object({
|
|
18028
18633
|
ref: nsRef("form"),
|
|
18029
|
-
name:
|
|
18030
|
-
fields:
|
|
18634
|
+
name: import_zod58.z.string(),
|
|
18635
|
+
fields: import_zod58.z.array(formFieldSchema)
|
|
18031
18636
|
});
|
|
18032
|
-
pageSchema =
|
|
18637
|
+
pageSchema = import_zod58.z.object({
|
|
18033
18638
|
ref: nsRef("page"),
|
|
18034
|
-
name:
|
|
18035
|
-
role:
|
|
18036
|
-
outline:
|
|
18639
|
+
name: import_zod58.z.string(),
|
|
18640
|
+
role: import_zod58.z.string().optional(),
|
|
18641
|
+
outline: import_zod58.z.string().optional(),
|
|
18037
18642
|
formRef: nsRef("form").optional(),
|
|
18038
18643
|
calendarRef: nsRef("calendar").optional()
|
|
18039
18644
|
});
|
|
18040
18645
|
FUNNEL_TARGETS = ["ghl", "external"];
|
|
18041
18646
|
FUNNEL_HOSTS = ["cloudflare", "vercel"];
|
|
18042
|
-
funnelSchema =
|
|
18647
|
+
funnelSchema = import_zod58.z.object({
|
|
18043
18648
|
ref: nsRef("funnel"),
|
|
18044
|
-
name:
|
|
18649
|
+
name: import_zod58.z.string(),
|
|
18045
18650
|
// Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
|
|
18046
18651
|
// "external" = the subscriber builds + hosts the site themselves (Cloudflare/
|
|
18047
18652
|
// Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
|
|
18048
18653
|
// see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
|
|
18049
18654
|
// deploy an external funnel; it surfaces the GHL-side wiring info.
|
|
18050
|
-
target:
|
|
18051
|
-
host:
|
|
18655
|
+
target: import_zod58.z.enum(FUNNEL_TARGETS).optional(),
|
|
18656
|
+
host: import_zod58.z.enum(FUNNEL_HOSTS).optional(),
|
|
18052
18657
|
// external only
|
|
18053
|
-
domain:
|
|
18658
|
+
domain: import_zod58.z.string().optional(),
|
|
18054
18659
|
// external only
|
|
18055
|
-
pages:
|
|
18660
|
+
pages: import_zod58.z.array(pageSchema)
|
|
18056
18661
|
});
|
|
18057
|
-
emailAssetSchema =
|
|
18662
|
+
emailAssetSchema = import_zod58.z.object({
|
|
18058
18663
|
ref: nsRef("email"),
|
|
18059
|
-
name:
|
|
18060
|
-
subject:
|
|
18061
|
-
bodyOutline:
|
|
18062
|
-
body:
|
|
18063
|
-
mergeTags:
|
|
18664
|
+
name: import_zod58.z.string(),
|
|
18665
|
+
subject: import_zod58.z.string().optional(),
|
|
18666
|
+
bodyOutline: import_zod58.z.string().optional(),
|
|
18667
|
+
body: import_zod58.z.string().optional(),
|
|
18668
|
+
mergeTags: import_zod58.z.array(import_zod58.z.string()).optional()
|
|
18064
18669
|
});
|
|
18065
|
-
smsAssetSchema =
|
|
18670
|
+
smsAssetSchema = import_zod58.z.object({
|
|
18066
18671
|
ref: nsRef("sms"),
|
|
18067
|
-
name:
|
|
18068
|
-
bodyOutline:
|
|
18069
|
-
body:
|
|
18070
|
-
mergeTags:
|
|
18672
|
+
name: import_zod58.z.string(),
|
|
18673
|
+
bodyOutline: import_zod58.z.string().optional(),
|
|
18674
|
+
body: import_zod58.z.string().optional(),
|
|
18675
|
+
mergeTags: import_zod58.z.array(import_zod58.z.string()).optional()
|
|
18071
18676
|
});
|
|
18072
|
-
emailTemplateSchema =
|
|
18677
|
+
emailTemplateSchema = import_zod58.z.object({
|
|
18073
18678
|
ref: nsRef("email_template"),
|
|
18074
|
-
name:
|
|
18075
|
-
subject:
|
|
18076
|
-
html:
|
|
18679
|
+
name: import_zod58.z.string().min(1),
|
|
18680
|
+
subject: import_zod58.z.string().min(1),
|
|
18681
|
+
html: import_zod58.z.string().min(1)
|
|
18077
18682
|
});
|
|
18078
|
-
smsTemplateSchema =
|
|
18683
|
+
smsTemplateSchema = import_zod58.z.object({
|
|
18079
18684
|
ref: nsRef("sms_template"),
|
|
18080
|
-
name:
|
|
18081
|
-
body:
|
|
18685
|
+
name: import_zod58.z.string().min(1),
|
|
18686
|
+
body: import_zod58.z.string().min(1)
|
|
18082
18687
|
});
|
|
18083
|
-
templatesSchema =
|
|
18084
|
-
emails:
|
|
18085
|
-
sms:
|
|
18688
|
+
templatesSchema = import_zod58.z.object({
|
|
18689
|
+
emails: import_zod58.z.array(emailTemplateSchema).optional(),
|
|
18690
|
+
sms: import_zod58.z.array(smsTemplateSchema).optional()
|
|
18086
18691
|
});
|
|
18087
|
-
waitUnit =
|
|
18692
|
+
waitUnit = import_zod58.z.enum(["minutes", "hours", "days"]);
|
|
18088
18693
|
branchActionOptions = [
|
|
18089
|
-
|
|
18090
|
-
|
|
18694
|
+
import_zod58.z.object({ type: import_zod58.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
|
|
18695
|
+
import_zod58.z.object({ type: import_zod58.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
|
|
18091
18696
|
// send_email / send_sms: point at a 5.8 asset (`emailRef`/`smsRef`, the 0.1
|
|
18092
18697
|
// way) or a 5.8a template (`templateRef`, v2). At least one is required —
|
|
18093
18698
|
// enforced by validateBuildPlan so the message reads the same as a dead ref.
|
|
18094
|
-
|
|
18095
|
-
type:
|
|
18699
|
+
import_zod58.z.object({
|
|
18700
|
+
type: import_zod58.z.literal("send_email"),
|
|
18096
18701
|
emailRef: nsRef("email").optional(),
|
|
18097
18702
|
templateRef: nsRef("email_template").optional()
|
|
18098
18703
|
}),
|
|
18099
|
-
|
|
18100
|
-
type:
|
|
18704
|
+
import_zod58.z.object({
|
|
18705
|
+
type: import_zod58.z.literal("send_sms"),
|
|
18101
18706
|
smsRef: nsRef("sms").optional(),
|
|
18102
18707
|
templateRef: nsRef("sms_template").optional()
|
|
18103
18708
|
}),
|
|
18104
|
-
|
|
18709
|
+
import_zod58.z.object({ type: import_zod58.z.literal("wait"), value: import_zod58.z.number().positive(), unit: waitUnit }),
|
|
18105
18710
|
// Appointment-relative wait ("wait until N BEFORE the appointment").
|
|
18106
18711
|
// Only works when the workflow has an appointment in context (i.e. an
|
|
18107
18712
|
// `appointment` trigger) — enforced by validateBuildPlan. Expands to GHL's
|
|
@@ -18110,91 +18715,91 @@ var init_plan = __esm({
|
|
|
18110
18715
|
// (GHL stores whole minutes). Only "before" is emitted today — that's the
|
|
18111
18716
|
// shape we captured + proved; "after" (post-appointment follow-up) is
|
|
18112
18717
|
// deferred until its shape is captured from a real workflow.
|
|
18113
|
-
|
|
18114
|
-
type:
|
|
18115
|
-
value:
|
|
18718
|
+
import_zod58.z.object({
|
|
18719
|
+
type: import_zod58.z.literal("wait_appointment"),
|
|
18720
|
+
value: import_zod58.z.number().int().positive(),
|
|
18116
18721
|
unit: waitUnit
|
|
18117
18722
|
}),
|
|
18118
18723
|
// internal_notification: WHO gets pinged is a plan ref (`userRef`) resolved
|
|
18119
18724
|
// by the executor, or the pending sentinel. `to` (a literal user id or the
|
|
18120
18725
|
// old "assigned_user" hint) is still accepted for 0.1 plans and warned on.
|
|
18121
18726
|
// One of the two is required — enforced by validateBuildPlan.
|
|
18122
|
-
|
|
18123
|
-
type:
|
|
18124
|
-
to:
|
|
18727
|
+
import_zod58.z.object({
|
|
18728
|
+
type: import_zod58.z.literal("internal_notification"),
|
|
18729
|
+
to: import_zod58.z.string().optional(),
|
|
18125
18730
|
userRef: userRefOrPendingSchema.optional(),
|
|
18126
|
-
title:
|
|
18127
|
-
body:
|
|
18731
|
+
title: import_zod58.z.string(),
|
|
18732
|
+
body: import_zod58.z.string()
|
|
18128
18733
|
}),
|
|
18129
|
-
|
|
18130
|
-
type:
|
|
18734
|
+
import_zod58.z.object({
|
|
18735
|
+
type: import_zod58.z.literal("update_contact_field"),
|
|
18131
18736
|
fieldRef: nsRef("field"),
|
|
18132
|
-
value:
|
|
18737
|
+
value: import_zod58.z.string()
|
|
18133
18738
|
}),
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
type:
|
|
18137
|
-
title:
|
|
18138
|
-
body:
|
|
18139
|
-
dueDate:
|
|
18140
|
-
assignedTo:
|
|
18739
|
+
import_zod58.z.object({ type: import_zod58.z.literal("add_notes"), body: import_zod58.z.string() }),
|
|
18740
|
+
import_zod58.z.object({
|
|
18741
|
+
type: import_zod58.z.literal("task_notification"),
|
|
18742
|
+
title: import_zod58.z.string(),
|
|
18743
|
+
body: import_zod58.z.string().optional(),
|
|
18744
|
+
dueDate: import_zod58.z.string().optional(),
|
|
18745
|
+
assignedTo: import_zod58.z.string().optional(),
|
|
18141
18746
|
/** v2: the plan user the task is assigned to (or the pending sentinel). */
|
|
18142
18747
|
userRef: userRefOrPendingSchema.optional()
|
|
18143
18748
|
}),
|
|
18144
18749
|
// assign_user (v2): GHL "Assign to user" — the contact's owner becomes the
|
|
18145
18750
|
// referenced plan user. Round-robin among several users is a calendar
|
|
18146
18751
|
// concern (teamMemberRefs), not this step's.
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
type:
|
|
18752
|
+
import_zod58.z.object({ type: import_zod58.z.literal("assign_user"), userRef: userRefOrPendingSchema }),
|
|
18753
|
+
import_zod58.z.object({ type: import_zod58.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
|
|
18754
|
+
import_zod58.z.object({ type: import_zod58.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
|
|
18755
|
+
import_zod58.z.object({
|
|
18756
|
+
type: import_zod58.z.literal("create_opportunity"),
|
|
18152
18757
|
pipelineRef: nsRef("pipeline"),
|
|
18153
18758
|
stageRef: nsRef("stage"),
|
|
18154
18759
|
// Opportunity name (merge fields allowed). Defaults to the contact's name.
|
|
18155
18760
|
// Required by GHL's create node; without it the create silently no-ops.
|
|
18156
|
-
name:
|
|
18761
|
+
name: import_zod58.z.string().optional(),
|
|
18157
18762
|
// Opportunity monetary value (the deal/sale dollar amount). A string so it
|
|
18158
18763
|
// can be a literal ("2500") OR a merge field ("{{contact.package_value}}").
|
|
18159
18764
|
// Optional — omitted → GHL leaves the value unset. Shape captured from Lux
|
|
18160
18765
|
// Bio "14. Package Sale". Lux models the lifecycle by pipeline STAGE, not GHL
|
|
18161
18766
|
// won/lost status, so a "won" opp = move to the closing stage WITH this value.
|
|
18162
|
-
value:
|
|
18767
|
+
value: import_zod58.z.string().optional()
|
|
18163
18768
|
}),
|
|
18164
|
-
|
|
18165
|
-
type:
|
|
18769
|
+
import_zod58.z.object({
|
|
18770
|
+
type: import_zod58.z.literal("update_opportunity"),
|
|
18166
18771
|
pipelineRef: nsRef("pipeline"),
|
|
18167
18772
|
stageRef: nsRef("stage"),
|
|
18168
18773
|
// Opportunity monetary value (see create_opportunity.value). Optional.
|
|
18169
|
-
value:
|
|
18774
|
+
value: import_zod58.z.string().optional()
|
|
18170
18775
|
}),
|
|
18171
|
-
|
|
18172
|
-
type:
|
|
18173
|
-
goalCondition:
|
|
18776
|
+
import_zod58.z.object({
|
|
18777
|
+
type: import_zod58.z.literal("goal_event"),
|
|
18778
|
+
goalCondition: import_zod58.z.string(),
|
|
18174
18779
|
// GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
|
|
18175
|
-
action:
|
|
18780
|
+
action: import_zod58.z.enum(["exit", "continue", "wait"]).optional()
|
|
18176
18781
|
})
|
|
18177
18782
|
];
|
|
18178
|
-
branchActionSchema =
|
|
18179
|
-
findOpportunitySchema =
|
|
18180
|
-
type:
|
|
18783
|
+
branchActionSchema = import_zod58.z.discriminatedUnion("type", branchActionOptions);
|
|
18784
|
+
findOpportunitySchema = import_zod58.z.object({
|
|
18785
|
+
type: import_zod58.z.literal("find_opportunity"),
|
|
18181
18786
|
pipelineRef: nsRef("pipeline"),
|
|
18182
|
-
found:
|
|
18183
|
-
notFound:
|
|
18787
|
+
found: import_zod58.z.array(branchActionSchema).default([]),
|
|
18788
|
+
notFound: import_zod58.z.array(branchActionSchema).default([])
|
|
18184
18789
|
});
|
|
18185
|
-
actionSchema =
|
|
18790
|
+
actionSchema = import_zod58.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
|
|
18186
18791
|
APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
|
|
18187
18792
|
CALL_STATUSES = ["busy", "canceled", "voicemail", "no-answer", "completed"];
|
|
18188
18793
|
NUMBER_VALIDATION_STATES = ["not_valid", "sms_incapable"];
|
|
18189
|
-
triggerSchema =
|
|
18190
|
-
type:
|
|
18794
|
+
triggerSchema = import_zod58.z.object({
|
|
18795
|
+
type: import_zod58.z.string(),
|
|
18191
18796
|
formRef: nsRef("form").optional(),
|
|
18192
18797
|
tagRef: nsRef("tag").optional(),
|
|
18193
18798
|
calendarRef: nsRef("calendar").optional(),
|
|
18194
18799
|
pipelineRef: nsRef("pipeline").optional(),
|
|
18195
18800
|
stageRef: nsRef("stage").optional(),
|
|
18196
18801
|
// Required for a native `appointment` trigger (the status it fires on).
|
|
18197
|
-
appointmentStatus:
|
|
18802
|
+
appointmentStatus: import_zod58.z.enum(APPOINTMENT_STATUSES).optional(),
|
|
18198
18803
|
// ── customer_reply scoping ──────────────────────────────────────
|
|
18199
18804
|
// A bare customer_reply trigger fires on EVERY inbound reply from EVERY
|
|
18200
18805
|
// contact. That is almost never what a plan means: "alert the owner when a
|
|
@@ -18208,27 +18813,27 @@ var init_plan = __esm({
|
|
|
18208
18813
|
// Both optional and additive — omitting them keeps the long-standing
|
|
18209
18814
|
// fires-on-any-reply baseline.
|
|
18210
18815
|
hasTagRef: nsRef("tag").optional(),
|
|
18211
|
-
replyIntent:
|
|
18816
|
+
replyIntent: import_zod58.z.enum(["positive", "negative"]).optional(),
|
|
18212
18817
|
// ── call_status scoping (missed-call text-back) ─────────────────
|
|
18213
18818
|
// GHL's call_status trigger fires on a completed call attempt. The states it
|
|
18214
18819
|
// exposes are exactly what a "missed call" means operationally. Required for
|
|
18215
18820
|
// a call_status trigger — without them the trigger would fire on EVERY call
|
|
18216
18821
|
// including answered ones, so the executor refuses rather than guess.
|
|
18217
|
-
callStatuses:
|
|
18218
|
-
callDirection:
|
|
18822
|
+
callStatuses: import_zod58.z.array(import_zod58.z.enum(CALL_STATUSES)).optional(),
|
|
18823
|
+
callDirection: import_zod58.z.enum(["inbound", "outbound"]).optional(),
|
|
18219
18824
|
// ── validation_error (GHL UI: "Number validation") ──────────────
|
|
18220
18825
|
// Fires after a phone number passes or fails a validation check. The UI shows
|
|
18221
18826
|
// "Not valid" / "SMS incapable"; the WIRE values are snake_case. Required for
|
|
18222
18827
|
// a validation_error trigger — without them the executor refuses rather than
|
|
18223
18828
|
// guess which failure states to fire on.
|
|
18224
|
-
numberValidation:
|
|
18829
|
+
numberValidation: import_zod58.z.array(import_zod58.z.enum(NUMBER_VALIDATION_STATES)).optional()
|
|
18225
18830
|
});
|
|
18226
|
-
workflowSchema =
|
|
18831
|
+
workflowSchema = import_zod58.z.object({
|
|
18227
18832
|
ref: nsRef("workflow"),
|
|
18228
|
-
name:
|
|
18833
|
+
name: import_zod58.z.string(),
|
|
18229
18834
|
trigger: triggerSchema.optional(),
|
|
18230
|
-
stopOnResponse:
|
|
18231
|
-
actions:
|
|
18835
|
+
stopOnResponse: import_zod58.z.boolean().optional(),
|
|
18836
|
+
actions: import_zod58.z.array(actionSchema).max(40)
|
|
18232
18837
|
// house rule: <=40 actions/workflow
|
|
18233
18838
|
});
|
|
18234
18839
|
HANDOFF_OWNER_LEGACY = {
|
|
@@ -18236,37 +18841,37 @@ var init_plan = __esm({
|
|
|
18236
18841
|
"JERRY-EXT": "OPERATOR-EXT",
|
|
18237
18842
|
"SASHA": "TEAM"
|
|
18238
18843
|
};
|
|
18239
|
-
handoffSchema =
|
|
18844
|
+
handoffSchema = import_zod58.z.object({
|
|
18240
18845
|
ref: nsRef("handoff"),
|
|
18241
|
-
owner:
|
|
18242
|
-
title:
|
|
18243
|
-
trigger:
|
|
18244
|
-
instruction:
|
|
18846
|
+
owner: import_zod58.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
|
|
18847
|
+
title: import_zod58.z.string(),
|
|
18848
|
+
trigger: import_zod58.z.string().optional(),
|
|
18849
|
+
instruction: import_zod58.z.string(),
|
|
18245
18850
|
produces: refSchema.nullable().optional(),
|
|
18246
|
-
successCheck:
|
|
18247
|
-
blocks:
|
|
18851
|
+
successCheck: import_zod58.z.string(),
|
|
18852
|
+
blocks: import_zod58.z.array(import_zod58.z.string()).optional()
|
|
18248
18853
|
});
|
|
18249
|
-
buildPlanSchema =
|
|
18250
|
-
schemaVersion:
|
|
18251
|
-
planId:
|
|
18252
|
-
briefId:
|
|
18253
|
-
preset:
|
|
18254
|
-
summary:
|
|
18255
|
-
users:
|
|
18256
|
-
pipelines:
|
|
18257
|
-
customFields:
|
|
18258
|
-
tags:
|
|
18259
|
-
customValues:
|
|
18260
|
-
calendars:
|
|
18261
|
-
forms:
|
|
18262
|
-
funnels:
|
|
18263
|
-
emails:
|
|
18264
|
-
sms:
|
|
18854
|
+
buildPlanSchema = import_zod58.z.object({
|
|
18855
|
+
schemaVersion: import_zod58.z.string(),
|
|
18856
|
+
planId: import_zod58.z.string(),
|
|
18857
|
+
briefId: import_zod58.z.string(),
|
|
18858
|
+
preset: import_zod58.z.string(),
|
|
18859
|
+
summary: import_zod58.z.string().optional(),
|
|
18860
|
+
users: import_zod58.z.array(userSchema).optional(),
|
|
18861
|
+
pipelines: import_zod58.z.array(pipelineSchema).optional(),
|
|
18862
|
+
customFields: import_zod58.z.array(customFieldSchema).optional(),
|
|
18863
|
+
tags: import_zod58.z.array(tagSchema).optional(),
|
|
18864
|
+
customValues: import_zod58.z.array(customValueSchema).optional(),
|
|
18865
|
+
calendars: import_zod58.z.array(calendarSchema).optional(),
|
|
18866
|
+
forms: import_zod58.z.array(formSchema).optional(),
|
|
18867
|
+
funnels: import_zod58.z.array(funnelSchema).optional(),
|
|
18868
|
+
emails: import_zod58.z.array(emailAssetSchema).optional(),
|
|
18869
|
+
sms: import_zod58.z.array(smsAssetSchema).optional(),
|
|
18265
18870
|
templates: templatesSchema.optional(),
|
|
18266
|
-
workflows:
|
|
18267
|
-
handoffs:
|
|
18268
|
-
buildOrder:
|
|
18269
|
-
idMap:
|
|
18871
|
+
workflows: import_zod58.z.array(workflowSchema).optional(),
|
|
18872
|
+
handoffs: import_zod58.z.array(handoffSchema).optional(),
|
|
18873
|
+
buildOrder: import_zod58.z.array(import_zod58.z.string()).optional(),
|
|
18874
|
+
idMap: import_zod58.z.record(import_zod58.z.string()).optional()
|
|
18270
18875
|
}).strict();
|
|
18271
18876
|
PLAN_ERROR_CODES = {
|
|
18272
18877
|
/** A workflow named like /nurture/i whose waits add up to < 30 days. */
|
|
@@ -19681,10 +20286,10 @@ function progressLine(e) {
|
|
|
19681
20286
|
}
|
|
19682
20287
|
}
|
|
19683
20288
|
function defaultProgressDir(base = appDataDir()) {
|
|
19684
|
-
return
|
|
20289
|
+
return path12.join(base, "progress");
|
|
19685
20290
|
}
|
|
19686
20291
|
function progressFilePath(dir, locationId2) {
|
|
19687
|
-
return
|
|
20292
|
+
return path12.join(dir, `${locationId2.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
|
|
19688
20293
|
}
|
|
19689
20294
|
function createProgressFileSink(locationId2, plan, dir = process.env[PROGRESS_DIR_ENV]) {
|
|
19690
20295
|
if (!dir) return null;
|
|
@@ -19692,10 +20297,10 @@ function createProgressFileSink(locationId2, plan, dir = process.env[PROGRESS_DI
|
|
|
19692
20297
|
const file = progressFilePath(dir, locationId2);
|
|
19693
20298
|
const write = () => {
|
|
19694
20299
|
try {
|
|
19695
|
-
|
|
20300
|
+
fs13.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
19696
20301
|
const tmp = `${file}.tmp`;
|
|
19697
|
-
|
|
19698
|
-
|
|
20302
|
+
fs13.writeFileSync(tmp, JSON.stringify({ locationId: locationId2, progress: tracker.snapshot() }), { mode: 384 });
|
|
20303
|
+
fs13.renameSync(tmp, file);
|
|
19699
20304
|
} catch {
|
|
19700
20305
|
}
|
|
19701
20306
|
};
|
|
@@ -19713,7 +20318,7 @@ function createProgressFileSink(locationId2, plan, dir = process.env[PROGRESS_DI
|
|
|
19713
20318
|
}
|
|
19714
20319
|
function readProgressSnapshot(dir, locationId2) {
|
|
19715
20320
|
try {
|
|
19716
|
-
const raw = JSON.parse(
|
|
20321
|
+
const raw = JSON.parse(fs13.readFileSync(progressFilePath(dir, locationId2), "utf8"));
|
|
19717
20322
|
const p = raw?.progress;
|
|
19718
20323
|
if (!p || !Array.isArray(p.groups) || typeof p.updatedAt !== "string") return null;
|
|
19719
20324
|
return p;
|
|
@@ -19734,12 +20339,12 @@ function tapPush(arr, onItem) {
|
|
|
19734
20339
|
};
|
|
19735
20340
|
Object.defineProperty(arr, "push", { value: tapped, writable: true, configurable: true, enumerable: false });
|
|
19736
20341
|
}
|
|
19737
|
-
var
|
|
20342
|
+
var fs13, path12, PROGRESS_GROUPS, GROUP_INDEX, TYPE_TO_GROUP, HALT_TYPE_TO_GROUP, CREATING_LINE, BuildProgressTracker, PROGRESS_DIR_ENV;
|
|
19738
20343
|
var init_build_progress = __esm({
|
|
19739
20344
|
"src/build-progress.ts"() {
|
|
19740
20345
|
"use strict";
|
|
19741
|
-
|
|
19742
|
-
|
|
20346
|
+
fs13 = __toESM(require("fs"));
|
|
20347
|
+
path12 = __toESM(require("path"));
|
|
19743
20348
|
init_executor();
|
|
19744
20349
|
init_credentials_store();
|
|
19745
20350
|
PROGRESS_GROUPS = [
|
|
@@ -19882,14 +20487,14 @@ var init_build_progress = __esm({
|
|
|
19882
20487
|
|
|
19883
20488
|
// src/build-journal.ts
|
|
19884
20489
|
function journalDir(base = appDataDir()) {
|
|
19885
|
-
return
|
|
20490
|
+
return path13.join(base, "build-journal");
|
|
19886
20491
|
}
|
|
19887
20492
|
function journalPath(locationId2, base) {
|
|
19888
|
-
return
|
|
20493
|
+
return path13.join(journalDir(base), `${locationId2}.jsonl`);
|
|
19889
20494
|
}
|
|
19890
20495
|
function newRunId(now = /* @__PURE__ */ new Date(), rand = defaultRand) {
|
|
19891
|
-
const
|
|
19892
|
-
const stamp = `${
|
|
20496
|
+
const iso2 = now.toISOString();
|
|
20497
|
+
const stamp = `${iso2.slice(0, 4)}${iso2.slice(5, 7)}${iso2.slice(8, 10)}T${iso2.slice(11, 13)}${iso2.slice(14, 16)}`;
|
|
19893
20498
|
return `r-${stamp}-${rand()}`;
|
|
19894
20499
|
}
|
|
19895
20500
|
function defaultRand() {
|
|
@@ -19931,16 +20536,16 @@ function buildRunRecord(input) {
|
|
|
19931
20536
|
}
|
|
19932
20537
|
function appendJournal(record, base) {
|
|
19933
20538
|
const dir = journalDir(base);
|
|
19934
|
-
|
|
20539
|
+
fs14.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
19935
20540
|
const file = journalPath(record.locationId, base);
|
|
19936
|
-
|
|
20541
|
+
fs14.appendFileSync(file, `${JSON.stringify(record)}
|
|
19937
20542
|
`, { mode: 384 });
|
|
19938
20543
|
return file;
|
|
19939
20544
|
}
|
|
19940
20545
|
function readJournal(locationId2, base) {
|
|
19941
20546
|
let raw;
|
|
19942
20547
|
try {
|
|
19943
|
-
raw =
|
|
20548
|
+
raw = fs14.readFileSync(journalPath(locationId2, base), "utf8");
|
|
19944
20549
|
} catch {
|
|
19945
20550
|
return [];
|
|
19946
20551
|
}
|
|
@@ -19984,12 +20589,12 @@ function describeRuns(locationId2, base) {
|
|
|
19984
20589
|
reverted: reverted.has(r.runId)
|
|
19985
20590
|
})).reverse();
|
|
19986
20591
|
}
|
|
19987
|
-
var
|
|
20592
|
+
var fs14, path13;
|
|
19988
20593
|
var init_build_journal = __esm({
|
|
19989
20594
|
"src/build-journal.ts"() {
|
|
19990
20595
|
"use strict";
|
|
19991
|
-
|
|
19992
|
-
|
|
20596
|
+
fs14 = __toESM(require("fs"));
|
|
20597
|
+
path13 = __toESM(require("path"));
|
|
19993
20598
|
init_credentials_store();
|
|
19994
20599
|
}
|
|
19995
20600
|
});
|
|
@@ -19997,11 +20602,11 @@ var init_build_journal = __esm({
|
|
|
19997
20602
|
// src/stage-runner.ts
|
|
19998
20603
|
function accountRulesPath() {
|
|
19999
20604
|
const override = process.env[ACCOUNT_RULES_ENV];
|
|
20000
|
-
return override && override.trim() ? override :
|
|
20605
|
+
return override && override.trim() ? override : path14.join(appDataDir(), "account-rules.json");
|
|
20001
20606
|
}
|
|
20002
20607
|
function readAccountRules(file = accountRulesPath()) {
|
|
20003
20608
|
try {
|
|
20004
|
-
const raw = JSON.parse(
|
|
20609
|
+
const raw = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
20005
20610
|
const pick = (v) => {
|
|
20006
20611
|
if (!v || typeof v !== "object" || Array.isArray(v)) return {};
|
|
20007
20612
|
return Object.fromEntries(
|
|
@@ -20053,10 +20658,10 @@ function stageBlockedBy(stages, stage) {
|
|
|
20053
20658
|
return null;
|
|
20054
20659
|
}
|
|
20055
20660
|
function claudeBin() {
|
|
20056
|
-
const fallback =
|
|
20057
|
-
const fromPath = (process.env.PATH || "").split(
|
|
20661
|
+
const fallback = path14.join(os3.homedir(), ".local", "bin", "claude");
|
|
20662
|
+
const fromPath = (process.env.PATH || "").split(path14.delimiter).map((d) => path14.join(d, "claude")).find((p) => {
|
|
20058
20663
|
try {
|
|
20059
|
-
|
|
20664
|
+
fs15.accessSync(p, fs15.constants.X_OK);
|
|
20060
20665
|
return true;
|
|
20061
20666
|
} catch {
|
|
20062
20667
|
return false;
|
|
@@ -20064,7 +20669,7 @@ function claudeBin() {
|
|
|
20064
20669
|
});
|
|
20065
20670
|
if (fromPath) return fromPath;
|
|
20066
20671
|
try {
|
|
20067
|
-
|
|
20672
|
+
fs15.accessSync(fallback, fs15.constants.X_OK);
|
|
20068
20673
|
return fallback;
|
|
20069
20674
|
} catch {
|
|
20070
20675
|
}
|
|
@@ -20121,7 +20726,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
20121
20726
|
const progressDir = opts.onProgress ? defaultProgressDir() : void 0;
|
|
20122
20727
|
if (progressDir) {
|
|
20123
20728
|
try {
|
|
20124
|
-
|
|
20729
|
+
fs15.rmSync(progressFilePath(progressDir, locationId2), { force: true });
|
|
20125
20730
|
} catch {
|
|
20126
20731
|
}
|
|
20127
20732
|
}
|
|
@@ -20205,14 +20810,14 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
20205
20810
|
});
|
|
20206
20811
|
});
|
|
20207
20812
|
}
|
|
20208
|
-
var import_child_process3,
|
|
20813
|
+
var import_child_process3, fs15, os3, path14, EMPTY_RULES, ACCOUNT_RULES_ENV, SANDBOX_ALLOWLIST, PROTECTED_LOCATIONS, STAFF_NOTIFICATION_RULE, MISSING_STEP_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES;
|
|
20209
20814
|
var init_stage_runner = __esm({
|
|
20210
20815
|
"src/stage-runner.ts"() {
|
|
20211
20816
|
"use strict";
|
|
20212
20817
|
import_child_process3 = require("child_process");
|
|
20213
|
-
|
|
20818
|
+
fs15 = __toESM(require("fs"));
|
|
20214
20819
|
os3 = __toESM(require("os"));
|
|
20215
|
-
|
|
20820
|
+
path14 = __toESM(require("path"));
|
|
20216
20821
|
init_credentials_store();
|
|
20217
20822
|
init_stage_boundary();
|
|
20218
20823
|
init_plan_guide();
|
|
@@ -20407,23 +21012,23 @@ function friendlyDate(stamp) {
|
|
|
20407
21012
|
if (isNaN(d.getTime())) return stamp;
|
|
20408
21013
|
return d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" });
|
|
20409
21014
|
}
|
|
20410
|
-
var
|
|
21015
|
+
var import_zod59, RunnerCheckSchema, RunnerResultSchema;
|
|
20411
21016
|
var init_handoff_pack = __esm({
|
|
20412
21017
|
"src/command-os/handoff-pack.ts"() {
|
|
20413
21018
|
"use strict";
|
|
20414
|
-
|
|
20415
|
-
RunnerCheckSchema =
|
|
20416
|
-
name:
|
|
20417
|
-
state:
|
|
20418
|
-
evidence:
|
|
20419
|
-
reason:
|
|
21019
|
+
import_zod59 = require("zod");
|
|
21020
|
+
RunnerCheckSchema = import_zod59.z.object({
|
|
21021
|
+
name: import_zod59.z.string().min(1),
|
|
21022
|
+
state: import_zod59.z.enum(["CHECKED", "DONE_UNCHECKED"]),
|
|
21023
|
+
evidence: import_zod59.z.string().optional(),
|
|
21024
|
+
reason: import_zod59.z.string().optional(),
|
|
20420
21025
|
/** One sentence a client can read: what is in place, or what is missing and why. No tool names, no ids. */
|
|
20421
|
-
plain:
|
|
21026
|
+
plain: import_zod59.z.string().optional()
|
|
20422
21027
|
}).strict();
|
|
20423
|
-
RunnerResultSchema =
|
|
20424
|
-
ok:
|
|
20425
|
-
checks:
|
|
20426
|
-
error:
|
|
21028
|
+
RunnerResultSchema = import_zod59.z.object({
|
|
21029
|
+
ok: import_zod59.z.boolean(),
|
|
21030
|
+
checks: import_zod59.z.array(RunnerCheckSchema).default([]),
|
|
21031
|
+
error: import_zod59.z.string().optional()
|
|
20427
21032
|
}).strict();
|
|
20428
21033
|
}
|
|
20429
21034
|
});
|
|
@@ -20837,7 +21442,7 @@ var init_work_queue = __esm({
|
|
|
20837
21442
|
// src/board-roster.ts
|
|
20838
21443
|
function readBoardState() {
|
|
20839
21444
|
try {
|
|
20840
|
-
const raw = JSON.parse(
|
|
21445
|
+
const raw = JSON.parse(fs16.readFileSync(path15.join(appDataDir(), "cockpit-state.json"), "utf8"));
|
|
20841
21446
|
if (raw && typeof raw === "object" && raw.clients && typeof raw.clients === "object") return raw;
|
|
20842
21447
|
} catch {
|
|
20843
21448
|
}
|
|
@@ -20854,12 +21459,12 @@ function knownClients(registry2, state) {
|
|
|
20854
21459
|
}
|
|
20855
21460
|
return [...out.values()];
|
|
20856
21461
|
}
|
|
20857
|
-
var
|
|
21462
|
+
var fs16, path15;
|
|
20858
21463
|
var init_board_roster = __esm({
|
|
20859
21464
|
"src/board-roster.ts"() {
|
|
20860
21465
|
"use strict";
|
|
20861
|
-
|
|
20862
|
-
|
|
21466
|
+
fs16 = __toESM(require("fs"));
|
|
21467
|
+
path15 = __toESM(require("path"));
|
|
20863
21468
|
init_credentials_store();
|
|
20864
21469
|
init_client_engagements();
|
|
20865
21470
|
}
|
|
@@ -20894,7 +21499,7 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
20894
21499
|
server2,
|
|
20895
21500
|
"get_client_engagements",
|
|
20896
21501
|
"Read what each of YOUR clients bought \u2014 the offer they are on, what they pay, monthly or one-time, when it started, when it renews, who runs the account, and any SOPs specific to them. Use this before quoting, renewing, writing a proposal or planning a client's week, instead of asking the user again. Also returns your recurring revenue as a FLOOR (the sum of active monthly clients that have a price recorded) plus a count of the ones that do not, because a total assembled from partial data is not a total. Pass locationId for one client, or nothing for all of them. The locationId in the reply is for calling set_client_engagement \u2014 refer to clients by name when you answer the user, never by id. Local file on this machine; never touches GoHighLevel.",
|
|
20897
|
-
{ locationId:
|
|
21502
|
+
{ locationId: import_zod60.z.string().optional().describe("One client's sub-account id. Omit for the whole roster.") },
|
|
20898
21503
|
async ({ locationId: locationId2 }) => {
|
|
20899
21504
|
const profile = readAgencyProfile();
|
|
20900
21505
|
const saved = readClientEngagements();
|
|
@@ -20924,24 +21529,24 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
20924
21529
|
"set_client_engagement",
|
|
20925
21530
|
"Record or update what ONE client bought \u2014 offer, price, monthly or one-time, start and renewal dates, status, who runs the account, and SOPs specific to them. Send only the fields you are changing; anything you omit is left exactly as it was, so this is safe to call repeatedly. To CLEAR a field pass null for it, and to remove the client's record entirely pass remove:true. Dates are YYYY-MM-DD. Local file on this machine; never touches GoHighLevel.",
|
|
20926
21531
|
{
|
|
20927
|
-
locationId:
|
|
21532
|
+
locationId: import_zod60.z.string().describe("The client's GHL sub-account id \u2014 the same id the board uses."),
|
|
20928
21533
|
// Every editable field is NULLABLE, not just optional. The description
|
|
20929
21534
|
// promises "pass null to clear", and a schema that rejects null would make
|
|
20930
21535
|
// that promise a lie the tests never caught, because they cleared fields by
|
|
20931
21536
|
// calling the store directly instead of through this surface (Codex,
|
|
20932
21537
|
// 2026-08-28). Absent = untouched; null = clear; a value = set.
|
|
20933
|
-
clientName:
|
|
20934
|
-
offer:
|
|
20935
|
-
price:
|
|
20936
|
-
cadence:
|
|
20937
|
-
currency:
|
|
20938
|
-
startedOn:
|
|
20939
|
-
renewsOn:
|
|
20940
|
-
status:
|
|
20941
|
-
owner:
|
|
20942
|
-
sops:
|
|
20943
|
-
notes:
|
|
20944
|
-
remove:
|
|
21538
|
+
clientName: import_zod60.z.string().nullable().optional().describe("How you refer to them, if it differs from the account name."),
|
|
21539
|
+
offer: import_zod60.z.string().nullable().optional().describe("Which of your offers they are on. Matched against your agency profile so a typo is reported."),
|
|
21540
|
+
price: import_zod60.z.number().min(0).nullable().optional().describe("What they actually pay \u2014 not your floor."),
|
|
21541
|
+
cadence: import_zod60.z.enum(["one-time", "monthly"]).nullable().optional(),
|
|
21542
|
+
currency: import_zod60.z.string().nullable().optional(),
|
|
21543
|
+
startedOn: import_zod60.z.string().nullable().optional().describe("YYYY-MM-DD."),
|
|
21544
|
+
renewsOn: import_zod60.z.string().nullable().optional().describe("YYYY-MM-DD."),
|
|
21545
|
+
status: import_zod60.z.enum(["active", "paused", "ended"]).nullable().optional(),
|
|
21546
|
+
owner: import_zod60.z.string().nullable().optional().describe("Which staff member runs this account."),
|
|
21547
|
+
sops: import_zod60.z.array(import_zod60.z.string()).nullable().optional().describe("SOPs that apply to this client only. Leave empty to use your agency's."),
|
|
21548
|
+
notes: import_zod60.z.string().nullable().optional(),
|
|
21549
|
+
remove: import_zod60.z.boolean().optional().describe("Delete this client's record entirely.")
|
|
20945
21550
|
},
|
|
20946
21551
|
async (args) => {
|
|
20947
21552
|
const { locationId: locationId2, remove, ...rest } = args;
|
|
@@ -20965,7 +21570,7 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
20965
21570
|
server2,
|
|
20966
21571
|
"client_this_week",
|
|
20967
21572
|
"Answer 'what should I be doing for this client this week' for ONE client, from what you already know: what they bought, where their build has stopped, what is waiting on you versus on Command OS, a renewal that is close, whether the price is under your own floor, and the SOP that applies. Names the client by name or by account id. Everything it cannot know is listed plainly rather than guessed. Local files on this machine; never touches GoHighLevel.",
|
|
20968
|
-
{ client:
|
|
21573
|
+
{ client: import_zod60.z.string().describe("The client's name as you refer to them, or their sub-account id.") },
|
|
20969
21574
|
async ({ client }) => {
|
|
20970
21575
|
const state = readBoardState();
|
|
20971
21576
|
const roster = knownClients(registry2, state);
|
|
@@ -20985,11 +21590,11 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
20985
21590
|
}
|
|
20986
21591
|
);
|
|
20987
21592
|
}
|
|
20988
|
-
var
|
|
21593
|
+
var import_zod60;
|
|
20989
21594
|
var init_client_engagements2 = __esm({
|
|
20990
21595
|
"src/tools/client-engagements.ts"() {
|
|
20991
21596
|
"use strict";
|
|
20992
|
-
|
|
21597
|
+
import_zod60 = require("zod");
|
|
20993
21598
|
init_tool_helpers();
|
|
20994
21599
|
init_agency_profile();
|
|
20995
21600
|
init_client_engagements();
|
|
@@ -21449,14 +22054,14 @@ function registerAuditReportTools(server2) {
|
|
|
21449
22054
|
"build_audit_report",
|
|
21450
22055
|
"Turn the output of `audit_workflows` into a client-ready assessment: plain-English findings with money attached where it can honestly be attached, ranked worst-first, plus an explicit list of what could NOT be checked. Call `audit_workflows` first and pass its result here \u2014 this does not re-read the account. Give `leadsPerMonth`, `closeRate` and `monthlyValuePerClient` if you know them and the findings get costed in dollars; leave them out and it says which input is missing instead of guessing. `monthlyValuePerClient` defaults to your saved minimum monthly fee from your agency profile. Use this to produce the document you send or present, not to diagnose \u2014 the diagnosing already happened.",
|
|
21451
22056
|
{
|
|
21452
|
-
audit:
|
|
21453
|
-
audience:
|
|
21454
|
-
accountName:
|
|
21455
|
-
locationId:
|
|
21456
|
-
leadsPerMonth:
|
|
21457
|
-
closeRate:
|
|
21458
|
-
monthlyValuePerClient:
|
|
21459
|
-
generatedAt:
|
|
22057
|
+
audit: import_zod61.z.record(import_zod61.z.unknown()).describe("The full JSON result returned by audit_workflows."),
|
|
22058
|
+
audience: import_zod61.z.enum(["internal", "prospect"]).optional().describe("Who reads it. 'internal' (default) is a report about an account you already run, and may use your own minimum monthly as what a client is worth. 'prospect' is a document about somebody else's business: your numbers are NEVER used, so findings stay uncosted unless you supply THEIR lead volume, close rate and client value."),
|
|
22059
|
+
accountName: import_zod61.z.string().describe("The client's name as they should see it on the document."),
|
|
22060
|
+
locationId: import_zod61.z.string().optional().describe("Sub-account id. Recorded on the document, never shown in the body."),
|
|
22061
|
+
leadsPerMonth: import_zod61.z.number().min(0).optional().describe("Leads this account actually receives per month, if known. Without it, findings are not costed in dollars."),
|
|
22062
|
+
closeRate: import_zod61.z.number().min(0).max(1).optional().describe("Share of reached leads that become clients, 0-1. Never guessed."),
|
|
22063
|
+
monthlyValuePerClient: import_zod61.z.number().min(0).optional().describe("What one new client is worth per month. Defaults to your agency profile's minimum monthly fee."),
|
|
22064
|
+
generatedAt: import_zod61.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
|
|
21460
22065
|
},
|
|
21461
22066
|
async (args) => {
|
|
21462
22067
|
const a = args;
|
|
@@ -21514,12 +22119,12 @@ function registerAuditReportTools(server2) {
|
|
|
21514
22119
|
"build_site_audit_report",
|
|
21515
22120
|
"Turn a website read-through into the assessment document you put in front of a PROSPECT \u2014 someone whose GoHighLevel account you do not have. Pass the verified observations from a site crawl and it returns plain-English findings, ranked, each one carrying the quote and the page it came from. Use it before a sales call, or alongside a receptionist demo, so the same read of their site fills both. It never invents money: a website cannot tell you what a lead is worth to that business, so findings come back uncosted unless you supply THEIR figures, and the document says why. It also never repeats a claim the crawl could not stand up \u2014 anything refused is returned separately, so you can see what was thrown out rather than wonder what was missed.",
|
|
21516
22121
|
{
|
|
21517
|
-
observations:
|
|
21518
|
-
businessName:
|
|
21519
|
-
leadsPerMonth:
|
|
21520
|
-
closeRate:
|
|
21521
|
-
monthlyValuePerClient:
|
|
21522
|
-
generatedAt:
|
|
22122
|
+
observations: import_zod61.z.record(import_zod61.z.unknown()).describe("The verified observations payload from the site read: site, pagesRead, observations, limits. Passed through as-is."),
|
|
22123
|
+
businessName: import_zod61.z.string().describe("The business's name as it should appear on the document."),
|
|
22124
|
+
leadsPerMonth: import_zod61.z.number().min(0).optional().describe("THEIR leads per month, if they told you. Never your own figure."),
|
|
22125
|
+
closeRate: import_zod61.z.number().min(0).max(1).optional().describe("THEIR close rate, 0-1, if they told you. Never guessed."),
|
|
22126
|
+
monthlyValuePerClient: import_zod61.z.number().min(0).optional().describe("What one client is worth to THEM per month, if they told you."),
|
|
22127
|
+
generatedAt: import_zod61.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
|
|
21523
22128
|
},
|
|
21524
22129
|
async (args) => {
|
|
21525
22130
|
const a = args;
|
|
@@ -21578,11 +22183,11 @@ function registerAuditReportTools(server2) {
|
|
|
21578
22183
|
}
|
|
21579
22184
|
);
|
|
21580
22185
|
}
|
|
21581
|
-
var
|
|
22186
|
+
var import_zod61;
|
|
21582
22187
|
var init_audit_report2 = __esm({
|
|
21583
22188
|
"src/tools/audit-report.ts"() {
|
|
21584
22189
|
"use strict";
|
|
21585
|
-
|
|
22190
|
+
import_zod61 = require("zod");
|
|
21586
22191
|
init_tool_helpers();
|
|
21587
22192
|
init_audit_report();
|
|
21588
22193
|
init_audit_translate();
|
|
@@ -21950,8 +22555,8 @@ function registerDailyBriefTools(server2, registry2) {
|
|
|
21950
22555
|
"daily_brief",
|
|
21951
22556
|
"The morning brief across ALL your clients: today's appointments first, then what is waiting on YOU, what Command OS is running, renewals inside 30 days, clients under your own price floor, and new leads overnight. Use it to answer 'what do I need to do today' or 'anything I'm missing' without opening a single sub-account. Reads today's calendar and new-contact counts live from each connected account; pass live=false for records only. Accounts that could not be read are named with the reason \u2014 it never reports an account as quiet when it simply could not get in.",
|
|
21952
22557
|
{
|
|
21953
|
-
live:
|
|
21954
|
-
hoursBack:
|
|
22558
|
+
live: import_zod62.z.boolean().optional().describe("Read today's calendars and lead counts from the connected accounts. Default true. false answers from local records alone."),
|
|
22559
|
+
hoursBack: import_zod62.z.number().min(1).max(72).optional().describe("Count new leads over this many hours instead of since midnight.")
|
|
21955
22560
|
},
|
|
21956
22561
|
async ({ live, hoursBack }) => {
|
|
21957
22562
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -21985,11 +22590,11 @@ function registerDailyBriefTools(server2, registry2) {
|
|
|
21985
22590
|
}
|
|
21986
22591
|
);
|
|
21987
22592
|
}
|
|
21988
|
-
var
|
|
22593
|
+
var import_zod62;
|
|
21989
22594
|
var init_daily_brief2 = __esm({
|
|
21990
22595
|
"src/tools/daily-brief.ts"() {
|
|
21991
22596
|
"use strict";
|
|
21992
|
-
|
|
22597
|
+
import_zod62 = require("zod");
|
|
21993
22598
|
init_tool_helpers();
|
|
21994
22599
|
init_ghl_client();
|
|
21995
22600
|
init_agency_profile();
|
|
@@ -22042,7 +22647,7 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
22042
22647
|
"list_snapshots",
|
|
22043
22648
|
"List the agency's GHL snapshots (id, name, type) so you can pick the right one by name before applying it to a sub-account. Reads the agency/company-scoped key (not a sub-account PIT). companyId defaults to the active location's company; pass it explicitly to target a different company you have the agency key for. Read-only.",
|
|
22044
22649
|
{
|
|
22045
|
-
companyId:
|
|
22650
|
+
companyId: import_zod63.z.string().optional().describe(
|
|
22046
22651
|
"Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
22047
22652
|
)
|
|
22048
22653
|
},
|
|
@@ -22067,11 +22672,11 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
22067
22672
|
"create_snapshot_share_link",
|
|
22068
22673
|
"Create a shareable load link for one of the agency's snapshots (returns a gohighlevel.com/?share=... URL to import the snapshot into a sub-account). Uses the agency/company-scoped key. WARNING: this is NOT idempotent \u2014 each call mints a NEW link, and there is no API to list or revoke links (revoke in the GHL UI under the snapshot's share settings). Pick share_type deliberately. Use list_snapshots first to get the snapshot id.",
|
|
22069
22674
|
{
|
|
22070
|
-
snapshot_id:
|
|
22071
|
-
share_type:
|
|
22675
|
+
snapshot_id: import_zod63.z.string().describe("The snapshot id to share (from list_snapshots)."),
|
|
22676
|
+
share_type: import_zod63.z.enum(SHARE_TYPES).describe(
|
|
22072
22677
|
"Share link type. 'link' = standard share link; 'permanent_link' = non-expiring; 'agency_link' = share to agencies; 'location_link' = load into a sub-account/location; 'marketplace_link' = marketplace listing. No default \u2014 choose intentionally."
|
|
22073
22678
|
),
|
|
22074
|
-
companyId:
|
|
22679
|
+
companyId: import_zod63.z.string().optional().describe(
|
|
22075
22680
|
"Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
22076
22681
|
)
|
|
22077
22682
|
},
|
|
@@ -22099,16 +22704,16 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
22099
22704
|
}
|
|
22100
22705
|
);
|
|
22101
22706
|
}
|
|
22102
|
-
var
|
|
22707
|
+
var import_zod63, SnapshotSchema, SnapshotsResponseSchema, ShareLinkResponseSchema, SHARE_TYPES;
|
|
22103
22708
|
var init_snapshots = __esm({
|
|
22104
22709
|
"src/tools/snapshots.ts"() {
|
|
22105
22710
|
"use strict";
|
|
22106
|
-
|
|
22711
|
+
import_zod63 = require("zod");
|
|
22107
22712
|
init_ghl_client();
|
|
22108
22713
|
init_tool_helpers();
|
|
22109
|
-
SnapshotSchema =
|
|
22110
|
-
SnapshotsResponseSchema =
|
|
22111
|
-
ShareLinkResponseSchema =
|
|
22714
|
+
SnapshotSchema = import_zod63.z.object({ id: import_zod63.z.string(), name: import_zod63.z.string(), type: import_zod63.z.string() }).passthrough();
|
|
22715
|
+
SnapshotsResponseSchema = import_zod63.z.object({ snapshots: import_zod63.z.array(SnapshotSchema) }).passthrough();
|
|
22716
|
+
ShareLinkResponseSchema = import_zod63.z.object({ id: import_zod63.z.string(), shareLink: import_zod63.z.string() }).passthrough();
|
|
22112
22717
|
SHARE_TYPES = [
|
|
22113
22718
|
"link",
|
|
22114
22719
|
"permanent_link",
|
|
@@ -22126,7 +22731,7 @@ function registerPhoneTools(server2, client) {
|
|
|
22126
22731
|
"list_phone_numbers",
|
|
22127
22732
|
"List the LC Phone numbers provisioned for a location (sid, number, label). Read-only. Use to verify or count purchased numbers (e.g. provisioning step 11). Number purchase is not exposed (billable write).",
|
|
22128
22733
|
{
|
|
22129
|
-
locationId:
|
|
22734
|
+
locationId: import_zod64.z.string().optional().describe("Defaults to the active location.")
|
|
22130
22735
|
},
|
|
22131
22736
|
async ({ locationId: locationId2 }) => {
|
|
22132
22737
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -22144,7 +22749,7 @@ function registerPhoneTools(server2, client) {
|
|
|
22144
22749
|
"list_number_pools",
|
|
22145
22750
|
"List LC Phone number pools configured for a location. Read-only.",
|
|
22146
22751
|
{
|
|
22147
|
-
locationId:
|
|
22752
|
+
locationId: import_zod64.z.string().optional().describe("Defaults to the active location.")
|
|
22148
22753
|
},
|
|
22149
22754
|
async ({ locationId: locationId2 }) => {
|
|
22150
22755
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -22154,15 +22759,15 @@ function registerPhoneTools(server2, client) {
|
|
|
22154
22759
|
}
|
|
22155
22760
|
);
|
|
22156
22761
|
}
|
|
22157
|
-
var
|
|
22762
|
+
var import_zod64, PhoneNumberSchema, NumbersResponseSchema, PoolsResponseSchema;
|
|
22158
22763
|
var init_phone = __esm({
|
|
22159
22764
|
"src/tools/phone.ts"() {
|
|
22160
22765
|
"use strict";
|
|
22161
|
-
|
|
22766
|
+
import_zod64 = require("zod");
|
|
22162
22767
|
init_tool_helpers();
|
|
22163
|
-
PhoneNumberSchema =
|
|
22164
|
-
NumbersResponseSchema =
|
|
22165
|
-
PoolsResponseSchema =
|
|
22768
|
+
PhoneNumberSchema = import_zod64.z.object({ sid: import_zod64.z.string(), value: import_zod64.z.string(), title: import_zod64.z.string().optional() }).passthrough();
|
|
22769
|
+
NumbersResponseSchema = import_zod64.z.object({ phoneNumbers: import_zod64.z.array(PhoneNumberSchema) }).passthrough();
|
|
22770
|
+
PoolsResponseSchema = import_zod64.z.object({ pools: import_zod64.z.array(import_zod64.z.object({}).passthrough()) }).passthrough();
|
|
22166
22771
|
}
|
|
22167
22772
|
});
|
|
22168
22773
|
|
|
@@ -22180,8 +22785,8 @@ function registerAccountHealthTools(server2, client) {
|
|
|
22180
22785
|
"get_account_health_summary",
|
|
22181
22786
|
"Account-health summary for a location, composed from existing reads (GHL has no reporting API). Returns: total contacts + NEW contacts in the window; total opportunities + counts by status (open/won/lost/abandoned); total conversations; phone-number count. Every metric is explicitly labeled all_time vs window (with start/end) \u2014 windowed and all-time numbers are never conflated. Sections that can't be read return status:'unavailable' (never a misleading 0). Revenue and appointments are intentionally excluded (not reachable / too costly via the public API).",
|
|
22182
22787
|
{
|
|
22183
|
-
locationId:
|
|
22184
|
-
windowDays:
|
|
22788
|
+
locationId: import_zod65.z.string().optional().describe("Defaults to the active location."),
|
|
22789
|
+
windowDays: import_zod65.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
|
|
22185
22790
|
},
|
|
22186
22791
|
async ({ locationId: locationId2, windowDays }) => {
|
|
22187
22792
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -22245,16 +22850,16 @@ function registerAccountHealthTools(server2, client) {
|
|
|
22245
22850
|
}
|
|
22246
22851
|
);
|
|
22247
22852
|
}
|
|
22248
|
-
var
|
|
22853
|
+
var import_zod65, MetaTotalSchema, TotalSchema2, NumbersSchema, OPP_STATUSES;
|
|
22249
22854
|
var init_account_health = __esm({
|
|
22250
22855
|
"src/tools/account-health.ts"() {
|
|
22251
22856
|
"use strict";
|
|
22252
|
-
|
|
22857
|
+
import_zod65 = require("zod");
|
|
22253
22858
|
init_tool_helpers();
|
|
22254
22859
|
init_contact_window();
|
|
22255
|
-
MetaTotalSchema =
|
|
22256
|
-
TotalSchema2 =
|
|
22257
|
-
NumbersSchema =
|
|
22860
|
+
MetaTotalSchema = import_zod65.z.object({ meta: import_zod65.z.object({ total: import_zod65.z.number() }).passthrough() }).passthrough();
|
|
22861
|
+
TotalSchema2 = import_zod65.z.object({ total: import_zod65.z.number() }).passthrough();
|
|
22862
|
+
NumbersSchema = import_zod65.z.object({ phoneNumbers: import_zod65.z.array(import_zod65.z.unknown()) }).passthrough();
|
|
22258
22863
|
OPP_STATUSES = ["open", "won", "lost", "abandoned"];
|
|
22259
22864
|
}
|
|
22260
22865
|
});
|
|
@@ -22729,11 +23334,11 @@ var init_customization = __esm({
|
|
|
22729
23334
|
|
|
22730
23335
|
// src/intake-overlay.ts
|
|
22731
23336
|
function overlayPath() {
|
|
22732
|
-
return
|
|
23337
|
+
return path16.join(appDataDir(), "intake-overlay.json");
|
|
22733
23338
|
}
|
|
22734
23339
|
function readOverlay() {
|
|
22735
23340
|
try {
|
|
22736
|
-
const raw = JSON.parse(
|
|
23341
|
+
const raw = JSON.parse(fs17.readFileSync(overlayPath(), "utf8"));
|
|
22737
23342
|
if (raw && typeof raw === "object") return raw;
|
|
22738
23343
|
} catch {
|
|
22739
23344
|
}
|
|
@@ -22741,14 +23346,14 @@ function readOverlay() {
|
|
|
22741
23346
|
}
|
|
22742
23347
|
function writeOverlay(layer) {
|
|
22743
23348
|
ensureAppDataDir();
|
|
22744
|
-
|
|
23349
|
+
fs17.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
|
|
22745
23350
|
}
|
|
22746
|
-
var
|
|
23351
|
+
var fs17, path16;
|
|
22747
23352
|
var init_intake_overlay = __esm({
|
|
22748
23353
|
"src/intake-overlay.ts"() {
|
|
22749
23354
|
"use strict";
|
|
22750
|
-
|
|
22751
|
-
|
|
23355
|
+
fs17 = __toESM(require("fs"));
|
|
23356
|
+
path16 = __toESM(require("path"));
|
|
22752
23357
|
init_credentials_store();
|
|
22753
23358
|
}
|
|
22754
23359
|
});
|
|
@@ -22810,11 +23415,11 @@ function validateBrief(input) {
|
|
|
22810
23415
|
warnings: []
|
|
22811
23416
|
};
|
|
22812
23417
|
}
|
|
22813
|
-
var
|
|
23418
|
+
var import_zod66, BRIEF_SCHEMA_VERSION, PRESETS, presetSchema, BRIEF_SOURCES, briefSourceSchema, pricePointSchema, staffMemberSchema, BRIEF_CALENDAR_TYPES, briefCalendarSchema, TRI_STATES, briefSchema;
|
|
22814
23419
|
var init_brief = __esm({
|
|
22815
23420
|
"src/intake-to-build/brief.ts"() {
|
|
22816
23421
|
"use strict";
|
|
22817
|
-
|
|
23422
|
+
import_zod66 = require("zod");
|
|
22818
23423
|
BRIEF_SCHEMA_VERSION = "0.1";
|
|
22819
23424
|
PRESETS = [
|
|
22820
23425
|
"generic",
|
|
@@ -22824,105 +23429,105 @@ var init_brief = __esm({
|
|
|
22824
23429
|
"ecom",
|
|
22825
23430
|
"agency"
|
|
22826
23431
|
];
|
|
22827
|
-
presetSchema =
|
|
23432
|
+
presetSchema = import_zod66.z.enum(PRESETS);
|
|
22828
23433
|
BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
|
|
22829
|
-
briefSourceSchema =
|
|
22830
|
-
pricePointSchema =
|
|
22831
|
-
name:
|
|
22832
|
-
price:
|
|
23434
|
+
briefSourceSchema = import_zod66.z.enum(BRIEF_SOURCES);
|
|
23435
|
+
pricePointSchema = import_zod66.z.object({
|
|
23436
|
+
name: import_zod66.z.string(),
|
|
23437
|
+
price: import_zod66.z.string()
|
|
22833
23438
|
});
|
|
22834
|
-
staffMemberSchema =
|
|
22835
|
-
name:
|
|
22836
|
-
email:
|
|
23439
|
+
staffMemberSchema = import_zod66.z.object({
|
|
23440
|
+
name: import_zod66.z.string().min(1),
|
|
23441
|
+
email: import_zod66.z.string().min(1),
|
|
22837
23442
|
/** Job title as the client wrote it ("Front desk", "Provider"); omitted when
|
|
22838
23443
|
* the line had none. The plan decides admin/user from it. */
|
|
22839
|
-
role:
|
|
22840
|
-
mobile:
|
|
23444
|
+
role: import_zod66.z.string().optional(),
|
|
23445
|
+
mobile: import_zod66.z.string().optional()
|
|
22841
23446
|
});
|
|
22842
23447
|
BRIEF_CALENDAR_TYPES = ["one_on_one", "round_robin", "class"];
|
|
22843
|
-
briefCalendarSchema =
|
|
22844
|
-
name:
|
|
22845
|
-
type:
|
|
22846
|
-
staffNames:
|
|
23448
|
+
briefCalendarSchema = import_zod66.z.object({
|
|
23449
|
+
name: import_zod66.z.string().min(1),
|
|
23450
|
+
type: import_zod66.z.enum(BRIEF_CALENDAR_TYPES),
|
|
23451
|
+
staffNames: import_zod66.z.array(import_zod66.z.string()),
|
|
22847
23452
|
/** Appointment length in minutes, when the client said one ("15 minutes").
|
|
22848
23453
|
* Lands on the plan calendar as slotDuration (finding 25). */
|
|
22849
|
-
durationMinutes:
|
|
23454
|
+
durationMinutes: import_zod66.z.number().int().positive().optional()
|
|
22850
23455
|
});
|
|
22851
23456
|
TRI_STATES = ["yes", "no", "unsure"];
|
|
22852
|
-
briefSchema =
|
|
22853
|
-
schemaVersion:
|
|
22854
|
-
briefId:
|
|
23457
|
+
briefSchema = import_zod66.z.object({
|
|
23458
|
+
schemaVersion: import_zod66.z.string(),
|
|
23459
|
+
briefId: import_zod66.z.string(),
|
|
22855
23460
|
preset: presetSchema,
|
|
22856
23461
|
briefSource: briefSourceSchema,
|
|
22857
23462
|
/** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
|
|
22858
|
-
extended:
|
|
22859
|
-
business:
|
|
22860
|
-
name:
|
|
22861
|
-
type:
|
|
22862
|
-
website:
|
|
22863
|
-
location:
|
|
22864
|
-
timezone:
|
|
23463
|
+
extended: import_zod66.z.record(import_zod66.z.unknown()).optional(),
|
|
23464
|
+
business: import_zod66.z.object({
|
|
23465
|
+
name: import_zod66.z.string(),
|
|
23466
|
+
type: import_zod66.z.string().optional(),
|
|
23467
|
+
website: import_zod66.z.string().optional(),
|
|
23468
|
+
location: import_zod66.z.string().optional(),
|
|
23469
|
+
timezone: import_zod66.z.string().optional(),
|
|
22865
23470
|
// Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
|
|
22866
23471
|
// the same tolerance reason as business.type (don't reject valid briefs).
|
|
22867
|
-
teamSize:
|
|
22868
|
-
monthlyLeadVolume:
|
|
22869
|
-
hours:
|
|
23472
|
+
teamSize: import_zod66.z.string().optional(),
|
|
23473
|
+
monthlyLeadVolume: import_zod66.z.string().optional(),
|
|
23474
|
+
hours: import_zod66.z.string().optional()
|
|
22870
23475
|
}).passthrough(),
|
|
22871
|
-
offer:
|
|
22872
|
-
summary:
|
|
23476
|
+
offer: import_zod66.z.object({
|
|
23477
|
+
summary: import_zod66.z.string().optional(),
|
|
22873
23478
|
// Parsed best-effort; tolerate a raw string when parsing was not possible.
|
|
22874
|
-
pricePoints:
|
|
22875
|
-
leadMagnet:
|
|
22876
|
-
avgDealValue:
|
|
23479
|
+
pricePoints: import_zod66.z.union([import_zod66.z.array(pricePointSchema), import_zod66.z.string()]).optional(),
|
|
23480
|
+
leadMagnet: import_zod66.z.string().optional(),
|
|
23481
|
+
avgDealValue: import_zod66.z.string().optional()
|
|
22877
23482
|
}).passthrough().optional(),
|
|
22878
|
-
audience:
|
|
22879
|
-
ideal:
|
|
22880
|
-
painPoints:
|
|
22881
|
-
objections:
|
|
23483
|
+
audience: import_zod66.z.object({
|
|
23484
|
+
ideal: import_zod66.z.string().optional(),
|
|
23485
|
+
painPoints: import_zod66.z.array(import_zod66.z.string()).optional(),
|
|
23486
|
+
objections: import_zod66.z.array(import_zod66.z.string()).optional()
|
|
22882
23487
|
}).passthrough().optional(),
|
|
22883
|
-
goal:
|
|
23488
|
+
goal: import_zod66.z.object({
|
|
22884
23489
|
// Kept as string: the form option labels are the canonical values, but
|
|
22885
23490
|
// the contract sample shortens some (e.g. "high-touch"). See §7 note.
|
|
22886
|
-
primary:
|
|
22887
|
-
salesStages:
|
|
22888
|
-
bookingNeeded:
|
|
22889
|
-
followUpStyle:
|
|
23491
|
+
primary: import_zod66.z.string(),
|
|
23492
|
+
salesStages: import_zod66.z.array(import_zod66.z.string()).optional(),
|
|
23493
|
+
bookingNeeded: import_zod66.z.boolean().optional(),
|
|
23494
|
+
followUpStyle: import_zod66.z.string().optional()
|
|
22890
23495
|
}).passthrough(),
|
|
22891
|
-
channels:
|
|
22892
|
-
email:
|
|
22893
|
-
sms:
|
|
22894
|
-
a2pStatus:
|
|
22895
|
-
payment:
|
|
22896
|
-
calendarConnected:
|
|
22897
|
-
social:
|
|
23496
|
+
channels: import_zod66.z.object({
|
|
23497
|
+
email: import_zod66.z.boolean().optional(),
|
|
23498
|
+
sms: import_zod66.z.boolean().optional(),
|
|
23499
|
+
a2pStatus: import_zod66.z.string().optional(),
|
|
23500
|
+
payment: import_zod66.z.string().optional(),
|
|
23501
|
+
calendarConnected: import_zod66.z.boolean().optional(),
|
|
23502
|
+
social: import_zod66.z.array(import_zod66.z.string()).optional(),
|
|
22898
23503
|
/** "Do you already have a phone number in GoHighLevel?" */
|
|
22899
|
-
hasPhoneNumber:
|
|
23504
|
+
hasPhoneNumber: import_zod66.z.enum(TRI_STATES).optional()
|
|
22900
23505
|
}).passthrough().optional(),
|
|
22901
23506
|
/** Section G — who is on the account and who gets pinged. */
|
|
22902
|
-
team:
|
|
22903
|
-
staff:
|
|
23507
|
+
team: import_zod66.z.object({
|
|
23508
|
+
staff: import_zod66.z.array(staffMemberSchema).optional(),
|
|
22904
23509
|
/** Who is notified about new leads: a staff name, or "the owner". */
|
|
22905
|
-
notifyName:
|
|
23510
|
+
notifyName: import_zod66.z.string().optional(),
|
|
22906
23511
|
/** Who takes booking / follow-up calls: a staff name, or "the owner". */
|
|
22907
|
-
callsName:
|
|
23512
|
+
callsName: import_zod66.z.string().optional()
|
|
22908
23513
|
}).passthrough().optional(),
|
|
22909
23514
|
/** Section H — every booking calendar the client asked for. */
|
|
22910
|
-
calendars:
|
|
23515
|
+
calendars: import_zod66.z.array(briefCalendarSchema).optional(),
|
|
22911
23516
|
/** Section I — copy inputs, verbatim. */
|
|
22912
|
-
voice:
|
|
22913
|
-
threeWords:
|
|
22914
|
-
signatureLine:
|
|
23517
|
+
voice: import_zod66.z.object({
|
|
23518
|
+
threeWords: import_zod66.z.string().optional(),
|
|
23519
|
+
signatureLine: import_zod66.z.string().optional()
|
|
22915
23520
|
}).passthrough().optional(),
|
|
22916
|
-
assets:
|
|
22917
|
-
existingPipeline:
|
|
22918
|
-
existingWorkflows:
|
|
22919
|
-
brand:
|
|
22920
|
-
notes:
|
|
23521
|
+
assets: import_zod66.z.object({
|
|
23522
|
+
existingPipeline: import_zod66.z.string().optional(),
|
|
23523
|
+
existingWorkflows: import_zod66.z.string().optional(),
|
|
23524
|
+
brand: import_zod66.z.string().optional(),
|
|
23525
|
+
notes: import_zod66.z.string().optional()
|
|
22921
23526
|
}).passthrough().optional(),
|
|
22922
|
-
flags:
|
|
23527
|
+
flags: import_zod66.z.array(import_zod66.z.string()).optional(),
|
|
22923
23528
|
/** Parse-time notes from the normalizer (a staff line it could not read,
|
|
22924
23529
|
* a calendar type it had to assume). Never fatal; surfaced by validate_brief. */
|
|
22925
|
-
warnings:
|
|
23530
|
+
warnings: import_zod66.z.array(import_zod66.z.string()).optional()
|
|
22926
23531
|
}).strict();
|
|
22927
23532
|
}
|
|
22928
23533
|
});
|
|
@@ -23287,9 +23892,9 @@ function presetForBusinessType(type) {
|
|
|
23287
23892
|
return "generic";
|
|
23288
23893
|
}
|
|
23289
23894
|
}
|
|
23290
|
-
function setPath(target,
|
|
23895
|
+
function setPath(target, path27, value) {
|
|
23291
23896
|
if (value === void 0) return;
|
|
23292
|
-
const parts =
|
|
23897
|
+
const parts = path27.split(".");
|
|
23293
23898
|
let node = target;
|
|
23294
23899
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
23295
23900
|
const k = parts[i];
|
|
@@ -23456,18 +24061,18 @@ var init_normalizer = __esm({
|
|
|
23456
24061
|
|
|
23457
24062
|
// src/plan-store.ts
|
|
23458
24063
|
function plansDir(base = appDataDir()) {
|
|
23459
|
-
return
|
|
24064
|
+
return path17.join(base, "plans");
|
|
23460
24065
|
}
|
|
23461
24066
|
function savePlanRecord(rec, base) {
|
|
23462
24067
|
const dir = plansDir(base);
|
|
23463
|
-
|
|
23464
|
-
const file =
|
|
23465
|
-
|
|
24068
|
+
fs18.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
24069
|
+
const file = path17.join(dir, `${rec.locationId}.json`);
|
|
24070
|
+
fs18.writeFileSync(file, JSON.stringify(rec, null, 2), { mode: 384 });
|
|
23466
24071
|
return file;
|
|
23467
24072
|
}
|
|
23468
24073
|
function readSavedPlan(locationId2, base) {
|
|
23469
24074
|
try {
|
|
23470
|
-
return JSON.parse(
|
|
24075
|
+
return JSON.parse(fs18.readFileSync(path17.join(plansDir(base), `${locationId2}.json`), "utf8"));
|
|
23471
24076
|
} catch {
|
|
23472
24077
|
return null;
|
|
23473
24078
|
}
|
|
@@ -23490,24 +24095,24 @@ function planSummary(rec) {
|
|
|
23490
24095
|
if (wfs.length) parts.push(`workflows: ${wfs.map((w) => `${w.name} (${Array.isArray(w.actions) ? w.actions.length : "?"} actions)`).join("; ")}`);
|
|
23491
24096
|
return parts.join("\n") || "(plan has no recognisable sections)";
|
|
23492
24097
|
}
|
|
23493
|
-
var
|
|
24098
|
+
var fs18, path17;
|
|
23494
24099
|
var init_plan_store = __esm({
|
|
23495
24100
|
"src/plan-store.ts"() {
|
|
23496
24101
|
"use strict";
|
|
23497
|
-
|
|
23498
|
-
|
|
24102
|
+
fs18 = __toESM(require("fs"));
|
|
24103
|
+
path17 = __toESM(require("path"));
|
|
23499
24104
|
init_credentials_store();
|
|
23500
24105
|
}
|
|
23501
24106
|
});
|
|
23502
24107
|
|
|
23503
24108
|
// src/intake-to-build/revert.ts
|
|
23504
|
-
function plainDate(
|
|
23505
|
-
const d = new Date(
|
|
23506
|
-
if (Number.isNaN(d.getTime())) return
|
|
24109
|
+
function plainDate(iso2, timeZone) {
|
|
24110
|
+
const d = new Date(iso2);
|
|
24111
|
+
if (Number.isNaN(d.getTime())) return iso2;
|
|
23507
24112
|
try {
|
|
23508
24113
|
return new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone }).format(d);
|
|
23509
24114
|
} catch {
|
|
23510
|
-
return
|
|
24115
|
+
return iso2.slice(0, 10);
|
|
23511
24116
|
}
|
|
23512
24117
|
}
|
|
23513
24118
|
function cannotReasonForType(type) {
|
|
@@ -24412,17 +25017,17 @@ function planFromPreset(input) {
|
|
|
24412
25017
|
}
|
|
24413
25018
|
function presetsRoot() {
|
|
24414
25019
|
const candidates = [
|
|
24415
|
-
|
|
25020
|
+
path18.join(__dirname, "..", "skills", "blueprint", "presets"),
|
|
24416
25021
|
// dist/ → package root
|
|
24417
|
-
|
|
25022
|
+
path18.join(__dirname, "..", "..", "skills", "blueprint", "presets"),
|
|
24418
25023
|
// src/intake-to-build in dev
|
|
24419
|
-
|
|
25024
|
+
path18.join(process.cwd(), "skills", "blueprint", "presets")
|
|
24420
25025
|
];
|
|
24421
|
-
return candidates.find((c) =>
|
|
25026
|
+
return candidates.find((c) => fs19.existsSync(c)) ?? null;
|
|
24422
25027
|
}
|
|
24423
25028
|
function readPresetFile(dir, base) {
|
|
24424
25029
|
try {
|
|
24425
|
-
const raw = JSON.parse(
|
|
25030
|
+
const raw = JSON.parse(fs19.readFileSync(path18.join(dir, `${base}.preset.json`), "utf8"));
|
|
24426
25031
|
if (raw && typeof raw === "object" && typeof raw.presetId === "string" && raw.skeleton) return raw;
|
|
24427
25032
|
return null;
|
|
24428
25033
|
} catch {
|
|
@@ -24434,7 +25039,7 @@ function loadPresetChoice(requested, briefPreset) {
|
|
|
24434
25039
|
if (!dir) {
|
|
24435
25040
|
return { ok: false, error: "The preset library is not installed with this copy of the tool (skills/blueprint/presets is missing) \u2014 reinstall, or author the plan by hand." };
|
|
24436
25041
|
}
|
|
24437
|
-
const bases =
|
|
25042
|
+
const bases = fs19.readdirSync(dir).filter((f) => f.endsWith(".preset.json")).map((f) => f.replace(/\.preset\.json$/, ""));
|
|
24438
25043
|
const tryMatch = (want) => {
|
|
24439
25044
|
const norm4 = slugify(want);
|
|
24440
25045
|
for (const base of bases) {
|
|
@@ -24465,12 +25070,12 @@ function loadPresetChoice(requested, briefPreset) {
|
|
|
24465
25070
|
if (generic) return { ok: true, preset: generic, file: "generic-client.preset.json", note: "Using the generic preset." };
|
|
24466
25071
|
return { ok: false, error: "No usable preset file was found in the library." };
|
|
24467
25072
|
}
|
|
24468
|
-
var
|
|
25073
|
+
var fs19, path18, PRESET_TOKEN_ROOTS, FLAG_ALIASES, INLINE_TOKEN_RE, ADMIN_ROLE_RE, BRIEF_CALENDAR_TYPE_MAP;
|
|
24469
25074
|
var init_plan_from_preset = __esm({
|
|
24470
25075
|
"src/intake-to-build/plan-from-preset.ts"() {
|
|
24471
25076
|
"use strict";
|
|
24472
|
-
|
|
24473
|
-
|
|
25077
|
+
fs19 = __toESM(require("fs"));
|
|
25078
|
+
path18 = __toESM(require("path"));
|
|
24474
25079
|
init_plan();
|
|
24475
25080
|
init_brief();
|
|
24476
25081
|
init_normalizer();
|
|
@@ -25608,15 +26213,15 @@ ${text2.slice(0, 300)}`);
|
|
|
25608
26213
|
};
|
|
25609
26214
|
}
|
|
25610
26215
|
function makePipelineApi(builderClient) {
|
|
25611
|
-
return async (method,
|
|
26216
|
+
return async (method, path27, body) => {
|
|
25612
26217
|
const headers = await builderClient.buildHeaders();
|
|
25613
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
26218
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path27}`;
|
|
25614
26219
|
const options = { method, headers };
|
|
25615
26220
|
if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
|
|
25616
26221
|
const response = await fetch(url, options);
|
|
25617
26222
|
if (!response.ok) {
|
|
25618
26223
|
const text2 = await response.text();
|
|
25619
|
-
throw new Error(`Pipeline API ${response.status}: ${method} ${
|
|
26224
|
+
throw new Error(`Pipeline API ${response.status}: ${method} ${path27}
|
|
25620
26225
|
${text2.slice(0, 300)}`);
|
|
25621
26226
|
}
|
|
25622
26227
|
const text = await response.text();
|
|
@@ -25629,17 +26234,17 @@ ${text2.slice(0, 300)}`);
|
|
|
25629
26234
|
};
|
|
25630
26235
|
}
|
|
25631
26236
|
function makeFunnelApi(builderClient) {
|
|
25632
|
-
return async (method,
|
|
26237
|
+
return async (method, path27, body) => {
|
|
25633
26238
|
const headers = await builderClient.buildHeaders();
|
|
25634
26239
|
headers.Origin = "https://app.gohighlevel.com";
|
|
25635
26240
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
25636
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
26241
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path27}`;
|
|
25637
26242
|
const options = { method, headers };
|
|
25638
26243
|
if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
|
|
25639
26244
|
const response = await fetch(url, options);
|
|
25640
26245
|
if (!response.ok) {
|
|
25641
26246
|
const text2 = await response.text();
|
|
25642
|
-
throw new Error(`Funnel API ${response.status}: ${method} ${
|
|
26247
|
+
throw new Error(`Funnel API ${response.status}: ${method} ${path27}
|
|
25643
26248
|
${text2.slice(0, 300)}`);
|
|
25644
26249
|
}
|
|
25645
26250
|
const text = await response.text();
|
|
@@ -26011,7 +26616,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26011
26616
|
"get_intake_question_set",
|
|
26012
26617
|
`Return the 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. Pass industry (e.g. "clinic", "med-spa") to see the tailored set \u2014 base questions + that industry's pack + the agency's own overlay, exactly what install_intake_form installs for that industry; omit it for the base set. Use this to review or render the intake before installing it.`,
|
|
26013
26618
|
{
|
|
26014
|
-
industry:
|
|
26619
|
+
industry: import_zod67.z.string().optional().describe(`Industry pack slug to compose in (${INDUSTRY_PACKS.map((p) => p.slug).join(", ")}). Omit for the base set.`)
|
|
26015
26620
|
},
|
|
26016
26621
|
async ({ industry }) => {
|
|
26017
26622
|
const agency = readOverlay();
|
|
@@ -26042,7 +26647,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26042
26647
|
"validate_brief",
|
|
26043
26648
|
"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.",
|
|
26044
26649
|
{
|
|
26045
|
-
brief:
|
|
26650
|
+
brief: import_zod67.z.record(import_zod67.z.unknown()).describe("The Brief object to validate.")
|
|
26046
26651
|
},
|
|
26047
26652
|
async ({ brief }) => validateBrief(brief)
|
|
26048
26653
|
);
|
|
@@ -26051,7 +26656,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26051
26656
|
"validate_build_plan",
|
|
26052
26657
|
"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}.",
|
|
26053
26658
|
{
|
|
26054
|
-
plan:
|
|
26659
|
+
plan: import_zod67.z.record(import_zod67.z.unknown()).describe("The Build Plan object to validate.")
|
|
26055
26660
|
},
|
|
26056
26661
|
async ({ plan }) => validateBuildPlan(plan)
|
|
26057
26662
|
);
|
|
@@ -26059,25 +26664,25 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26059
26664
|
"apply_build_plan",
|
|
26060
26665
|
'Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. PREFERRED for a fresh build: pass fromPreset:true + the \xA74 brief and the plan is COMPOSED SERVER-SIDE from the industry preset in milliseconds (structure, workflows, send-ready copy token-filled from the brief) \u2014 never author 100 workflow actions by hand when a preset covers the brief; see the fromPreset parameter. 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), calendars, AND forms: 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). Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole account user (auto-assigns you as the team member); with 0 or 2+ users they\'re surfaced as a manual step, not auto-staffed to a guess. CALENDAR TEAM: after creating any calendar that names staff, the team is READ BACK \u2014 a 200 is not evidence. A one-off `event` calendar has no team-member concept (GHL accepts teamMembers and drops them silently), so a calendar naming staff is never built as one, and if the people still do not land the run reports it AND emits a manual step naming the person and the calendar (manual type `calendar_team`) \u2014 never a silent success. Forms build with their standard + custom fields (custom fieldRefs resolve to the real fields created earlier in the run). Funnels: a GHL funnel (target:"ghl", default) builds structurally (funnel + named steps; page content/HTML is a manual step \u2014 plans carry outlines); a funnel with target:"external" is NOT built or deployed here \u2014 the user builds + hosts the site themselves (Cloudflare/Vercel) and wires its form back to this GHL sub-account (surfaced as a manual wiring step). Workflows build as DRAFT with all their logical actions expanded to native GHL JSON (incl. opportunity create/move steps, re-enabled v3.41.0) and chained; a contact_tag trigger is built automatically, other trigger types are surfaced as a manual step; the operator reviews + publishes. STAFF (Tier 1 v2): plan.users are created through the agency key (the create_user path \u2014 bound by email when they already exist, no invented password) BEFORE calendars and workflows, so calendars get their named team members (teamMemberRefs) and notification / task steps target real user ids (userRef). Without an agency key the run still proceeds: those users are pending, and every step that notifies them is STILL BUILT with a placeholder target and flagged staff_pending ("Add a staff member, then assign this step") \u2014 never dropped, never pointed at a user from another account. TEMPLATES: plan.templates (emails + sms) are saved as account-level email templates / SMS snippets before the workflows (never-clobber by name); the steps keep the same copy inline and the template ids come back in `templates`. Email templates are created in the editor\'s own format (vibe-editor) so they open and edit in Marketing \u2192 Emails \u2192 Templates, and each saved body is READ BACK from GHL\'s preview \u2014 a template whose copy is not seen there is reported with a warning and counted in templatesUnverified, never claimed as written. Always confirms the active location and validates the plan before any write. RE-RUNS: pass useSavedPlan:true (no plan) to build from the plan saved on this machine by the earlier execute; a differently named plan is refused while a saved one exists unless replaceSavedPlan:true \u2014 re-authoring a plan on a re-run drifts and duplicates objects (a second pipeline beside the first).',
|
|
26061
26666
|
{
|
|
26062
|
-
plan:
|
|
26063
|
-
fromPreset:
|
|
26064
|
-
brief:
|
|
26065
|
-
preset:
|
|
26066
|
-
copy:
|
|
26067
|
-
|
|
26068
|
-
ref:
|
|
26069
|
-
subject:
|
|
26070
|
-
html:
|
|
26071
|
-
body:
|
|
26667
|
+
plan: import_zod67.z.record(import_zod67.z.unknown()).optional().describe("The approved \xA75 Build Plan object. Omit it and pass useSavedPlan:true to build from the plan already saved for this account."),
|
|
26668
|
+
fromPreset: import_zod67.z.boolean().optional().describe("PLAN-FROM-PRESET (PRD row 4d): compose the \xA75 plan SERVER-SIDE from the industry preset + the \xA74 brief, deterministically, in milliseconds \u2014 never author workflow actions by hand when a preset covers the brief. Pass `brief` (required) and optionally `preset` and `copy`; omit `plan`. The composed plan carries the preset's full structure (workflows, templates with complete send-ready copy token-filled from the brief, staff users, calendars with the brief's appointment lengths, pipeline stages, notification targets) and then flows through the exact same validate \u2192 dry_run/execute path as a passed plan, including being saved on execute. The dry_run response lists copySlots \u2014 the template refs whose subject/body the model MAY refine via `copy` on the execute call. If the preset cannot cover the brief the tool answers phase:\"compose\" with the reasons; only then author a plan by hand."),
|
|
26669
|
+
brief: import_zod67.z.record(import_zod67.z.unknown()).optional().describe("fromPreset only: the \xA74 Brief (from normalize_submission_to_brief, or assembled from the cockpit intake answers)."),
|
|
26670
|
+
preset: import_zod67.z.string().optional().describe("fromPreset only: which preset to compose from \u2014 a preset id (med_spa, clinic, coach, local_service, ecom, generic), an alias (dental, medspa, home_services\u2026), or an industry slug (med-spa, local-service, ecommerce\u2026). Defaults to the brief's own preset field; an unknown value falls back to the generic preset (noted in the response)."),
|
|
26671
|
+
copy: import_zod67.z.array(
|
|
26672
|
+
import_zod67.z.object({
|
|
26673
|
+
ref: import_zod67.z.string().describe("email_template.* or sms_template.* ref from the composed plan"),
|
|
26674
|
+
subject: import_zod67.z.string().optional(),
|
|
26675
|
+
html: import_zod67.z.string().optional(),
|
|
26676
|
+
body: import_zod67.z.string().optional()
|
|
26072
26677
|
})
|
|
26073
26678
|
).optional().describe("fromPreset only: client-specific copy overrides \u2014 subject/html for an email template, body for an SMS template. Copy ONLY; structure is never overridable. Overrides land on the template AND its reviewable asset. An unknown ref is an error, never a silent no-op."),
|
|
26074
|
-
useSavedPlan:
|
|
26075
|
-
replaceSavedPlan:
|
|
26076
|
-
mode:
|
|
26077
|
-
locationId:
|
|
26078
|
-
metHandoffs:
|
|
26079
|
-
publishWorkflows:
|
|
26080
|
-
onConflict:
|
|
26679
|
+
useSavedPlan: import_zod67.z.boolean().optional().describe("Load the approved plan saved for the active location by an earlier execute (plans/<locationId>.json on this machine) instead of passing one. The right choice for ANY re-run: re-authoring a plan drifts and duplicates objects."),
|
|
26680
|
+
replaceSavedPlan: import_zod67.z.boolean().optional().describe("Only with a new `plan` whose planId differs from the saved one: confirms the operator approved the new plan, so it replaces the saved plan on execute. Without it, a differently named plan is refused while a saved plan exists."),
|
|
26681
|
+
mode: import_zod67.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
|
|
26682
|
+
locationId: import_zod67.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
|
|
26683
|
+
metHandoffs: import_zod67.z.array(import_zod67.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.'),
|
|
26684
|
+
publishWorkflows: import_zod67.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
|
|
26685
|
+
onConflict: import_zod67.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
|
|
26081
26686
|
},
|
|
26082
26687
|
async ({ plan: planArgIn, fromPreset, brief: briefArg, preset: presetArg, copy: copyArg, useSavedPlan, replaceSavedPlan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
|
|
26083
26688
|
try {
|
|
@@ -26360,9 +26965,9 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26360
26965
|
"revert_build",
|
|
26361
26966
|
`Undo a Blueprint build: remove the objects a recorded apply_build_plan run CREATED in the current sub-account, and nothing else. PREVIEWS BY DEFAULT \u2014 with no confirm it writes nothing and returns a plain-English list of exactly what would be removed, grouped, plus what it will leave alone and why. Pass confirm:"DELETE" to actually remove them. Acts on one recorded run: pass runId, or omit it for the most recent run in the ACTIVE account (runs from any other sub-account are in a different journal and can never be reached from here). The one hard rule: it removes only objects the run's never-clobber ledger recorded as CREATED \u2014 anything the build merely BOUND to (a pipeline, tag, calendar or workflow that was already in the account) is never touched, so a build run against an account that already had assets cannot delete the client's own work. Deletes run in the exact reverse of the build order (workflows first, then templates, funnels and their pages, forms, calendars, custom values, tags, custom fields, pipelines) so nothing is removed while something else still points at it. A refusal from GoHighLevel never aborts the run: refusals are collected and reported at the end with what to do about each. After the deletes the account is re-read independently and each object is reported as confirmed gone or as accepted-but-unconfirmed. Idempotent: run it twice and the second run finds nothing left and says so. Two kinds are deliberately NEVER removed automatically \u2014 staff users (a person's login; removing it can strip them off appointments and assignments) and text-message templates (GoHighLevel exposes no delete for them) \u2014 both are listed with where to remove them by hand.`,
|
|
26362
26967
|
{
|
|
26363
|
-
runId:
|
|
26364
|
-
confirm:
|
|
26365
|
-
locationId:
|
|
26968
|
+
runId: import_zod67.z.string().optional().describe('The recorded build run to undo (the `runId` apply_build_plan returned, e.g. "r-20260827T0142-9c1e"). Omit for the most recent run recorded for the active sub-account.'),
|
|
26969
|
+
confirm: import_zod67.z.literal("DELETE").optional().describe('Omit for a preview (no writes at all). Pass "DELETE" to actually remove the objects the preview listed.'),
|
|
26970
|
+
locationId: import_zod67.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first).")
|
|
26366
26971
|
},
|
|
26367
26972
|
async ({ runId, confirm: confirm2, locationId: locationId2 }) => {
|
|
26368
26973
|
try {
|
|
@@ -26540,10 +27145,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26540
27145
|
"install_intake_form",
|
|
26541
27146
|
`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 Command OS cockpit asks \u2014 so no cockpit answer is left without a form field; omit it for the base set. 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 (e.g. to add an industry's questions to a form installed without one) instead of creating a new one.`,
|
|
26542
27147
|
{
|
|
26543
|
-
dryRun:
|
|
26544
|
-
formId:
|
|
26545
|
-
formName:
|
|
26546
|
-
industry:
|
|
27148
|
+
dryRun: import_zod67.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
|
|
27149
|
+
formId: import_zod67.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
|
|
27150
|
+
formName: import_zod67.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`),
|
|
27151
|
+
industry: import_zod67.z.string().optional().describe(`Industry pack to install with the base set (${INDUSTRY_PACKS.map((p) => p.slug).join(", ")}). Omit for the base set only.`)
|
|
26547
27152
|
},
|
|
26548
27153
|
async ({ dryRun, formId, formName, industry }) => {
|
|
26549
27154
|
try {
|
|
@@ -26646,10 +27251,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26646
27251
|
"normalize_submission_to_brief",
|
|
26647
27252
|
'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).',
|
|
26648
27253
|
{
|
|
26649
|
-
formId:
|
|
26650
|
-
submissionId:
|
|
26651
|
-
fieldMap:
|
|
26652
|
-
preset:
|
|
27254
|
+
formId: import_zod67.z.string().describe("The intake form ID (from install_intake_form)."),
|
|
27255
|
+
submissionId: import_zod67.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
|
|
27256
|
+
fieldMap: import_zod67.z.record(import_zod67.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
|
|
27257
|
+
preset: import_zod67.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
|
|
26653
27258
|
},
|
|
26654
27259
|
async ({ formId, submissionId, fieldMap, preset }) => {
|
|
26655
27260
|
try {
|
|
@@ -26678,7 +27283,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26678
27283
|
const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
|
|
26679
27284
|
resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
|
|
26680
27285
|
}
|
|
26681
|
-
const presetSchema2 =
|
|
27286
|
+
const presetSchema2 = import_zod67.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
|
|
26682
27287
|
const presetParsed = presetSchema2.safeParse(preset);
|
|
26683
27288
|
const brief = normalizeSubmissionToBrief({
|
|
26684
27289
|
others,
|
|
@@ -26699,11 +27304,11 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
26699
27304
|
}
|
|
26700
27305
|
);
|
|
26701
27306
|
}
|
|
26702
|
-
var
|
|
27307
|
+
var import_zod67, customFieldItemSchema, sleep5;
|
|
26703
27308
|
var init_intake_to_build = __esm({
|
|
26704
27309
|
"src/tools/intake-to-build.ts"() {
|
|
26705
27310
|
"use strict";
|
|
26706
|
-
|
|
27311
|
+
import_zod67 = require("zod");
|
|
26707
27312
|
init_tool_helpers();
|
|
26708
27313
|
init_form_builder();
|
|
26709
27314
|
init_calendars();
|
|
@@ -26725,16 +27330,16 @@ var init_intake_to_build = __esm({
|
|
|
26725
27330
|
init_plan_from_preset();
|
|
26726
27331
|
init_executor();
|
|
26727
27332
|
init_execute();
|
|
26728
|
-
customFieldItemSchema =
|
|
26729
|
-
id:
|
|
26730
|
-
name:
|
|
26731
|
-
fieldKey:
|
|
26732
|
-
dataType:
|
|
26733
|
-
model:
|
|
26734
|
-
parentId:
|
|
26735
|
-
position:
|
|
26736
|
-
dateAdded:
|
|
26737
|
-
picklistOptions:
|
|
27333
|
+
customFieldItemSchema = import_zod67.z.object({
|
|
27334
|
+
id: import_zod67.z.string(),
|
|
27335
|
+
name: import_zod67.z.string(),
|
|
27336
|
+
fieldKey: import_zod67.z.string(),
|
|
27337
|
+
dataType: import_zod67.z.string(),
|
|
27338
|
+
model: import_zod67.z.string().optional(),
|
|
27339
|
+
parentId: import_zod67.z.string().optional(),
|
|
27340
|
+
position: import_zod67.z.number().optional(),
|
|
27341
|
+
dateAdded: import_zod67.z.string().optional(),
|
|
27342
|
+
picklistOptions: import_zod67.z.array(import_zod67.z.string()).optional()
|
|
26738
27343
|
}).passthrough();
|
|
26739
27344
|
sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26740
27345
|
}
|
|
@@ -26762,6 +27367,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
26762
27367
|
registerUserGuideTools(wrap(USER_GUIDE_MODULE));
|
|
26763
27368
|
registerAgencyProfileTools(wrap(AGENCY_PROFILE_MODULE));
|
|
26764
27369
|
registerAssessmentTools(wrap(ASSESSMENT_MODULE), client);
|
|
27370
|
+
registerConnectorTools(wrap(CONNECTORS_MODULE));
|
|
26765
27371
|
registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
|
|
26766
27372
|
registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
|
|
26767
27373
|
registerDailyBriefTools(wrap(DAILY_BRIEF_MODULE), registry2);
|
|
@@ -26810,7 +27416,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
26810
27416
|
}
|
|
26811
27417
|
return { registeredTools, gatedTools, planGatedTools };
|
|
26812
27418
|
}
|
|
26813
|
-
var publicApiTools, internalApiTools, VALIDATORS_MODULE, DIAGNOSTICS_MODULE, LOCATION_SWITCHER_MODULE, SNAPSHOTS_MODULE, ACCOUNT_EXPORT_MODULE, FORM_BUILDER_MODULE, INTAKE_TO_BUILD_MODULE, EMAIL_BUILDER_MODULE, FUNNEL_QA_MODULE, USER_GUIDE_MODULE, AGENCY_PROFILE_MODULE, ASSESSMENT_MODULE, CLIENT_ENGAGEMENTS_MODULE, AUDIT_REPORT_MODULE, DAILY_BRIEF_MODULE, CHECKUP_MODULE, KNOWN_MODULES, PUBLIC_API_MODULES, INTERNAL_API_MODULES, DUAL_CLIENT_MODULES;
|
|
27419
|
+
var publicApiTools, internalApiTools, VALIDATORS_MODULE, DIAGNOSTICS_MODULE, LOCATION_SWITCHER_MODULE, SNAPSHOTS_MODULE, ACCOUNT_EXPORT_MODULE, FORM_BUILDER_MODULE, INTAKE_TO_BUILD_MODULE, EMAIL_BUILDER_MODULE, FUNNEL_QA_MODULE, USER_GUIDE_MODULE, AGENCY_PROFILE_MODULE, ASSESSMENT_MODULE, CONNECTORS_MODULE, CLIENT_ENGAGEMENTS_MODULE, AUDIT_REPORT_MODULE, DAILY_BRIEF_MODULE, CHECKUP_MODULE, KNOWN_MODULES, PUBLIC_API_MODULES, INTERNAL_API_MODULES, DUAL_CLIENT_MODULES;
|
|
26814
27420
|
var init_tools = __esm({
|
|
26815
27421
|
"src/tools/index.ts"() {
|
|
26816
27422
|
"use strict";
|
|
@@ -26864,6 +27470,7 @@ var init_tools = __esm({
|
|
|
26864
27470
|
init_user_guide();
|
|
26865
27471
|
init_agency_profile2();
|
|
26866
27472
|
init_assessment();
|
|
27473
|
+
init_connectors();
|
|
26867
27474
|
init_client_engagements2();
|
|
26868
27475
|
init_audit_report2();
|
|
26869
27476
|
init_daily_brief2();
|
|
@@ -26935,11 +27542,13 @@ var init_tools = __esm({
|
|
|
26935
27542
|
USER_GUIDE_MODULE = "user-guide";
|
|
26936
27543
|
AGENCY_PROFILE_MODULE = "agency-profile";
|
|
26937
27544
|
ASSESSMENT_MODULE = "assessment";
|
|
27545
|
+
CONNECTORS_MODULE = "connectors";
|
|
26938
27546
|
CLIENT_ENGAGEMENTS_MODULE = "client-engagements";
|
|
26939
27547
|
AUDIT_REPORT_MODULE = "audit-report";
|
|
26940
27548
|
DAILY_BRIEF_MODULE = "daily-brief";
|
|
26941
27549
|
CHECKUP_MODULE = "checkup";
|
|
26942
27550
|
KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
27551
|
+
"connectors",
|
|
26943
27552
|
"assessment",
|
|
26944
27553
|
...publicApiTools.map(([, label]) => label),
|
|
26945
27554
|
...internalApiTools.map(([, label]) => label),
|
|
@@ -26960,6 +27569,7 @@ var init_tools = __esm({
|
|
|
26960
27569
|
ACCOUNT_EXPORT_MODULE
|
|
26961
27570
|
]);
|
|
26962
27571
|
PUBLIC_API_MODULES = /* @__PURE__ */ new Set([
|
|
27572
|
+
"connectors",
|
|
26963
27573
|
"assessment",
|
|
26964
27574
|
...publicApiTools.map(([, label]) => label),
|
|
26965
27575
|
USER_GUIDE_MODULE,
|
|
@@ -27011,7 +27621,7 @@ function defaultIO() {
|
|
|
27011
27621
|
return {
|
|
27012
27622
|
out: (l) => process.stdout.write(l + "\n"),
|
|
27013
27623
|
err: (l) => process.stderr.write(l + "\n"),
|
|
27014
|
-
rename: (from, to) =>
|
|
27624
|
+
rename: (from, to) => fs21.renameSync(from, to),
|
|
27015
27625
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
27016
27626
|
};
|
|
27017
27627
|
}
|
|
@@ -27021,32 +27631,32 @@ function candidateConfigPaths(opts) {
|
|
|
27021
27631
|
const file = "claude_desktop_config.json";
|
|
27022
27632
|
const candidates = [];
|
|
27023
27633
|
if (platform2 === "darwin") {
|
|
27024
|
-
candidates.push(
|
|
27634
|
+
candidates.push(path20.join(home, "Library", "Application Support", "Claude", file));
|
|
27025
27635
|
} else if (platform2 === "win32") {
|
|
27026
|
-
const appData = opts?.appData ?? process.env.APPDATA ??
|
|
27027
|
-
candidates.push(
|
|
27028
|
-
const localAppData = opts?.localAppData ?? process.env.LOCALAPPDATA ??
|
|
27029
|
-
const packagesDir =
|
|
27636
|
+
const appData = opts?.appData ?? process.env.APPDATA ?? path20.join(home, "AppData", "Roaming");
|
|
27637
|
+
candidates.push(path20.join(appData, "Claude", file));
|
|
27638
|
+
const localAppData = opts?.localAppData ?? process.env.LOCALAPPDATA ?? path20.join(home, "AppData", "Local");
|
|
27639
|
+
const packagesDir = path20.join(localAppData, "Packages");
|
|
27030
27640
|
try {
|
|
27031
|
-
for (const entry of
|
|
27641
|
+
for (const entry of fs21.readdirSync(packagesDir)) {
|
|
27032
27642
|
if (entry.startsWith("Claude_")) {
|
|
27033
|
-
candidates.push(
|
|
27643
|
+
candidates.push(path20.join(packagesDir, entry, "LocalCache", "Roaming", "Claude", file));
|
|
27034
27644
|
}
|
|
27035
27645
|
}
|
|
27036
27646
|
} catch {
|
|
27037
27647
|
}
|
|
27038
27648
|
} else {
|
|
27039
|
-
candidates.push(
|
|
27649
|
+
candidates.push(path20.join(home, ".config", "Claude", file));
|
|
27040
27650
|
}
|
|
27041
27651
|
return candidates;
|
|
27042
27652
|
}
|
|
27043
27653
|
function resolveConfigPath(explicitPath) {
|
|
27044
27654
|
if (explicitPath) {
|
|
27045
|
-
const p =
|
|
27046
|
-
return { configPath: p, exists:
|
|
27655
|
+
const p = path20.resolve(explicitPath);
|
|
27656
|
+
return { configPath: p, exists: fs21.existsSync(p) };
|
|
27047
27657
|
}
|
|
27048
27658
|
const candidates = candidateConfigPaths();
|
|
27049
|
-
const existing = candidates.filter((c) =>
|
|
27659
|
+
const existing = candidates.filter((c) => fs21.existsSync(c));
|
|
27050
27660
|
if (existing.length > 1) {
|
|
27051
27661
|
throw new InstallStop(
|
|
27052
27662
|
EXIT_REFUSED,
|
|
@@ -27063,22 +27673,22 @@ function resolveConfigPath(explicitPath) {
|
|
|
27063
27673
|
function resolveSymlinkPolicy(configPath, explicitPath, home = os5.homedir()) {
|
|
27064
27674
|
let st;
|
|
27065
27675
|
try {
|
|
27066
|
-
st =
|
|
27676
|
+
st = fs21.lstatSync(configPath);
|
|
27067
27677
|
} catch {
|
|
27068
27678
|
return configPath;
|
|
27069
27679
|
}
|
|
27070
27680
|
if (!st.isSymbolicLink()) return configPath;
|
|
27071
27681
|
let real;
|
|
27072
27682
|
try {
|
|
27073
|
-
real =
|
|
27683
|
+
real = fs21.realpathSync(configPath);
|
|
27074
27684
|
} catch {
|
|
27075
27685
|
throw new InstallStop(
|
|
27076
27686
|
EXIT_REFUSED,
|
|
27077
27687
|
`${configPath} is a link that points to a file that does not exist. Fix or remove the link, or run again with --path to a real file. Nothing was changed.`
|
|
27078
27688
|
);
|
|
27079
27689
|
}
|
|
27080
|
-
const rel =
|
|
27081
|
-
const outsideHome = rel.startsWith("..") ||
|
|
27690
|
+
const rel = path20.relative(path20.resolve(home), real);
|
|
27691
|
+
const outsideHome = rel.startsWith("..") || path20.isAbsolute(rel);
|
|
27082
27692
|
if (outsideHome && !explicitPath) {
|
|
27083
27693
|
throw new InstallStop(
|
|
27084
27694
|
EXIT_REFUSED,
|
|
@@ -27088,8 +27698,8 @@ function resolveSymlinkPolicy(configPath, explicitPath, home = os5.homedir()) {
|
|
|
27088
27698
|
return real;
|
|
27089
27699
|
}
|
|
27090
27700
|
function readConfigBytes(configPath) {
|
|
27091
|
-
const bytes =
|
|
27092
|
-
const stat =
|
|
27701
|
+
const bytes = fs21.readFileSync(configPath);
|
|
27702
|
+
const stat = fs21.statSync(configPath);
|
|
27093
27703
|
if (bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 254 && bytes[1] === 255)) {
|
|
27094
27704
|
throw new InstallStop(
|
|
27095
27705
|
EXIT_REFUSED,
|
|
@@ -27163,23 +27773,23 @@ function backupStamp(now) {
|
|
|
27163
27773
|
}
|
|
27164
27774
|
function chooseBackupPath(configPath, now = /* @__PURE__ */ new Date()) {
|
|
27165
27775
|
const base = `${configPath}.bak-${backupStamp(now)}`;
|
|
27166
|
-
if (!
|
|
27776
|
+
if (!fs21.existsSync(base)) return base;
|
|
27167
27777
|
for (let i = 2; ; i++) {
|
|
27168
27778
|
const candidate = `${base}-${i}`;
|
|
27169
|
-
if (!
|
|
27779
|
+
if (!fs21.existsSync(candidate)) return candidate;
|
|
27170
27780
|
}
|
|
27171
27781
|
}
|
|
27172
27782
|
function writeVerifiedBackup(configPath, originalBytes) {
|
|
27173
27783
|
const backupPath = chooseBackupPath(configPath);
|
|
27174
27784
|
try {
|
|
27175
|
-
|
|
27176
|
-
const readBack =
|
|
27785
|
+
fs21.writeFileSync(backupPath, originalBytes);
|
|
27786
|
+
const readBack = fs21.readFileSync(backupPath);
|
|
27177
27787
|
if (!readBack.equals(originalBytes)) {
|
|
27178
27788
|
throw new Error("backup read-back did not match");
|
|
27179
27789
|
}
|
|
27180
27790
|
} catch (e) {
|
|
27181
27791
|
try {
|
|
27182
|
-
|
|
27792
|
+
fs21.rmSync(backupPath, { force: true });
|
|
27183
27793
|
} catch {
|
|
27184
27794
|
}
|
|
27185
27795
|
throw new InstallStop(
|
|
@@ -27198,9 +27808,9 @@ function baselineOf(bytes, stat) {
|
|
|
27198
27808
|
function assertNotStale(configPath, baseline) {
|
|
27199
27809
|
let ok = false;
|
|
27200
27810
|
try {
|
|
27201
|
-
const st =
|
|
27811
|
+
const st = fs21.statSync(configPath);
|
|
27202
27812
|
if (st.size === baseline.size && st.mtimeMs === baseline.mtimeMs) {
|
|
27203
|
-
ok = sha2562(
|
|
27813
|
+
ok = sha2562(fs21.readFileSync(configPath)) === baseline.hash;
|
|
27204
27814
|
} else {
|
|
27205
27815
|
ok = false;
|
|
27206
27816
|
}
|
|
@@ -27215,14 +27825,14 @@ function assertNotStale(configPath, baseline) {
|
|
|
27215
27825
|
}
|
|
27216
27826
|
}
|
|
27217
27827
|
async function atomicReplace(opts) {
|
|
27218
|
-
const dir =
|
|
27219
|
-
const tempPath =
|
|
27220
|
-
const fd =
|
|
27828
|
+
const dir = path20.dirname(opts.configPath);
|
|
27829
|
+
const tempPath = path20.join(dir, `.${path20.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}`);
|
|
27830
|
+
const fd = fs21.openSync(tempPath, "w");
|
|
27221
27831
|
try {
|
|
27222
|
-
|
|
27223
|
-
|
|
27832
|
+
fs21.writeFileSync(fd, opts.newBytes);
|
|
27833
|
+
fs21.fsyncSync(fd);
|
|
27224
27834
|
} finally {
|
|
27225
|
-
|
|
27835
|
+
fs21.closeSync(fd);
|
|
27226
27836
|
}
|
|
27227
27837
|
try {
|
|
27228
27838
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -27247,17 +27857,17 @@ async function atomicReplace(opts) {
|
|
|
27247
27857
|
}
|
|
27248
27858
|
} catch (e) {
|
|
27249
27859
|
try {
|
|
27250
|
-
|
|
27860
|
+
fs21.rmSync(tempPath, { force: true });
|
|
27251
27861
|
} catch {
|
|
27252
27862
|
}
|
|
27253
27863
|
throw e;
|
|
27254
27864
|
}
|
|
27255
27865
|
try {
|
|
27256
|
-
const dirFd =
|
|
27866
|
+
const dirFd = fs21.openSync(dir, "r");
|
|
27257
27867
|
try {
|
|
27258
|
-
|
|
27868
|
+
fs21.fsyncSync(dirFd);
|
|
27259
27869
|
} finally {
|
|
27260
|
-
|
|
27870
|
+
fs21.closeSync(dirFd);
|
|
27261
27871
|
}
|
|
27262
27872
|
} catch {
|
|
27263
27873
|
}
|
|
@@ -27309,7 +27919,7 @@ async function runInstall(argv, ioOverride) {
|
|
|
27309
27919
|
try {
|
|
27310
27920
|
const resolved2 = resolveConfigPath(flags.path);
|
|
27311
27921
|
const configPath = resolveSymlinkPolicy(resolved2.configPath, flags.path !== void 0);
|
|
27312
|
-
const exists =
|
|
27922
|
+
const exists = fs21.existsSync(configPath);
|
|
27313
27923
|
let read = null;
|
|
27314
27924
|
let outcome;
|
|
27315
27925
|
if (exists) {
|
|
@@ -27339,7 +27949,7 @@ async function runInstall(argv, ioOverride) {
|
|
|
27339
27949
|
return EXIT_OK;
|
|
27340
27950
|
}
|
|
27341
27951
|
const backupPath = exists && read ? writeVerifiedBackup(configPath, read.bytes) : null;
|
|
27342
|
-
if (!exists)
|
|
27952
|
+
if (!exists) fs21.mkdirSync(path20.dirname(configPath), { recursive: true });
|
|
27343
27953
|
await atomicReplace({
|
|
27344
27954
|
configPath,
|
|
27345
27955
|
newBytes,
|
|
@@ -27357,13 +27967,13 @@ async function runInstall(argv, ioOverride) {
|
|
|
27357
27967
|
return EXIT_ABORTED;
|
|
27358
27968
|
}
|
|
27359
27969
|
}
|
|
27360
|
-
var
|
|
27970
|
+
var fs21, os5, path20, import_node_crypto5, import_node_util, import_json5, SERVER_KEY, DESIRED_ENTRY, EXIT_OK, EXIT_USAGE, EXIT_REFUSED, EXIT_ABORTED, RENAME_RETRY_DELAYS_MS, LOCK_ERROR_CODES, BOM_UTF8, InstallStop;
|
|
27361
27971
|
var init_config_installer = __esm({
|
|
27362
27972
|
"src/config-installer.ts"() {
|
|
27363
27973
|
"use strict";
|
|
27364
|
-
|
|
27974
|
+
fs21 = __toESM(require("node:fs"));
|
|
27365
27975
|
os5 = __toESM(require("node:os"));
|
|
27366
|
-
|
|
27976
|
+
path20 = __toESM(require("node:path"));
|
|
27367
27977
|
import_node_crypto5 = require("node:crypto");
|
|
27368
27978
|
import_node_util = require("node:util");
|
|
27369
27979
|
import_json5 = __toESM(require("json5"));
|
|
@@ -27394,7 +28004,7 @@ var require_package = __commonJS({
|
|
|
27394
28004
|
"package.json"(exports2, module2) {
|
|
27395
28005
|
module2.exports = {
|
|
27396
28006
|
name: "@elitedcs/ghl-mcp",
|
|
27397
|
-
version: "3.
|
|
28007
|
+
version: "3.74.0",
|
|
27398
28008
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
27399
28009
|
description: "GoHighLevel MCP Server for Claude. 247 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.",
|
|
27400
28010
|
main: "dist/index.js",
|
|
@@ -27492,7 +28102,7 @@ function launchCommand(port = 7300) {
|
|
|
27492
28102
|
return `npx -y @elitedcs/ghl-mcp@latest dashboard --port=${port}`;
|
|
27493
28103
|
}
|
|
27494
28104
|
function installingEntry() {
|
|
27495
|
-
return
|
|
28105
|
+
return path22.join(__dirname, "index.js");
|
|
27496
28106
|
}
|
|
27497
28107
|
function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
|
|
27498
28108
|
const cmd = launchCommand(port);
|
|
@@ -27500,12 +28110,12 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
|
|
|
27500
28110
|
const systemApps = "/Applications";
|
|
27501
28111
|
let base = home;
|
|
27502
28112
|
try {
|
|
27503
|
-
|
|
28113
|
+
fs23.accessSync(systemApps, fs23.constants.W_OK);
|
|
27504
28114
|
base = "";
|
|
27505
28115
|
} catch {
|
|
27506
28116
|
}
|
|
27507
|
-
const appDir = base ?
|
|
27508
|
-
const macOSDir =
|
|
28117
|
+
const appDir = base ? path22.join(home, "Applications", "Command OS.app") : path22.join(systemApps, "Command OS.app");
|
|
28118
|
+
const macOSDir = path22.join(appDir, "Contents", "MacOS");
|
|
27509
28119
|
const script = [
|
|
27510
28120
|
"#!/bin/bash",
|
|
27511
28121
|
"# GHL Command \u2014 Command OS launcher (regenerate: ghl-mcp install-launcher)",
|
|
@@ -27534,13 +28144,13 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
|
|
|
27534
28144
|
targetPath: appDir,
|
|
27535
28145
|
humanLocation: base ? "your personal Applications folder at ~/Applications (Finder \u2192 Go \u2192 Home \u2192 Applications)" : "your Applications folder \u2014 open Finder \u2192 Applications and look for \u201CCommand OS\u201D",
|
|
27536
28146
|
files: [
|
|
27537
|
-
{ path:
|
|
27538
|
-
{ path:
|
|
28147
|
+
{ path: path22.join(macOSDir, "command-os"), contents: script, executable: true },
|
|
28148
|
+
{ path: path22.join(appDir, "Contents", "Info.plist"), contents: plist, executable: false }
|
|
27539
28149
|
]
|
|
27540
28150
|
};
|
|
27541
28151
|
}
|
|
27542
28152
|
if (platform2 === "win32") {
|
|
27543
|
-
const target2 =
|
|
28153
|
+
const target2 = path22.join(home, "Desktop", "Command OS.cmd");
|
|
27544
28154
|
return {
|
|
27545
28155
|
platform: platform2,
|
|
27546
28156
|
targetPath: target2,
|
|
@@ -27553,7 +28163,7 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
|
|
|
27553
28163
|
].join("\r\n"), executable: false }]
|
|
27554
28164
|
};
|
|
27555
28165
|
}
|
|
27556
|
-
const target =
|
|
28166
|
+
const target = path22.join(home, ".local", "share", "applications", "command-os.desktop");
|
|
27557
28167
|
return {
|
|
27558
28168
|
platform: platform2,
|
|
27559
28169
|
targetPath: target,
|
|
@@ -27580,8 +28190,8 @@ function installLauncher(argv = []) {
|
|
|
27580
28190
|
const plan = planLauncher(process.platform, os6.homedir(), port, installingEntry());
|
|
27581
28191
|
try {
|
|
27582
28192
|
for (const f of plan.files) {
|
|
27583
|
-
|
|
27584
|
-
|
|
28193
|
+
fs23.mkdirSync(path22.dirname(f.path), { recursive: true });
|
|
28194
|
+
fs23.writeFileSync(f.path, f.contents, { mode: f.executable ? 493 : 420 });
|
|
27585
28195
|
}
|
|
27586
28196
|
} catch (e) {
|
|
27587
28197
|
process.stderr.write(`
|
|
@@ -27601,13 +28211,13 @@ function installLauncher(argv = []) {
|
|
|
27601
28211
|
].join("\n"));
|
|
27602
28212
|
return 0;
|
|
27603
28213
|
}
|
|
27604
|
-
var
|
|
28214
|
+
var fs23, os6, path22;
|
|
27605
28215
|
var init_launcher = __esm({
|
|
27606
28216
|
"src/launcher.ts"() {
|
|
27607
28217
|
"use strict";
|
|
27608
|
-
|
|
28218
|
+
fs23 = __toESM(require("fs"));
|
|
27609
28219
|
os6 = __toESM(require("os"));
|
|
27610
|
-
|
|
28220
|
+
path22 = __toESM(require("path"));
|
|
27611
28221
|
}
|
|
27612
28222
|
});
|
|
27613
28223
|
|
|
@@ -28514,11 +29124,11 @@ var init_client_status = __esm({
|
|
|
28514
29124
|
|
|
28515
29125
|
// src/command-os/client-status-placement.ts
|
|
28516
29126
|
function statusStorePath(base = appDataDir()) {
|
|
28517
|
-
return
|
|
29127
|
+
return path23.join(base, "client-status.json");
|
|
28518
29128
|
}
|
|
28519
29129
|
function readStatusStore(base) {
|
|
28520
29130
|
try {
|
|
28521
|
-
const raw = JSON.parse(
|
|
29131
|
+
const raw = JSON.parse(fs24.readFileSync(statusStorePath(base), "utf8"));
|
|
28522
29132
|
if (raw && raw.v === 1 && raw.clients) return raw;
|
|
28523
29133
|
} catch {
|
|
28524
29134
|
}
|
|
@@ -28526,8 +29136,8 @@ function readStatusStore(base) {
|
|
|
28526
29136
|
}
|
|
28527
29137
|
function writeStatusStore(store, base) {
|
|
28528
29138
|
const file = statusStorePath(base);
|
|
28529
|
-
|
|
28530
|
-
|
|
29139
|
+
fs24.mkdirSync(path23.dirname(file), { recursive: true, mode: 448 });
|
|
29140
|
+
fs24.writeFileSync(file, JSON.stringify(store, null, 2), { mode: 384 });
|
|
28531
29141
|
}
|
|
28532
29142
|
function readStatusRecord(locationId2, base) {
|
|
28533
29143
|
return readStatusStore(base).clients[locationId2] ?? { locationId: locationId2 };
|
|
@@ -28559,9 +29169,9 @@ function describePlacement(placement) {
|
|
|
28559
29169
|
return `A real page inside the client's own GoHighLevel account, at /${placement.slug ?? STATUS_SLUG}${placement.publicUrl ? ` \u2014 ${placement.publicUrl}` : ""}. Every re-check replaces that one page; it never adds a second copy.`;
|
|
28560
29170
|
}
|
|
28561
29171
|
if (placement.kind === "agency-site") {
|
|
28562
|
-
return `A file on your own site: ${
|
|
29172
|
+
return `A file on your own site: ${path23.join(placement.directory, placement.fileName ?? "status.html")}${placement.publicUrl ? `, which you publish at ${placement.publicUrl}` : ""}. The same file is rewritten every time, so the address never changes.`;
|
|
28563
29173
|
}
|
|
28564
|
-
return `A page written to ${
|
|
29174
|
+
return `A page written to ${path23.join(placement.directory, placement.fileName ?? "status.html")} for you to publish as an Artifact from your own Claude${placement.publicUrl ? `, currently at ${placement.publicUrl}` : ""}.`;
|
|
28565
29175
|
}
|
|
28566
29176
|
function placementReadiness(placement) {
|
|
28567
29177
|
if (!placement) return ["Choose where this client's page should live: their own GoHighLevel account, your own site, or an Artifact from your own Claude."];
|
|
@@ -28620,12 +29230,12 @@ function artifactPublishSteps(filePath, existingUrl) {
|
|
|
28620
29230
|
"Send the client the link once. From then on they just revisit it."
|
|
28621
29231
|
];
|
|
28622
29232
|
}
|
|
28623
|
-
var
|
|
29233
|
+
var fs24, path23, STATUS_SLUG, OUR_HOSTS;
|
|
28624
29234
|
var init_client_status_placement = __esm({
|
|
28625
29235
|
"src/command-os/client-status-placement.ts"() {
|
|
28626
29236
|
"use strict";
|
|
28627
|
-
|
|
28628
|
-
|
|
29237
|
+
fs24 = __toESM(require("node:fs"));
|
|
29238
|
+
path23 = __toESM(require("node:path"));
|
|
28629
29239
|
init_credentials_store();
|
|
28630
29240
|
init_client_status();
|
|
28631
29241
|
STATUS_SLUG = "status";
|
|
@@ -28752,24 +29362,24 @@ var init_client_status_page = __esm({
|
|
|
28752
29362
|
|
|
28753
29363
|
// src/command-os/client-status-publish.ts
|
|
28754
29364
|
function handoffSnapshotDir(base = appDataDir()) {
|
|
28755
|
-
return
|
|
29365
|
+
return path24.join(base, "handoffs");
|
|
28756
29366
|
}
|
|
28757
29367
|
function handoffSnapshotPath(locationId2, base) {
|
|
28758
|
-
return
|
|
29368
|
+
return path24.join(handoffSnapshotDir(base), `${locationId2}-handoff.json`);
|
|
28759
29369
|
}
|
|
28760
29370
|
function readHandoffSnapshot(locationId2, base) {
|
|
28761
29371
|
try {
|
|
28762
|
-
return JSON.parse(
|
|
29372
|
+
return JSON.parse(fs25.readFileSync(handoffSnapshotPath(locationId2, base), "utf8"));
|
|
28763
29373
|
} catch {
|
|
28764
29374
|
return null;
|
|
28765
29375
|
}
|
|
28766
29376
|
}
|
|
28767
29377
|
function localCopyPath(locationId2, base = appDataDir()) {
|
|
28768
|
-
return
|
|
29378
|
+
return path24.join(base, "client-status", `${locationId2}-status.html`);
|
|
28769
29379
|
}
|
|
28770
29380
|
function readVerifyOutcome(locationId2, base = appDataDir()) {
|
|
28771
29381
|
try {
|
|
28772
|
-
const st = JSON.parse(
|
|
29382
|
+
const st = JSON.parse(fs25.readFileSync(path24.join(base, "cockpit-state.json"), "utf8"));
|
|
28773
29383
|
const o = st.outcomes?.[`${locationId2}:4`];
|
|
28774
29384
|
if (!o) return null;
|
|
28775
29385
|
const src = o.plain ?? { summary: o.summary, issues: o.issues };
|
|
@@ -28779,9 +29389,9 @@ function readVerifyOutcome(locationId2, base = appDataDir()) {
|
|
|
28779
29389
|
}
|
|
28780
29390
|
}
|
|
28781
29391
|
function writeFileFor(dir, fileName, html2) {
|
|
28782
|
-
|
|
28783
|
-
const file =
|
|
28784
|
-
|
|
29392
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
29393
|
+
const file = path24.join(dir, fileName);
|
|
29394
|
+
fs25.writeFileSync(file, html2);
|
|
28785
29395
|
return file;
|
|
28786
29396
|
}
|
|
28787
29397
|
async function refreshClientStatus(opts) {
|
|
@@ -28813,7 +29423,7 @@ async function refreshClientStatus(opts) {
|
|
|
28813
29423
|
);
|
|
28814
29424
|
const html2 = renderClientStatusHtml(input);
|
|
28815
29425
|
const localPath = localCopyPath(opts.locationId, opts.base);
|
|
28816
|
-
if (!opts.dryRun) writeFileFor(
|
|
29426
|
+
if (!opts.dryRun) writeFileFor(path24.dirname(localPath), path24.basename(localPath), html2);
|
|
28817
29427
|
const placement = record.placement;
|
|
28818
29428
|
const missing = placementReadiness(placement);
|
|
28819
29429
|
if (!placement || missing.length) {
|
|
@@ -28858,7 +29468,7 @@ async function publishTo(placement, input, html2, opts) {
|
|
|
28858
29468
|
if (placement.kind === "agency-site") {
|
|
28859
29469
|
assertNotOurHost(placement.publicUrl, "The address you chose for this page");
|
|
28860
29470
|
const fileName = placement.fileName ?? "status.html";
|
|
28861
|
-
const destinationPath = opts.dryRun ?
|
|
29471
|
+
const destinationPath = opts.dryRun ? path24.join(placement.directory, fileName) : writeFileFor(placement.directory, fileName, html2);
|
|
28862
29472
|
return {
|
|
28863
29473
|
...placement.publicUrl && { url: placement.publicUrl },
|
|
28864
29474
|
destinationPath,
|
|
@@ -28872,7 +29482,7 @@ async function publishTo(placement, input, html2, opts) {
|
|
|
28872
29482
|
const safety = artifactSafety(html2);
|
|
28873
29483
|
if (!safety.ok) throw new Error(safety.problems.join(" "));
|
|
28874
29484
|
const fileName = placement.fileName ?? "status.html";
|
|
28875
|
-
const destinationPath = opts.dryRun ?
|
|
29485
|
+
const destinationPath = opts.dryRun ? path24.join(placement.directory, fileName) : writeFileFor(placement.directory, fileName, html2);
|
|
28876
29486
|
assertNotOurHost(placement.publicUrl, "The Artifact link saved for this client");
|
|
28877
29487
|
return {
|
|
28878
29488
|
...placement.publicUrl && { url: placement.publicUrl },
|
|
@@ -28911,12 +29521,12 @@ async function refreshAfterVerify(locationId2, accountName, emit2, base) {
|
|
|
28911
29521
|
emit2(`The client's status page was not updated: ${e instanceof Error ? e.message : String(e)}`);
|
|
28912
29522
|
}
|
|
28913
29523
|
}
|
|
28914
|
-
var
|
|
29524
|
+
var fs25, path24;
|
|
28915
29525
|
var init_client_status_publish = __esm({
|
|
28916
29526
|
"src/command-os/client-status-publish.ts"() {
|
|
28917
29527
|
"use strict";
|
|
28918
|
-
|
|
28919
|
-
|
|
29528
|
+
fs25 = __toESM(require("node:fs"));
|
|
29529
|
+
path24 = __toESM(require("node:path"));
|
|
28920
29530
|
init_credentials_store();
|
|
28921
29531
|
init_branding();
|
|
28922
29532
|
init_client_status();
|
|
@@ -29231,11 +29841,11 @@ function recoverStuckStages(state) {
|
|
|
29231
29841
|
return { state: { ...state, clients }, recovered };
|
|
29232
29842
|
}
|
|
29233
29843
|
function cockpitStatePath() {
|
|
29234
|
-
return
|
|
29844
|
+
return path25.join(appDataDir(), "cockpit-state.json");
|
|
29235
29845
|
}
|
|
29236
29846
|
function readCockpitState() {
|
|
29237
29847
|
try {
|
|
29238
|
-
const raw = JSON.parse(
|
|
29848
|
+
const raw = JSON.parse(fs26.readFileSync(cockpitStatePath(), "utf8"));
|
|
29239
29849
|
if (raw && raw.v === 1 && raw.clients && typeof raw.clients === "object") return raw;
|
|
29240
29850
|
} catch {
|
|
29241
29851
|
}
|
|
@@ -29243,7 +29853,7 @@ function readCockpitState() {
|
|
|
29243
29853
|
}
|
|
29244
29854
|
function writeCockpitState(state) {
|
|
29245
29855
|
ensureAppDataDir();
|
|
29246
|
-
|
|
29856
|
+
fs26.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
|
|
29247
29857
|
}
|
|
29248
29858
|
function setStage(state, locationId2, stageIndex, status) {
|
|
29249
29859
|
if (!Number.isInteger(stageIndex) || stageIndex < 0 || stageIndex >= STAGES.length) throw new Error("bad stage index");
|
|
@@ -29385,7 +29995,7 @@ function notify(message) {
|
|
|
29385
29995
|
`);
|
|
29386
29996
|
try {
|
|
29387
29997
|
ensureAppDataDir();
|
|
29388
|
-
|
|
29998
|
+
fs26.appendFileSync(path25.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
29389
29999
|
`);
|
|
29390
30000
|
} catch {
|
|
29391
30001
|
}
|
|
@@ -31036,12 +31646,12 @@ async function runDashboard(argv) {
|
|
|
31036
31646
|
process.on("SIGTERM", stop);
|
|
31037
31647
|
});
|
|
31038
31648
|
}
|
|
31039
|
-
var
|
|
31649
|
+
var fs26, path25, http, import_child_process4, import_child_process5, osmod, STAGES, ACCOUNT_TYPES, activeRun, UPGRADE_MSG, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG;
|
|
31040
31650
|
var init_dashboard = __esm({
|
|
31041
31651
|
"src/dashboard.ts"() {
|
|
31042
31652
|
"use strict";
|
|
31043
|
-
|
|
31044
|
-
|
|
31653
|
+
fs26 = __toESM(require("fs"));
|
|
31654
|
+
path25 = __toESM(require("path"));
|
|
31045
31655
|
http = __toESM(require("http"));
|
|
31046
31656
|
import_child_process4 = require("child_process");
|
|
31047
31657
|
init_credentials_store();
|
|
@@ -31088,8 +31698,8 @@ var init_dashboard = __esm({
|
|
|
31088
31698
|
|
|
31089
31699
|
// src/index.ts
|
|
31090
31700
|
var dotenv2 = __toESM(require("dotenv"));
|
|
31091
|
-
var
|
|
31092
|
-
var
|
|
31701
|
+
var path26 = __toESM(require("path"));
|
|
31702
|
+
var fs27 = __toESM(require("fs"));
|
|
31093
31703
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
31094
31704
|
|
|
31095
31705
|
// src/version-compare.ts
|
|
@@ -31157,20 +31767,20 @@ init_credentials_store();
|
|
|
31157
31767
|
init_setup_tool();
|
|
31158
31768
|
|
|
31159
31769
|
// src/tools/skills.ts
|
|
31160
|
-
var
|
|
31770
|
+
var import_zod68 = require("zod");
|
|
31161
31771
|
|
|
31162
31772
|
// src/skill-installer.ts
|
|
31163
31773
|
var import_node_crypto4 = require("node:crypto");
|
|
31164
|
-
var
|
|
31165
|
-
var
|
|
31774
|
+
var fs20 = __toESM(require("node:fs"));
|
|
31775
|
+
var path19 = __toESM(require("node:path"));
|
|
31166
31776
|
var os4 = __toESM(require("node:os"));
|
|
31167
31777
|
function sha256(buf) {
|
|
31168
31778
|
return (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
31169
31779
|
}
|
|
31170
31780
|
function bundledSkillsDir(baseDir) {
|
|
31171
|
-
const candidate =
|
|
31781
|
+
const candidate = path19.resolve(baseDir, "..", "skills");
|
|
31172
31782
|
try {
|
|
31173
|
-
return
|
|
31783
|
+
return fs20.statSync(candidate).isDirectory() ? candidate : null;
|
|
31174
31784
|
} catch {
|
|
31175
31785
|
return null;
|
|
31176
31786
|
}
|
|
@@ -31178,10 +31788,10 @@ function bundledSkillsDir(baseDir) {
|
|
|
31178
31788
|
function listFiles(root) {
|
|
31179
31789
|
const out = [];
|
|
31180
31790
|
const walk = (dir) => {
|
|
31181
|
-
for (const entry of
|
|
31182
|
-
const full =
|
|
31791
|
+
for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
|
|
31792
|
+
const full = path19.join(dir, entry.name);
|
|
31183
31793
|
if (entry.isDirectory()) walk(full);
|
|
31184
|
-
else if (entry.isFile()) out.push(
|
|
31794
|
+
else if (entry.isFile()) out.push(path19.relative(root, full));
|
|
31185
31795
|
}
|
|
31186
31796
|
};
|
|
31187
31797
|
walk(root);
|
|
@@ -31189,7 +31799,7 @@ function listFiles(root) {
|
|
|
31189
31799
|
}
|
|
31190
31800
|
function readMarker(markerPath) {
|
|
31191
31801
|
try {
|
|
31192
|
-
const parsed = JSON.parse(
|
|
31802
|
+
const parsed = JSON.parse(fs20.readFileSync(markerPath, "utf8"));
|
|
31193
31803
|
return parsed && typeof parsed === "object" && parsed.files ? parsed : null;
|
|
31194
31804
|
} catch {
|
|
31195
31805
|
return null;
|
|
@@ -31202,35 +31812,35 @@ function installBundledSkills(opts) {
|
|
|
31202
31812
|
skippedUserModified: [],
|
|
31203
31813
|
unchanged: [],
|
|
31204
31814
|
errors: [],
|
|
31205
|
-
targetDir: opts.targetDir ??
|
|
31815
|
+
targetDir: opts.targetDir ?? path19.join(os4.homedir(), ".claude", "skills"),
|
|
31206
31816
|
bundledDir: null
|
|
31207
31817
|
};
|
|
31208
31818
|
const bundled = bundledSkillsDir(opts.baseDir);
|
|
31209
31819
|
result.bundledDir = bundled;
|
|
31210
31820
|
if (!bundled) return result;
|
|
31211
31821
|
try {
|
|
31212
|
-
|
|
31822
|
+
fs20.mkdirSync(result.targetDir, { recursive: true });
|
|
31213
31823
|
} catch (e) {
|
|
31214
31824
|
result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
|
|
31215
31825
|
return result;
|
|
31216
31826
|
}
|
|
31217
|
-
const markerPath =
|
|
31827
|
+
const markerPath = path19.join(result.targetDir, ".ghl-command-skills.json");
|
|
31218
31828
|
const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
|
|
31219
31829
|
const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
|
|
31220
31830
|
for (const rel of listFiles(bundled)) {
|
|
31221
31831
|
try {
|
|
31222
|
-
const src =
|
|
31223
|
-
const dest =
|
|
31832
|
+
const src = fs20.readFileSync(path19.join(bundled, rel));
|
|
31833
|
+
const dest = path19.join(result.targetDir, rel);
|
|
31224
31834
|
const srcHash = sha256(src);
|
|
31225
31835
|
let destBuf = null;
|
|
31226
31836
|
try {
|
|
31227
|
-
destBuf =
|
|
31837
|
+
destBuf = fs20.readFileSync(dest);
|
|
31228
31838
|
} catch {
|
|
31229
31839
|
destBuf = null;
|
|
31230
31840
|
}
|
|
31231
31841
|
if (destBuf === null) {
|
|
31232
|
-
|
|
31233
|
-
|
|
31842
|
+
fs20.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
31843
|
+
fs20.writeFileSync(dest, src);
|
|
31234
31844
|
newMarker.files[rel] = srcHash;
|
|
31235
31845
|
result.installed.push(rel);
|
|
31236
31846
|
continue;
|
|
@@ -31243,7 +31853,7 @@ function installBundledSkills(opts) {
|
|
|
31243
31853
|
}
|
|
31244
31854
|
const lastWritten = marker.files[rel];
|
|
31245
31855
|
if (lastWritten && destHash === lastWritten) {
|
|
31246
|
-
|
|
31856
|
+
fs20.writeFileSync(dest, src);
|
|
31247
31857
|
newMarker.files[rel] = srcHash;
|
|
31248
31858
|
result.updated.push(rel);
|
|
31249
31859
|
} else {
|
|
@@ -31254,7 +31864,7 @@ function installBundledSkills(opts) {
|
|
|
31254
31864
|
}
|
|
31255
31865
|
}
|
|
31256
31866
|
try {
|
|
31257
|
-
|
|
31867
|
+
fs20.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
|
|
31258
31868
|
} catch (e) {
|
|
31259
31869
|
result.errors.push(`marker write failed: ${String(e)}`);
|
|
31260
31870
|
}
|
|
@@ -31278,7 +31888,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
|
|
|
31278
31888
|
"install_skills",
|
|
31279
31889
|
"Install (or repair) the guided skills bundled with GHL Command \u2014 Blueprint (build a whole client account from one intake), Clone Site (clone and rebrand a live web page for a client, with a rights declaration and a pre-launch liability report), and GHL Reports (verified counts, lists, and weekly reports without token burn) \u2014 into your ~/.claude/skills/ so Claude can use them. Runs automatically on startup; call this to verify what is installed, or to re-install after deleting a skill. NEVER overwrites files you have edited (your version is kept and reported). Restart Claude after install for new skills to load.",
|
|
31280
31890
|
{
|
|
31281
|
-
targetDir:
|
|
31891
|
+
targetDir: import_zod68.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
|
|
31282
31892
|
},
|
|
31283
31893
|
async ({ targetDir }) => {
|
|
31284
31894
|
try {
|
|
@@ -31389,8 +31999,8 @@ init_version_check();
|
|
|
31389
31999
|
|
|
31390
32000
|
// src/cli.ts
|
|
31391
32001
|
var import_node_util2 = require("node:util");
|
|
31392
|
-
var
|
|
31393
|
-
var
|
|
32002
|
+
var fs22 = __toESM(require("fs"));
|
|
32003
|
+
var path21 = __toESM(require("path"));
|
|
31394
32004
|
var import_crypto2 = require("crypto");
|
|
31395
32005
|
init_ghl_client();
|
|
31396
32006
|
init_token_registry();
|
|
@@ -31441,9 +32051,9 @@ function errLine(msg3) {
|
|
|
31441
32051
|
function preflightWritable() {
|
|
31442
32052
|
try {
|
|
31443
32053
|
const dir = ensureAppDataDir();
|
|
31444
|
-
const probe =
|
|
31445
|
-
|
|
31446
|
-
|
|
32054
|
+
const probe = path21.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
|
|
32055
|
+
fs22.writeFileSync(probe, "ok");
|
|
32056
|
+
fs22.unlinkSync(probe);
|
|
31447
32057
|
return true;
|
|
31448
32058
|
} catch (error) {
|
|
31449
32059
|
errLine(`Config dir is not writable: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -31697,7 +32307,7 @@ var bundledPkg = require_package();
|
|
|
31697
32307
|
var pkg = (() => {
|
|
31698
32308
|
try {
|
|
31699
32309
|
const onDisk = JSON.parse(
|
|
31700
|
-
|
|
32310
|
+
fs27.readFileSync(path26.resolve(__dirname, "..", "package.json"), "utf8")
|
|
31701
32311
|
);
|
|
31702
32312
|
if (typeof onDisk.version === "string" && onDisk.version.length > 0) {
|
|
31703
32313
|
return { version: onDisk.version };
|
|
@@ -31710,7 +32320,7 @@ dotenv2.config();
|
|
|
31710
32320
|
setPkgVersion(pkg.version);
|
|
31711
32321
|
{
|
|
31712
32322
|
const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
31713
|
-
if (configDirOverride && !
|
|
32323
|
+
if (configDirOverride && !path26.isAbsolute(configDirOverride)) {
|
|
31714
32324
|
process.stderr.write(
|
|
31715
32325
|
`[ghl-mcp] GHL_MCP_CONFIG_DIR must be an absolute path (got "${configDirOverride}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.
|
|
31716
32326
|
`
|
|
@@ -31723,20 +32333,20 @@ process.on("unhandledRejection", (reason) => {
|
|
|
31723
32333
|
`);
|
|
31724
32334
|
});
|
|
31725
32335
|
function hardenSecretFilePerms() {
|
|
31726
|
-
const repoDir =
|
|
32336
|
+
const repoDir = path26.resolve(__dirname, "..");
|
|
31727
32337
|
const candidates = [
|
|
31728
|
-
{ file:
|
|
32338
|
+
{ file: path26.join(repoDir, "start-mcp.sh"), mode: 448 },
|
|
31729
32339
|
// Legacy registry location (pre-migration); new location lives in app-data.
|
|
31730
|
-
{ file:
|
|
32340
|
+
{ file: path26.join(repoDir, ".ghl-tokens.json"), mode: 384 },
|
|
31731
32341
|
{ file: tokenRegistryPath(), mode: 384 }
|
|
31732
32342
|
];
|
|
31733
32343
|
for (const { file, mode } of candidates) {
|
|
31734
32344
|
let current;
|
|
31735
32345
|
try {
|
|
31736
|
-
if (!
|
|
31737
|
-
current =
|
|
32346
|
+
if (!fs27.existsSync(file)) continue;
|
|
32347
|
+
current = fs27.statSync(file).mode & 511;
|
|
31738
32348
|
if (current !== mode) {
|
|
31739
|
-
|
|
32349
|
+
fs27.chmodSync(file, mode);
|
|
31740
32350
|
}
|
|
31741
32351
|
} catch (error) {
|
|
31742
32352
|
const message = error instanceof Error ? error.message : String(error);
|