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

View file

@ -10,7 +10,7 @@ if (!API_KEY) {
async function api(method, path, body) {
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}`, {
method,

View file

@ -116,8 +116,8 @@ async function main() {
data: {
type: 'sequenceState',
relationships: {
prospect: { data: { type: 'prospect', id: parseInt(prospectId) } },
sequence: { data: { type: 'sequence', id: parseInt(sequenceId) } },
prospect: { data: { type: 'prospect', id: prospectId } },
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 })
if (args.country) params.set('country', args.country)
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
}
case 'pages-per-visit': {
@ -78,7 +78,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country)
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
}
case 'avg-duration': {
@ -89,7 +89,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country)
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
}
case 'bounce-rate': {
@ -100,7 +100,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country)
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
}
case 'sources': {
@ -110,7 +110,7 @@ async function main() {
if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break }
const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
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
}
default:
@ -125,7 +125,7 @@ async function main() {
if (!args.end) { result = { error: '--end required (YYYY-MM)' }; break }
const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
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
}
@ -139,7 +139,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country)
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
}
case 'keywords-paid': {
@ -150,7 +150,7 @@ async function main() {
const params = new URLSearchParams({ start_date: args.start, end_date: args.end })
if (args.country) params.set('country', args.country)
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
}
default:
@ -161,14 +161,14 @@ async function main() {
case 'competitors': {
const domain = args.domain
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
}
case 'category-rank': {
const domain = args.domain
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
}
@ -178,7 +178,7 @@ async function main() {
if (!args.start) { result = { error: '--start 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 })
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
}

View file

@ -76,7 +76,7 @@ async function main() {
if (args['max-rows']) body.max_rows = parseInt(args['max-rows'], 10)
if (args['start-date']) body.start_date = args['start-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
}

View file

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

View file

@ -23,19 +23,19 @@ AI content platform for crafting content that wins AI search. Build and execute
### List Flows
```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
```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
```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": {
@ -48,25 +48,25 @@ POST https://api.airops.com/v1/workspaces/{workspace_id}/flows/{flow_id}/execute
### List Runs for a Flow
```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
```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
```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
```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": {

View file

@ -181,7 +181,7 @@ POST https://api.close.com/api/v1/task/
- Rate limits based on organization plan
- 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
## Relevant Skills

View file

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

View file

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