@koda-sl/baker-cli 0.170.0 → 0.173.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/README.md +17 -3
- package/dist/cli.js +428 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2673,7 +2673,7 @@ Permissions enforced server-side:
|
|
|
2673
2673
|
|
|
2674
2674
|
### `baker mcp connected | list | add | remove`
|
|
2675
2675
|
|
|
2676
|
-
See
|
|
2676
|
+
See everything a chat can reach — managed integrations, custom servers, and connected platforms — and manage **custom MCP servers** by pointing the agent at any remote HTTPS MCP endpoint. Registered tools appear as `mcp__<name>__*` on the next chat message.
|
|
2677
2677
|
|
|
2678
2678
|
```bash
|
|
2679
2679
|
baker mcp connected
|
|
@@ -2684,6 +2684,7 @@ baker mcp add --name mine --url https://my.dev/mcp --scope user
|
|
|
2684
2684
|
baker mcp remove --name weather
|
|
2685
2685
|
```
|
|
2686
2686
|
|
|
2687
|
+
- `connected` returns three lists. `managed` and `custom` are MCP tools the agent calls directly; `platforms` are the accounts the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager, LinkedIn Ads), each with its login and the resources it may read. A platform has **no** `mcp__*` tools — it is read through its own command family — so it is listed here to keep the agent from concluding a working connection is missing.
|
|
2687
2688
|
- `--url` must be the **full MCP endpoint** (commonly ending in `/mcp`), HTTPS and public; loopback / private / cloud-metadata hosts are rejected. A bare domain frequently accepts the OAuth sign-in and then serves no tools, so every address is handshake-checked after it changes: `connected` and `list` report `usable: false` plus a `problem` (and a `suggestedAddress` when the server advertises a better endpoint) for a server that isn't serving tools, or `lastCheck: failed` when the last check only hit an outage — that one may work anyway, and a later turn re-checks it automatically. Repointing a server at a different host clears its saved sign-in: a token belongs to the host that issued it, so the server has to be connected again.
|
|
2688
2689
|
- Scope is two independent choices — audience (just you / everyone) × level (this company / whole org):
|
|
2689
2690
|
- `--scope company` (default) — everyone in this company.
|
|
@@ -2730,9 +2731,9 @@ baker history list --category ads --full # ad writes with raw metadata
|
|
|
2730
2731
|
|
|
2731
2732
|
---
|
|
2732
2733
|
|
|
2733
|
-
### `baker hubspot forms list | view` · `baker hubspot meetings list | view`
|
|
2734
|
+
### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup` · `baker hubspot meetings list | view`
|
|
2734
2735
|
|
|
2735
|
-
Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there,
|
|
2736
|
+
Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, the leads those forms received, and the workflows, deal pipelines, and contact records a lead runs through afterwards. Fetched from HubSpot live on every call; nothing is cached and nothing is ever written back.
|
|
2736
2737
|
|
|
2737
2738
|
The reason this exists: a form's **post-submit action** (inline thank-you vs redirect to a URL), its embed version, its consent mode, and a calendar's booking fields are *not* persisted onto the flow node when a form is picked. They cannot be read from `_data.json` or `baker flows show` — only from here.
|
|
2738
2739
|
|
|
@@ -2743,6 +2744,12 @@ baker hubspot forms list --embed-type legacy # only legacy-embed forms
|
|
|
2743
2744
|
baker hubspot forms view <formId> # fields, consent mode, full configuration
|
|
2744
2745
|
baker hubspot forms view <formId> --full # + the raw HubSpot payload
|
|
2745
2746
|
baker hubspot forms view <formId> --as-node # the form.external blob for a flow node
|
|
2747
|
+
baker hubspot forms submissions <formId> # how many leads it received, and when
|
|
2748
|
+
baker hubspot forms submissions <formId> --days 90 --full
|
|
2749
|
+
baker hubspot workflows list --enabled-only # what runs after a lead is captured
|
|
2750
|
+
baker hubspot pipelines list # where a lead lands, and the stages after it
|
|
2751
|
+
baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
|
|
2752
|
+
baker hubspot contacts lookup lead@example.com # was this one lead picked up?
|
|
2746
2753
|
baker hubspot meetings list # every calendar + whether booking redirects
|
|
2747
2754
|
baker hubspot meetings view <slug> # booking-form fields for one calendar
|
|
2748
2755
|
baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
|
|
@@ -2750,9 +2757,16 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
|
|
|
2750
2757
|
|
|
2751
2758
|
**`forms list` flags:** `--search <text>` (name substring), `--redirecting-only`, `--embed-type legacy|v4|unknown`. Rows carry only what it takes to choose; `portalId` is returned once on `data`, not per row.
|
|
2752
2759
|
**`forms view`:** positional `<formId>`; `--full` adds the untouched HubSpot payload; `--as-node` returns the resource blob instead of the readable view.
|
|
2760
|
+
**`forms submissions`:** positional `<formId>`; `--days <1-365>` (default 30) sets the window; `--full` adds each submission's field values. The default response is counts and dates only — `total`, `inWindow`, `lastSubmittedAt`, and per-day counts — because submissions carry the lead's own contact details. `truncated: true` means the page budget ran out before the window was covered, so `inWindow` is a floor rather than an exact count.
|
|
2761
|
+
**`workflows list` flags:** `--search <text>`, `--enabled-only`. Disabled workflows are listed too — a form wired to one that is off looks connected and does nothing.
|
|
2762
|
+
**`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
|
|
2763
|
+
**`contacts summary` flags:** `--days <1-365>` (default 30) over contact creation date. Returns counts only — total contacts, how many have an owner, and the lifecycle-stage split — never a name or an email. `truncated: true` means the page budget ran out, so the counts are floors.
|
|
2764
|
+
**`contacts lookup`:** positional `<email>`; returns that contact's lifecycle stage, owner, the account (company) it was filed under, and associated deals with their stage. `dealsReadable: false` / `companyReadable: false` mean the connection does not cover deals or accounts, so an empty `deals` or a null `company` is unknown rather than absent. `found: false` means no contact with that email — the lead never reached the CRM, which is a finding rather than an error.
|
|
2753
2765
|
**`meetings list` flags:** `--search <text>`, `--redirecting-only`.
|
|
2754
2766
|
**`meetings view`:** positional `<slug>` — the only reliable source for a calendar's booking fields (HubSpot omits `customParams` from the list endpoint, so `fieldCount: null` there means unknown, not zero); `--as-node` as above.
|
|
2755
2767
|
|
|
2768
|
+
**`HUBSPOT_RECONNECT_REQUIRED`** is returned when HubSpot is connected but that account never granted access to what the command needs — a HubSpot grant is fixed at the moment the user approves it, so an account connected before a capability shipped cannot serve it. It is not the same as `NO_HUBSPOT_CONNECTION`: the connection works, and only the user can widen it by reconnecting from Brain → Integrations → Tools. The error carries a `fix` describing exactly that.
|
|
2769
|
+
|
|
2756
2770
|
**`--as-node`** returns exactly the object a flow node's `form.external` expects, so an agent can wire a HubSpot step by editing the flow's `_data.json` instead of interrupting the user with the `request_flow_input` approval form. It includes two fields the dashboard picker omits and the renderer needs: `configuration.postSubmitAction` (drives post-submit redirect suppression) and `region` (selects the v4 embed host — without it a v4 form on a eu1/ap1 portal loads the wrong script host).
|
|
2757
2771
|
|
|
2758
2772
|
**Response:**
|
package/dist/cli.js
CHANGED
|
@@ -2983,6 +2983,124 @@ var hubspotFormsViewResponseSchema = z10.object({
|
|
|
2983
2983
|
external: hubspotFormNodeExternalSchema.optional()
|
|
2984
2984
|
})
|
|
2985
2985
|
});
|
|
2986
|
+
var HUBSPOT_MISSING_GRANT_MARKER = "has not granted access to";
|
|
2987
|
+
var hubspotFormSubmissionsRequestSchema = z10.object({
|
|
2988
|
+
formId: z10.string().min(1),
|
|
2989
|
+
/** Window in days, counted back from now. */
|
|
2990
|
+
days: z10.number().int().min(1).max(365).default(30),
|
|
2991
|
+
/** Include each submission's field values — personal data, so opt-in. */
|
|
2992
|
+
full: z10.boolean().optional()
|
|
2993
|
+
});
|
|
2994
|
+
var hubspotSubmissionValueSchema = z10.object({ name: z10.string(), value: z10.string() });
|
|
2995
|
+
var hubspotFormSubmissionsResponseSchema = z10.object({
|
|
2996
|
+
ok: z10.literal(true),
|
|
2997
|
+
data: z10.object({
|
|
2998
|
+
/** Submissions read from HubSpot, across every page fetched. */
|
|
2999
|
+
total: z10.number().int(),
|
|
3000
|
+
/** Submissions inside the requested window. */
|
|
3001
|
+
inWindow: z10.number().int(),
|
|
3002
|
+
lastSubmittedAt: z10.number().nullable(),
|
|
3003
|
+
byDay: z10.array(z10.object({ day: z10.string(), count: z10.number().int() })),
|
|
3004
|
+
/**
|
|
3005
|
+
* True when the page budget ran out before the window was fully covered, so
|
|
3006
|
+
* `inWindow` is a floor rather than a count. Reported rather than hidden:
|
|
3007
|
+
* a truncated read must not be mistaken for a complete one.
|
|
3008
|
+
*/
|
|
3009
|
+
truncated: z10.boolean(),
|
|
3010
|
+
/** Present only when `full` was requested; null otherwise. */
|
|
3011
|
+
submissions: z10.array(
|
|
3012
|
+
z10.object({
|
|
3013
|
+
submittedAt: z10.number(),
|
|
3014
|
+
pageUrl: z10.string().nullable(),
|
|
3015
|
+
values: z10.array(hubspotSubmissionValueSchema)
|
|
3016
|
+
})
|
|
3017
|
+
).nullable()
|
|
3018
|
+
})
|
|
3019
|
+
});
|
|
3020
|
+
var hubspotWorkflowsListRequestSchema = z10.object({
|
|
3021
|
+
search: z10.string().min(1).max(200).optional(),
|
|
3022
|
+
/** Only workflows that are currently on. */
|
|
3023
|
+
enabledOnly: z10.boolean().optional()
|
|
3024
|
+
});
|
|
3025
|
+
var hubspotWorkflowSummarySchema = z10.object({
|
|
3026
|
+
id: z10.string(),
|
|
3027
|
+
name: z10.string(),
|
|
3028
|
+
enabled: z10.boolean(),
|
|
3029
|
+
type: z10.string().nullable(),
|
|
3030
|
+
updatedAt: z10.number().nullable()
|
|
3031
|
+
});
|
|
3032
|
+
var hubspotWorkflowsListResponseSchema = z10.object({
|
|
3033
|
+
ok: z10.literal(true),
|
|
3034
|
+
data: z10.object({ workflows: z10.array(hubspotWorkflowSummarySchema) })
|
|
3035
|
+
});
|
|
3036
|
+
var hubspotContactsSummaryRequestSchema = z10.object({
|
|
3037
|
+
/** Window in days, counted back from now, over contact creation date. */
|
|
3038
|
+
days: z10.number().int().min(1).max(365).default(30)
|
|
3039
|
+
});
|
|
3040
|
+
var hubspotContactsSummaryResponseSchema = z10.object({
|
|
3041
|
+
ok: z10.literal(true),
|
|
3042
|
+
data: z10.object({
|
|
3043
|
+
total: z10.number().int(),
|
|
3044
|
+
withOwner: z10.number().int(),
|
|
3045
|
+
withoutOwner: z10.number().int(),
|
|
3046
|
+
byStage: z10.array(z10.object({ stage: z10.string(), count: z10.number().int() })),
|
|
3047
|
+
/** True when the page budget ran out, so the counts are floors. */
|
|
3048
|
+
truncated: z10.boolean()
|
|
3049
|
+
})
|
|
3050
|
+
});
|
|
3051
|
+
var hubspotContactLookupRequestSchema = z10.object({
|
|
3052
|
+
email: z10.string().min(1).max(320)
|
|
3053
|
+
});
|
|
3054
|
+
var hubspotContactLookupResponseSchema = z10.object({
|
|
3055
|
+
ok: z10.literal(true),
|
|
3056
|
+
data: z10.object({
|
|
3057
|
+
/** False means the lead never reached the CRM — a finding, not an error. */
|
|
3058
|
+
found: z10.boolean(),
|
|
3059
|
+
contact: z10.object({
|
|
3060
|
+
id: z10.string(),
|
|
3061
|
+
lifecycleStage: z10.string().nullable(),
|
|
3062
|
+
ownerId: z10.string().nullable(),
|
|
3063
|
+
createdAt: z10.string().nullable()
|
|
3064
|
+
}).nullable(),
|
|
3065
|
+
deals: z10.array(z10.object({ id: z10.string(), stage: z10.string().nullable(), pipeline: z10.string().nullable() })),
|
|
3066
|
+
/**
|
|
3067
|
+
* False when the connection never granted deal access, which makes an empty
|
|
3068
|
+
* `deals` unknown rather than absent. Deal access is granted separately from
|
|
3069
|
+
* contact access, so one can be present without the other — and "no deals"
|
|
3070
|
+
* is the headline finding of this command, so it must not be guessed.
|
|
3071
|
+
*/
|
|
3072
|
+
dealsReadable: z10.boolean(),
|
|
3073
|
+
/**
|
|
3074
|
+
* The account the contact was filed under. In B2B a workflow usually
|
|
3075
|
+
* creates it on contact creation, so its absence is a finding — but only
|
|
3076
|
+
* when `companyReadable` is true.
|
|
3077
|
+
*/
|
|
3078
|
+
company: z10.object({ id: z10.string(), name: z10.string().nullable(), domain: z10.string().nullable() }).nullable(),
|
|
3079
|
+
/**
|
|
3080
|
+
* False when the connection never granted company access, which makes
|
|
3081
|
+
* `company: null` unknown rather than absent. Company access is granted
|
|
3082
|
+
* separately from contact access, so one can be present without the other.
|
|
3083
|
+
*/
|
|
3084
|
+
companyReadable: z10.boolean()
|
|
3085
|
+
})
|
|
3086
|
+
});
|
|
3087
|
+
var hubspotPipelinesListRequestSchema = z10.object({});
|
|
3088
|
+
var hubspotPipelineStageSchema = z10.object({
|
|
3089
|
+
id: z10.string(),
|
|
3090
|
+
label: z10.string(),
|
|
3091
|
+
/** HubSpot's win probability for the stage, as it returns it (a decimal string). */
|
|
3092
|
+
probability: z10.string().nullable()
|
|
3093
|
+
});
|
|
3094
|
+
var hubspotPipelineSchema = z10.object({
|
|
3095
|
+
id: z10.string(),
|
|
3096
|
+
label: z10.string(),
|
|
3097
|
+
/** Ordered as they appear in HubSpot, so the sequence is the real one. */
|
|
3098
|
+
stages: z10.array(hubspotPipelineStageSchema)
|
|
3099
|
+
});
|
|
3100
|
+
var hubspotPipelinesListResponseSchema = z10.object({
|
|
3101
|
+
ok: z10.literal(true),
|
|
3102
|
+
data: z10.object({ pipelines: z10.array(hubspotPipelineSchema) })
|
|
3103
|
+
});
|
|
2986
3104
|
var hubspotMeetingFieldSchema = z10.object({
|
|
2987
3105
|
name: z10.string(),
|
|
2988
3106
|
label: z10.string(),
|
|
@@ -26571,6 +26689,20 @@ Full guide: __tooling__/docs/tools/baker/history.md`
|
|
|
26571
26689
|
import { defineCommand as defineCommand114 } from "citty";
|
|
26572
26690
|
var EMBED_TYPES = ["legacy", "v4", "unknown"];
|
|
26573
26691
|
function failNotConnected(err) {
|
|
26692
|
+
if (err instanceof ApiError && err.code === "UNAUTHORIZED" && err.message.includes(HUBSPOT_MISSING_GRANT_MARKER)) {
|
|
26693
|
+
writeJson({
|
|
26694
|
+
ok: false,
|
|
26695
|
+
error: {
|
|
26696
|
+
code: "HUBSPOT_RECONNECT_REQUIRED",
|
|
26697
|
+
message: err.message,
|
|
26698
|
+
fix: {
|
|
26699
|
+
action: "reconnect",
|
|
26700
|
+
explanation: "AGENT INSTRUCTION: HubSpot IS connected \u2014 do not tell the user it is missing, and do not retry. This account was connected before this capability existed, so its access does not cover it. Only the user can widen it: tell them to open the dashboard, go to Brain -> Integrations -> Tools and reconnect HubSpot; it works from their next message. Do not abort the wider task: say which part you could not read, skip it, and finish everything else."
|
|
26701
|
+
}
|
|
26702
|
+
}
|
|
26703
|
+
});
|
|
26704
|
+
process.exit(1);
|
|
26705
|
+
}
|
|
26574
26706
|
if (err instanceof ApiError && err.code === "NOT_FOUND" && err.message.includes("No HubSpot account is connected")) {
|
|
26575
26707
|
writeJson({
|
|
26576
26708
|
ok: false,
|
|
@@ -26624,6 +26756,47 @@ registerSchema({
|
|
|
26624
26756
|
}
|
|
26625
26757
|
}
|
|
26626
26758
|
});
|
|
26759
|
+
registerSchema({
|
|
26760
|
+
command: "hubspot.forms.submissions",
|
|
26761
|
+
description: "How many leads a form actually received over a window, and on which days \u2014 the difference between 'this form is live' and 'this form is working'. Start here when asked whether a form or a campaign is producing leads. Returns counts and dates only; the leads' own answers need --full and are personal data, so do not put them in a report. Pair with `hubspot pipelines list` to see whether those leads were then worked.",
|
|
26762
|
+
args: {
|
|
26763
|
+
formId: { type: "positional", description: "HubSpot form id (from `baker hubspot forms list`)", required: true },
|
|
26764
|
+
days: { type: "string", description: "Window in days, 1-365 (default 30)", required: false },
|
|
26765
|
+
full: {
|
|
26766
|
+
type: "boolean",
|
|
26767
|
+
description: "Include each submission's field values \u2014 personal data, request only when actually needed",
|
|
26768
|
+
required: false,
|
|
26769
|
+
default: false
|
|
26770
|
+
}
|
|
26771
|
+
}
|
|
26772
|
+
});
|
|
26773
|
+
registerSchema({
|
|
26774
|
+
command: "hubspot.contacts.summary",
|
|
26775
|
+
description: "Whether the leads that arrived recently are actually being worked, as counts: how many contacts were created in the window, how many have someone assigned, and how they split across lifecycle stages. This is the 'have these leads been treated?' answer. Returns no names or emails \u2014 use `hubspot contacts lookup` for one specific person.",
|
|
26776
|
+
args: {
|
|
26777
|
+
days: { type: "string", description: "Window in days, 1-365 (default 30)", required: false }
|
|
26778
|
+
}
|
|
26779
|
+
});
|
|
26780
|
+
registerSchema({
|
|
26781
|
+
command: "hubspot.contacts.lookup",
|
|
26782
|
+
description: "One contact by email: its lifecycle stage, whether anyone is assigned, the account (company) it was filed under, and any deals it is attached to with their stage. Use it to settle whether a specific lead was picked up. `found: false` means the lead never reached the CRM at all \u2014 that is a finding, not an error. `dealsReadable: false` / `companyReadable: false` mean deals or account details were not readable on this connection, so an empty deals list or a null company is unknown rather than absent.",
|
|
26783
|
+
args: {
|
|
26784
|
+
email: { type: "positional", description: "The contact's email address", required: true }
|
|
26785
|
+
}
|
|
26786
|
+
});
|
|
26787
|
+
registerSchema({
|
|
26788
|
+
command: "hubspot.workflows.list",
|
|
26789
|
+
description: "Every workflow on the connected account, with whether it is switched on. Read it when auditing what happens to a lead after it is captured \u2014 a form wired to a workflow that is off looks connected and does nothing.",
|
|
26790
|
+
args: {
|
|
26791
|
+
search: { type: "string", description: "Case-insensitive substring match on the workflow name", required: false },
|
|
26792
|
+
"enabled-only": { type: "boolean", description: "Only workflows that are on", required: false, default: false }
|
|
26793
|
+
}
|
|
26794
|
+
});
|
|
26795
|
+
registerSchema({
|
|
26796
|
+
command: "hubspot.pipelines.list",
|
|
26797
|
+
description: "The account's deal pipelines and their stages, in the order they run. Use it to answer where a lead lands once captured and whether it moved \u2014 the 'have these leads been worked?' half of a form audit.",
|
|
26798
|
+
args: {}
|
|
26799
|
+
});
|
|
26627
26800
|
registerSchema({
|
|
26628
26801
|
command: "hubspot.meetings.list",
|
|
26629
26802
|
description: "Every HubSpot meeting link (calendar) on the connected account, with just enough to choose one: name, slug, link type (personal / group / round-robin), whether booking redirects somewhere, and how many booking fields it asks for (null = unknown, read it with `meetings view`). Next step is usually `hubspot meetings view <slug> --as-node`, which gives you the blob to write straight into a hubspotMeeting step.",
|
|
@@ -26833,9 +27006,217 @@ var meetingsViewCommand = defineCommand114({
|
|
|
26833
27006
|
}
|
|
26834
27007
|
}
|
|
26835
27008
|
});
|
|
27009
|
+
var formsSubmissionsCommand = defineCommand114({
|
|
27010
|
+
meta: {
|
|
27011
|
+
name: "submissions",
|
|
27012
|
+
description: "How many leads a form received, and when. Example: baker hubspot forms submissions <formId> --days 30"
|
|
27013
|
+
},
|
|
27014
|
+
args: {
|
|
27015
|
+
formId: { type: "positional", description: "HubSpot form id", required: true },
|
|
27016
|
+
days: { type: "string", description: "Window in days (default 30)", required: false },
|
|
27017
|
+
full: { type: "boolean", description: "Include each lead's answers", required: false }
|
|
27018
|
+
},
|
|
27019
|
+
run: async ({ args }) => {
|
|
27020
|
+
try {
|
|
27021
|
+
const days = args.days === void 0 ? 30 : Number(args.days);
|
|
27022
|
+
if (!Number.isInteger(days) || days < 1 || days > 365) {
|
|
27023
|
+
throw new ApiError("VALIDATION_ERROR", "--days must be a whole number between 1 and 365.");
|
|
27024
|
+
}
|
|
27025
|
+
const response = await apiPost("/api/hubspot/forms/submissions", {
|
|
27026
|
+
formId: args.formId,
|
|
27027
|
+
days,
|
|
27028
|
+
...args.full ? { full: true } : {}
|
|
27029
|
+
});
|
|
27030
|
+
const { inWindow, total, truncated, lastSubmittedAt } = response.data;
|
|
27031
|
+
const hints = [];
|
|
27032
|
+
if (inWindow === 0) {
|
|
27033
|
+
hints.push(
|
|
27034
|
+
total === 0 ? "This form has received nothing at all \u2014 check it is the form actually embedded on the page before concluding the campaign is at fault." : `No leads in the last ${days} days, though the form has ${total} older submission(s). It worked once and has gone quiet.`
|
|
27035
|
+
);
|
|
27036
|
+
}
|
|
27037
|
+
if (truncated) {
|
|
27038
|
+
hints.push(
|
|
27039
|
+
"Stopped before reaching the end of the window, so `inWindow` is a floor, not a count. Say so rather than reporting it as exact."
|
|
27040
|
+
);
|
|
27041
|
+
}
|
|
27042
|
+
if (!args.full && inWindow > 0) {
|
|
27043
|
+
hints.push(
|
|
27044
|
+
"Counts only \u2014 the leads' own answers are not included. Add --full if you genuinely need the field values; it returns personal data, so do not paste it into a report or a chat message."
|
|
27045
|
+
);
|
|
27046
|
+
}
|
|
27047
|
+
if (lastSubmittedAt !== null) {
|
|
27048
|
+
hints.push(
|
|
27049
|
+
"To find out whether these leads were worked, cross-check the deal pipeline: `baker hubspot pipelines list`."
|
|
27050
|
+
);
|
|
27051
|
+
}
|
|
27052
|
+
writeJson({ ...response, meta: { count: inWindow }, ...hints.length > 0 ? { hints } : {} });
|
|
27053
|
+
} catch (err) {
|
|
27054
|
+
failNotConnected(err);
|
|
27055
|
+
}
|
|
27056
|
+
}
|
|
27057
|
+
});
|
|
27058
|
+
var workflowsListCommand = defineCommand114({
|
|
27059
|
+
meta: {
|
|
27060
|
+
name: "list",
|
|
27061
|
+
description: "List HubSpot workflows. Example: baker hubspot workflows list --enabled-only"
|
|
27062
|
+
},
|
|
27063
|
+
args: {
|
|
27064
|
+
search: { type: "string", description: "Match on workflow name", required: false },
|
|
27065
|
+
"enabled-only": { type: "boolean", description: "Only workflows that are on", required: false }
|
|
27066
|
+
},
|
|
27067
|
+
run: async ({ args }) => {
|
|
27068
|
+
try {
|
|
27069
|
+
const body = {};
|
|
27070
|
+
if (args.search) {
|
|
27071
|
+
body.search = args.search;
|
|
27072
|
+
}
|
|
27073
|
+
if (args["enabled-only"]) {
|
|
27074
|
+
body.enabledOnly = true;
|
|
27075
|
+
}
|
|
27076
|
+
const response = await apiPost("/api/hubspot/workflows/list", body);
|
|
27077
|
+
const { workflows } = response.data;
|
|
27078
|
+
const off = workflows.filter((workflow) => !workflow.enabled);
|
|
27079
|
+
const hints = [];
|
|
27080
|
+
if (workflows.length === 0) {
|
|
27081
|
+
hints.push("No workflows matched. Drop --search/--enabled-only, or the account genuinely has none.");
|
|
27082
|
+
}
|
|
27083
|
+
if (off.length > 0) {
|
|
27084
|
+
hints.push(
|
|
27085
|
+
`${off.length} of these are switched off. A form wired to a workflow that is off looks connected and does nothing \u2014 worth flagging to the user.`
|
|
27086
|
+
);
|
|
27087
|
+
}
|
|
27088
|
+
writeJson({ ...response, meta: { count: workflows.length }, ...hints.length > 0 ? { hints } : {} });
|
|
27089
|
+
} catch (err) {
|
|
27090
|
+
failNotConnected(err);
|
|
27091
|
+
}
|
|
27092
|
+
}
|
|
27093
|
+
});
|
|
27094
|
+
var pipelinesListCommand = defineCommand114({
|
|
27095
|
+
meta: {
|
|
27096
|
+
name: "list",
|
|
27097
|
+
description: "List HubSpot deal pipelines and their stages. Example: baker hubspot pipelines list"
|
|
27098
|
+
},
|
|
27099
|
+
args: {},
|
|
27100
|
+
run: async () => {
|
|
27101
|
+
try {
|
|
27102
|
+
const response = await apiPost("/api/hubspot/pipelines/list", {});
|
|
27103
|
+
const { pipelines } = response.data;
|
|
27104
|
+
const hints = pipelines.length === 0 ? ["This account has no deal pipelines, so there is nowhere for a lead to be worked."] : ["Stages are listed in the order they run in HubSpot, so the first stage is where a new lead lands."];
|
|
27105
|
+
writeJson({ ...response, meta: { count: pipelines.length }, hints });
|
|
27106
|
+
} catch (err) {
|
|
27107
|
+
failNotConnected(err);
|
|
27108
|
+
}
|
|
27109
|
+
}
|
|
27110
|
+
});
|
|
27111
|
+
var contactsSummaryCommand = defineCommand114({
|
|
27112
|
+
meta: {
|
|
27113
|
+
name: "summary",
|
|
27114
|
+
description: "Whether recent leads are being worked, as counts. Example: baker hubspot contacts summary --days 30"
|
|
27115
|
+
},
|
|
27116
|
+
args: {
|
|
27117
|
+
days: { type: "string", description: "Window in days (default 30)", required: false }
|
|
27118
|
+
},
|
|
27119
|
+
run: async ({ args }) => {
|
|
27120
|
+
try {
|
|
27121
|
+
const days = args.days === void 0 ? 30 : Number(args.days);
|
|
27122
|
+
if (!Number.isInteger(days) || days < 1 || days > 365) {
|
|
27123
|
+
throw new ApiError("VALIDATION_ERROR", "--days must be a whole number between 1 and 365.");
|
|
27124
|
+
}
|
|
27125
|
+
const response = await apiPost("/api/hubspot/contacts/summary", { days });
|
|
27126
|
+
const { total, withOwner, withoutOwner, byStage, truncated } = response.data;
|
|
27127
|
+
const hints = [];
|
|
27128
|
+
if (total === 0) {
|
|
27129
|
+
hints.push(`No contacts were created in the last ${days} days \u2014 nothing reached the CRM in that window.`);
|
|
27130
|
+
} else if (withOwner === 0) {
|
|
27131
|
+
hints.push(
|
|
27132
|
+
`None of these ${total} contacts has anyone assigned. That is the strongest available signal that the leads are arriving and nobody is picking them up \u2014 worth raising with the user.`
|
|
27133
|
+
);
|
|
27134
|
+
} else if (withoutOwner > withOwner) {
|
|
27135
|
+
hints.push(`${withoutOwner} of ${total} contacts have nobody assigned \u2014 more unworked than worked.`);
|
|
27136
|
+
}
|
|
27137
|
+
const unset = byStage.find((entry) => entry.stage === "unset");
|
|
27138
|
+
if (unset && unset.count > 0) {
|
|
27139
|
+
hints.push(
|
|
27140
|
+
`${unset.count} contact(s) have no lifecycle stage set at all, so they were created and never staged.`
|
|
27141
|
+
);
|
|
27142
|
+
}
|
|
27143
|
+
if (truncated) {
|
|
27144
|
+
hints.push("Stopped before reading every contact in the window, so these counts are floors, not totals.");
|
|
27145
|
+
}
|
|
27146
|
+
writeJson({ ...response, meta: { count: total }, ...hints.length > 0 ? { hints } : {} });
|
|
27147
|
+
} catch (err) {
|
|
27148
|
+
failNotConnected(err);
|
|
27149
|
+
}
|
|
27150
|
+
}
|
|
27151
|
+
});
|
|
27152
|
+
function contactLookupHints(data) {
|
|
27153
|
+
const { found, contact, deals, dealsReadable, company, companyReadable } = data;
|
|
27154
|
+
if (!found) {
|
|
27155
|
+
return [
|
|
27156
|
+
"No contact with that email. The lead never reached the CRM \u2014 check the form's submissions before assuming the CRM lost it."
|
|
27157
|
+
];
|
|
27158
|
+
}
|
|
27159
|
+
const hints = [];
|
|
27160
|
+
if (contact?.ownerId === null) {
|
|
27161
|
+
hints.push("Nobody is assigned to this contact.");
|
|
27162
|
+
}
|
|
27163
|
+
if (!dealsReadable) {
|
|
27164
|
+
hints.push(
|
|
27165
|
+
"Deals were not read: this HubSpot connection does not cover them. Do not report the lead as having no deal \u2014 you cannot tell. The user can enable it by reconnecting HubSpot."
|
|
27166
|
+
);
|
|
27167
|
+
} else if (deals.length === 0) {
|
|
27168
|
+
hints.push("No deal is associated, so this contact was never taken into the pipeline.");
|
|
27169
|
+
}
|
|
27170
|
+
if (!companyReadable) {
|
|
27171
|
+
hints.push(
|
|
27172
|
+
"Account details were not read: this HubSpot connection does not cover them. Do not report the lead as having no account \u2014 you cannot tell. The user can enable it by reconnecting HubSpot."
|
|
27173
|
+
);
|
|
27174
|
+
} else if (company === null) {
|
|
27175
|
+
hints.push(
|
|
27176
|
+
"No account is associated either. Most B2B setups create one automatically when a contact arrives, so this usually means the lead was never processed at all \u2014 not that the account is missing."
|
|
27177
|
+
);
|
|
27178
|
+
}
|
|
27179
|
+
return hints;
|
|
27180
|
+
}
|
|
27181
|
+
var contactsLookupCommand = defineCommand114({
|
|
27182
|
+
meta: {
|
|
27183
|
+
name: "lookup",
|
|
27184
|
+
description: "Find one contact by email and see whether it was worked. Example: baker hubspot contacts lookup a@b.com"
|
|
27185
|
+
},
|
|
27186
|
+
args: {
|
|
27187
|
+
email: { type: "positional", description: "The contact's email address", required: true }
|
|
27188
|
+
},
|
|
27189
|
+
run: async ({ args }) => {
|
|
27190
|
+
try {
|
|
27191
|
+
const response = await apiPost("/api/hubspot/contacts/lookup", {
|
|
27192
|
+
email: args.email
|
|
27193
|
+
});
|
|
27194
|
+
const hints = contactLookupHints(response.data);
|
|
27195
|
+
writeJson({
|
|
27196
|
+
...response,
|
|
27197
|
+
meta: { count: response.data.found ? 1 : 0 },
|
|
27198
|
+
...hints.length > 0 ? { hints } : {}
|
|
27199
|
+
});
|
|
27200
|
+
} catch (err) {
|
|
27201
|
+
failNotConnected(err);
|
|
27202
|
+
}
|
|
27203
|
+
}
|
|
27204
|
+
});
|
|
27205
|
+
var contactsCommand = defineCommand114({
|
|
27206
|
+
meta: { name: "contacts", description: "Contacts on the connected HubSpot account." },
|
|
27207
|
+
subCommands: { summary: contactsSummaryCommand, lookup: contactsLookupCommand }
|
|
27208
|
+
});
|
|
26836
27209
|
var formsCommand = defineCommand114({
|
|
26837
27210
|
meta: { name: "forms", description: "HubSpot forms on the connected account." },
|
|
26838
|
-
subCommands: { list: formsListCommand, view: formsViewCommand }
|
|
27211
|
+
subCommands: { list: formsListCommand, view: formsViewCommand, submissions: formsSubmissionsCommand }
|
|
27212
|
+
});
|
|
27213
|
+
var workflowsCommand = defineCommand114({
|
|
27214
|
+
meta: { name: "workflows", description: "HubSpot workflows on the connected account." },
|
|
27215
|
+
subCommands: { list: workflowsListCommand }
|
|
27216
|
+
});
|
|
27217
|
+
var pipelinesCommand = defineCommand114({
|
|
27218
|
+
meta: { name: "pipelines", description: "HubSpot deal pipelines on the connected account." },
|
|
27219
|
+
subCommands: { list: pipelinesListCommand }
|
|
26839
27220
|
});
|
|
26840
27221
|
var meetingsCommand = defineCommand114({
|
|
26841
27222
|
meta: { name: "meetings", description: "HubSpot meeting links (calendars) on the connected account." },
|
|
@@ -26844,21 +27225,37 @@ var meetingsCommand = defineCommand114({
|
|
|
26844
27225
|
var hubspotCommand = defineCommand114({
|
|
26845
27226
|
meta: {
|
|
26846
27227
|
name: "hubspot",
|
|
26847
|
-
description: `Read the connected HubSpot account \u2014 forms
|
|
27228
|
+
description: `Read the connected HubSpot account \u2014 forms, the leads they received, workflows, deal pipelines and
|
|
27229
|
+
meeting links (calendars) \u2014 and get the blob to wire a form into a Form step yourself.
|
|
26848
27230
|
|
|
26849
27231
|
Examples:
|
|
26850
27232
|
baker hubspot forms list # every form: embed type + post-submit action
|
|
26851
27233
|
baker hubspot forms list --redirecting-only # which forms redirect away on submit
|
|
26852
27234
|
baker hubspot forms view <formId> # fields, consent mode, full configuration
|
|
26853
27235
|
baker hubspot forms view <formId> --as-node # the form.external blob -> write into the step
|
|
27236
|
+
baker hubspot forms submissions <formId> # how many leads it got, and when
|
|
27237
|
+
baker hubspot workflows list --enabled-only # what runs after a lead is captured
|
|
27238
|
+
baker hubspot pipelines list # where a lead lands, and the stages after it
|
|
27239
|
+
baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
|
|
27240
|
+
baker hubspot contacts lookup a@b.com # was this one lead picked up?
|
|
26854
27241
|
baker hubspot meetings list # every calendar + whether booking redirects
|
|
26855
27242
|
baker hubspot meetings view <slug> --as-node # same, for a hubspotMeeting step
|
|
26856
27243
|
|
|
27244
|
+
An account connected before one of these existed cannot serve it \u2014 you get HUBSPOT_RECONNECT_REQUIRED, which
|
|
27245
|
+
means HubSpot IS connected and the user has to reconnect it to widen what Baker may read. Report that and
|
|
27246
|
+
carry on with the rest; never call it broken and never retry.
|
|
27247
|
+
|
|
26857
27248
|
Nothing here is secret, so HubSpot steps do not need request_flow_input: take --as-node and edit the
|
|
26858
27249
|
flow's _data.json. Use the approval form only when no account is connected or the right form is unclear.
|
|
26859
27250
|
Full guide: __tooling__/docs/tools/baker/hubspot.md`
|
|
26860
27251
|
},
|
|
26861
|
-
subCommands: {
|
|
27252
|
+
subCommands: {
|
|
27253
|
+
forms: formsCommand,
|
|
27254
|
+
workflows: workflowsCommand,
|
|
27255
|
+
pipelines: pipelinesCommand,
|
|
27256
|
+
contacts: contactsCommand,
|
|
27257
|
+
meetings: meetingsCommand
|
|
27258
|
+
}
|
|
26862
27259
|
});
|
|
26863
27260
|
|
|
26864
27261
|
// src/commands/images/index.ts
|
|
@@ -30946,6 +31343,21 @@ Subcommands:
|
|
|
30946
31343
|
|
|
30947
31344
|
// src/commands/mcp/index.ts
|
|
30948
31345
|
import { defineCommand as defineCommand141 } from "citty";
|
|
31346
|
+
|
|
31347
|
+
// src/commands/mcp/platforms.ts
|
|
31348
|
+
function readsKey(label) {
|
|
31349
|
+
const [first = "", ...rest] = label.split(" ");
|
|
31350
|
+
return [first.toLowerCase(), ...rest].join("");
|
|
31351
|
+
}
|
|
31352
|
+
function platformEntry(platform) {
|
|
31353
|
+
return {
|
|
31354
|
+
platform: platform.label,
|
|
31355
|
+
...platform.account ? { account: platform.account } : {},
|
|
31356
|
+
...platform.readsLabel && platform.reads.length > 0 ? { [readsKey(platform.readsLabel)]: platform.reads } : {}
|
|
31357
|
+
};
|
|
31358
|
+
}
|
|
31359
|
+
|
|
31360
|
+
// src/commands/mcp/index.ts
|
|
30949
31361
|
var SCOPES = ["user", "user_org", "company", "org"];
|
|
30950
31362
|
function parseScope(raw) {
|
|
30951
31363
|
const scope = raw === void 0 ? "company" : String(raw);
|
|
@@ -30993,18 +31405,21 @@ function fail6(err) {
|
|
|
30993
31405
|
writeJson({ ok: false, error: { code: "NETWORK_ERROR", message: "Unexpected error" } });
|
|
30994
31406
|
process.exit(1);
|
|
30995
31407
|
}
|
|
31408
|
+
var PLATFORMS_HINT = "`platforms` are connected accounts you reach through `baker` commands (HubSpot, Google Ads, GA4, Search Console, Tag Manager, \u2026), NOT mcp__* tools \u2014 there is no tool to call for them. A platform listed here IS connected: never tell the user to connect it, and never conclude it is missing because it has no mcp__* entry. Read what it holds with its own command family (`baker hubspot \u2026`, `baker ads google \u2026`, `baker ga4 \u2026`, and so on).";
|
|
30996
31409
|
registerSchema({
|
|
30997
31410
|
command: "mcp.connected",
|
|
30998
|
-
description: "List
|
|
31411
|
+
description: "List everything this chat can reach: managed integrations (Attio, Slack, Gmail, Google Sheets, \u2026), custom MCP servers, and the platforms the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager) which you read through their own `baker` commands. Start here when the user mentions an external tool or platform.",
|
|
30999
31412
|
args: {}
|
|
31000
31413
|
});
|
|
31001
31414
|
var connectedCommand = defineCommand141({
|
|
31002
31415
|
meta: {
|
|
31003
31416
|
name: "connected",
|
|
31004
|
-
description: `
|
|
31417
|
+
description: `Everything this chat can reach \u2014 managed integrations, custom MCP servers, and connected platforms.
|
|
31005
31418
|
|
|
31006
31419
|
Managed tools ride the composio servers (mcp__composio_<scope>__*); custom servers expose mcp__<name>__*.
|
|
31007
|
-
|
|
31420
|
+
Platforms (HubSpot, Google Ads, GA4, \u2026) have no mcp__* tools at all \u2014 you read them with their own
|
|
31421
|
+
\`baker\` command family, and one listed here is connected and ready to use.
|
|
31422
|
+
A tool or platform the user names that is NOT listed anywhere here is simply not connected yet.`
|
|
31008
31423
|
},
|
|
31009
31424
|
run: async () => {
|
|
31010
31425
|
try {
|
|
@@ -31018,7 +31433,11 @@ A tool the user names that is NOT listed here is simply not connected yet.`
|
|
|
31018
31433
|
...c.readOnly ? { readOnly: true } : {}
|
|
31019
31434
|
}));
|
|
31020
31435
|
const custom = data.custom.map(customEntry);
|
|
31436
|
+
const platforms = (data.platforms ?? []).map(platformEntry);
|
|
31021
31437
|
const hints = [];
|
|
31438
|
+
if (platforms.length > 0) {
|
|
31439
|
+
hints.push(PLATFORMS_HINT);
|
|
31440
|
+
}
|
|
31022
31441
|
if (data.custom.some((s) => s.health && !s.health.ok && !s.health.transient)) {
|
|
31023
31442
|
hints.push(BROKEN_SERVER_HINT);
|
|
31024
31443
|
}
|
|
@@ -31034,11 +31453,11 @@ A tool the user names that is NOT listed here is simply not connected yet.`
|
|
|
31034
31453
|
);
|
|
31035
31454
|
}
|
|
31036
31455
|
hints.push(
|
|
31037
|
-
"Not listed = not connected. Managed app (Slack, HubSpot,
|
|
31456
|
+
"Not listed anywhere = not connected. Managed app (Slack, Attio, \u2026) or platform (HubSpot, Google Ads, GA4, \u2026) \u2192 the user connects it in the dashboard: Brain \u2192 Integrations \u2192 Tools. Custom HTTPS MCP endpoint \u2192 `baker mcp add`."
|
|
31038
31457
|
);
|
|
31039
31458
|
writeJson({
|
|
31040
31459
|
ok: true,
|
|
31041
|
-
data: { ...data.managedDisabled ? { managedDisabled: true } : {}, managed, custom },
|
|
31460
|
+
data: { ...data.managedDisabled ? { managedDisabled: true } : {}, managed, custom, platforms },
|
|
31042
31461
|
hints
|
|
31043
31462
|
});
|
|
31044
31463
|
} catch (err) {
|
|
@@ -31171,7 +31590,7 @@ Tools from a registered server appear as mcp__<name>__* on the next message.
|
|
|
31171
31590
|
Managed here are **company-scoped** servers; user- and org-scoped servers are managed in the dashboard.
|
|
31172
31591
|
|
|
31173
31592
|
Start here:
|
|
31174
|
-
baker mcp connected # everything this chat can reach: managed integrations
|
|
31593
|
+
baker mcp connected # everything this chat can reach: managed integrations, custom servers, connected platforms
|
|
31175
31594
|
|
|
31176
31595
|
Examples:
|
|
31177
31596
|
baker mcp add --name weather --url https://mcp.example.com/mcp
|