@koda-sl/baker-cli 0.115.0 → 0.116.0-dev.86609d2a7
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 +34 -3
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-AVJ3B6LC.js +97 -0
- package/dist/chunk-AVJ3B6LC.js.map +1 -0
- package/dist/chunk-ISFSUDEQ.js +264 -0
- package/dist/chunk-ISFSUDEQ.js.map +1 -0
- package/dist/chunk-K47Q73CK.js +156 -0
- package/dist/chunk-K47Q73CK.js.map +1 -0
- package/dist/{chunk-ZWMMCLJ4.js → chunk-KBWQRZL2.js} +8 -29
- package/dist/chunk-KBWQRZL2.js.map +1 -0
- package/dist/chunk-YTEAWSEM.js +39 -0
- package/dist/chunk-YTEAWSEM.js.map +1 -0
- package/dist/cli.js +3191 -2166
- package/dist/cli.js.map +1 -1
- package/dist/client-PGOTU24X.js +15 -0
- package/dist/client-PGOTU24X.js.map +1 -0
- package/dist/engine/index.js +2 -1
- package/dist/env-TD4VXCQ7.js +10 -0
- package/dist/env-TD4VXCQ7.js.map +1 -0
- package/dist/output-2LGBISBQ.js +18 -0
- package/dist/output-2LGBISBQ.js.map +1 -0
- package/dist/shared-CQC3YRBA.js +23 -0
- package/dist/shared-CQC3YRBA.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-ZWMMCLJ4.js.map +0 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Use `--output` to change format:
|
|
|
82
82
|
|
|
83
83
|
### Ad Platforms (`baker ads`)
|
|
84
84
|
|
|
85
|
-
Multi-platform ad
|
|
85
|
+
Multi-platform ad commands. Google, Meta, and LinkedIn support both reads and **staged writes** (create/update campaigns, ad sets/ad groups, ads, creatives, audiences — applied on chat publish); X (Twitter) is read + analytics. TikTok is coming.
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
@@ -862,6 +862,36 @@ baker ads meta pixels --account-id act_123
|
|
|
862
862
|
baker ads meta pixels --pixel-id 9988 --stats --days 7
|
|
863
863
|
```
|
|
864
864
|
|
|
865
|
+
#### Staged writes (`baker ads meta campaigns|adsets|ads|creatives|audiences|media|draft`)
|
|
866
|
+
|
|
867
|
+
Meta writes mirror the Google/LinkedIn model: they **never hit the Graph API when you run them**. Each op is validated at stage time (field shapes, budget rules, page/Instagram identity ownership, CBO/ABO exclusivity, status-transition legality), shows in the dashboard chat as a pending Meta change, and applies only when the chat is published. Meta has no atomic batch mutate, so ops apply **sequentially** on publish — one failure skips its dependents but the publish completes.
|
|
868
|
+
|
|
869
|
+
If the company's `metaAdsWriteEnabled` flag is off (the default), publish runs the identical lifecycle **simulated** — fake `sim_meta_*` ids, zero Meta calls. The stage response and `baker ads meta draft` show `mode: "simulated"`.
|
|
870
|
+
|
|
871
|
+
Chain a full ad from scratch with `meta_temp_*` refs (each create returns one):
|
|
872
|
+
|
|
873
|
+
```bash
|
|
874
|
+
# 1. (optional) upload creative media from the Baker library → publishes an image hash
|
|
875
|
+
baker ads meta media upload --account-id act_123 --kind image --image-id <bakerImageId> # → meta_temp_img
|
|
876
|
+
|
|
877
|
+
# 2. campaign → ad set → creative → ad
|
|
878
|
+
baker ads meta campaigns create --account-id act_123 --name "Q3 Prospecting" --objective OUTCOME_LEADS --daily-budget 50 # → meta_temp_camp
|
|
879
|
+
baker ads meta adsets create --account-id act_123 --campaign meta_temp_camp --name "US 25-54" \
|
|
880
|
+
--optimization-goal OFFSITE_CONVERSIONS --billing-event IMPRESSIONS --pixel-id 111 --custom-event-type LEAD \
|
|
881
|
+
--targeting-file targeting.json # → meta_temp_as
|
|
882
|
+
baker ads meta creatives create --account-id act_123 --page 555 --instagram-user 777 \
|
|
883
|
+
--message "Save money on pet insurance" --link https://example.com --headline "Best cover" \
|
|
884
|
+
--image-ref meta_temp_img --cta SHOP_NOW --standard-enhancements on # → meta_temp_cr
|
|
885
|
+
baker ads meta ads create --account-id act_123 --name "Ad 1" --adset meta_temp_as --creative meta_temp_cr
|
|
886
|
+
|
|
887
|
+
# review + edit before publish
|
|
888
|
+
baker ads meta draft
|
|
889
|
+
baker ads meta draft remove meta_temp_cr # removing a create cascades to dependents
|
|
890
|
+
baker ads meta draft clear
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
Status changes are `pause|resume|archive` subcommands on `campaigns`/`adsets`/`ads`. `duplicate` reconstructs a paused copy from the live snapshot (`--replace` also pauses the original once the copy publishes). Carousel/dynamic creatives and complex targeting go via `--file` (creativeType `carousel`/`dynamic`; full Meta targeting spec). Advantage+ Creative enhancements: `--standard-enhancements on|off`, or per-feature toggles via a `--file` `enhancements.features` map.
|
|
894
|
+
|
|
865
895
|
#### Smart defaults (so agents don't enumerate the API)
|
|
866
896
|
|
|
867
897
|
**`insights`** — the workhorse:
|
|
@@ -909,7 +939,7 @@ The HTTP backend exposes more endpoints (catalogs, ad-studies, ad-images, labels
|
|
|
909
939
|
|
|
910
940
|
- `spend` is a decimal string in account currency, not an integer.
|
|
911
941
|
- `effective_status` ≠ `status`. The dashboard shows effective_status (e.g. `WITH_ISSUES`, `PENDING_REVIEW`, `DISAPPROVED`, `ADSET_PAUSED`).
|
|
912
|
-
- Meta creatives are effectively immutable once attached. Editing copy/image/CTA =
|
|
942
|
+
- Meta creatives are effectively immutable once attached. Editing copy/image/CTA = stage a new creative + `ads update <id> --creative <ref>` (or `creatives duplicate`), not an edit of the live creative — see [Staged writes](#staged-writes-baker-ads-meta-campaignsadsetsadscreativesaudiencesmediadraft) above.
|
|
913
943
|
- Currency offsets are non-uniform (JPY/KRW = 1, KWD/BHD = 1000, most = 100). Read `account.currency` before doing budget math.
|
|
914
944
|
|
|
915
945
|
---
|
|
@@ -1080,7 +1110,7 @@ Notes:
|
|
|
1080
1110
|
- All write commands take `--file <json>` payloads; explicit flags override file keys. `baker schema ads.linkedin.campaigns.create` for exact args.
|
|
1081
1111
|
- Money flags (`--bid`, `--daily-budget`, `--total-budget`) require `--currency`.
|
|
1082
1112
|
- Creative media comes from the Baker library (`--image-id`/`--video-id` from `baker images`/`baker videos` — uploaded to LinkedIn at publish) or as LinkedIn URNs (`--image-urn`/`--video-urn`). Formats: `image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs`; complex formats take `--file` with the full content object; conversation ads take `--file` with the message flow (`{message: {subject, body, senderName?, buttons[]}}` — buttons `NESTED` (with `nestedMessage`) or `LANDING_PAGE` (with `landingPageUrl`), ≤25 messages, bodies ≤500 chars, labels ≤25). Limits: headline ≤70, text-ad 25/75, intro soft-truncates at 600 chars. TLA sponsors an existing post via `--post-urn`.
|
|
1083
|
-
- Lead forms are file-first (`lead-forms create --file form.json`): name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion).
|
|
1113
|
+
- Lead forms are file-first (`lead-forms create --file form.json`). Required: name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion). Each question is a predefined profile field (`{ name, predefinedField: "EMAIL" }` — Contact/Work/Company/Education/Demographic library) or a custom question (`{ name, questionType: "SINGLE_LINE_TEXT" | "MULTIPLE_CHOICE", options?: [...] }`; ≤3 custom, MULTIPLE_CHOICE needs 2–30 options). Also supported: `locale {country,language}`, `formImageId`/`formImageUrn` (banner), `privacyPolicyText`, `consents[]` (≤5 disclosure checkboxes `{text, required}`), `hiddenFields[]` (≤20 `{name, value}` tracking fields), `legalDisclaimer`, `thankYou {message, cta, landingUrl | appointmentUrl}` (Calendly/Chili Piper booking link). The staged preview emits non-blocking best-practice warnings when a form has no qualifying question, no confirmation message/action, or no consent checkbox.
|
|
1084
1114
|
|
|
1085
1115
|
#### `audit` — playbook diagnostic
|
|
1086
1116
|
|
|
@@ -4449,6 +4479,7 @@ This CLI is designed for AI agent consumption. Key patterns:
|
|
|
4449
4479
|
- **0.104.0**: `baker actions tags ...` commands now type their `/api/actions/tags...` request/response payloads from the shared `@baker/api` contract package instead of hand-written local interfaces. No command, flag, or output-shape changes.
|
|
4450
4480
|
- **0.105.0**: `baker images ...`, `baker videos ...`, and `baker testimonials ...` commands now type their `/api/{images,videos,testimonials}/...` request/response payloads from the shared `@baker/api` contract package instead of hand-written local interfaces. No command, flag, or output-shape changes.
|
|
4451
4481
|
- **0.106.0**: `baker ads linkedin` gains staged write commands — `campaign-groups`/`campaigns`/`creatives` create|update|pause|resume|(archive|)duplicate, `audiences create|upload`, `conversions create|update`, `lead-forms create|update`, plus `draft [remove|clear]` for review/undo. Ops validate at stage time, apply on chat publish, and run simulated (`urn:li:simulated:*`) unless LinkedIn writes are enabled for the company.
|
|
4482
|
+
- **0.116.0**: `lead-forms create` models the full Campaign Manager form — `locale`, form banner image (`formImageId`/`formImageUrn`), predefined profile-field questions (validated enum) vs custom questions (`SINGLE_LINE_TEXT`/`MULTIPLE_CHOICE` with `options`, ≤3 custom), `privacyPolicyText`, disclosure `consents[]` (≤5), tracking `hiddenFields[]` (≤20), and `thankYou` confirmation CTA + landing/appointment link. Staged preview surfaces best-practice warnings (no qualifying question, no confirmation, no consent). No breaking flag changes.
|
|
4452
4483
|
|
|
4453
4484
|
## Publishing
|
|
4454
4485
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
__commonJS,
|
|
29
|
+
__toESM
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-5WRI5ZAA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
handleConnectionError,
|
|
3
|
+
writeAdsJson
|
|
4
|
+
} from "./chunk-ISFSUDEQ.js";
|
|
5
|
+
import {
|
|
6
|
+
ApiError
|
|
7
|
+
} from "./chunk-K47Q73CK.js";
|
|
8
|
+
import {
|
|
9
|
+
getEnv
|
|
10
|
+
} from "./chunk-YTEAWSEM.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/ads/meta/shared.ts
|
|
13
|
+
var DAY_MS = 864e5;
|
|
14
|
+
function handleMetaError(err) {
|
|
15
|
+
if (err instanceof ApiError) {
|
|
16
|
+
if (err.code === "UNAUTHORIZED" || err.code === "NOT_FOUND") {
|
|
17
|
+
handleConnectionError("meta_ads", err.message);
|
|
18
|
+
}
|
|
19
|
+
const envelope = {
|
|
20
|
+
ok: false,
|
|
21
|
+
error: {
|
|
22
|
+
code: err.code,
|
|
23
|
+
message: err.message,
|
|
24
|
+
fix: { action: "reject", explanation: err.message },
|
|
25
|
+
retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
writeAdsJson(envelope);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const message = err instanceof Error ? err.message : "Unexpected error";
|
|
32
|
+
writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message } });
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
var ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;
|
|
36
|
+
function resolveAccountIdArg(args) {
|
|
37
|
+
const fromArgs = args["account-id"];
|
|
38
|
+
const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;
|
|
39
|
+
if (!id) {
|
|
40
|
+
writeAdsJson({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: "MISSING_ACCOUNT_ID",
|
|
44
|
+
message: "Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs."
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!ACCOUNT_ID_RE.test(id)) {
|
|
50
|
+
writeAdsJson({
|
|
51
|
+
ok: false,
|
|
52
|
+
error: {
|
|
53
|
+
code: "INVALID_ACCOUNT_ID",
|
|
54
|
+
message: `Invalid account ID "${id}". Expected numeric ID, optionally prefixed with "act_".`
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
return id.startsWith("act_") ? id : `act_${id}`;
|
|
60
|
+
}
|
|
61
|
+
function getDateRange(args) {
|
|
62
|
+
const datePreset = args["date-preset"];
|
|
63
|
+
const since = args.since;
|
|
64
|
+
const until = args.until;
|
|
65
|
+
if (since && until) return { since, until };
|
|
66
|
+
return { datePreset: datePreset ?? "last_7d" };
|
|
67
|
+
}
|
|
68
|
+
function todayIso() {
|
|
69
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
70
|
+
}
|
|
71
|
+
function daysAgoIso(days) {
|
|
72
|
+
return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);
|
|
73
|
+
}
|
|
74
|
+
function csvOrJson(args) {
|
|
75
|
+
return args.output ?? "json";
|
|
76
|
+
}
|
|
77
|
+
function resolveEffectiveStatus(args) {
|
|
78
|
+
if (args["all-statuses"]) {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const explicit = args["effective-status"];
|
|
82
|
+
if (explicit) {
|
|
83
|
+
return explicit;
|
|
84
|
+
}
|
|
85
|
+
return "ACTIVE";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
handleMetaError,
|
|
90
|
+
resolveAccountIdArg,
|
|
91
|
+
getDateRange,
|
|
92
|
+
todayIso,
|
|
93
|
+
daysAgoIso,
|
|
94
|
+
csvOrJson,
|
|
95
|
+
resolveEffectiveStatus
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=chunk-AVJ3B6LC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/ads/meta/shared.ts"],"sourcesContent":["import { ApiError } from \"../../../client.ts\";\nimport { getEnv } from \"../../../env.ts\";\nimport { handleConnectionError } from \"../../../error-handler.ts\";\nimport { writeAdsJson } from \"../output.ts\";\nimport type { AdsErrorEnvelope } from \"../types.ts\";\n\nconst DAY_MS = 86_400_000;\n\nexport function handleMetaError(err: unknown): never {\n if (err instanceof ApiError) {\n if (err.code === \"UNAUTHORIZED\" || err.code === \"NOT_FOUND\") {\n handleConnectionError(\"meta_ads\", err.message);\n }\n const envelope: AdsErrorEnvelope = {\n ok: false,\n error: {\n code: err.code,\n message: err.message,\n fix: { action: \"reject\", explanation: err.message },\n retryable: err.code === \"RATE_LIMITED\" || err.code === \"INTERNAL_ERROR\",\n },\n };\n writeAdsJson(envelope);\n process.exit(1);\n }\n const message = err instanceof Error ? err.message : \"Unexpected error\";\n writeAdsJson({ ok: false, error: { code: \"NETWORK_ERROR\", message } });\n process.exit(1);\n}\n\nconst ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;\n\n/** Pull the default Meta ad account ID from `--account-id` arg or `BAKER_META_AD_ACCOUNT_ID` env. */\nexport function resolveAccountIdArg(args: Record<string, unknown>): string {\n const fromArgs = args[\"account-id\"] as string | undefined;\n const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;\n if (!id) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"MISSING_ACCOUNT_ID\",\n message: \"Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs.\",\n },\n });\n process.exit(1);\n }\n if (!ACCOUNT_ID_RE.test(id)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_ACCOUNT_ID\",\n message: `Invalid account ID \"${id}\". Expected numeric ID, optionally prefixed with \"act_\".`,\n },\n });\n process.exit(1);\n }\n return id.startsWith(\"act_\") ? id : `act_${id}`;\n}\n\nexport function getDateRange(args: Record<string, unknown>): { datePreset?: string; since?: string; until?: string } {\n const datePreset = args[\"date-preset\"] as string | undefined;\n const since = args.since as string | undefined;\n const until = args.until as string | undefined;\n if (since && until) return { since, until };\n return { datePreset: datePreset ?? \"last_7d\" };\n}\n\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nexport function daysAgoIso(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);\n}\n\nexport function csvOrJson(args: Record<string, unknown>): string {\n return (args.output as string | undefined) ?? \"json\";\n}\n\n/**\n * Default to ACTIVE-only on listings — that's almost always what an AI agent or\n * a human glance wants. Pass `--effective-status ACTIVE,PAUSED` to override, or\n * `--all-statuses` to drop the filter entirely.\n */\nexport function resolveEffectiveStatus(args: Record<string, unknown>): string | undefined {\n if (args[\"all-statuses\"]) {\n return undefined;\n }\n const explicit = args[\"effective-status\"] as string | undefined;\n if (explicit) {\n return explicit;\n }\n return \"ACTIVE\";\n}\n"],"mappings":";;;;;;;;;;;;AAMA,IAAM,SAAS;AAER,SAAS,gBAAgB,KAAqB;AACnD,MAAI,eAAe,UAAU;AAC3B,QAAI,IAAI,SAAS,kBAAkB,IAAI,SAAS,aAAa;AAC3D,4BAAsB,YAAY,IAAI,OAAO;AAAA,IAC/C;AACA,UAAM,WAA6B;AAAA,MACjC,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM,IAAI;AAAA,QACV,SAAS,IAAI;AAAA,QACb,KAAK,EAAE,QAAQ,UAAU,aAAa,IAAI,QAAQ;AAAA,QAClD,WAAW,IAAI,SAAS,kBAAkB,IAAI,SAAS;AAAA,MACzD;AAAA,IACF;AACA,iBAAa,QAAQ;AACrB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,eAAa,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,iBAAiB,QAAQ,EAAE,CAAC;AACrE,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,gBAAgB;AAGf,SAAS,oBAAoB,MAAuC;AACzE,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,KAAK,YAAY,OAAO,EAAE;AAChC,MAAI,CAAC,IAAI;AACP,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,CAAC,cAAc,KAAK,EAAE,GAAG;AAC3B,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,uBAAuB,EAAE;AAAA,MACpC;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,SAAO,GAAG,WAAW,MAAM,IAAI,KAAK,OAAO,EAAE;AAC/C;AAEO,SAAS,aAAa,MAAwF;AACnH,QAAM,aAAa,KAAK,aAAa;AACrC,QAAM,QAAQ,KAAK;AACnB,QAAM,QAAQ,KAAK;AACnB,MAAI,SAAS,MAAO,QAAO,EAAE,OAAO,MAAM;AAC1C,SAAO,EAAE,YAAY,cAAc,UAAU;AAC/C;AAEO,SAAS,WAAmB;AACjC,UAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC7C;AAEO,SAAS,WAAW,MAAsB;AAC/C,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACvE;AAEO,SAAS,UAAU,MAAuC;AAC/D,SAAQ,KAAK,UAAiC;AAChD;AAOO,SAAS,uBAAuB,MAAmD;AACxF,MAAI,KAAK,cAAc,GAAG;AACxB,WAAO;AAAA,EACT;AACA,QAAM,WAAW,KAAK,kBAAkB;AACxC,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
apiGet
|
|
3
|
+
} from "./chunk-K47Q73CK.js";
|
|
4
|
+
import {
|
|
5
|
+
getEnv
|
|
6
|
+
} from "./chunk-YTEAWSEM.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/ads/cache.ts
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var CACHE_DIR = join(homedir(), ".baker", "cache", "ads");
|
|
14
|
+
function ensureDir(dir) {
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function hashKey(key) {
|
|
20
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
21
|
+
}
|
|
22
|
+
function cachePath(category, key) {
|
|
23
|
+
const dir = join(CACHE_DIR, category);
|
|
24
|
+
ensureDir(dir);
|
|
25
|
+
return join(dir, `${hashKey(key)}.json`);
|
|
26
|
+
}
|
|
27
|
+
function cacheGet(category, key) {
|
|
28
|
+
const path = cachePath(category, key);
|
|
29
|
+
if (!existsSync(path)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(path, "utf-8");
|
|
34
|
+
const entry = JSON.parse(raw);
|
|
35
|
+
if (entry.expiresAt < Date.now()) {
|
|
36
|
+
rmSync(path, { force: true });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return entry;
|
|
40
|
+
} catch {
|
|
41
|
+
rmSync(path, { force: true });
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function cacheSet(category, key, data, ttlMs, fields) {
|
|
46
|
+
const path = cachePath(category, key);
|
|
47
|
+
const entry = {
|
|
48
|
+
expiresAt: Date.now() + ttlMs,
|
|
49
|
+
data,
|
|
50
|
+
fields
|
|
51
|
+
};
|
|
52
|
+
writeFileSync(path, JSON.stringify(entry), "utf-8");
|
|
53
|
+
}
|
|
54
|
+
var HOUR = 60 * 60 * 1e3;
|
|
55
|
+
var MINUTE = 60 * 1e3;
|
|
56
|
+
function getQueryTtl(query) {
|
|
57
|
+
const upper = query.toUpperCase();
|
|
58
|
+
if (upper.includes("TODAY")) {
|
|
59
|
+
return 15 * MINUTE;
|
|
60
|
+
}
|
|
61
|
+
if (upper.includes("LAST_7_DAYS") || upper.includes("LAST_14_DAYS")) {
|
|
62
|
+
return 1 * HOUR;
|
|
63
|
+
}
|
|
64
|
+
return 6 * HOUR;
|
|
65
|
+
}
|
|
66
|
+
function buildQueryCacheKey(customerId, query) {
|
|
67
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
68
|
+
return `${customerId}:${today}:${query.trim().replace(/\s+/g, " ")}`;
|
|
69
|
+
}
|
|
70
|
+
function getManagerIdForCustomer(customerId) {
|
|
71
|
+
const cached = cacheGet("accounts", "list");
|
|
72
|
+
if (!cached) return void 0;
|
|
73
|
+
const account = cached.data.find((a) => a.id === customerId);
|
|
74
|
+
if (account?.access_type === "managed") return account.manager_id;
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/error-handler.ts
|
|
79
|
+
function handleConnectionError(platform, originalMessage) {
|
|
80
|
+
const platformName = {
|
|
81
|
+
google_ads: "Google Ads",
|
|
82
|
+
ga4: "Google Analytics 4",
|
|
83
|
+
gsc: "Google Search Console",
|
|
84
|
+
x_ads: "X Ads",
|
|
85
|
+
meta_ads: "Meta Ads",
|
|
86
|
+
linkedin_ads: "LinkedIn Ads"
|
|
87
|
+
}[platform];
|
|
88
|
+
const alternative = {
|
|
89
|
+
google_ads: "keywords-for-site or research advertisers",
|
|
90
|
+
ga4: "lighthouse for page performance",
|
|
91
|
+
gsc: "keywords-for-site or research keyword-gap",
|
|
92
|
+
x_ads: "research advertisers",
|
|
93
|
+
meta_ads: "research advertisers or research keyword-ideas",
|
|
94
|
+
linkedin_ads: "research advertisers (LinkedIn Ad Library) or research keyword-ideas"
|
|
95
|
+
}[platform];
|
|
96
|
+
const code = {
|
|
97
|
+
google_ads: "NO_GOOGLE_ADS_CONNECTION",
|
|
98
|
+
ga4: "NO_GA4_CONNECTION",
|
|
99
|
+
gsc: "NO_GSC_CONNECTION",
|
|
100
|
+
x_ads: "NO_X_ADS_CONNECTION",
|
|
101
|
+
meta_ads: "NO_META_ADS_CONNECTION",
|
|
102
|
+
linkedin_ads: "NO_LINKEDIN_ADS_CONNECTION"
|
|
103
|
+
}[platform];
|
|
104
|
+
const envelope = {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: {
|
|
107
|
+
code,
|
|
108
|
+
message: originalMessage || `No ${platformName} connection found for this company.`,
|
|
109
|
+
fix: {
|
|
110
|
+
action: "authenticate",
|
|
111
|
+
explanation: `AGENT INSTRUCTION: You cannot resolve this error programmatically. The user has not connected their ${platformName} account. Step 1: Inform the user that they must connect ${platformName} in their Baker dashboard settings for direct data queries. Step 2: Do not fail the overall audit or playbook. Document the missing connection, skip this specific check, and proceed with the rest of your tasks. (Optional fallback: use 'baker research ${alternative}' for estimated external insights).`
|
|
112
|
+
},
|
|
113
|
+
retryable: false
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
writeJsonEnvelope(envelope);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/commands/ads/output.ts
|
|
121
|
+
function writeAdsJson(envelope) {
|
|
122
|
+
process.stdout.write(`${JSON.stringify(envelope, null, 2)}
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
function writeJsonEnvelope(envelope) {
|
|
126
|
+
process.stdout.write(`${JSON.stringify(envelope, null, 2)}
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
function toCsvRow(values) {
|
|
130
|
+
return values.map((v) => {
|
|
131
|
+
if (v.includes(",") || v.includes('"') || v.includes("\n")) {
|
|
132
|
+
return `"${v.replace(/"/g, '""')}"`;
|
|
133
|
+
}
|
|
134
|
+
return v;
|
|
135
|
+
}).join(",");
|
|
136
|
+
}
|
|
137
|
+
function flattenRow(row) {
|
|
138
|
+
const flat = {};
|
|
139
|
+
for (const [key, val] of Object.entries(row)) {
|
|
140
|
+
flat[key] = typeof val === "object" && val !== null ? JSON.stringify(val) : String(val ?? "");
|
|
141
|
+
}
|
|
142
|
+
return flat;
|
|
143
|
+
}
|
|
144
|
+
function writeAdsCsv(data, fields) {
|
|
145
|
+
if (data.length === 0) return;
|
|
146
|
+
const cols = fields ?? Object.keys(data[0] ?? {});
|
|
147
|
+
process.stdout.write(`${toCsvRow(cols)}
|
|
148
|
+
`);
|
|
149
|
+
for (const row of data) {
|
|
150
|
+
const flat = flattenRow(row);
|
|
151
|
+
process.stdout.write(`${toCsvRow(cols.map((f) => flat[f] ?? ""))}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function writeAdsJsonl(data) {
|
|
156
|
+
for (const row of data) {
|
|
157
|
+
process.stdout.write(`${JSON.stringify(row)}
|
|
158
|
+
`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function writeAdsMd(data, fields) {
|
|
162
|
+
if (data.length === 0) return;
|
|
163
|
+
const cols = fields ?? Object.keys(data[0] ?? {});
|
|
164
|
+
process.stdout.write(`| ${cols.join(" | ")} |
|
|
165
|
+
`);
|
|
166
|
+
process.stdout.write(`| ${cols.map(() => "---").join(" | ")} |
|
|
167
|
+
`);
|
|
168
|
+
for (const row of data) {
|
|
169
|
+
const flat = flattenRow(row);
|
|
170
|
+
process.stdout.write(`| ${cols.map((f) => flat[f] ?? "").join(" | ")} |
|
|
171
|
+
`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function writeAdsOutput(data, format, fields) {
|
|
175
|
+
switch (format) {
|
|
176
|
+
case "csv":
|
|
177
|
+
writeAdsCsv(data, fields);
|
|
178
|
+
break;
|
|
179
|
+
case "jsonl":
|
|
180
|
+
writeAdsJsonl(data);
|
|
181
|
+
break;
|
|
182
|
+
case "md":
|
|
183
|
+
writeAdsMd(data, fields);
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
writeAdsJson({ ok: true, data });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function fetchAccounts(useCache = true) {
|
|
190
|
+
if (useCache) {
|
|
191
|
+
const cached = cacheGet("accounts", "list");
|
|
192
|
+
if (cached) return cached.data;
|
|
193
|
+
}
|
|
194
|
+
const params = !useCache ? { "skip-cache": "true" } : void 0;
|
|
195
|
+
const data = await apiGet("/api/ads/google/accounts", params);
|
|
196
|
+
if (useCache) {
|
|
197
|
+
cacheSet("accounts", "list", data, 60 * 60 * 1e3);
|
|
198
|
+
}
|
|
199
|
+
return data;
|
|
200
|
+
}
|
|
201
|
+
async function resolveCustomerId(args) {
|
|
202
|
+
const fromArgs = args["customer-id"];
|
|
203
|
+
const customerId = fromArgs || getEnv().BAKER_GOOGLE_ADS_CUSTOMER_ID;
|
|
204
|
+
if (customerId) {
|
|
205
|
+
if (!/^\d{10}$/.test(customerId)) {
|
|
206
|
+
writeAdsJson({
|
|
207
|
+
ok: false,
|
|
208
|
+
error: {
|
|
209
|
+
code: "INVALID_CUSTOMER_ID",
|
|
210
|
+
message: "Customer ID must be exactly 10 digits without dashes. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID."
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
return customerId;
|
|
216
|
+
}
|
|
217
|
+
const useCache = !args["no-cache"];
|
|
218
|
+
try {
|
|
219
|
+
const accounts = await fetchAccounts(useCache);
|
|
220
|
+
const [single] = accounts;
|
|
221
|
+
if (accounts.length === 1 && single) {
|
|
222
|
+
process.stderr.write(`Using account "${single.name}" (${single.id})
|
|
223
|
+
`);
|
|
224
|
+
return single.id;
|
|
225
|
+
}
|
|
226
|
+
if (accounts.length === 0) {
|
|
227
|
+
handleConnectionError("google_ads");
|
|
228
|
+
}
|
|
229
|
+
const list = accounts.map((a) => ` ${a.id} ${a.name}`).join("\n");
|
|
230
|
+
writeAdsJson({
|
|
231
|
+
ok: false,
|
|
232
|
+
error: {
|
|
233
|
+
code: "MULTIPLE_ACCOUNTS",
|
|
234
|
+
message: `Multiple accounts found. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID:
|
|
235
|
+
${list}`
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
process.exit(1);
|
|
239
|
+
} catch {
|
|
240
|
+
writeAdsJson({
|
|
241
|
+
ok: false,
|
|
242
|
+
error: {
|
|
243
|
+
code: "INVALID_CUSTOMER_ID",
|
|
244
|
+
message: "Could not auto-detect account. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID."
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export {
|
|
252
|
+
cacheGet,
|
|
253
|
+
cacheSet,
|
|
254
|
+
getQueryTtl,
|
|
255
|
+
buildQueryCacheKey,
|
|
256
|
+
getManagerIdForCustomer,
|
|
257
|
+
handleConnectionError,
|
|
258
|
+
writeAdsJson,
|
|
259
|
+
writeJsonEnvelope,
|
|
260
|
+
toCsvRow,
|
|
261
|
+
writeAdsOutput,
|
|
262
|
+
resolveCustomerId
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=chunk-ISFSUDEQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/ads/cache.ts","../src/error-handler.ts","../src/commands/ads/output.ts"],"sourcesContent":["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 { writeJsonEnvelope } from \"./commands/ads/output.ts\";\n\ntype ConnectionPlatform = \"google_ads\" | \"ga4\" | \"gsc\" | \"x_ads\" | \"meta_ads\" | \"linkedin_ads\";\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 = {\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 }[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: \"authenticate\",\n explanation: `AGENT INSTRUCTION: You cannot resolve this error programmatically. The user has not connected their ${platformName} account. Step 1: Inform the user that they must connect ${platformName} in their Baker dashboard settings for direct data queries. Step 2: Do not fail the overall audit or playbook. Document the missing connection, skip this specific check, and proceed with the rest of your tasks. (Optional fallback: use 'baker research ${alternative}' for estimated external insights).`,\n },\n retryable: false,\n },\n };\n\n writeJsonEnvelope(envelope);\n process.exit(1);\n}\n","import { apiGet } from \"../../client.ts\";\nimport { getEnv } from \"../../env.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\nexport function writeAdsOutput(data: Array<Record<string, unknown>>, format: string, fields?: string[]): 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 writeAdsJson({ ok: true, data });\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\nimport { handleConnectionError } from \"../../error-handler.ts\";\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 {\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":[]}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getEnv
|
|
3
|
+
} from "./chunk-YTEAWSEM.js";
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var MAX_RATE_LIMIT_RETRIES = 3;
|
|
7
|
+
var MAX_TOTAL_WAIT_MS = 2 * 60 * 1e3;
|
|
8
|
+
async function fetchWithRateLimitRetry(url, init) {
|
|
9
|
+
let totalWaited = 0;
|
|
10
|
+
for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
11
|
+
const response = await fetch(url, init);
|
|
12
|
+
if (response.status !== 429 || attempt >= MAX_RATE_LIMIT_RETRIES) {
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
15
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
16
|
+
const waitMs = retryAfterHeader ? Number(retryAfterHeader) * 1e3 : 2e3 * 2 ** attempt;
|
|
17
|
+
if (totalWaited + waitMs > MAX_TOTAL_WAIT_MS) {
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
totalWaited += waitMs;
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
22
|
+
}
|
|
23
|
+
return fetch(url, init);
|
|
24
|
+
}
|
|
25
|
+
var ApiError = class extends Error {
|
|
26
|
+
code;
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "ApiError";
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var CONVEX_ID_RE = /^[a-zA-Z0-9_]+$/;
|
|
34
|
+
function hasControlCharacters(value) {
|
|
35
|
+
for (let i = 0; i < value.length; i++) {
|
|
36
|
+
const code = value.charCodeAt(i);
|
|
37
|
+
if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function validateStringValue(value) {
|
|
44
|
+
if (hasControlCharacters(value)) {
|
|
45
|
+
throw new ApiError("VALIDATION_ERROR", "String value contains invalid control characters");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function validateConvexId(id) {
|
|
49
|
+
if (!CONVEX_ID_RE.test(id)) {
|
|
50
|
+
throw new ApiError("VALIDATION_ERROR", `Invalid ID format: "${id}". Expected alphanumeric string.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function sanitizeParams(params) {
|
|
54
|
+
const sanitized = {};
|
|
55
|
+
for (const [key, value] of Object.entries(params)) {
|
|
56
|
+
validateStringValue(value);
|
|
57
|
+
sanitized[key] = value;
|
|
58
|
+
}
|
|
59
|
+
return sanitized;
|
|
60
|
+
}
|
|
61
|
+
function mapHttpError(status) {
|
|
62
|
+
if (status === 401 || status === 403) {
|
|
63
|
+
return "UNAUTHORIZED";
|
|
64
|
+
}
|
|
65
|
+
if (status === 404) {
|
|
66
|
+
return "NOT_FOUND";
|
|
67
|
+
}
|
|
68
|
+
if (status === 422 || status === 400) {
|
|
69
|
+
return "VALIDATION_ERROR";
|
|
70
|
+
}
|
|
71
|
+
if (status === 429) {
|
|
72
|
+
return "RATE_LIMITED";
|
|
73
|
+
}
|
|
74
|
+
return "INTERNAL_ERROR";
|
|
75
|
+
}
|
|
76
|
+
async function handleResponse(response) {
|
|
77
|
+
const body = await response.text();
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
let message = `HTTP ${response.status}: ${response.statusText}`;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(body);
|
|
82
|
+
if (typeof parsed.error === "string") {
|
|
83
|
+
message = parsed.error;
|
|
84
|
+
} else if (parsed.error?.message) {
|
|
85
|
+
message = parsed.error.message;
|
|
86
|
+
} else if (parsed.message) {
|
|
87
|
+
message = parsed.message;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
throw new ApiError(mapHttpError(response.status), message);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(body);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new ApiError("INTERNAL_ERROR", "Failed to parse API response as JSON");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function apiGet(path, params) {
|
|
100
|
+
const env = getEnv();
|
|
101
|
+
const url = new URL(path, env.BAKER_API_URL);
|
|
102
|
+
if (params) {
|
|
103
|
+
const clean = sanitizeParams(params);
|
|
104
|
+
for (const [key, value] of Object.entries(clean)) {
|
|
105
|
+
url.searchParams.set(key, value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let response;
|
|
109
|
+
try {
|
|
110
|
+
response = await fetchWithRateLimitRetry(url.toString(), {
|
|
111
|
+
method: "GET",
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: `Bearer ${env.BAKER_API_KEY}`,
|
|
114
|
+
Accept: "application/json"
|
|
115
|
+
},
|
|
116
|
+
signal: AbortSignal.timeout(6e4)
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
120
|
+
throw new ApiError("TIMEOUT", "Request timed out after 60 seconds");
|
|
121
|
+
}
|
|
122
|
+
throw new ApiError("NETWORK_ERROR", `Request failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
123
|
+
}
|
|
124
|
+
return handleResponse(response);
|
|
125
|
+
}
|
|
126
|
+
async function apiPost(path, body, opts) {
|
|
127
|
+
const env = getEnv();
|
|
128
|
+
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await fetchWithRateLimitRetry(new URL(path, env.BAKER_API_URL).toString(), {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${env.BAKER_API_KEY}`,
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
Accept: "application/json"
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(body),
|
|
139
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
143
|
+
throw new ApiError("TIMEOUT", `Request timed out after ${Math.round(timeoutMs / 1e3)} seconds`);
|
|
144
|
+
}
|
|
145
|
+
throw new ApiError("NETWORK_ERROR", `Request failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
146
|
+
}
|
|
147
|
+
return handleResponse(response);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
ApiError,
|
|
152
|
+
validateConvexId,
|
|
153
|
+
apiGet,
|
|
154
|
+
apiPost
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=chunk-K47Q73CK.js.map
|