@koda-sl/baker-cli 0.373.0 → 0.375.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 CHANGED
@@ -1306,6 +1306,9 @@ baker analytics map --platform google --set kw=keyword # name a parameter, se
1306
1306
  baker analytics map --platform google --remove kw # take that answer back
1307
1307
  baker analytics conversions # what counts, what each produced, and which Forms count nothing
1308
1308
  baker analytics conversions --candidates --output md # everything these pages do, and which of it is counted, as tables
1309
+ baker analytics conversions --candidates # the same as compact JSON: one line per page event, Form keys folded onto `flows`
1310
+ baker analytics conversions --candidates --flow contact # only that Form's event keys, one line each
1311
+ baker analytics conversions --candidates --full --output json # every field of every candidate, Form keys unfolded
1309
1312
  baker analytics conversions --flow contact # one Form's endings as event keys, traffic or not
1310
1313
  baker analytics conversions --event page:request_demo --name "Demo requested"
1311
1314
  baker analytics conversions --event page:a --event submit:b --name "Lead" # both, as one number
@@ -1357,7 +1360,7 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1357
1360
 
1358
1361
  **Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did. A value that is not a whole positive number (`--page /pricing`) is refused before any request, naming `--landing` as the flag that narrows a report to one page of the site.
1359
1362
 
1360
- **A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
1363
+ **A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. The JSON is compact by default: each page or system event is one line (`eventKey`, `events`, `sessions`, `countedAs` when counted), and a Form's keys fold onto its `flows` row as `keys` fired and `events` — dropping that row's empty `counted` and `noConversion` — or onto a `formCandidates` row for a Form not in `flows`. `--candidates --flow <slug>` narrows the list to that Form's keys, and `--full` returns every field (`origin`, `nodeType`, `flowSlug`, `lastSeen`, `sources`, `doors`) of every key, unfolded. `--output md` always folds a Form's keys into one row per Form. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
1361
1364
 
1362
1365
  **The name is the grouping, so the CLI defends it.** Repeat `--event` to count several events under one `--name` in a single call; `--rename "<old>" --to "<new>"` renames every event under an outcome, and renaming *onto* a name that already exists **merges** them into one row and one number — the fix for a company whose Forms each invented their own word for the same thing. `--remove-name` drops an outcome entirely, `--remove` a single event key. A name that differs from one already counted only by case, accent, punctuation or a plural is **refused**: Baker groups by exact name, so "Booked a Call" beside "Booked a call" is a number split in half with both halves plausible. Every answer leads with the names already in use, and a company that has named nothing gets a standard set to start from — after which the rule is reuse, in that company's own language.
1363
1366
 
@@ -3678,7 +3681,7 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3678
3681
  **`meetings list` flags:** `--search <text>`, `--redirecting-only`.
3679
3682
  **`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.
3680
3683
 
3681
- **`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.
3684
+ **`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 → Platforms. The error carries a `fix` describing exactly that.
3682
3685
 
3683
3686
  **`--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).
3684
3687
 
@@ -3697,7 +3700,7 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3697
3700
  }
3698
3701
  ```
3699
3702
 
3700
- No write surface against HubSpot itself — a form's fields, redirect, or consent text are changed inside HubSpot. Wiring one into a Form step is done with `--as-node` (above), not `request_flow_input`; the approval form is only needed when no account is connected or the right form is ambiguous. When no HubSpot account is connected, every command exits 1 with `NO_HUBSPOT_CONNECTION` and a `fix` telling the agent to route the user to **Brain → Integrations → Tools**.
3703
+ No write surface against HubSpot itself — a form's fields, redirect, or consent text are changed inside HubSpot. Wiring one into a Form step is done with `--as-node` (above), not `request_flow_input`; the approval form is only needed when no account is connected or the right form is ambiguous. When no HubSpot account is connected, every command exits 1 with `NO_HUBSPOT_CONNECTION` and a `fix` telling the agent to route the user to **Brain → Integrations → Platforms**.
3701
3704
 
3702
3705
  `meetings list` reports `redirectKnown: false` when HubSpot omitted the calendar's booking settings; `redirectUrl: null` alongside it means *unknown*, not "no redirect". `--redirecting-only` deliberately keeps those rows rather than answering "none" from missing data.
3703
3706
 
@@ -26,7 +26,7 @@ var PLATFORM_NAME = {
26
26
  function connectionFixExplanation(platform) {
27
27
  const name = PLATFORM_NAME[platform];
28
28
  const slug = REQUEST_CONNECTION_PLATFORM[platform];
29
- return `${name} is not connected for this company (or no account has been picked). Don't send the user to Settings or end the turn here \u2014 call the \`request_connection\` tool with { platform: "${slug}", reason } and they connect and pick their accounts from inside the chat. If the \`request_connection\` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Tools \u2192 ${name}. Then carry on with the rest of the task and say what stays unavailable until it is connected.`;
29
+ return `${name} is not connected for this company (or no account has been picked). Don't send the user to Settings or end the turn here \u2014 call the \`request_connection\` tool with { platform: "${slug}", reason } and they connect and pick their accounts from inside the chat. If the \`request_connection\` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Platforms \u2192 ${name}. Then carry on with the rest of the task and say what stays unavailable until it is connected.`;
30
30
  }
31
31
  function handleConnectionError(platform, originalMessage) {
32
32
  const platformName = PLATFORM_NAME[platform];
@@ -297,4 +297,4 @@ export {
297
297
  writeAdsOutput,
298
298
  resolveCustomerId
299
299
  };
300
- //# sourceMappingURL=chunk-ZI7TPBXB.js.map
300
+ //# sourceMappingURL=chunk-BJP4G3X4.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\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\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 process.stdout.write(`${toCsvRow(cols)}\\n`);\n for (const row of data) {\n const flat = flattenRow(row);\n process.stdout.write(`${toCsvRow(cols.map((f) => flat[f] ?? \"\"))}\\n`);\n }\n}\n\nfunction writeAdsJsonl(data: Array<Record<string, unknown>>): void {\n for (const row of data) {\n process.stdout.write(`${JSON.stringify(row)}\\n`);\n }\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 process.stdout.write(`| ${cols.join(\" | \")} |\\n`);\n process.stdout.write(`| ${cols.map(() => \"---\").join(\" | \")} |\\n`);\n for (const row of data) {\n const flat = flattenRow(row);\n process.stdout.write(`| ${cols.map((f) => flat[f] ?? \"\").join(\" | \")} |\\n`);\n }\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":[]}
@@ -2,7 +2,7 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-ZI7TPBXB.js";
5
+ } from "./chunk-BJP4G3X4.js";
6
6
  import {
7
7
  ApiError
8
8
  } from "./chunk-MF7YA62W.js";
@@ -120,4 +120,4 @@ export {
120
120
  resolveEffectiveStatus,
121
121
  statusFilterHints
122
122
  };
123
- //# sourceMappingURL=chunk-4ACU7GBN.js.map
123
+ //# sourceMappingURL=chunk-N3H66QUV.js.map
package/dist/cli.js CHANGED
@@ -80,7 +80,7 @@ import {
80
80
  resolveEffectiveStatus,
81
81
  statusFilterHints,
82
82
  todayIso
83
- } from "./chunk-4ACU7GBN.js";
83
+ } from "./chunk-N3H66QUV.js";
84
84
  import {
85
85
  buildQueryCacheKey,
86
86
  cacheGet,
@@ -96,7 +96,7 @@ import {
96
96
  writeAdsJson,
97
97
  writeAdsOutput,
98
98
  writeJsonEnvelope
99
- } from "./chunk-ZI7TPBXB.js";
99
+ } from "./chunk-BJP4G3X4.js";
100
100
  import {
101
101
  ApiError,
102
102
  apiGet,
@@ -9080,17 +9080,17 @@ function text(value) {
9080
9080
  return typeof value === "string" ? value : "";
9081
9081
  }
9082
9082
  function draftOpNormalizer(record, full) {
9083
- const compact3 = {
9083
+ const compact4 = {
9084
9084
  kind: text(record.kind),
9085
9085
  name: text(record.targetName),
9086
9086
  ref: text(record.tempId) || text(record.targetActionId),
9087
9087
  tags: Array.isArray(record.tags) ? record.tags.join(",") : ""
9088
9088
  };
9089
9089
  if (!full) {
9090
- return compact3;
9090
+ return compact4;
9091
9091
  }
9092
9092
  return {
9093
- ...compact3,
9093
+ ...compact4,
9094
9094
  description: text(record.description),
9095
9095
  note: text(record.note),
9096
9096
  reason: text(record.reason),
@@ -9365,10 +9365,10 @@ var listCommand2 = defineCommand8({
9365
9365
  process.stdout.write(renderMarkdown(response.data, hints2, { full: args5.full === true }));
9366
9366
  return;
9367
9367
  }
9368
- const compact3 = !args5.full && response.ok;
9369
- const shaped = compact3 ? { ...response, data: compactListData(response.data) } : response;
9368
+ const compact4 = !args5.full && response.ok;
9369
+ const shaped = compact4 ? { ...response, data: compactListData(response.data) } : response;
9370
9370
  const envelope = hints2.length > 0 ? { ...shaped, hints: hints2 } : shaped;
9371
- if (compact3) {
9371
+ if (compact4) {
9372
9372
  process.stdout.write(`${JSON.stringify(envelope)}
9373
9373
  `);
9374
9374
  return;
@@ -14759,8 +14759,8 @@ function normalizePhoneV1(raw) {
14759
14759
  var PHONE_PUNCTUATION = /[\s()./-]/g;
14760
14760
  var E164 = /^\+[1-9]\d{7,14}$/;
14761
14761
  function normalizePhoneV2(raw) {
14762
- const compact3 = raw.replace(PHONE_PUNCTUATION, "");
14763
- const e164 = compact3.startsWith("00") ? `+${compact3.slice(2)}` : compact3;
14762
+ const compact4 = raw.replace(PHONE_PUNCTUATION, "");
14763
+ const e164 = compact4.startsWith("00") ? `+${compact4.slice(2)}` : compact4;
14764
14764
  return E164.test(e164) ? e164 : null;
14765
14765
  }
14766
14766
 
@@ -26620,8 +26620,8 @@ Example: baker ads meta ads retag 120252158038400247 --url-tags "utm_source={{si
26620
26620
  run: async ({ args: args5 }) => {
26621
26621
  const { apiPost: apiPost2 } = await import("./client-GR6YL3YX.js");
26622
26622
  const { requireChatId: requireChatId2 } = await import("./env-4P5HOGNA.js");
26623
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-KMEPFOFT.js");
26624
- const { handleMetaError: handleMetaError2 } = await import("./shared-26NWGQ5G.js");
26623
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-AFWVXYWA.js");
26624
+ const { handleMetaError: handleMetaError2 } = await import("./shared-SDKT2Y7Z.js");
26625
26625
  try {
26626
26626
  const accountId = bareAccountId2(args5);
26627
26627
  const urlTags = typeof args5["url-tags"] === "string" ? args5["url-tags"] : void 0;
@@ -26768,8 +26768,8 @@ function duplicateCommand2(entity, label) {
26768
26768
  run: async ({ args: args5 }) => {
26769
26769
  const { apiPost: apiPost2 } = await import("./client-GR6YL3YX.js");
26770
26770
  const { requireChatId: requireChatId2 } = await import("./env-4P5HOGNA.js");
26771
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-KMEPFOFT.js");
26772
- const { handleMetaError: handleMetaError2 } = await import("./shared-26NWGQ5G.js");
26771
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-AFWVXYWA.js");
26772
+ const { handleMetaError: handleMetaError2 } = await import("./shared-SDKT2Y7Z.js");
26773
26773
  try {
26774
26774
  const accountId = bareAccountId2(args5);
26775
26775
  const chatId = requireChatId2();
@@ -31887,6 +31887,7 @@ function parseConversionsArgs(raw, argv) {
31887
31887
  removeName: onlyOnce(raw["remove-name"], "--remove-name", argv),
31888
31888
  flowSlug: onlyOnce(raw.flow, "--flow", argv),
31889
31889
  candidates: raw.candidates === true,
31890
+ full: raw.full === true,
31890
31891
  days: readDays(raw.days)
31891
31892
  };
31892
31893
  checkFlagShape(args5);
@@ -31953,6 +31954,72 @@ function conversionsWrite(args5, existing) {
31953
31954
  return { ...set.length > 0 ? { set } : {}, ...remove.length > 0 ? { remove } : {} };
31954
31955
  }
31955
31956
 
31957
+ // src/commands/analytics/conversionCandidates.ts
31958
+ function compact2(row) {
31959
+ return {
31960
+ eventKey: row.eventKey,
31961
+ events: row.events,
31962
+ sessions: row.sessions,
31963
+ ...row.isCounted ? { countedAs: row.countedAs } : {}
31964
+ };
31965
+ }
31966
+ function foldByForm(rows) {
31967
+ const byForm = /* @__PURE__ */ new Map();
31968
+ for (const row of rows) {
31969
+ const form = byForm.get(row.flowSlug) ?? { form: row.flowSlug, keys: 0, countedKeys: 0, events: 0 };
31970
+ form.keys += 1;
31971
+ if (row.isCounted) form.countedKeys += 1;
31972
+ form.events += row.events;
31973
+ byForm.set(row.flowSlug, form);
31974
+ }
31975
+ return [...byForm.values()];
31976
+ }
31977
+ function candidatesForFlow(catalogue, flowSlug) {
31978
+ return flowSlug === void 0 ? [...catalogue] : catalogue.filter((row) => row.flowSlug === flowSlug);
31979
+ }
31980
+ var FOLDED_FORMS_HINT = "A Form's candidates are folded into its `flows` row (`keys` fired, `events`; no `counted` means it counts nothing), or a `formCandidates` row when not listed. `--candidates --flow <slug>` lists that Form's keys; `--full` adds every field of every key.";
31981
+ function candidatesView(rows, options) {
31982
+ if (options.full) return { candidates: rows };
31983
+ if (options.flowSlug !== void 0) return { candidates: rows.map(compact2) };
31984
+ const forms = foldByForm(rows.filter((row) => row.flowSlug !== ""));
31985
+ return {
31986
+ candidates: rows.filter((row) => row.flowSlug === "").map(compact2),
31987
+ ...forms.length > 0 ? { formCandidates: forms } : {}
31988
+ };
31989
+ }
31990
+ function fullFlagHints(args5, format) {
31991
+ if (!args5.full || args5.candidates && format !== "md") return [];
31992
+ return ["`--full` only applies to `--candidates --output json`, so it changed nothing here."];
31993
+ }
31994
+ function compactConversionsEnvelope(envelope, options) {
31995
+ const { candidates, flows, ...data } = envelope.data;
31996
+ if (candidates === void 0) return { ...envelope };
31997
+ const { formCandidates, ...view } = candidatesView(candidates, options);
31998
+ const traffic = new Map(formCandidates?.map((form) => [form.form, form]));
31999
+ const merged = flows?.map(({ slug, counted, noConversion }) => {
32000
+ const form = traffic.get(slug);
32001
+ traffic.delete(slug);
32002
+ return {
32003
+ slug,
32004
+ ...counted.length > 0 ? { counted } : {},
32005
+ ...noConversion !== null ? { noConversion } : {},
32006
+ keys: form?.keys ?? 0,
32007
+ events: form?.events ?? 0
32008
+ };
32009
+ });
32010
+ const hints2 = [...envelope.hints ?? [], ...formCandidates ? [FOLDED_FORMS_HINT] : []];
32011
+ return {
32012
+ ...envelope,
32013
+ data: {
32014
+ ...data,
32015
+ ...merged ? { flows: merged } : {},
32016
+ ...view,
32017
+ ...traffic.size > 0 ? { formCandidates: [...traffic.values()] } : {}
32018
+ },
32019
+ ...hints2.length > 0 ? { hints: hints2 } : {}
32020
+ };
32021
+ }
32022
+
31956
32023
  // src/commands/analytics/conversionHints.ts
31957
32024
  var NAMED_CANDIDATES = 3;
31958
32025
  function conversionHints(definitions, candidates, askedForCandidates) {
@@ -32278,7 +32345,7 @@ function trafficByForm(candidates) {
32278
32345
  return byForm;
32279
32346
  }
32280
32347
  var NO_TRAFFIC = { "endings seen": 0, "endings counted": 0, events: 0, lastSeen: "" };
32281
- var FORM_KEYS_HINT = "A Form's candidates are one row per Form: `baker analytics conversions --flow <slug>` lists that Form's event keys, and `--candidates --output json` lists every key.";
32348
+ var FORM_KEYS_HINT = "A Form's candidates are one row per Form: `baker analytics conversions --flow <slug>` lists that Form's event keys, and `--candidates --full --output json` lists every key with every field.";
32282
32349
  function renderConversionsMarkdown(envelope) {
32283
32350
  const { definitions, flow, flows, candidates } = envelope.data;
32284
32351
  const data = {
@@ -32315,7 +32382,7 @@ function renderConversionsMarkdown(envelope) {
32315
32382
  return renderMarkdown3({ data, ...hints2.length > 0 ? { hints: hints2 } : {} });
32316
32383
  }
32317
32384
  function withFormatHint(envelope, format) {
32318
- const hints2 = [...envelope.hints ?? [], ...formatHint(envelope.data, format)];
32385
+ const hints2 = [...envelope.hints ?? [], ...formatHint({ ...envelope.data }, format)];
32319
32386
  return { ...envelope, ...hints2.length > 0 ? { hints: hints2 } : {} };
32320
32387
  }
32321
32388
 
@@ -33380,7 +33447,7 @@ var sourcesResponseSchema = z41.strictObject({
33380
33447
  import { defineCommand as defineCommand98 } from "citty";
33381
33448
 
33382
33449
  // src/commands/hubspot/refusal.ts
33383
- var CONTINUE = "Keep going with the wider task: say which part you could not read, skip it, and finish everything else. If the `request_connection` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Tools \u2192 HubSpot.";
33450
+ var CONTINUE = "Keep going with the wider task: say which part you could not read, skip it, and finish everything else. If the `request_connection` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Platforms \u2192 HubSpot.";
33384
33451
  function hubspotRefusal(code, message) {
33385
33452
  if (code === "NOT_FOUND" && message.includes(HUBSPOT_NOT_CONNECTED_MESSAGE)) {
33386
33453
  return {
@@ -34473,13 +34540,13 @@ function readFlowConversions2(slug, definitions) {
34473
34540
  endings: joinFlowConversions(flowEventKeys(tree, slug), definitions)
34474
34541
  };
34475
34542
  }
34476
- function conversionsEnvelope(args5, definitions, candidates, serverHints = []) {
34543
+ function conversionsEnvelope(args5, definitions, candidates, leadingHints = []) {
34477
34544
  const flow = args5.flowSlug === void 0 ? null : readFlowConversions2(args5.flowSlug, definitions);
34478
34545
  const forms = flow === null ? readWorkspaceFlows() : [];
34479
34546
  const coverage = flowCoverage(forms, definitions);
34480
34547
  const page = pageCandidates(definitions, candidates, args5.candidates);
34481
34548
  const hints2 = [
34482
- ...serverHints,
34549
+ ...leadingHints,
34483
34550
  ...flow ? flowConversionHints(flow.slug, flow.endings, flow.noConversionReason) : [],
34484
34551
  ...flowCoverageHints(coverage),
34485
34552
  ...stagedHints(definitions),
@@ -34492,7 +34559,7 @@ function conversionsEnvelope(args5, definitions, candidates, serverHints = []) {
34492
34559
  definitions,
34493
34560
  ...flow ? { flow } : {},
34494
34561
  ...coverage.length > 0 ? { flows: coverage } : {},
34495
- ...args5.candidates ? { candidates: page.candidates } : {}
34562
+ ...args5.candidates ? { candidates: candidatesForFlow(page.candidates, args5.flowSlug) } : {}
34496
34563
  },
34497
34564
  ...hints2.length > 0 ? { hints: hints2 } : {}
34498
34565
  };
@@ -34536,12 +34603,17 @@ var conversionsCommand3 = (() => {
34536
34603
  },
34537
34604
  candidates: {
34538
34605
  type: "boolean",
34539
- description: "Also list every event these pages actually produced, ranked by volume, with whether it is already counted. This is where an event key comes from \u2014 do not invent one. Off by default because a busy company produces hundreds of rows",
34606
+ description: "Also list every event these pages actually produced, ranked by volume, with whether it is already counted. This is where an event key comes from \u2014 do not invent one. Compact by default in JSON: each page or system event is one line (eventKey, events, sessions, countedAs when counted) and a Form's keys fold onto its `flows` row as `keys` and `events` (or a `formCandidates` row for a Form not in `flows`). Add --flow <slug> for that Form's keys only, and --full --output json for every field of every row",
34607
+ required: false
34608
+ },
34609
+ full: {
34610
+ type: "boolean",
34611
+ description: "With --candidates: every field of every candidate (origin, nodeType, flowSlug, lastSeen, sources, doors) and every Form key unfolded. Large on a busy company \u2014 narrow it with --flow first",
34540
34612
  required: false
34541
34613
  },
34542
34614
  flow: {
34543
34615
  type: "string",
34544
- description: "A Form slug. Lists that Form's own endings already spelled as event keys, each with the name Baker counts it under or nothing \u2014 read off the Form's file, so it works on a Form built minutes ago that has never been visited, which is exactly when --candidates is empty. Use it whenever you create or edit a Form: it is the only way to see that the Form counts nothing before it is published counting nothing",
34616
+ description: "A Form slug. Lists that Form's own endings already spelled as event keys, each with the name Baker counts it under or nothing \u2014 read off the Form's file, so it works on a Form built minutes ago that has never been visited, which is exactly when --candidates is empty. With --candidates, narrows the candidates to that Form's keys. Use it whenever you create or edit a Form: it is the only way to see that the Form counts nothing before it is published counting nothing",
34545
34617
  required: false
34546
34618
  },
34547
34619
  days: {
@@ -34559,7 +34631,7 @@ var conversionsCommand3 = (() => {
34559
34631
  return defineCommand99({
34560
34632
  meta: {
34561
34633
  name: "conversions",
34562
- description: "What this company counts as a conversion \u2014 the numerator of every conversion rate on Measure. A page event (a Form ending, a data-baker-* event) and an event one of the client's systems sends (a HubSpot event made with `baker analytics sources --create-hubspot`, an order the server posts) both count; a system's event is credited to the last landing its person viewed. A HubSpot event is `page:<its name>` here \u2014 `--event 'page:Deal won' --name 'Deal won'`. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. Inside a chat a change is STAGED: the person sees it under Sources in the chat's changes, on the event's own row and it applies when they publish, retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers. A read shows a proposal marked `staged`. Call an event by its name when you talk to the person, never by its key. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
34634
+ description: "What this company counts as a conversion \u2014 the numerator of every conversion rate on Measure. A page event (a Form ending, a data-baker-* event) and an event one of the client's systems sends (a HubSpot event made with `baker analytics sources --create-hubspot`, an order the server posts) both count; a system's event is credited to the last landing its person viewed. A HubSpot event is `page:<its name>` here \u2014 `--event 'page:Deal won' --name 'Deal won'`. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. It is compact: a Form's keys fold onto its `flows` row, `--candidates --flow <slug>` lists one Form's keys, and `--full --output json` returns every field of every row. Then `--event <key> --name 'Booked a call'` to count it. Inside a chat a change is STAGED: the person sees it under Sources in the chat's changes, on the event's own row and it applies when they publish, retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers. A read shows a proposal marked `staged`. Call an event by its name when you talk to the person, never by its key. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
34563
34635
  },
34564
34636
  args: args5,
34565
34637
  run: async ({ args: raw }) => {
@@ -34579,10 +34651,10 @@ var conversionsCommand3 = (() => {
34579
34651
  args6,
34580
34652
  mergeStoredWithCounts(stored, report.data.conversions ?? []),
34581
34653
  report.data.eventCatalog ?? [],
34582
- answer.hints ?? []
34654
+ [...answer.hints ?? [], ...fullFlagHints(args6, format)]
34583
34655
  );
34584
34656
  if (format === "md") process.stdout.write(renderConversionsMarkdown(envelope));
34585
- else writeJsonEnvelope(withFormatHint(envelope, format));
34657
+ else writeJsonEnvelope(withFormatHint(compactConversionsEnvelope(envelope, args6), format));
34586
34658
  } catch (err) {
34587
34659
  if (err instanceof ConversionArgsError) {
34588
34660
  handleError3(new ApiError("VALIDATION_ERROR", err.message));
@@ -51139,12 +51211,12 @@ function handleError5(err) {
51139
51211
  ...readOnly ? {
51140
51212
  fix: {
51141
51213
  action: "reconnect",
51142
- explanation: "The Google Analytics connection is read-only. Ask the user to reconnect Google Analytics \u2014 in the dashboard, Brain \u2192 Integrations \u2192 Tools \u2192 Google Analytics \u2014 which grants the permission to change the property. Reporting (`baker ga4 query`, `baker ga4 audit`) keeps working meanwhile, so carry on with the rest of the job and tell them what you will set up once they have reconnected. Don't claim anything was configured."
51214
+ explanation: "The Google Analytics connection is read-only. Ask the user to reconnect Google Analytics \u2014 in the dashboard, Brain \u2192 Integrations \u2192 Platforms \u2192 Google Analytics \u2014 which grants the permission to change the property. Reporting (`baker ga4 query`, `baker ga4 audit`) keeps working meanwhile, so carry on with the rest of the job and tell them what you will set up once they have reconnected. Don't claim anything was configured."
51143
51215
  }
51144
51216
  } : err.code === "UNAUTHORIZED" || err.code === "FORBIDDEN" && isNotConnectedError(err.code, err.message) ? {
51145
51217
  fix: {
51146
51218
  action: "request_connection",
51147
- explanation: 'Google Analytics is not connected for this company yet, or no property has been picked. Don\'t send the user to Settings \u2014 call the `request_connection` tool with { platform: "google-analytics", reason } and they connect and pick their property from inside the chat. If the `request_connection` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Tools \u2192 Google Analytics. Then carry on with the rest of the task.'
51219
+ explanation: 'Google Analytics is not connected for this company yet, or no property has been picked. Don\'t send the user to Settings \u2014 call the `request_connection` tool with { platform: "google-analytics", reason } and they connect and pick their property from inside the chat. If the `request_connection` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Platforms \u2192 Google Analytics. Then carry on with the rest of the task.'
51148
51220
  }
51149
51221
  } : {},
51150
51222
  retryable: RETRYABLE_CODES.has(err.code)
@@ -54495,13 +54567,13 @@ function compactHit(hit) {
54495
54567
  };
54496
54568
  }
54497
54569
  function providerHitNormalizer(record, full) {
54498
- const compact3 = compactHit(record);
54570
+ const compact4 = compactHit(record);
54499
54571
  if (!full) {
54500
- return compact3;
54572
+ return compact4;
54501
54573
  }
54502
54574
  const group = asRecord2(record.group);
54503
54575
  return {
54504
- ...compact3,
54576
+ ...compact4,
54505
54577
  externalUrl: str(record.externalUrl),
54506
54578
  externalId: str(record.externalId),
54507
54579
  description: str(record.description),
@@ -55263,7 +55335,7 @@ registerSchema({
55263
55335
  }
55264
55336
  });
55265
55337
  var POLL_INTERVAL_MS3 = 4e3;
55266
- function compact2(doc) {
55338
+ function compact3(doc) {
55267
55339
  return {
55268
55340
  layeredImageId: doc.layeredImageId,
55269
55341
  imageId: doc.imageId,
@@ -55353,7 +55425,7 @@ var layerizeCommand = defineCommand157({
55353
55425
  });
55354
55426
  process.exit(1);
55355
55427
  }
55356
- writeJson({ ok: true, data: full ? settled : compact2(settled), hints: hintsFor3(settled) });
55428
+ writeJson({ ok: true, data: full ? settled : compact3(settled), hints: hintsFor3(settled) });
55357
55429
  } catch (err) {
55358
55430
  if (err instanceof ApiError) {
55359
55431
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
@@ -55392,7 +55464,7 @@ var layersCommand = defineCommand157({
55392
55464
  process.exit(1);
55393
55465
  }
55394
55466
  const doc = await apiGet("/api/images/layers", { imageId });
55395
- writeJson({ ok: true, data: args5.full ? doc : compact2(doc), hints: hintsFor3(doc) });
55467
+ writeJson({ ok: true, data: args5.full ? doc : compact3(doc), hints: hintsFor3(doc) });
55396
55468
  } catch (err) {
55397
55469
  if (err instanceof ApiError) {
55398
55470
  writeJson({
@@ -62249,7 +62321,7 @@ function customEntry(server) {
62249
62321
  ...failed && server.health?.suggestedUrl ? { suggestedAddress: server.health.suggestedUrl } : {}
62250
62322
  };
62251
62323
  }
62252
- var BROKEN_SERVER_HINT = "A server marked usable=false will NOT answer: its tools are absent from this turn. Say plainly that the tool is not working and relay `problem`; never claim its tools are loading, coming next message, or temporarily unavailable. `problem` names what a person has to fix \u2014 the address, the sign-in, or the key the server was added with \u2014 so pass on that fix and no other: never tell the user to reconnect a server whose `problem` is its address or its key. The user makes the fix in the dashboard (Brain \u2192 Integrations \u2192 MCPs). If `suggestedAddress` is present, that is the address to use.";
62324
+ var BROKEN_SERVER_HINT = "A server marked usable=false will NOT answer: its tools are absent from this turn. Say plainly that the tool is not working and relay `problem`; never claim its tools are loading, coming next message, or temporarily unavailable. `problem` names what a person has to fix \u2014 the address, the sign-in, or the key the server was added with \u2014 so pass on that fix and no other: never tell the user to reconnect a server whose `problem` is its address or its key. The user makes the fix in the dashboard (Brain \u2192 Integrations \u2192 More apps). If `suggestedAddress` is present, that is the address to use.";
62253
62325
  var LAST_CHECK_FAILED_HINT = "A server with lastCheck=failed was unreachable or erroring when it was last checked \u2014 that may already have passed, and its tools ARE loaded this turn. Try the tool; if it works, say nothing about the check. If it fails, report `problem` as what happened rather than guessing.";
62254
62326
  function parseHeaders(raw, rawArgs) {
62255
62327
  const list = repeatedValues(rawArgs, "header", raw);
@@ -62321,7 +62393,7 @@ A tool or platform the user names that is NOT listed anywhere here is simply not
62321
62393
  );
62322
62394
  }
62323
62395
  hints2.push(
62324
- "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`."
62396
+ "Not listed anywhere = not connected. Managed app (Slack, Attio, \u2026) \u2192 the user connects it in the dashboard: Brain \u2192 Integrations \u2192 More apps. Platform (HubSpot, Google Ads, GA4, \u2026) \u2192 Brain \u2192 Integrations \u2192 Platforms. Custom HTTPS MCP endpoint \u2192 `baker mcp add`."
62325
62397
  );
62326
62398
  writeJson({
62327
62399
  ok: true,
@@ -65021,7 +65093,7 @@ function adByHandHint(input) {
65021
65093
 
65022
65094
  // src/commands/studio/batch.ts
65023
65095
  function projectBatch(generation, full) {
65024
- const compact3 = {
65096
+ const compact4 = {
65025
65097
  generationId: generation.generationId,
65026
65098
  kind: generation.kind,
65027
65099
  status: generation.status,
@@ -65038,9 +65110,9 @@ function projectBatch(generation, full) {
65038
65110
  ...variant.error ? { error: variant.error } : {}
65039
65111
  }))
65040
65112
  };
65041
- if (!full) return compact3;
65113
+ if (!full) return compact4;
65042
65114
  return {
65043
- ...compact3,
65115
+ ...compact4,
65044
65116
  ...generation.skillId ? { skillId: generation.skillId } : {},
65045
65117
  ...generation.imageSettings ? { imageSettings: generation.imageSettings } : {},
65046
65118
  ...generation.clipSettings ? { clipSettings: generation.clipSettings } : {},
@@ -66304,7 +66376,7 @@ function handleError8(err) {
66304
66376
  ...err.code === "UNAUTHORIZED" || err.code === "FORBIDDEN" && isNotConnectedError(err.code, err.message) ? {
66305
66377
  fix: {
66306
66378
  action: "request_connection",
66307
- explanation: 'Tag Manager is not connected for this company yet, or no container has been picked. Don\'t send the user to Settings \u2014 call the `request_connection` tool with { platform: "google-tag-manager", reason } and they connect and pick their containers (they can pick more than one) from inside the chat. `request_tag_input` is not the route: that form manages the container snippet on the site, not what runs inside the container, and it cannot pick a container. If the `request_connection` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Tools \u2192 Google Tag Manager, where they also pick the container. Then carry on with the rest of the task.'
66379
+ explanation: 'Tag Manager is not connected for this company yet, or no container has been picked. Don\'t send the user to Settings \u2014 call the `request_connection` tool with { platform: "google-tag-manager", reason } and they connect and pick their containers (they can pick more than one) from inside the chat. `request_tag_input` is not the route: that form manages the container snippet on the site, not what runs inside the container, and it cannot pick a container. If the `request_connection` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Platforms \u2192 Google Tag Manager, where they also pick the container. Then carry on with the rest of the task.'
66308
66380
  }
66309
66381
  } : {},
66310
66382
  retryable: RETRYABLE_CODES2.has(err.code)
@@ -68240,12 +68312,12 @@ function compactAdvertiser(record) {
68240
68312
  };
68241
68313
  }
68242
68314
  function advertiserNormalizer(record, full) {
68243
- const compact3 = compactAdvertiser(record);
68315
+ const compact4 = compactAdvertiser(record);
68244
68316
  if (!full) {
68245
- return compact3;
68317
+ return compact4;
68246
68318
  }
68247
68319
  return {
68248
- ...compact3,
68320
+ ...compact4,
68249
68321
  scraped_name: record.scraped_name ?? null,
68250
68322
  image_url: record.image_url ?? null,
68251
68323
  platforms: Array.isArray(record.platforms) ? record.platforms : [],
@@ -68806,12 +68878,12 @@ function followingHints(records) {
68806
68878
  return hints2;
68807
68879
  }
68808
68880
  function followingNormalizer(record, full) {
68809
- const compact3 = compactFollowed(record);
68881
+ const compact4 = compactFollowed(record);
68810
68882
  if (!full) {
68811
- return compact3;
68883
+ return compact4;
68812
68884
  }
68813
68885
  return {
68814
- ...compact3,
68886
+ ...compact4,
68815
68887
  image_url: record.image_url ?? null,
68816
68888
  platforms: Array.isArray(record.platforms) ? record.platforms : []
68817
68889
  };