@elitedcs/ghl-mcp 3.62.1 → 3.64.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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.64.0 — four silent failures fixed, four more guides open
4
+
5
+ Every fix here is the same shape: a call that succeeded, returned
6
+ plausible-looking data, and was wrong. None of them raised an error, so none of
7
+ them were visible until a guide was written against them and run for real.
8
+
9
+ - **Fix: comparing two sub-accounts reported the other one as empty.** A
10
+ Private Integration key only works on the sub-account it was created in, so
11
+ reading a second account returned 403 — which `compare_locations` caught,
12
+ stored as null, and counted as zero. Comparing a client account against your
13
+ template answered "nothing is missing". It now uses each account's own saved
14
+ key (the same registry `switch_location` uses), reports a section it genuinely
15
+ cannot read as "unavailable" rather than as zero, and returns the NAMES
16
+ present in one account and missing from the other — which it always claimed to
17
+ do but never did. Live-proven: a template that read as "0 pipelines,
18
+ 0 workflows, 0 tags" actually holds 8, 12 and 11.
19
+ - **Fix: filtering reviews by star rating did nothing.** The filter used a field
20
+ name GoHighLevel does not have, and an unknown filter field is dropped
21
+ silently, so asking for five-star reviews returned two-star ones. Correct
22
+ field and range form captured from GoHighLevel's own reputation filter.
23
+ Adds `minRating`/`maxRating`, so "every review under four stars" now works.
24
+ - **Fix: `get_users` advertised `limit` and `skip`.** GoHighLevel rejects both
25
+ outright. Anyone who used them got an error instead of users. Removed; the
26
+ endpoint returns every user in one response.
27
+ - Four more guides open in the library, each proven against a real account
28
+ before shipping: see and manage appointments, create forms and read
29
+ submissions, watch your reviews, account admin basics. 24 open, 6 still
30
+ being written.
31
+
32
+ ## 3.63.0 — the guide library grows from 3 guides to 20
33
+
34
+ - **`get_user_guide` now opens a 20-guide library.** Seventeen guides were
35
+ written and proven live but had never shipped, so anyone running
36
+ `get user guide` was still getting the three-guide edition. The bundled
37
+ offline guide goes from 43KB to 111KB: 20 open guides, 10 still marked
38
+ "Available in the app" until their prompts are proven. Every prompt marked
39
+ "Proven live" was run against a real GoHighLevel account before it shipped.
40
+ - Same library renders to ghlcommand.com/skills, so the in-product guide and
41
+ the website always match.
42
+ - **Fix: reading appointments returned nothing.** `get_calendar_events` sent
43
+ ISO timestamps, but GHL's `/calendars/events` needs epoch milliseconds and
44
+ answers a wrong-format request with `200` and an empty array instead of an
45
+ error — so a calendar holding real confirmed bookings read back as "nothing
46
+ booked". Now converts via the timezone-aware helper free-slots already used,
47
+ plus an optional `timezone` parameter so a bare `YYYY-MM-DD` window resolves
48
+ in the sub-account's timezone (end date inclusive). Live-proven both
49
+ directions.
50
+
3
51
  ## 3.62.1 — update_pipeline works again (Bug 8)
4
52
 
5
53
  - **Fix: `update_pipeline` no longer 422s.** GHL's pipeline PUT rejects unknown
package/dist/index.js CHANGED
@@ -2854,7 +2854,7 @@ var require_package = __commonJS({
2854
2854
  "package.json"(exports2, module2) {
2855
2855
  module2.exports = {
2856
2856
  name: "@elitedcs/ghl-mcp",
2857
- version: "3.62.1",
2857
+ version: "3.64.0",
2858
2858
  mcpName: "io.github.drjerryrelth/ghl-command",
2859
2859
  description: "GoHighLevel MCP Server for Claude. 235 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.",
2860
2860
  main: "dist/index.js",
@@ -7643,7 +7643,7 @@ function toEpochMillis(value, opts = {}) {
7643
7643
  const trimmed = value.trim();
7644
7644
  if (/^\d+$/.test(trimmed)) {
7645
7645
  if (trimmed.length === 13) return trimmed;
7646
- throw new Error(`Ambiguous numeric date "${value}" for free-slots \u2014 pass epoch MILLISECONDS (13 digits), a YYYY-MM-DD date, or a full ISO datetime.`);
7646
+ throw new Error(`Ambiguous numeric date "${value}" \u2014 pass epoch MILLISECONDS (13 digits), a YYYY-MM-DD date, or a full ISO datetime.`);
7647
7647
  }
7648
7648
  const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
7649
7649
  if (dateOnly) {
@@ -7658,7 +7658,7 @@ function toEpochMillis(value, opts = {}) {
7658
7658
  }
7659
7659
  const parsed = Date.parse(trimmed);
7660
7660
  if (!Number.isNaN(parsed)) return String(parsed);
7661
- throw new Error(`Invalid date "${value}" for free-slots \u2014 use YYYY-MM-DD, a full ISO datetime, or epoch milliseconds.`);
7661
+ throw new Error(`Invalid date "${value}" \u2014 use YYYY-MM-DD, a full ISO datetime, or epoch milliseconds.`);
7662
7662
  }
7663
7663
  function buildFreeSlotsParams(args) {
7664
7664
  const params = {
@@ -7669,6 +7669,16 @@ function buildFreeSlotsParams(args) {
7669
7669
  if (args.userId !== void 0) params.userId = args.userId;
7670
7670
  return params;
7671
7671
  }
7672
+ function buildCalendarEventsParams(args) {
7673
+ const params = {
7674
+ locationId: args.locationId,
7675
+ startTime: toEpochMillis(args.startTime, { timeZone: args.timezone }),
7676
+ endTime: toEpochMillis(args.endTime, { endOfDay: true, timeZone: args.timezone })
7677
+ };
7678
+ if (args.calendarId !== void 0) params.calendarId = args.calendarId;
7679
+ if (args.userId !== void 0) params.userId = args.userId;
7680
+ return params;
7681
+ }
7672
7682
  function registerCalendarTools(server2, client) {
7673
7683
  safeTool(
7674
7684
  server2,
@@ -7789,20 +7799,25 @@ function registerCalendarTools(server2, client) {
7789
7799
  "List calendar events for a location within a time range",
7790
7800
  {
7791
7801
  locationId: import_zod12.z.string().optional().describe("Location ID (falls back to GHL_LOCATION_ID env var)"),
7792
- startTime: import_zod12.z.string().describe("Start time in ISO format"),
7793
- endTime: import_zod12.z.string().describe("End time in ISO format"),
7802
+ startTime: import_zod12.z.string().describe(
7803
+ "Start of the window. Accepts a date (YYYY-MM-DD), a full ISO datetime, or epoch milliseconds. A bare date is taken as start-of-day in `timezone` (UTC if unset). GHL needs epoch ms here; the tool converts for you."
7804
+ ),
7805
+ endTime: import_zod12.z.string().describe(
7806
+ "End of the window. Accepts a date (YYYY-MM-DD), a full ISO datetime, or epoch milliseconds. A bare date is taken as END of that day, inclusive, in `timezone`."
7807
+ ),
7808
+ timezone: import_zod12.z.string().optional().describe("IANA timezone (e.g. America/Phoenix) used to resolve bare YYYY-MM-DD dates. Defaults to UTC."),
7794
7809
  calendarId: import_zod12.z.string().optional().describe("Filter by calendar ID"),
7795
7810
  userId: import_zod12.z.string().optional().describe("Filter by user ID")
7796
7811
  },
7797
- async ({ locationId: locationId2, startTime, endTime, calendarId, userId }) => {
7798
- const resolvedLocationId = client.resolveLocationId(locationId2);
7799
- const params = {
7800
- locationId: resolvedLocationId,
7812
+ async ({ locationId: locationId2, startTime, endTime, timezone, calendarId, userId }) => {
7813
+ const params = buildCalendarEventsParams({
7814
+ locationId: client.resolveLocationId(locationId2),
7801
7815
  startTime,
7802
- endTime
7803
- };
7804
- if (calendarId !== void 0) params.calendarId = calendarId;
7805
- if (userId !== void 0) params.userId = userId;
7816
+ endTime,
7817
+ timezone,
7818
+ calendarId,
7819
+ userId
7820
+ });
7806
7821
  return await client.get("/calendars/events", { params });
7807
7822
  }
7808
7823
  );
@@ -8817,18 +8832,17 @@ function registerUserTools(server2, client) {
8817
8832
  safeTool(
8818
8833
  server2,
8819
8834
  "get_users",
8820
- "List users for a location",
8835
+ "List every user on a location, with their role and permissions. Returns them all in one response \u2014 GHL's users endpoint takes no paging parameters.",
8821
8836
  {
8822
- locationId: import_zod22.z.string().optional().describe("GHL Location ID (optional if GHL_LOCATION_ID is set)"),
8823
- limit: import_zod22.z.number().optional().describe("Max number of users to return"),
8824
- skip: import_zod22.z.number().optional().describe("Number of users to skip")
8837
+ locationId: import_zod22.z.string().optional().describe("GHL Location ID (optional if GHL_LOCATION_ID is set)")
8825
8838
  },
8826
- async ({ locationId: locationId2, limit, skip }) => {
8839
+ // No limit/skip: GHL rejects them outright with 422 "property limit should
8840
+ // not exist". They were advertised on this tool for months, so anyone who
8841
+ // took the schema at its word got an error instead of a page of users
8842
+ // (live-verified 2026-08-08).
8843
+ async ({ locationId: locationId2 }) => {
8827
8844
  const resolvedLocationId = client.resolveLocationId(locationId2);
8828
- const params = { locationId: resolvedLocationId };
8829
- if (limit !== void 0) params.limit = limit;
8830
- if (skip !== void 0) params.skip = skip;
8831
- return client.get("/users/", { params });
8845
+ return client.get("/users/", { params: { locationId: resolvedLocationId } });
8832
8846
  }
8833
8847
  );
8834
8848
  safeTool(
@@ -13323,7 +13337,7 @@ var import_zod46 = require("zod");
13323
13337
  function delay2(ms) {
13324
13338
  return new Promise((resolve8) => setTimeout(resolve8, ms));
13325
13339
  }
13326
- function registerAccountExportTools(server2, client) {
13340
+ function registerAccountExportTools(server2, client, registry2) {
13327
13341
  const builderClient = WorkflowBuilderClient.fromEnv();
13328
13342
  server2.tool(
13329
13343
  "export_account",
@@ -13456,7 +13470,7 @@ function registerAccountExportTools(server2, client) {
13456
13470
  );
13457
13471
  server2.tool(
13458
13472
  "compare_locations",
13459
- "Compare two GHL sub-accounts side by side \u2014 shows differences in pipelines, workflows, custom fields, tags, forms, and funnels. Useful for ensuring consistency across locations or auditing before/after changes.",
13473
+ "Compare two GHL sub-accounts side by side. For pipelines, workflows, custom fields, tags and forms it returns the count on each side plus the NAMES present in one account and missing from the other, so a setup gap reads directly. If a section cannot be read (commonly the API key has no access to the second location), that section is reported as unavailable with the reason \u2014 never as a count of zero.",
13460
13474
  {
13461
13475
  locationA: import_zod46.z.string().describe("First Location ID."),
13462
13476
  locationB: import_zod46.z.string().describe("Second Location ID.")
@@ -13468,66 +13482,57 @@ function registerAccountExportTools(server2, client) {
13468
13482
  locationA,
13469
13483
  locationB
13470
13484
  };
13471
- const fetchData = async (locId) => {
13472
- const data = {};
13473
- try {
13474
- data.location = await client.get(`/locations/${locId}`);
13475
- } catch {
13476
- data.location = null;
13477
- }
13478
- await delay2(100);
13485
+ const SECTIONS = ["pipelines", "workflows", "customFields", "tags", "forms"];
13486
+ const namesFrom = (payload) => {
13487
+ const list = Array.isArray(payload) ? payload : payload && typeof payload === "object" ? Object.values(payload).find(Array.isArray) : void 0;
13488
+ if (!Array.isArray(list)) return [];
13489
+ return list.map(
13490
+ (item) => item && typeof item === "object" ? String(item.name ?? item.id ?? "") : String(item)
13491
+ ).filter(Boolean);
13492
+ };
13493
+ const read = async (fn) => {
13479
13494
  try {
13480
- data.pipelines = await client.get("/opportunities/pipelines", { params: { locationId: locId } });
13481
- } catch {
13482
- data.pipelines = null;
13495
+ return { names: namesFrom(await fn()) };
13496
+ } catch (err) {
13497
+ return { error: err instanceof Error ? err.message : String(err) };
13483
13498
  }
13499
+ };
13500
+ const fetchData = async (locId) => {
13501
+ const data = {};
13502
+ data.pipelines = await read(() => client.get("/opportunities/pipelines", { params: { locationId: locId } }));
13484
13503
  await delay2(100);
13485
- try {
13486
- data.workflows = await client.get("/workflows/", { params: { locationId: locId } });
13487
- } catch {
13488
- data.workflows = null;
13489
- }
13504
+ data.workflows = await read(() => client.get("/workflows/", { params: { locationId: locId } }));
13490
13505
  await delay2(100);
13491
- try {
13492
- data.customFields = await client.get("/locations/" + locId + "/customFields");
13493
- } catch {
13494
- data.customFields = null;
13495
- }
13506
+ data.customFields = await read(() => client.get(`/locations/${locId}/customFields`));
13496
13507
  await delay2(100);
13497
- try {
13498
- data.tags = await client.get("/locations/" + locId + "/tags");
13499
- } catch {
13500
- data.tags = null;
13501
- }
13508
+ data.tags = await read(() => client.get(`/locations/${locId}/tags`));
13502
13509
  await delay2(100);
13503
- try {
13504
- data.forms = await client.get("/forms/", { params: { locationId: locId } });
13505
- } catch {
13506
- data.forms = null;
13507
- }
13510
+ data.forms = await read(() => client.get("/forms/", { params: { locationId: locId } }));
13508
13511
  return data;
13509
13512
  };
13510
- const [dataA, dataB] = await Promise.all([fetchData(locationA), fetchData(locationB)]);
13511
- const count = (obj, key) => {
13512
- if (!obj || !obj[key]) return 0;
13513
- const v = obj[key];
13514
- if (Array.isArray(v)) return v.length;
13515
- if (v && typeof v === "object" && !Array.isArray(v)) {
13516
- for (const nested of Object.values(v)) {
13517
- if (Array.isArray(nested)) return nested.length;
13518
- }
13513
+ const originalKey = client.getApiKey();
13514
+ const usedRegisteredKey = [];
13515
+ const fetchWithOwnKey = async (locId) => {
13516
+ const token = registry2?.getToken(locId);
13517
+ if (token?.apiKey && token.apiKey !== originalKey) {
13518
+ client.setApiKey(token.apiKey);
13519
+ usedRegisteredKey.push(locId);
13520
+ }
13521
+ try {
13522
+ return await fetchData(locId);
13523
+ } finally {
13524
+ client.setApiKey(originalKey);
13519
13525
  }
13520
- return "?";
13521
- };
13522
- diff.comparison = {
13523
- pipelines: { A: count(dataA, "pipelines"), B: count(dataB, "pipelines") },
13524
- workflows: { A: count(dataA, "workflows"), B: count(dataB, "workflows") },
13525
- customFields: { A: count(dataA, "customFields"), B: count(dataB, "customFields") },
13526
- tags: { A: count(dataA, "tags"), B: count(dataB, "tags") },
13527
- forms: { A: count(dataA, "forms"), B: count(dataB, "forms") }
13528
13526
  };
13529
- diff.locationA_data = dataA;
13530
- diff.locationB_data = dataB;
13527
+ const dataA = await fetchWithOwnKey(locationA);
13528
+ const dataB = await fetchWithOwnKey(locationB);
13529
+ const { comparison, unavailable } = buildLocationComparison(dataA, dataB, SECTIONS);
13530
+ diff.comparison = comparison;
13531
+ if (usedRegisteredKey.length) diff.usedRegisteredKeyFor = usedRegisteredKey;
13532
+ if (unavailable.length) {
13533
+ diff.unavailable = unavailable;
13534
+ diff.warning = "Some sections could not be read. Those sections are marked unavailable \u2014 do NOT read them as empty. A Private Integration key only works on the sub-account it was created in, so register each account's own key (register_location) before comparing them.";
13535
+ }
13531
13536
  return jsonResponse(diff);
13532
13537
  } catch (error) {
13533
13538
  return errorResponse(error);
@@ -13535,6 +13540,35 @@ function registerAccountExportTools(server2, client) {
13535
13540
  }
13536
13541
  );
13537
13542
  }
13543
+ function buildLocationComparison(dataA, dataB, sections) {
13544
+ const unavailable = [];
13545
+ const comparison = {};
13546
+ for (const key of sections) {
13547
+ const a = dataA[key] ?? { error: "not read" };
13548
+ const b = dataB[key] ?? { error: "not read" };
13549
+ if ("error" in a) unavailable.push(`locationA.${key}: ${a.error}`);
13550
+ if ("error" in b) unavailable.push(`locationB.${key}: ${b.error}`);
13551
+ if ("error" in a || "error" in b) {
13552
+ comparison[key] = {
13553
+ A: "error" in a ? "unavailable" : a.names.length,
13554
+ B: "error" in b ? "unavailable" : b.names.length,
13555
+ comparable: false,
13556
+ note: "One side could not be read, so no gap can be reported for this section."
13557
+ };
13558
+ continue;
13559
+ }
13560
+ const setA = new Set(a.names);
13561
+ const setB = new Set(b.names);
13562
+ comparison[key] = {
13563
+ A: a.names.length,
13564
+ B: b.names.length,
13565
+ comparable: true,
13566
+ onlyInA: a.names.filter((n) => !setB.has(n)),
13567
+ onlyInB: b.names.filter((n) => !setA.has(n))
13568
+ };
13569
+ }
13570
+ return { comparison, unavailable };
13571
+ }
13538
13572
 
13539
13573
  // src/tools/workflow-cloner.ts
13540
13574
  var import_zod47 = require("zod");
@@ -13811,17 +13845,22 @@ ${text2}`);
13811
13845
  safeTool(
13812
13846
  server2,
13813
13847
  "list_reviews",
13814
- "List the reviews a location has received (Google, Facebook, etc.) with rating, author, text, reply status, and source. Supports paging and an optional rating filter. NOTE: location is resolved through nested filter params internally \u2014 a flat locationId is what caused the long-standing 'No Location Found' error, now fixed. Responding to a review is not yet available via API. Requires Firebase auth.",
13848
+ "List the reviews a location has received (Google, Facebook, etc.) with rating, author, text, reply status, sentiment score, and source. Filter by an exact star rating or a range \u2014 'everything under 4 stars' is minRating 1 + maxRating 3. NOTE: location is resolved through nested filter params internally \u2014 a flat locationId is what caused the long-standing 'No Location Found' error, now fixed. Responding to a review is not yet available via API. Requires Firebase auth.",
13815
13849
  {
13816
13850
  locationId: import_zod49.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
13817
13851
  pageNumber: import_zod49.z.number().optional().describe("1-based page number. Defaults to 1."),
13818
13852
  pageSize: import_zod49.z.number().optional().describe("Results per page. Defaults to 10."),
13819
- rating: import_zod49.z.number().optional().describe("Optional: only return reviews with this star rating (1-5)."),
13853
+ rating: import_zod49.z.number().min(1).max(5).optional().describe("Exact star rating (1-5). Takes precedence over minRating/maxRating."),
13854
+ minRating: import_zod49.z.number().min(1).max(5).optional().describe("Lowest star rating to include (1-5), inclusive."),
13855
+ maxRating: import_zod49.z.number().min(1).max(5).optional().describe("Highest star rating to include (1-5), inclusive. For 'under 4 stars', pass 3."),
13820
13856
  includeDeleted: import_zod49.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
13821
13857
  },
13822
- async ({ locationId: locationId2, pageNumber, pageSize, rating, includeDeleted }) => {
13858
+ async ({ locationId: locationId2, pageNumber, pageSize, rating, minRating, maxRating, includeDeleted }) => {
13823
13859
  const loc = locationId2 ?? client.locationId;
13824
- return reputationRequest("GET", buildReviewsQuery(loc, { pageNumber, pageSize, rating, includeDeleted }));
13860
+ return reputationRequest(
13861
+ "GET",
13862
+ buildReviewsQuery(loc, { pageNumber, pageSize, rating, minRating, maxRating, includeDeleted })
13863
+ );
13825
13864
  }
13826
13865
  );
13827
13866
  }
@@ -13832,10 +13871,17 @@ function buildReviewsQuery(locationId2, opts = {}) {
13832
13871
  `filterParams[deleted][0][value]=${opts.includeDeleted ? "true" : "false"}`,
13833
13872
  `filterParams[deleted][0][condition]=eq`
13834
13873
  ];
13874
+ const bounds = [];
13835
13875
  if (opts.rating !== void 0) {
13836
- q2.push(`filterParams[rating][0][value]=${opts.rating}`);
13837
- q2.push(`filterParams[rating][0][condition]=eq`);
13876
+ bounds.push([opts.rating, "gte"], [opts.rating, "lte"]);
13877
+ } else {
13878
+ if (opts.minRating !== void 0) bounds.push([opts.minRating, "gte"]);
13879
+ if (opts.maxRating !== void 0) bounds.push([opts.maxRating, "lte"]);
13838
13880
  }
13881
+ bounds.forEach(([value, condition], i) => {
13882
+ q2.push(`filterParams[starRating][${i}][value]=${value}`);
13883
+ q2.push(`filterParams[starRating][${i}][condition]=${condition}`);
13884
+ });
13839
13885
  q2.push(`sortParams[dateAdded]=-1`);
13840
13886
  q2.push(`pageNumber=${opts.pageNumber ?? 1}`);
13841
13887
  q2.push(`pageSize=${opts.pageSize ?? 10}`);
@@ -18717,7 +18763,6 @@ var publicApiTools = [
18717
18763
  [registerWebhookTools, "webhooks"],
18718
18764
  [registerDocumentTools, "documents"],
18719
18765
  [registerBulkOperationTools, "bulk-operations"],
18720
- [registerAccountExportTools, "account-export"],
18721
18766
  [registerTemplateDeployerTools, "template-deployer"],
18722
18767
  [registerPhoneTools, "phone"],
18723
18768
  [registerAccountHealthTools, "account-health"]
@@ -18738,6 +18783,7 @@ var VALIDATORS_MODULE = "validators";
18738
18783
  var DIAGNOSTICS_MODULE = "diagnostics";
18739
18784
  var LOCATION_SWITCHER_MODULE = "location-switcher";
18740
18785
  var SNAPSHOTS_MODULE = "snapshots";
18786
+ var ACCOUNT_EXPORT_MODULE = "account-export";
18741
18787
  var FORM_BUILDER_MODULE = "form-builder";
18742
18788
  var INTAKE_TO_BUILD_MODULE = "intake-to-build";
18743
18789
  var FUNNEL_QA_MODULE = "funnel-qa";
@@ -18752,7 +18798,8 @@ var KNOWN_MODULES = /* @__PURE__ */ new Set([
18752
18798
  VALIDATORS_MODULE,
18753
18799
  DIAGNOSTICS_MODULE,
18754
18800
  LOCATION_SWITCHER_MODULE,
18755
- SNAPSHOTS_MODULE
18801
+ SNAPSHOTS_MODULE,
18802
+ ACCOUNT_EXPORT_MODULE
18756
18803
  ]);
18757
18804
  function registerAllTools(server2, client, registry2, mcpVersion, env = process.env, tier = "full", outOfBand) {
18758
18805
  const config3 = parseAllowlist(env);
@@ -18781,6 +18828,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
18781
18828
  () => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
18782
18829
  );
18783
18830
  registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
18831
+ registerAccountExportTools(wrap(ACCOUNT_EXPORT_MODULE), client, registry2);
18784
18832
  registerLocationSwitcherTools(
18785
18833
  wrap(LOCATION_SWITCHER_MODULE),
18786
18834
  client,