From ebdf1dd2f1980ee200a2841ebb69ccff783fcbba Mon Sep 17 00:00:00 2001 From: Corey Haines <34802794+coreyhaines31@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:31:29 -0800 Subject: [PATCH] fix: correct API issues found by second codex review across 14 CLIs Fixes from thorough codex review (o3 high reasoning, 5 parallel batches): Validation fixes: - customer-io: add ID validation for customer/campaign commands, event name check - dub: fix links get to use /links/info endpoint, add --id validation - google-search-console: fix countries to use ['country'] only, add --url validation - mention-me: add --customer-id validation on referral/share/reward commands - tolt: add --id validation for affiliates get/update Auth & API fixes: - apollo: move API key from header to JSON body, fix search endpoint path - rewardful: change from Bearer to Basic auth - hotjar: split OAuth URL (unversioned) from resource URL (v2) - amplitude: wrap retention e param in JSON array - snov: change list prospects from GET to POST with JSON body - optimizely: change archive from DELETE to PATCH status=archived - google-ads: fix budget body field from camelCase to snake_case - resend: change webhook field from endpoint to url, add validation - linkedin-ads: add required campaignGroup URN, fix numeric amount types Co-Authored-By: Claude Opus 4.6 --- tools/clis/amplitude.js | 2 +- tools/clis/apollo.js | 9 ++++----- tools/clis/customer-io.js | 23 ++++++++++++++++++----- tools/clis/dub.js | 6 +++++- tools/clis/google-ads.js | 2 +- tools/clis/google-search-console.js | 3 ++- tools/clis/hotjar.js | 5 +++-- tools/clis/linkedin-ads.js | 6 ++++-- tools/clis/mention-me.js | 11 +++++++++-- tools/clis/optimizely.js | 2 +- tools/clis/resend.js | 3 ++- tools/clis/rewardful.js | 3 ++- tools/clis/snov.js | 8 ++++---- tools/clis/tolt.js | 8 ++++++-- 14 files changed, 62 insertions(+), 29 deletions(-) diff --git a/tools/clis/amplitude.js b/tools/clis/amplitude.js index 7a1d701..84128c4 100755 --- a/tools/clis/amplitude.js +++ b/tools/clis/amplitude.js @@ -149,7 +149,7 @@ async function main() { params.set('start', args.start) params.set('end', args.end) if (args.event) { - params.set('e', JSON.stringify({ event_type: args.event })) + params.set('e', JSON.stringify([{ event_type: args.event }])) } result = await queryApi('GET', '/retention', params) break diff --git a/tools/clis/apollo.js b/tools/clis/apollo.js index d51e3fa..a7967ed 100755 --- a/tools/clis/apollo.js +++ b/tools/clis/apollo.js @@ -9,17 +9,16 @@ if (!API_KEY) { } async function api(method, path, body) { + const authBody = body ? { ...body, api_key: API_KEY } : { api_key: API_KEY } if (args['dry-run']) { - return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'x-api-key': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Content-Type': 'application/json' }, body: { ...authBody, api_key: '***' } } } const res = await fetch(`${BASE_URL}${path}`, { method, headers: { - 'x-api-key': API_KEY, 'Content-Type': 'application/json', - 'Accept': 'application/json', }, - body: body ? JSON.stringify(body) : undefined, + body: JSON.stringify(authBody), }) const text = await res.text() try { @@ -67,7 +66,7 @@ async function main() { if (args.seniorities) body.person_seniorities = args.seniorities.split(',') if (args['employee-ranges']) body.organization_num_employees_ranges = args['employee-ranges'].split(',').map(r => r.trim()) if (args.keywords) body.q_keywords = args.keywords - result = await api('POST', '/mixed_people/api_search', body) + result = await api('POST', '/mixed_people/search', body) break } case 'enrich': { diff --git a/tools/clis/customer-io.js b/tools/clis/customer-io.js index 66a943d..12addf7 100755 --- a/tools/clis/customer-io.js +++ b/tools/clis/customer-io.js @@ -94,6 +94,7 @@ async function main() { switch (sub) { case 'identify': { const customerId = rest[0] || args.id + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } const body = {} if (args.email) body.email = args.email if (args['first-name']) body.first_name = args['first-name'] @@ -106,16 +107,20 @@ async function main() { } case 'get': { const customerId = rest[0] || args.id + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } result = await appApi('GET', `/customers/${customerId}/attributes`) break } case 'delete': { const customerId = rest[0] || args.id + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } result = await trackApi('DELETE', `/customers/${customerId}`) break } case 'track-event': { const customerId = rest[0] || args.id + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } + if (!args.name) { result = { error: '--name required (event name)' }; break } const body = { name: args.name } if (args.data) body.data = JSON.parse(args.data) result = await trackApi('POST', `/customers/${customerId}/events`, body) @@ -131,18 +136,26 @@ async function main() { case 'list': result = await appApi('GET', '/campaigns') break - case 'get': - result = await appApi('GET', `/campaigns/${rest[0]}`) + case 'get': { + const campaignId = rest[0] || args.id + if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break } + result = await appApi('GET', `/campaigns/${campaignId}`) break - case 'metrics': - result = await appApi('GET', `/campaigns/${rest[0]}/metrics`) + } + case 'metrics': { + const campaignId = rest[0] || args.id + if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break } + result = await appApi('GET', `/campaigns/${campaignId}/metrics`) break + } case 'trigger': { + const campaignId = rest[0] || args.id + if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break } const body = {} if (args.emails) body.emails = args.emails.split(',') if (args.ids) body.ids = args.ids.split(',') if (args.data) body.data = JSON.parse(args.data) - result = await appApi('POST', `/campaigns/${rest[0]}/triggers`, body) + result = await appApi('POST', `/campaigns/${campaignId}/triggers`, body) break } default: diff --git a/tools/clis/dub.js b/tools/clis/dub.js index ea9aebe..dba6b93 100755 --- a/tools/clis/dub.js +++ b/tools/clis/dub.js @@ -77,10 +77,13 @@ async function main() { const params = new URLSearchParams() if (args.domain) params.set('domain', args.domain) if (args.key) params.set('key', args.key) - result = await api('GET', `/links?${params}`) + if (args['link-id']) params.set('linkId', args['link-id']) + if (args['external-id']) params.set('externalId', args['external-id']) + result = await api('GET', `/links/info?${params}`) break } case 'update': { + if (!args.id) { result = { error: '--id required (link ID)' }; break } const body = {} if (args.url) body.url = args.url if (args.tags) body.tags = args.tags.split(',') @@ -88,6 +91,7 @@ async function main() { break } case 'delete': + if (!args.id) { result = { error: '--id required (link ID)' }; break } result = await api('DELETE', `/links/${args.id}`) break case 'bulk-create': { diff --git a/tools/clis/google-ads.js b/tools/clis/google-ads.js index 88f08b2..81c35a4 100755 --- a/tools/clis/google-ads.js +++ b/tools/clis/google-ads.js @@ -155,7 +155,7 @@ async function main() { operations: [{ update: { resourceName: `customers/${CUSTOMER_ID}/campaignBudgets/${args.id}`, - amountMicros, + amount_micros: amountMicros, }, updateMask: 'amount_micros', }], diff --git a/tools/clis/google-search-console.js b/tools/clis/google-search-console.js index 17c1641..ceecf8f 100755 --- a/tools/clis/google-search-console.js +++ b/tools/clis/google-search-console.js @@ -90,7 +90,7 @@ async function main() { result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body) break case 'countries': - body.dimensions = ['country', 'query'] + body.dimensions = ['country'] result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body) break default: @@ -106,6 +106,7 @@ async function main() { } switch (sub) { case 'url': + if (!args.url) { result = { error: '--url required (URL to inspect)' }; break } result = await api('POST', '/v1/urlInspection/index:inspect', { inspectionUrl: args.url, siteUrl: siteUrl, diff --git a/tools/clis/hotjar.js b/tools/clis/hotjar.js index 313dc78..e1fb505 100755 --- a/tools/clis/hotjar.js +++ b/tools/clis/hotjar.js @@ -2,7 +2,8 @@ const CLIENT_ID = process.env.HOTJAR_CLIENT_ID const CLIENT_SECRET = process.env.HOTJAR_CLIENT_SECRET -const BASE_URL = 'https://api.hotjar.io/v1' +const OAUTH_URL = 'https://api.hotjar.io' +const BASE_URL = 'https://api.hotjar.io/v2' if (!CLIENT_ID || !CLIENT_SECRET) { console.error(JSON.stringify({ error: 'HOTJAR_CLIENT_ID and HOTJAR_CLIENT_SECRET environment variables required' })) @@ -13,7 +14,7 @@ let cachedToken = null async function getToken() { if (cachedToken) return cachedToken - const res = await fetch(`${BASE_URL}/oauth/token`, { + const res = await fetch(`${OAUTH_URL}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `grant_type=client_credentials&client_id=${encodeURIComponent(CLIENT_ID)}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`, diff --git a/tools/clis/linkedin-ads.js b/tools/clis/linkedin-ads.js index 3ca4981..801500f 100755 --- a/tools/clis/linkedin-ads.js +++ b/tools/clis/linkedin-ads.js @@ -76,17 +76,19 @@ async function main() { } case 'create': { if (!args['account-id'] || !args.name) { result = { error: '--account-id and --name required' }; break } + if (!args['campaign-group-id']) { result = { error: '--campaign-group-id required' }; break } const body = { account: `urn:li:sponsoredAccount:${args['account-id']}`, + campaignGroup: `urn:li:sponsoredCampaignGroup:${args['campaign-group-id']}`, name: args.name, type: args.type || 'SPONSORED_UPDATES', costType: args['cost-type'] || 'CPC', unitCost: { - amount: args['unit-cost'] || '5.00', + amount: parseFloat(args['unit-cost'] || '5.00'), currencyCode: 'USD', }, dailyBudget: { - amount: args['daily-budget'] || '100.00', + amount: parseFloat(args['daily-budget'] || '100.00'), currencyCode: 'USD', }, status: 'PAUSED', diff --git a/tools/clis/mention-me.js b/tools/clis/mention-me.js index fba1f58..56e9881 100755 --- a/tools/clis/mention-me.js +++ b/tools/clis/mention-me.js @@ -76,10 +76,14 @@ async function main() { case 'referrals': switch (sub) { - case 'get': - result = await api('GET', `/referral/${rest[0]}`) + case 'get': { + const refId = rest[0] || args.id + if (!refId) { result = { error: 'Referral ID required (positional arg or --id)' }; break } + result = await api('GET', `/referral/${refId}`) break + } case 'list': { + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } result = await api('GET', `/referrer/${args['customer-id']}/referrals`) break } @@ -91,6 +95,7 @@ async function main() { case 'share-links': switch (sub) { case 'get': + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } result = await api('GET', `/referrer/${args['customer-id']}/share-links`) break default: @@ -101,9 +106,11 @@ async function main() { case 'rewards': switch (sub) { case 'get': + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } result = await api('GET', `/referrer/${args['customer-id']}/rewards`) break case 'redeem': { + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } const body = {} if (args['reward-id']) body.reward_id = args['reward-id'] if (args['order-number']) body.order_number = args['order-number'] diff --git a/tools/clis/optimizely.js b/tools/clis/optimizely.js index fb0c6a3..26a2318 100755 --- a/tools/clis/optimizely.js +++ b/tools/clis/optimizely.js @@ -135,7 +135,7 @@ async function main() { case 'archive': { const id = args.id if (!id) { result = { error: '--id required' }; break } - result = await api('DELETE', `/experiments/${id}`) + result = await api('PATCH', `/experiments/${id}`, { status: 'archived' }) break } default: diff --git a/tools/clis/resend.js b/tools/clis/resend.js index b0b9082..3e56f21 100755 --- a/tools/clis/resend.js +++ b/tools/clis/resend.js @@ -202,8 +202,9 @@ async function main() { result = await api('GET', `/webhooks/${rest[0]}`) break case 'create': { + if (!args.url) { result = { error: '--url required (webhook URL)' }; break } const events = args.events?.split(',') || ['email.sent', 'email.delivered', 'email.bounced'] - result = await api('POST', '/webhooks', { endpoint: args.endpoint, events }) + result = await api('POST', '/webhooks', { url: args.url, events }) break } case 'delete': diff --git a/tools/clis/rewardful.js b/tools/clis/rewardful.js index da2af02..9da3bba 100755 --- a/tools/clis/rewardful.js +++ b/tools/clis/rewardful.js @@ -9,13 +9,14 @@ if (!API_KEY) { } async function api(method, path, body) { + const auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64') if (args['dry-run']) { return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } } const res = await fetch(`${BASE_URL}${path}`, { method, headers: { - 'Authorization': `Bearer ${API_KEY}`, + 'Authorization': auth, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, diff --git a/tools/clis/snov.js b/tools/clis/snov.js index 0d47016..161f415 100755 --- a/tools/clis/snov.js +++ b/tools/clis/snov.js @@ -148,10 +148,10 @@ async function main() { case 'prospects': { const id = args.id if (!id) { result = { error: '--id required' }; break } - const params = new URLSearchParams({ listId: id }) - if (args.page) params.set('page', args.page) - if (args['per-page']) params.set('perPage', args['per-page']) - result = await api('GET', `/prospect-list?${params.toString()}`) + const body = { listId: id } + if (args.page) body.page = Number(args.page) + if (args['per-page']) body.perPage = Number(args['per-page']) + result = await api('POST', '/prospect-list', body) break } default: diff --git a/tools/clis/tolt.js b/tools/clis/tolt.js index 226b68c..42b513a 100755 --- a/tools/clis/tolt.js +++ b/tools/clis/tolt.js @@ -60,9 +60,12 @@ async function main() { case 'list': result = await api('GET', '/affiliates') break - case 'get': - result = await api('GET', `/affiliates/${rest[0]}`) + case 'get': { + const id = rest[0] || args.id + if (!id) { result = { error: 'Affiliate ID required (positional arg or --id)' }; break } + result = await api('GET', `/affiliates/${id}`) break + } case 'create': { const body = {} if (args.email) body.email = args.email @@ -71,6 +74,7 @@ async function main() { break } case 'update': { + if (!args.id) { result = { error: '--id required (affiliate ID)' }; break } const body = {} if (args['commission-rate']) body.commission_rate = Number(args['commission-rate']) if (args['payout-method']) body.payout_method = args['payout-method']