@koda-sl/baker-cli 0.429.0 → 0.430.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.
Files changed (28) hide show
  1. package/README.md +54 -4
  2. package/dist/{chunk-Q6F5UNOU.js → chunk-6Z4ETVEZ.js} +3 -3
  3. package/dist/{chunk-ZH7YAR5N.js → chunk-DL7VXQEI.js} +3 -3
  4. package/dist/{chunk-PHLFNYGQ.js → chunk-RM2TTRYQ.js} +4 -4
  5. package/dist/{chunk-RFZYRG2Q.js → chunk-UKGTWBKW.js} +11 -3
  6. package/dist/chunk-UKGTWBKW.js.map +1 -0
  7. package/dist/{chunk-FQOKJYKN.js → chunk-WJIYNKLI.js} +12 -4
  8. package/dist/chunk-WJIYNKLI.js.map +1 -0
  9. package/dist/{chunk-GCTR4MDE.js → chunk-Y77DA7EO.js} +2 -2
  10. package/dist/cli.js +42961 -40450
  11. package/dist/cli.js.map +1 -1
  12. package/dist/{client-GS7T7ETG.js → client-UUKOCY3Q.js} +4 -4
  13. package/dist/engine/index.js +3 -3
  14. package/dist/{env-SL22S6CM.js → env-Q3KQLCU5.js} +4 -2
  15. package/dist/output-V44OD5O3.js +22 -0
  16. package/dist/{shared-XR54KFMH.js → shared-3THDUK2O.js} +6 -6
  17. package/package.json +1 -1
  18. package/dist/chunk-FQOKJYKN.js.map +0 -1
  19. package/dist/chunk-RFZYRG2Q.js.map +0 -1
  20. package/dist/output-OFDKEPPJ.js +0 -18
  21. /package/dist/{chunk-Q6F5UNOU.js.map → chunk-6Z4ETVEZ.js.map} +0 -0
  22. /package/dist/{chunk-ZH7YAR5N.js.map → chunk-DL7VXQEI.js.map} +0 -0
  23. /package/dist/{chunk-PHLFNYGQ.js.map → chunk-RM2TTRYQ.js.map} +0 -0
  24. /package/dist/{chunk-GCTR4MDE.js.map → chunk-Y77DA7EO.js.map} +0 -0
  25. /package/dist/{client-GS7T7ETG.js.map → client-UUKOCY3Q.js.map} +0 -0
  26. /package/dist/{env-SL22S6CM.js.map → env-Q3KQLCU5.js.map} +0 -0
  27. /package/dist/{output-OFDKEPPJ.js.map → output-V44OD5O3.js.map} +0 -0
  28. /package/dist/{shared-XR54KFMH.js.map → shared-3THDUK2O.js.map} +0 -0
package/README.md CHANGED
@@ -1116,10 +1116,23 @@ baker ads linkedin analytics --level campaign --campaign-id 1234 \
1116
1116
  # "Top companies seeing this campaign" (ABM feedback loop) — org names auto-resolved
1117
1117
  baker ads linkedin top-companies --campaign-id 1234 --last-days 30
1118
1118
 
1119
+ # "Which companies are our ads reaching?" — one row per company over the whole period,
1120
+ # most engaged first ({org, name, impressions, clicks, engagements, costLocal}).
1121
+ # Defaults: last 12 calendar months, the only selected ad account, top 100 (--full for all).
1122
+ baker ads linkedin companies --account-id 1234
1123
+ baker ads linkedin companies --by month --min-impressions 50 # months[] per company
1124
+ baker ads linkedin companies --campaign-id 1,2 --by campaign # campaigns[] per company
1125
+ baker ads linkedin companies --with-domains --stats --output csv # + domain/domainSource; read-quality stats
1126
+
1119
1127
  # "Who are these org URNs?" — resolve org URNs to company names (probe leakage)
1120
1128
  baker ads linkedin resolve --urns urn:li:organization:17719,urn:li:organization:19022
1121
1129
  baker ads linkedin resolve --ids 17719,19022 --output csv
1122
1130
 
1131
+ # "What are these companies' website domains?" — for matching against CRM companies.
1132
+ # Per org: vanityName (LinkedIn page), website, domain, domainSource (website | subdomain | none);
1133
+ # ids LinkedIn doesn't know land in `errors`. Shortlinks are never followed.
1134
+ baker ads linkedin resolve --ids 1035,1337 --with-domains
1135
+
1123
1136
  # "Who is in our audience by industry/seniority/function/title — all at once?"
1124
1137
  baker ads linkedin demographics --campaign-id 1234 --last-days 30
1125
1138
 
@@ -1188,6 +1201,7 @@ creatives # default ACTIVE intended-status
1188
1201
 
1189
1202
  analytics # see "Smart defaults" above (3-axis: level × intent × pivot)
1190
1203
  demographics # sweep all firmographic pivots in one call
1204
+ companies # which companies the ads reached, one row per company (--by month|campaign, --with-domains)
1191
1205
  top-companies # pivot=MEMBER_COMPANY shortcut (ABM)
1192
1206
  conversation # pivot=CONVERSATION_NODE_BUTTON shortcut (Sponsored Messaging)
1193
1207
 
@@ -1207,6 +1221,7 @@ audit --format md # deliverable-ready markdown table
1207
1221
  campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
1208
1222
  campaigns create|update|pause|resume|archive|duplicate
1209
1223
  campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
1224
+ campaigns exclude-companies <ad-set-id> --org <ids> # stop an ad set reaching these companies
1210
1225
  account url-params # UTMs for every ad in the whole account
1211
1226
  url-params [--ad-sets] # read what is tagged now, and what lands twice
1212
1227
  creatives create|update|pause|resume|duplicate
@@ -1247,9 +1262,16 @@ baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=pa
1247
1262
  baker ads linkedin campaigns update 123456 --audience-expansion off --lan off
1248
1263
  baker ads linkedin campaigns update 123456 --bid 7.50 --daily-budget 100 --currency USD
1249
1264
 
1250
- # Matched audiences (CSV with header; email columns SHA-256 hashed locally)
1265
+ # Matched audiences (CSV with header; email columns SHA-256 hashed locally).
1266
+ # Company lists take companyName,domain,organizationUrn — organizationUrn matches the company page exactly
1251
1267
  baker ads linkedin audiences create --name "ABM Tier-1" --type company-list --list-file accounts.csv
1252
1268
 
1269
+ # Stop an ad set reaching customers / open deals — add-only; at publish Baker re-reads the ad set
1270
+ # and merges into its targeting as it stands then. --org takes company ids, ≤200 per call;
1271
+ # a longer list goes into a company-list audience passed as --segment
1272
+ baker ads linkedin campaigns exclude-companies 123456 --org 1035,1441
1273
+ baker ads linkedin campaigns exclude-companies 123456 --segment urn:li:dmpSegment:42
1274
+
1253
1275
  # Conversions with playbook-default windows
1254
1276
  baker ads linkedin conversions create --name "Demo booked" --type LEAD --method INSIGHT_TAG \
1255
1277
  --post-click-window 30 --view-window 7
@@ -1311,7 +1333,7 @@ Each finding: `{id, area, check, status, severity, evidence, fix: {explanation,
1311
1333
  - OAuth tokens auto-refresh server-side. On 401, reconnect LinkedIn from dashboard → integrations.
1312
1334
  - Account scoping: backend rejects calls against an account not in the picker selection.
1313
1335
  - Pinned API version: `Linkedin-Version: 202606` (declared in `oauth/constants.ts`). Bump as a coordinated change — LinkedIn deprecates monthly with a ~12-month support window. The floor is `TRACKING_PARAM_DYNAMIC_MIN_API_VERSION` (`CREATIVE_NAME` needs 202606), asserted in `ads/linkedin/utils.test.ts`.
1314
- - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, bid-pricing/forecast 6h, audience-size 1h.
1336
+ - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, org domains 7d, bid-pricing/forecast 6h, audience-size 1h.
1315
1337
  - `--no-cache` on any command skips both cache layers (the CLI's own and the server's).
1316
1338
 
1317
1339
  #### LinkedIn-specific gotchas
@@ -3925,7 +3947,7 @@ baker setup-links list # active lin
3925
3947
 
3926
3948
  ---
3927
3949
 
3928
- ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot deals properties | fields` · `baker hubspot meetings list | view`
3950
+ ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot companies search` · `baker hubspot deals list | properties | fields` · `baker hubspot meetings list | view`
3929
3951
 
3930
3952
  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.
3931
3953
 
@@ -3947,6 +3969,8 @@ baker hubspot pipelines list # where a lead lands, and the st
3947
3969
  baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
3948
3970
  baker hubspot contacts lookup lead@example.com # was this one lead picked up?
3949
3971
  baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
3972
+ baker hubspot companies search --domains acme.com,globex.io --with-deals # CRM accounts by domain, with their deals
3973
+ baker hubspot deals list --pipeline default --modified-since 2026-01-01 # deals: amount, stage, closed won
3950
3974
  baker hubspot meetings list # every calendar + whether booking redirects
3951
3975
  baker hubspot meetings view <slug> # booking-form fields for one calendar
3952
3976
  baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
@@ -3957,7 +3981,9 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3957
3981
  **`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.
3958
3982
  **`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.
3959
3983
  **`workflows view`:** positional `<workflowId>` (the id from `workflows list`, and the one in HubSpot's own URL — the command translates it to HubSpot's separate internal flow id for you). Returns the enrolment criteria and whether it re-enrols (`null` = HubSpot does not report it for that criteria type, so it is unknown rather than off), plus every step with the property it writes. On a `LIST_BRANCH` step, branches come back in **evaluation order** (`order: 1` wins over `order: 2`) — a broad branch above a narrower one silently swallows the records the narrow one was written for, and HubSpot reports nothing. A `STATIC_BRANCH` matches on a value rather than top down, so its `order` is display only. `steps` is **not** in execution order; follow `nextActionId`. `--full` adds the raw payload, the only place the branch filter criteria (which property is compared to which value) can be read.
3960
- **`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
3984
+ **`pipelines list`:** no flags; stages come back in the order they run in HubSpot, each with `isClosed` and `isWon` (closed with probability 1).
3985
+ **`companies search` flags:** `--domains <a.com,b.com>` (up to 100; a URL or `www.` is reduced to the bare domain), `--names <"Acme, Inc.;Globex">` (semicolon-separated), `--with-deals`, `--limit <1-500>` (default 100), `--output json|csv|jsonl|md`. Rows: `{ id, name, domain, linkedinPage, deals? }`; `notFound` lists the searched domains no CRM company carries; `truncated: true` means more matched than `--limit`.
3986
+ **`deals list` flags:** `--pipeline <id,id>`, `--modified-since <YYYY-MM-DD>`, `--limit <1-1000>` (default 200), `--after <cursor>`, `--output json|csv|jsonl|md`. Rows, oldest change first: `{ id, name, amount, currency, pipelineId, stageId, stageLabel, isClosed, isWon, createdAt, closedAt, modifiedAt, companyIds }` — `amount: null` is none entered. `next` is the `--after` for the following page; `restartFrom` appears when HubSpot's 10,000-result search ceiling is reached (read on with `--modified-since <restartFrom>`); `companiesReadable: false` means the connection cannot read companies, so no deal carries `companyIds`.
3961
3987
  **`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.
3962
3988
  **`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.
3963
3989
  **`contacts properties`:** no flags; returns every contact property with a closed list of values — `{ name, label, groupName, options: [{ value, label }] }` — the ones a HubSpot-read source can watch. Names are HubSpot's internal ones; hand them to `baker analytics sources --create-hubspot --property <name> --value <value>` as printed. No personal data.
@@ -3989,6 +4015,30 @@ No write surface against HubSpot itself — a form's fields, redirect, or consen
3989
4015
 
3990
4016
  ---
3991
4017
 
4018
+ ### `baker accounts influenced | engagement | warm | company | campaigns | crm`
4019
+
4020
+ Account insights: which companies the LinkedIn ads are warming, and which CRM deals they influenced. Reads Baker's nightly copy of LinkedIn and the CRM, so it is fast and covers up to 24 months; nothing is changed anywhere. Only for companies with Account insights switched on — otherwise every command exits 1 with `ACCOUNTS_NOT_ENABLED` and an `error.fix` pointing at the live read (`baker ads linkedin companies`, plus `baker hubspot companies search`). `--start`/`--end` must be real calendar days within 24 calendar months; a bad flag or an unsupported `--output` exits 1 with `INVALID_ARGS`.
4021
+
4022
+ ```bash
4023
+ baker accounts influenced # influenced pipeline, last 12 calendar months
4024
+ baker accounts influenced --basis revenue --start 2026-01-01 --end 2026-06-30
4025
+ baker accounts influenced --full # every deal in the period, influenced or not
4026
+ baker accounts engagement --last-months 3 --limit 50 # companies reached, New/Rising, CRM match and deals
4027
+ baker accounts engagement --campaign-id 123456 --output csv
4028
+ baker accounts warm --output csv > warm.csv # companyName,domain,organizationUrn
4029
+ baker accounts company acme.com # or a LinkedIn company id / urn:li:organization:N
4030
+ baker accounts campaigns --last-months 6 --output md # per campaign: reached, engaged, influenced pipeline (--basis revenue)
4031
+ baker accounts influenced --model awareness --threshold 10 --output csv # another rule, this call only
4032
+ baker accounts warm --min 0 # every New/Rising company, below the threshold too
4033
+ baker accounts crm status # deals from HubSpot or sent by the client; what arrived, linked Google Sheets
4034
+ baker accounts crm check --file deals.csv # judge a deals or company CSV offline, nothing sent
4035
+ baker accounts crm contract # the send-your-CRM contract, as Markdown for a developer
4036
+ ```
4037
+
4038
+ A deal is **influenced** when its company reached the company's influence rule — by default 3+ LinkedIn ad engagements (Awareness: 7+ impressions; Intent: 3+ clicks) — in the 12 months up to and including the month the deal was created (`--basis pipeline`, deals not won) or closed won (`--basis revenue`). It is a labelled rule, not credit: campaign rows overlap, and money is reported per currency, never summed across currencies. Deals whose company match is only a guess come back under `lowConfidence` and stay out of `summary` until the match is confirmed in Measure → Accounts. Without the deals set up there — HubSpot's pipelines and won/lost stages, or Another CRM — `influenced` and `campaigns` exit 1 with `ACCOUNTS_NOT_SET_UP`. A CRM Baker cannot read sends its companies and deals to `POST /v1/accounts/crm` (a `crm_ingest` key, `X-Baker-Mode: test` to rehearse, up to 500 records per request, records naming a person refused), is uploaded as a CSV in Measure → Accounts, or lives in a Google Sheet linked there that Baker reads every night (a row deleted from the sheet leaves Baker). A file is a deals export or a company list — a company needs only its website or LinkedIn page; `crm check` runs the same rules offline and `crm contract` prints the whole contract. The CLI never sends deals or links a sheet. `--model engagement|awareness|intent` and `--threshold N` on `influenced`, `campaigns` and `warm` read with another rule for that call only; the saved setting is unchanged. A **warm** account is New or Rising, reached the company's influence threshold this period on the rule's metric (`--min N` overrides, `--min 0` drops it), and has no open deal, no won deal and no deal lost in the last 6 months; `engagement` and `warm` rows carry `dealStatus` (`won` > `open` > `lost` > `none`), `engagement --output csv` adds `lastMonth` and `previousImpressions/Clicks/Engagements`, and `campaigns` / `company` carry each campaign's `campaignName` (null when LinkedIn could not name it). `company` returns `months` (the company's totals), `byCampaign` (the same numbers per campaign — overlapping, never add them) and its `deals` with `touchValue` and `influenced`. `warm --output csv` is the file `baker ads linkedin audiences create --type company-list --list-file` takes.
4039
+
4040
+ ---
4041
+
3992
4042
  ### `baker chats list | view | transcript | diff`
3993
4043
 
3994
4044
  Read-only inspection of **other Sessions (chats) on the same account** — so an agent can reuse prior work ("like we did in that other chat, but for X"). Same-account only; never crosses a company boundary and has no write surface.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-GCTR4MDE.js";
4
+ } from "./chunk-Y77DA7EO.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-FQOKJYKN.js";
7
+ } from "./chunk-WJIYNKLI.js";
8
8
 
9
9
  // src/client.ts
10
10
  var MAX_RATE_LIMIT_RETRIES = 3;
@@ -208,4 +208,4 @@ export {
208
208
  apiGet,
209
209
  apiPost
210
210
  };
211
- //# sourceMappingURL=chunk-Q6F5UNOU.js.map
211
+ //# sourceMappingURL=chunk-6Z4ETVEZ.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-GCTR4MDE.js";
4
+ } from "./chunk-Y77DA7EO.js";
5
5
  import {
6
6
  API_KEY_FORMAT_MESSAGE,
7
7
  DIRECT_ROUTE,
@@ -22,7 +22,7 @@ import {
22
22
  refuseNonPublicUrl,
23
23
  requireConnection,
24
24
  shouldEscalate
25
- } from "./chunk-FQOKJYKN.js";
25
+ } from "./chunk-WJIYNKLI.js";
26
26
 
27
27
  // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
28
28
  var require_safe_stable_stringify = __commonJS({
@@ -18543,4 +18543,4 @@ export {
18543
18543
  defaultRegistry,
18544
18544
  createEngineFromEnv
18545
18545
  };
18546
- //# sourceMappingURL=chunk-ZH7YAR5N.js.map
18546
+ //# sourceMappingURL=chunk-DL7VXQEI.js.map
@@ -2,13 +2,13 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-RFZYRG2Q.js";
5
+ } from "./chunk-UKGTWBKW.js";
6
6
  import {
7
7
  ApiError
8
- } from "./chunk-Q6F5UNOU.js";
8
+ } from "./chunk-6Z4ETVEZ.js";
9
9
  import {
10
10
  getEnv
11
- } from "./chunk-FQOKJYKN.js";
11
+ } from "./chunk-WJIYNKLI.js";
12
12
 
13
13
  // src/commands/ads/meta/shared.ts
14
14
  var DAY_MS = 864e5;
@@ -133,4 +133,4 @@ export {
133
133
  resolveEffectiveStatus,
134
134
  statusFilterHints
135
135
  };
136
- //# sourceMappingURL=chunk-PHLFNYGQ.js.map
136
+ //# sourceMappingURL=chunk-RM2TTRYQ.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiGet
4
- } from "./chunk-Q6F5UNOU.js";
4
+ } from "./chunk-6Z4ETVEZ.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-FQOKJYKN.js";
7
+ } from "./chunk-WJIYNKLI.js";
8
8
 
9
9
  // src/error-handler.ts
10
10
  var REQUEST_CONNECTION_PLATFORM = {
@@ -153,6 +153,12 @@ function writeJsonEnvelope(envelope) {
153
153
  process.stdout.write(`${JSON.stringify(envelope, null, 2)}
154
154
  `);
155
155
  }
156
+ var TABLE_OUTPUT_FORMATS = ["json", "csv", "jsonl", "md"];
157
+ function outputFormatRefusal(format, supported) {
158
+ if (supported.includes(format)) return null;
159
+ const list = supported.length > 1 ? `${supported.slice(0, -1).join(", ")} or ${supported.at(-1)}` : supported.join("");
160
+ return `--output ${format} is not supported \u2014 use ${list}`;
161
+ }
156
162
  function toCsvRow(values) {
157
163
  return values.map((v) => {
158
164
  if (v.includes(",") || v.includes('"') || v.includes("\n")) {
@@ -294,8 +300,10 @@ export {
294
300
  getManagerIdForCustomer,
295
301
  writeAdsJson,
296
302
  writeJsonEnvelope,
303
+ TABLE_OUTPUT_FORMATS,
304
+ outputFormatRefusal,
297
305
  toCsvRow,
298
306
  writeAdsOutput,
299
307
  resolveCustomerId
300
308
  };
301
- //# sourceMappingURL=chunk-RFZYRG2Q.js.map
309
+ //# sourceMappingURL=chunk-UKGTWBKW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/error-handler.ts","../src/commands/ads/cache.ts","../src/commands/ads/output.ts"],"sourcesContent":["import { writeJsonEnvelope } from \"./commands/ads/output.ts\";\n\ntype ConnectionPlatform = \"google_ads\" | \"ga4\" | \"gsc\" | \"x_ads\" | \"meta_ads\" | \"linkedin_ads\";\n\n/**\n * Platforms the agent can get connected without the user leaving the chat, via\n * the blocking `request_connection` tool, and the id that tool expects. Every\n * platform this CLI can talk to is here — keep it in step with\n * `CONNECTION_PLATFORMS` in `packages/bridge/src/hono/connection-input.ts`: a\n * platform the tool accepts but this map omits sends the user to Settings for a\n * door the agent could have opened itself.\n */\nconst REQUEST_CONNECTION_PLATFORM: Record<ConnectionPlatform, string> = {\n google_ads: \"google-ads\",\n ga4: \"google-analytics\",\n gsc: \"google-search-console\",\n linkedin_ads: \"linkedin\",\n meta_ads: \"meta\",\n x_ads: \"x-ads\",\n};\n\nconst PLATFORM_NAME: Record<ConnectionPlatform, string> = {\n google_ads: \"Google Ads\",\n ga4: \"Google Analytics 4\",\n gsc: \"Google Search Console\",\n x_ads: \"X Ads\",\n meta_ads: \"Meta Ads\",\n linkedin_ads: \"LinkedIn Ads\",\n};\n\n/**\n * How to tell the agent to get a platform connected. Never \"send them to\n * Settings\" for a platform `request_connection` covers — that ends the turn on a\n * wall the agent could have opened itself, and it now covers every platform this\n * CLI talks to. The fallback line matters too: if the `request_connection` call\n * fails, the agent must still be able to say where the connect button is instead\n * of stalling.\n */\nexport function connectionFixExplanation(platform: ConnectionPlatform): string {\n const name = PLATFORM_NAME[platform];\n const slug = REQUEST_CONNECTION_PLATFORM[platform];\n return (\n `${name} is not connected for this company (or no account has been picked). Don't send the user to Settings ` +\n `or end the turn here — call the \\`request_connection\\` tool with { platform: \"${slug}\", reason } and ` +\n `they connect and pick their accounts from inside the chat. If the \\`request_connection\\` call fails, say so ` +\n `plainly and give the exact route: dashboard → Brain → Integrations → Platforms → ${name}. Then carry on with the ` +\n `rest of the task and say what stays unavailable until it is connected.`\n );\n}\n\n/**\n * Shared error handler for \"No Connection\" scenarios.\n * Provides a structured error for AI agents with a suggested fix and alternative commands.\n */\nexport function handleConnectionError(platform: ConnectionPlatform, originalMessage?: string): never {\n const platformName = PLATFORM_NAME[platform];\n\n const alternative = {\n google_ads: \"keywords-for-site or research advertisers\",\n ga4: \"lighthouse for page performance\",\n gsc: \"keywords-for-site or research keyword-gap\",\n x_ads: \"research advertisers\",\n meta_ads: \"research advertisers or research keyword-ideas\",\n linkedin_ads: \"research advertisers (LinkedIn Ad Library) or research keyword-ideas\",\n }[platform];\n\n const code = {\n google_ads: \"NO_GOOGLE_ADS_CONNECTION\",\n ga4: \"NO_GA4_CONNECTION\",\n gsc: \"NO_GSC_CONNECTION\",\n x_ads: \"NO_X_ADS_CONNECTION\",\n meta_ads: \"NO_META_ADS_CONNECTION\",\n linkedin_ads: \"NO_LINKEDIN_ADS_CONNECTION\",\n }[platform];\n\n const envelope = {\n ok: false,\n error: {\n code,\n message: originalMessage || `No ${platformName} connection found for this company.`,\n fix: {\n action: \"request_connection\",\n explanation: `${connectionFixExplanation(platform)} (Optional fallback for estimated external insights: 'baker research ${alternative}'.)`,\n },\n retryable: false,\n },\n };\n\n writeJsonEnvelope(envelope);\n process.exit(1);\n}\n\n/**\n * Does this API failure mean \"the platform isn't connected\"? The read commands\n * surface the backend's own 404/403 verbatim, so the connect fix has to be\n * recognised from the code the client mapped it to plus the message.\n */\nexport function isNotConnectedError(code: string, message: string): boolean {\n if (code !== \"NOT_FOUND\" && code !== \"UNAUTHORIZED\" && code !== \"FORBIDDEN\") {\n return false;\n }\n // The last alternative is the write-path 403 (\"customer X is not in this\n // company's connected Google Ads accounts\") — a different sentence for the\n // same remedy: connect, and pick the account.\n return /no .* connection found|not connected|no .* (selected|picked)|is not in .* connected/i.test(message);\n}\n\n/**\n * Does this failure warrant the connect form? A dead token (401) always does —\n * whatever the wording, the remedy is to sign in again.\n *\n * A 404 or 403 only does when the backend says so. Both codes are overloaded:\n * 403 is also an asset-level refusal on a live connection (reconnecting cannot\n * grant it), and 404 is also how \"no staged op <ref> in this draft\" and a\n * mistyped `--chat` come back. Answering either with \"ask the user to connect\n * Meta Ads\" sends the agent to fix something that was never broken, and the\n * real missing-connection 404 says \"No <platform> connection found\" anyway.\n */\nexport function needsConnectionFix(code: string, message: string): boolean {\n if (code === \"UNAUTHORIZED\") {\n return true;\n }\n return isNotConnectedError(code, message);\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { CacheEntry } from \"./types.ts\";\n\nconst CACHE_DIR = join(homedir(), \".baker\", \"cache\", \"ads\");\n\nfunction ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n\nfunction hashKey(key: string): string {\n return createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 16);\n}\n\nfunction cachePath(category: string, key: string): string {\n const dir = join(CACHE_DIR, category);\n ensureDir(dir);\n return join(dir, `${hashKey(key)}.json`);\n}\n\nexport function cacheGet<T>(category: string, key: string): CacheEntry<T> | null {\n const path = cachePath(category, key);\n if (!existsSync(path)) {\n return null;\n }\n try {\n const raw = readFileSync(path, \"utf-8\");\n const entry = JSON.parse(raw) as CacheEntry<T>;\n if (entry.expiresAt < Date.now()) {\n rmSync(path, { force: true });\n return null;\n }\n return entry;\n } catch {\n rmSync(path, { force: true });\n return null;\n }\n}\n\nexport function cacheSet<T>(\n category: string,\n key: string,\n data: T,\n ttlMs: number,\n fields?: Record<string, string>,\n): void {\n const path = cachePath(category, key);\n const entry: CacheEntry<T> = {\n expiresAt: Date.now() + ttlMs,\n data,\n fields,\n };\n writeFileSync(path, JSON.stringify(entry), \"utf-8\");\n}\n\nconst HOUR = 60 * 60 * 1000;\nconst MINUTE = 60 * 1000;\n\nexport function getQueryTtl(query: string): number {\n const upper = query.toUpperCase();\n if (upper.includes(\"TODAY\")) {\n return 15 * MINUTE;\n }\n if (upper.includes(\"LAST_7_DAYS\") || upper.includes(\"LAST_14_DAYS\")) {\n return 1 * HOUR;\n }\n // Historical data (BETWEEN, LAST_30_DAYS, LAST_90_DAYS, etc.)\n return 6 * HOUR;\n}\n\nexport function buildQueryCacheKey(customerId: string, query: string): string {\n const today = new Date().toISOString().slice(0, 10);\n return `${customerId}:${today}:${query.trim().replace(/\\s+/g, \" \")}`;\n}\n\ninterface CachedAccount {\n id: string;\n access_type: \"direct\" | \"managed\";\n manager_id?: string;\n}\n\nexport function getManagerIdForCustomer(customerId: string): string | undefined {\n const cached = cacheGet<CachedAccount[]>(\"accounts\", \"list\");\n if (!cached) return undefined;\n const account = cached.data.find((a) => a.id === customerId);\n if (account?.access_type === \"managed\") return account.manager_id;\n return undefined;\n}\n","import { ApiError, apiGet } from \"../../client.ts\";\nimport { getEnv } from \"../../env.ts\";\nimport { handleConnectionError, isNotConnectedError } from \"../../error-handler.ts\";\nimport type { QueryContext } from \"../../geo-context.ts\";\nimport { cacheGet, cacheSet } from \"./cache.ts\";\nimport type { AdsErrorEnvelope, AdsFileSummary, AdsSuccessEnvelope } from \"./types.ts\";\n\ntype AdsOutput =\n | AdsSuccessEnvelope<unknown>\n | AdsErrorEnvelope\n | AdsFileSummary\n | { ok: true; data: unknown; query_context?: QueryContext; cached?: true; fields?: Record<string, string> }\n | { ok: false; error: { code: string; message: string } };\n\nexport function writeAdsJson(envelope: AdsOutput): void {\n process.stdout.write(`${JSON.stringify(envelope, null, 2)}\\n`);\n}\n\nexport function writeJsonEnvelope(envelope: Record<string, unknown>): void {\n process.stdout.write(`${JSON.stringify(envelope, null, 2)}\\n`);\n}\n\n/** The formats `writeAdsOutput` prints. */\nexport const TABLE_OUTPUT_FORMATS = [\"json\", \"csv\", \"jsonl\", \"md\"] as const;\n\n/** Why `--output <format>` cannot be printed, or null — rather than quietly falling back to JSON. */\nexport function outputFormatRefusal(format: string, supported: readonly string[]): string | null {\n if (supported.includes(format)) return null;\n const list =\n supported.length > 1 ? `${supported.slice(0, -1).join(\", \")} or ${supported.at(-1)}` : supported.join(\"\");\n return `--output ${format} is not supported — use ${list}`;\n}\n\nexport function toCsvRow(values: string[]): string {\n return values\n .map((v) => {\n if (v.includes(\",\") || v.includes('\"') || v.includes(\"\\n\")) {\n return `\"${v.replace(/\"/g, '\"\"')}\"`;\n }\n return v;\n })\n .join(\",\");\n}\n\nfunction flattenRow(row: Record<string, unknown>): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, val] of Object.entries(row)) {\n flat[key] = typeof val === \"object\" && val !== null ? JSON.stringify(val) : String(val ?? \"\");\n }\n return flat;\n}\n\n/**\n * One write per table: a line-by-line write piped into `head` hits a closed pipe on\n * the next line and crashes with EPIPE, where a single write lands in the pipe buffer.\n */\nfunction writeWhole(lines: string[]): void {\n process.stdout.write(`${lines.join(\"\\n\")}\\n`);\n}\n\nfunction writeAdsCsv(data: Array<Record<string, unknown>>, fields?: string[]): void {\n if (data.length === 0) return;\n const cols = fields ?? Object.keys(data[0] ?? {});\n const lines = [toCsvRow(cols)];\n for (const row of data) {\n const flat = flattenRow(row);\n lines.push(toCsvRow(cols.map((f) => flat[f] ?? \"\")));\n }\n writeWhole(lines);\n}\n\nfunction writeAdsJsonl(data: Array<Record<string, unknown>>): void {\n if (data.length === 0) return;\n writeWhole(data.map((row) => JSON.stringify(row)));\n}\n\n/** A `|` or line break in a name would split its cell and shift every later column. */\nfunction mdCell(value: string): string {\n return value.replace(/\\|/g, \"\\\\|\").replace(/\\r?\\n/g, \" \");\n}\n\nfunction writeAdsMd(data: Array<Record<string, unknown>>, fields?: string[]): void {\n if (data.length === 0) return;\n const cols = fields ?? Object.keys(data[0] ?? {});\n const lines = [`| ${cols.join(\" | \")} |`, `| ${cols.map(() => \"---\").join(\" | \")} |`];\n for (const row of data) {\n const flat = flattenRow(row);\n lines.push(`| ${cols.map((f) => mdCell(flat[f] ?? \"\")).join(\" | \")} |`);\n }\n writeWhole(lines);\n}\n\n/**\n * `hints` ride in the JSON envelope; csv/jsonl/md stdout has no room for them,\n * and an empty result is exactly when the hint matters, so they go to stderr.\n */\nexport function writeAdsOutput(\n data: Array<Record<string, unknown>>,\n format: string,\n fields?: string[],\n hints: string[] = [],\n): void {\n switch (format) {\n case \"csv\":\n writeAdsCsv(data, fields);\n break;\n case \"jsonl\":\n writeAdsJsonl(data);\n break;\n case \"md\":\n writeAdsMd(data, fields);\n break;\n default:\n writeJsonEnvelope(hints.length > 0 ? { ok: true, data, hints } : { ok: true, data });\n return;\n }\n for (const hint of hints) process.stderr.write(`${hint}\\n`);\n}\n\ninterface AccountInfo {\n id: string;\n name: string;\n access_type: \"direct\" | \"managed\";\n level: number;\n manager_id?: string;\n}\n\nasync function fetchAccounts(useCache = true): Promise<AccountInfo[]> {\n if (useCache) {\n const cached = cacheGet<AccountInfo[]>(\"accounts\", \"list\");\n if (cached) return cached.data;\n }\n const params = !useCache ? { \"skip-cache\": \"true\" } : undefined;\n const data = await apiGet<AccountInfo[]>(\"/api/ads/google/accounts\", params);\n if (useCache) {\n cacheSet(\"accounts\", \"list\", data, 60 * 60 * 1000);\n }\n return data;\n}\n\nexport async function resolveCustomerId(args: Record<string, unknown>): Promise<string> {\n const fromArgs = args[\"customer-id\"] as string | undefined;\n const customerId = fromArgs || getEnv().BAKER_GOOGLE_ADS_CUSTOMER_ID;\n\n if (customerId) {\n if (!/^\\d{10}$/.test(customerId)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_CUSTOMER_ID\",\n message:\n \"Customer ID must be exactly 10 digits without dashes. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID.\",\n },\n });\n process.exit(1);\n }\n return customerId;\n }\n\n const useCache = !args[\"no-cache\"];\n\n try {\n const accounts = await fetchAccounts(useCache);\n const [single] = accounts;\n if (accounts.length === 1 && single) {\n process.stderr.write(`Using account \"${single.name}\" (${single.id})\\n`);\n return single.id;\n }\n if (accounts.length === 0) {\n handleConnectionError(\"google_ads\");\n }\n const list = accounts.map((a) => ` ${a.id} ${a.name}`).join(\"\\n\");\n writeAdsJson({\n ok: false,\n error: {\n code: \"MULTIPLE_ACCOUNTS\",\n message: `Multiple accounts found. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID:\\n${list}`,\n },\n });\n process.exit(1);\n } catch (err) {\n // \"No connection\" reaches here as a 404/403 from the accounts lookup. Reporting it as\n // an unusable customer id sends the agent hunting for an id that cannot exist yet.\n if (err instanceof ApiError && isNotConnectedError(err.code, err.message)) {\n handleConnectionError(\"google_ads\", err.message);\n }\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_CUSTOMER_ID\",\n message: \"Could not auto-detect account. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID.\",\n },\n });\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}
@@ -1316,6 +1316,15 @@ function debugLogSetting() {
1316
1316
  const raw = process.env.BAKER_DEBUG_LOG?.trim();
1317
1317
  return raw ? raw : void 0;
1318
1318
  }
1319
+ var MissingChatIdError = class extends Error {
1320
+ code = "MISSING_CHAT_ID";
1321
+ constructor() {
1322
+ super(
1323
+ "BAKER_CHAT_ID is not set. This command stages changes against a chat \u2014 run it from a chat-attached environment."
1324
+ );
1325
+ this.name = "MissingChatIdError";
1326
+ }
1327
+ };
1319
1328
  function canvasAssetOriginsSetting() {
1320
1329
  const raw = process.env.BAKER_CANVAS_ASSET_ORIGINS?.trim();
1321
1330
  return raw ? raw : void 0;
@@ -1328,9 +1337,7 @@ function optionalConnection() {
1328
1337
  function requireChatId() {
1329
1338
  const env = getEnv();
1330
1339
  if (!env.BAKER_CHAT_ID) {
1331
- throw new Error(
1332
- "BAKER_CHAT_ID is not set. This command stages changes against a chat \u2014 run it from a chat-attached environment."
1333
- );
1340
+ throw new MissingChatIdError();
1334
1341
  }
1335
1342
  return env.BAKER_CHAT_ID;
1336
1343
  }
@@ -1402,6 +1409,7 @@ export {
1402
1409
  requireConnection,
1403
1410
  optionalChatId,
1404
1411
  debugLogSetting,
1412
+ MissingChatIdError,
1405
1413
  canvasAssetOriginsSetting,
1406
1414
  optionalConnection,
1407
1415
  requireChatId,
@@ -1415,4 +1423,4 @@ export {
1415
1423
  captureVimeoCookie,
1416
1424
  childEnvWith
1417
1425
  };
1418
- //# sourceMappingURL=chunk-FQOKJYKN.js.map
1426
+ //# sourceMappingURL=chunk-WJIYNKLI.js.map