@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 +6 -3
- package/dist/{chunk-ZI7TPBXB.js → chunk-BJP4G3X4.js} +2 -2
- package/dist/chunk-BJP4G3X4.js.map +1 -0
- package/dist/{chunk-4ACU7GBN.js → chunk-N3H66QUV.js} +2 -2
- package/dist/cli.js +117 -45
- package/dist/cli.js.map +1 -1
- package/dist/{output-KMEPFOFT.js → output-AFWVXYWA.js} +2 -2
- package/dist/{shared-26NWGQ5G.js → shared-SDKT2Y7Z.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-ZI7TPBXB.js.map +0 -1
- /package/dist/{chunk-4ACU7GBN.js.map → chunk-N3H66QUV.js.map} +0 -0
- /package/dist/{output-KMEPFOFT.js.map → output-AFWVXYWA.js.map} +0 -0
- /package/dist/{shared-26NWGQ5G.js.map → shared-SDKT2Y7Z.js.map} +0 -0
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 →
|
|
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 →
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
9090
|
+
return compact4;
|
|
9091
9091
|
}
|
|
9092
9092
|
return {
|
|
9093
|
-
...
|
|
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
|
|
9369
|
-
const shaped =
|
|
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 (
|
|
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
|
|
14763
|
-
const e164 =
|
|
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-
|
|
26624
|
-
const { handleMetaError: handleMetaError2 } = await import("./shared-
|
|
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-
|
|
26772
|
-
const { handleMetaError: handleMetaError2 } = await import("./shared-
|
|
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
|
|
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,
|
|
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
|
-
...
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
54570
|
+
const compact4 = compactHit(record);
|
|
54499
54571
|
if (!full) {
|
|
54500
|
-
return
|
|
54572
|
+
return compact4;
|
|
54501
54573
|
}
|
|
54502
54574
|
const group = asRecord2(record.group);
|
|
54503
54575
|
return {
|
|
54504
|
-
...
|
|
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
|
|
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 :
|
|
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 :
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
65113
|
+
if (!full) return compact4;
|
|
65042
65114
|
return {
|
|
65043
|
-
...
|
|
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
|
|
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
|
|
68315
|
+
const compact4 = compactAdvertiser(record);
|
|
68244
68316
|
if (!full) {
|
|
68245
|
-
return
|
|
68317
|
+
return compact4;
|
|
68246
68318
|
}
|
|
68247
68319
|
return {
|
|
68248
|
-
...
|
|
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
|
|
68881
|
+
const compact4 = compactFollowed(record);
|
|
68810
68882
|
if (!full) {
|
|
68811
|
-
return
|
|
68883
|
+
return compact4;
|
|
68812
68884
|
}
|
|
68813
68885
|
return {
|
|
68814
|
-
...
|
|
68886
|
+
...compact4,
|
|
68815
68887
|
image_url: record.image_url ?? null,
|
|
68816
68888
|
platforms: Array.isArray(record.platforms) ? record.platforms : []
|
|
68817
68889
|
};
|