@koda-sl/baker-cli 0.121.0-dev.775026ddd → 0.121.0-dev.a2cdb40bc
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 +108 -43
- 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-GQIOFHVI.js → chunk-MWFJ5NOP.js} +132 -275
- package/dist/chunk-MWFJ5NOP.js.map +1 -0
- package/dist/chunk-YTEAWSEM.js +39 -0
- package/dist/chunk-YTEAWSEM.js.map +1 -0
- package/dist/cli.js +4389 -2321
- 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.d.ts +1 -30
- 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-GQIOFHVI.js.map +0 -1
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { getEnv } from \"./env.ts\";\n\nconst MAX_RATE_LIMIT_RETRIES = 3;\nconst MAX_TOTAL_WAIT_MS = 2 * 60 * 1000;\n\nasync function fetchWithRateLimitRetry(url: string, init: RequestInit): Promise<Response> {\n let totalWaited = 0;\n\n for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {\n const response = await fetch(url, init);\n\n if (response.status !== 429 || attempt >= MAX_RATE_LIMIT_RETRIES) {\n return response;\n }\n\n const retryAfterHeader = response.headers.get(\"Retry-After\");\n const waitMs = retryAfterHeader ? Number(retryAfterHeader) * 1000 : 2000 * 2 ** attempt;\n\n if (totalWaited + waitMs > MAX_TOTAL_WAIT_MS) {\n return response;\n }\n\n totalWaited += waitMs;\n await new Promise((resolve) => setTimeout(resolve, waitMs));\n }\n\n return fetch(url, init);\n}\n\ntype ErrorCode =\n | \"UNAUTHORIZED\"\n | \"NOT_FOUND\"\n | \"VALIDATION_ERROR\"\n | \"RATE_LIMITED\"\n | \"INTERNAL_ERROR\"\n | \"NETWORK_ERROR\"\n | \"TIMEOUT\"\n | \"IMAGE_PROCESSING_ERROR\";\n\nexport class ApiError extends Error {\n code: ErrorCode;\n\n constructor(code: ErrorCode, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.code = code;\n }\n}\n\nconst CONVEX_ID_RE = /^[a-zA-Z0-9_]+$/;\n\nfunction hasControlCharacters(value: string): boolean {\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n // Allow tab (9), newline (10), carriage return (13)\n if (code < 32 && code !== 9 && code !== 10 && code !== 13) {\n return true;\n }\n }\n return false;\n}\n\nfunction validateStringValue(value: string): void {\n if (hasControlCharacters(value)) {\n throw new ApiError(\"VALIDATION_ERROR\", \"String value contains invalid control characters\");\n }\n}\n\nexport function validateConvexId(id: string): void {\n if (!CONVEX_ID_RE.test(id)) {\n throw new ApiError(\"VALIDATION_ERROR\", `Invalid ID format: \"${id}\". Expected alphanumeric string.`);\n }\n}\n\nfunction sanitizeParams(params: Record<string, string>): Record<string, string> {\n const sanitized: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n validateStringValue(value);\n sanitized[key] = value;\n }\n return sanitized;\n}\n\nfunction mapHttpError(status: number): ErrorCode {\n if (status === 401 || status === 403) {\n return \"UNAUTHORIZED\";\n }\n if (status === 404) {\n return \"NOT_FOUND\";\n }\n if (status === 422 || status === 400) {\n return \"VALIDATION_ERROR\";\n }\n if (status === 429) {\n return \"RATE_LIMITED\";\n }\n return \"INTERNAL_ERROR\";\n}\n\nasync function handleResponse<T>(response: Response): Promise<T> {\n const body = await response.text();\n\n if (!response.ok) {\n let message = `HTTP ${response.status}: ${response.statusText}`;\n try {\n const parsed = JSON.parse(body) as { error?: string | { message?: string }; message?: string };\n if (typeof parsed.error === \"string\") {\n message = parsed.error;\n } else if (parsed.error?.message) {\n message = parsed.error.message;\n } else if (parsed.message) {\n message = parsed.message;\n }\n } catch {\n // Use default message\n }\n throw new ApiError(mapHttpError(response.status), message);\n }\n\n try {\n return JSON.parse(body) as T;\n } catch {\n throw new ApiError(\"INTERNAL_ERROR\", \"Failed to parse API response as JSON\");\n }\n}\n\nexport async function apiGet<T>(path: string, params?: Record<string, string>): Promise<T> {\n const env = getEnv();\n const url = new URL(path, env.BAKER_API_URL);\n if (params) {\n const clean = sanitizeParams(params);\n for (const [key, value] of Object.entries(clean)) {\n url.searchParams.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetchWithRateLimitRetry(url.toString(), {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${env.BAKER_API_KEY}`,\n Accept: \"application/json\",\n },\n signal: AbortSignal.timeout(60_000),\n });\n } catch (err) {\n if (err instanceof Error && (err.name === \"TimeoutError\" || err.name === \"AbortError\")) {\n throw new ApiError(\"TIMEOUT\", \"Request timed out after 60 seconds\");\n }\n throw new ApiError(\"NETWORK_ERROR\", `Request failed: ${err instanceof Error ? err.message : \"Unknown error\"}`);\n }\n\n return handleResponse<T>(response);\n}\n\nexport async function apiPost<T>(path: string, body: unknown, opts?: { timeoutMs?: number }): Promise<T> {\n const env = getEnv();\n const timeoutMs = opts?.timeoutMs ?? 60_000;\n let response: Response;\n try {\n response = await fetchWithRateLimitRetry(new URL(path, env.BAKER_API_URL).toString(), {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${env.BAKER_API_KEY}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (err) {\n if (err instanceof Error && (err.name === \"TimeoutError\" || err.name === \"AbortError\")) {\n throw new ApiError(\"TIMEOUT\", `Request timed out after ${Math.round(timeoutMs / 1000)} seconds`);\n }\n throw new ApiError(\"NETWORK_ERROR\", `Request failed: ${err instanceof Error ? err.message : \"Unknown error\"}`);\n }\n\n return handleResponse<T>(response);\n}\n"],"mappings":"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","names":[]}
|