fix: address codex review findings across new CLI tools

- supermetrics: fix query endpoint /query → /query/data/json
- airops: fix base URL /v1 → /public_api/v1
- zoominfo: fix auth --dry-run leaking real JWT, add response validation
- outreach: remove parseInt() on JSON:API string IDs (caused NaN)
- similarweb: add encodeURIComponent on domain in all URL paths
- coupler: fix dry-run auth mask from '***' to 'Bearer ***'
- clay: allow name-based enrich (--first-name + --last-name + --domain)
- pendo.md: fix guide state from 'published' to 'public'
- close.md: fix rate limit header names to ratelimit-*

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Corey Haines 2026-03-02 23:08:36 -08:00
parent 4ff486a702
commit 9a2bb97784
11 changed files with 41 additions and 31 deletions

View file

@ -2,7 +2,7 @@
const API_KEY = process.env.AIROPS_API_KEY const API_KEY = process.env.AIROPS_API_KEY
const WORKSPACE_ID = process.env.AIROPS_WORKSPACE_ID const WORKSPACE_ID = process.env.AIROPS_WORKSPACE_ID
const BASE_URL = 'https://api.airops.com/v1' const BASE_URL = 'https://api.airops.com/public_api/v1'
if (!API_KEY) { if (!API_KEY) {
console.error(JSON.stringify({ error: 'AIROPS_API_KEY environment variable required' })) console.error(JSON.stringify({ error: 'AIROPS_API_KEY environment variable required' }))

View file

@ -104,8 +104,9 @@ async function main() {
if (args.linkedin) body.linkedin_url = args.linkedin if (args.linkedin) body.linkedin_url = args.linkedin
if (args['first-name']) body.first_name = args['first-name'] if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name'] if (args['last-name']) body.last_name = args['last-name']
if (!args.email && !args.linkedin) { if (args['first-name'] && args['last-name'] && args.domain) body.domain = args.domain
result = { error: '--email or --linkedin required' } if (!args.email && !args.linkedin && !(args['first-name'] && args['last-name'] && args.domain)) {
result = { error: '--email or --linkedin required (or --first-name + --last-name + --domain)' }
break break
} }
result = await api('POST', '/people/enrich', body) result = await api('POST', '/people/enrich', body)
@ -140,7 +141,7 @@ async function main() {
'add-row': 'tables add-row --id <table_id> --data <json>', 'add-row': 'tables add-row --id <table_id> --data <json>',
}, },
people: { people: {
enrich: 'people enrich --email <email> | --linkedin <url>', enrich: 'people enrich --email <email> | --linkedin <url> | --first-name <n> --last-name <n> --domain <d>',
}, },
companies: { companies: {
enrich: 'companies enrich --domain <domain>', enrich: 'companies enrich --domain <domain>',

View file

@ -10,7 +10,7 @@ if (!API_KEY) {
async function api(method, path, body) { async function api(method, path, body) {
if (args['dry-run']) { if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json' }, body: body || undefined } return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': 'Bearer ***', 'Content-Type': 'application/json' }, body: body || undefined }
} }
const res = await fetch(`${BASE_URL}${path}`, { const res = await fetch(`${BASE_URL}${path}`, {
method, method,

View file

@ -116,8 +116,8 @@ async function main() {
data: { data: {
type: 'sequenceState', type: 'sequenceState',
relationships: { relationships: {
prospect: { data: { type: 'prospect', id: parseInt(prospectId) } }, prospect: { data: { type: 'prospect', id: prospectId } },
sequence: { data: { type: 'sequence', id: parseInt(sequenceId) } }, sequence: { data: { type: 'sequence', id: sequenceId } },
}, },
}, },
} }

View file

@ -67,7 +67,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
if (args.granularity) params.set('granularity', args.granularity) if (args.granularity) params.set('granularity', args.granularity)
result = await api('GET', `/website/${domain}/total-traffic-and-engagement/visits?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/total-traffic-and-engagement/visits?${params.toString()}`)
break break
} }
case 'pages-per-visit': { case 'pages-per-visit': {
@ -78,7 +78,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
if (args.granularity) params.set('granularity', args.granularity) if (args.granularity) params.set('granularity', args.granularity)
result = await api('GET', `/website/${domain}/total-traffic-and-engagement/pages-per-visit?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/total-traffic-and-engagement/pages-per-visit?${params.toString()}`)
break break
} }
case 'avg-duration': { case 'avg-duration': {
@ -89,7 +89,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
if (args.granularity) params.set('granularity', args.granularity) if (args.granularity) params.set('granularity', args.granularity)
result = await api('GET', `/website/${domain}/total-traffic-and-engagement/average-visit-duration?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/total-traffic-and-engagement/average-visit-duration?${params.toString()}`)
break break
} }
case 'bounce-rate': { case 'bounce-rate': {
@ -100,7 +100,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
if (args.granularity) params.set('granularity', args.granularity) if (args.granularity) params.set('granularity', args.granularity)
result = await api('GET', `/website/${domain}/total-traffic-and-engagement/bounce-rate?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/total-traffic-and-engagement/bounce-rate?${params.toString()}`)
break break
} }
case 'sources': { case 'sources': {
@ -110,7 +110,7 @@ async function main() {
if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break } if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break }
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
result = await api('GET', `/website/${domain}/traffic-sources/overview?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/traffic-sources/overview?${params.toString()}`)
break break
} }
default: default:
@ -125,7 +125,7 @@ async function main() {
if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break } if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break }
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
result = await api('GET', `/website/${domain}/traffic-sources/referrals?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/traffic-sources/referrals?${params.toString()}`)
break break
} }
@ -139,7 +139,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
if (args.limit) params.set('limit', args.limit) if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/website/${domain}/search/organic-search-keywords?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/search/organic-search-keywords?${params.toString()}`)
break break
} }
case 'keywords-paid': { case 'keywords-paid': {
@ -150,7 +150,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country) if (args.country) params.set('country', args.country)
if (args.limit) params.set('limit', args.limit) if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/website/${domain}/search/paid-search-keywords?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/search/paid-search-keywords?${params.toString()}`)
break break
} }
default: default:
@ -161,14 +161,14 @@ async function main() {
case 'competitors': { case 'competitors': {
const domain = args.domain const domain = args.domain
if (!domain) { result = { error: '--domain required' }; break } if (!domain) { result = { error: '--domain required' }; break }
result = await api('GET', `/website/${domain}/similar-sites/similarsites`) result = await api('GET', `/website/${encodeURIComponent(domain)}/similar-sites/similarsites`)
break break
} }
case 'category-rank': { case 'category-rank': {
const domain = args.domain const domain = args.domain
if (!domain) { result = { error: '--domain required' }; break } if (!domain) { result = { error: '--domain required' }; break }
result = await api('GET', `/website/${domain}/category-rank/category-rank`) result = await api('GET', `/website/${encodeURIComponent(domain)}/category-rank/category-rank`)
break break
} }
@ -178,7 +178,7 @@ async function main() {
if (!args.start) { result = { error: '--start required (YYYY-MM)' }; break } if (!args.start) { result = { error: '--start required (YYYY-MM)' }; break }
if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break } if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break }
const params = new URLSearchParams({ start_date: args.start, end_date: args.end }) const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
result = await api('GET', `/website/${domain}/geo/traffic-by-country?${params.toString()}`) result = await api('GET', `/website/${encodeURIComponent(domain)}/geo/traffic-by-country?${params.toString()}`)
break break
} }

View file

@ -76,7 +76,7 @@ async function main() {
if (args['max-rows']) body.max_rows = parseInt(args['max-rows'], 10) if (args['max-rows']) body.max_rows = parseInt(args['max-rows'], 10)
if (args['start-date']) body.start_date = args['start-date'] if (args['start-date']) body.start_date = args['start-date']
if (args['end-date']) body.end_date = args['end-date'] if (args['end-date']) body.end_date = args['end-date']
result = await api('POST', '/query', body) result = await api('POST', '/query/data/json', body)
break break
} }

View file

@ -22,11 +22,16 @@ async function authenticate() {
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password }),
}) })
const text = await res.text() const text = await res.text()
if (!res.ok) {
throw new Error(`Authentication failed (${res.status}): ${text}`)
}
try { try {
const data = JSON.parse(text) const data = JSON.parse(text)
if (!data.jwt) throw new Error('No JWT in response')
ACCESS_TOKEN = data.jwt ACCESS_TOKEN = data.jwt
return ACCESS_TOKEN return ACCESS_TOKEN
} catch { } catch (e) {
if (e.message === 'No JWT in response') throw e
throw new Error(`Authentication failed: ${text}`) throw new Error(`Authentication failed: ${text}`)
} }
} }
@ -82,6 +87,10 @@ async function main() {
switch (cmd) { switch (cmd) {
case 'auth': { case 'auth': {
if (args['dry-run']) {
result = { _dry_run: true, action: 'authenticate', url: `${BASE_URL}/authenticate`, jwt: '***' }
break
}
const token = await authenticate() const token = await authenticate()
result = { jwt: token } result = { jwt: token }
break break

View file

@ -23,19 +23,19 @@ AI content platform for crafting content that wins AI search. Build and execute
### List Flows ### List Flows
```bash ```bash
GET https://api.airops.com/v1/workspaces/{workspace_id}/flows GET https://api.airops.com/public_api/v1/workspaces/{workspace_id}/flows
``` ```
### Get Flow Details ### Get Flow Details
```bash ```bash
GET https://api.airops.com/v1/workspaces/{workspace_id}/flows/{flow_id} GET https://api.airops.com/public_api/v1/workspaces/{workspace_id}/flows/{flow_id}
``` ```
### Execute a Flow ### Execute a Flow
```bash ```bash
POST https://api.airops.com/v1/workspaces/{workspace_id}/flows/{flow_id}/execute POST https://api.airops.com/public_api/v1/workspaces/{workspace_id}/flows/{flow_id}/execute
{ {
"inputs": { "inputs": {
@ -48,25 +48,25 @@ POST https://api.airops.com/v1/workspaces/{workspace_id}/flows/{flow_id}/execute
### List Runs for a Flow ### List Runs for a Flow
```bash ```bash
GET https://api.airops.com/v1/workspaces/{workspace_id}/flows/{flow_id}/runs GET https://api.airops.com/public_api/v1/workspaces/{workspace_id}/flows/{flow_id}/runs
``` ```
### Get Run Status ### Get Run Status
```bash ```bash
GET https://api.airops.com/v1/workspaces/{workspace_id}/runs/{run_id} GET https://api.airops.com/public_api/v1/workspaces/{workspace_id}/runs/{run_id}
``` ```
### List Workflows ### List Workflows
```bash ```bash
GET https://api.airops.com/v1/workspaces/{workspace_id}/workflows GET https://api.airops.com/public_api/v1/workspaces/{workspace_id}/workflows
``` ```
### Execute a Workflow ### Execute a Workflow
```bash ```bash
POST https://api.airops.com/v1/workspaces/{workspace_id}/workflows/{workflow_id}/execute POST https://api.airops.com/public_api/v1/workspaces/{workspace_id}/workflows/{workflow_id}/execute
{ {
"inputs": { "inputs": {

View file

@ -181,7 +181,7 @@ POST https://api.close.com/api/v1/task/
- Rate limits based on organization plan - Rate limits based on organization plan
- Standard: ~100 requests/minute - Standard: ~100 requests/minute
- Responses include `X-Rate-Limit-Limit` and `X-Rate-Limit-Remaining` headers - Responses include `ratelimit-limit` and `ratelimit-remaining` headers
- 429 responses include `Retry-After` header - 429 responses include `Retry-After` header
## Relevant Skills ## Relevant Skills

View file

@ -46,7 +46,7 @@ GET https://app.pendo.io/api/v1/page/{pageId}
### List Guides ### List Guides
```bash ```bash
GET https://app.pendo.io/api/v1/guide?state=published GET https://app.pendo.io/api/v1/guide?state=public
``` ```
### Get Guide Details ### Get Guide Details

View file

@ -22,7 +22,7 @@ Marketing data pipeline that connects 200+ marketing platforms. Pulls data from
### Query a Data Source ### Query a Data Source
```bash ```bash
POST https://api.supermetrics.com/enterprise/v2/query POST https://api.supermetrics.com/enterprise/v2/query/data/json
{ {
"ds_id": "GA4", "ds_id": "GA4",
@ -39,7 +39,7 @@ POST https://api.supermetrics.com/enterprise/v2/query
### Query with Filters ### Query with Filters
```bash ```bash
POST https://api.supermetrics.com/enterprise/v2/query POST https://api.supermetrics.com/enterprise/v2/query/data/json
{ {
"ds_id": "AW", "ds_id": "AW",