scrapercity 1.0.0
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/AGENT_DOCS.txt +205 -0
- package/README.md +128 -0
- package/SKILL.md +98 -0
- package/bin/cli.mjs +518 -0
- package/bin/mcp.mjs +474 -0
- package/lib/client.mjs +215 -0
- package/package.json +30 -0
package/lib/client.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// lib/client.mjs — Shared ScraperCity API client
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
|
|
6
|
+
const BASE = 'https://app.scrapercity.com'
|
|
7
|
+
const RC_PATH = path.join(os.homedir(), '.scrapercityrc')
|
|
8
|
+
|
|
9
|
+
// ── Auth ──────────────────────────────────────────────────────
|
|
10
|
+
export function getApiKey() {
|
|
11
|
+
// 1) env var
|
|
12
|
+
if (process.env.SCRAPERCITY_API_KEY) return process.env.SCRAPERCITY_API_KEY
|
|
13
|
+
// 2) rc file
|
|
14
|
+
if (fs.existsSync(RC_PATH)) {
|
|
15
|
+
const rc = JSON.parse(fs.readFileSync(RC_PATH, 'utf-8'))
|
|
16
|
+
if (rc.apiKey) return rc.apiKey
|
|
17
|
+
}
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveApiKey(key) {
|
|
22
|
+
fs.writeFileSync(RC_PATH, JSON.stringify({ apiKey: key }), { mode: 0o600 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── HTTP helpers ──────────────────────────────────────────────
|
|
26
|
+
async function req(method, urlPath, body) {
|
|
27
|
+
const key = getApiKey()
|
|
28
|
+
if (!key) throw new Error('No API key. Run: scrapercity login OR export SCRAPERCITY_API_KEY=...')
|
|
29
|
+
|
|
30
|
+
const opts = {
|
|
31
|
+
method,
|
|
32
|
+
headers: {
|
|
33
|
+
'Authorization': `Bearer ${key}`,
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'User-Agent': 'scrapercity-cli/1.0'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (body && method !== 'GET') opts.body = JSON.stringify(body)
|
|
39
|
+
|
|
40
|
+
const url = urlPath.startsWith('http') ? urlPath : `${BASE}${urlPath}`
|
|
41
|
+
const res = await fetch(url, opts)
|
|
42
|
+
const text = await res.text()
|
|
43
|
+
|
|
44
|
+
let data
|
|
45
|
+
try { data = JSON.parse(text) } catch { data = { raw: text } }
|
|
46
|
+
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const msg = data.error || data.message || `HTTP ${res.status}`
|
|
49
|
+
const err = new Error(msg)
|
|
50
|
+
err.status = res.status
|
|
51
|
+
err.data = data
|
|
52
|
+
throw err
|
|
53
|
+
}
|
|
54
|
+
return data
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const get = (p) => req('GET', p)
|
|
58
|
+
const post = (p, b) => req('POST', p, b)
|
|
59
|
+
|
|
60
|
+
// ── Wallet & Runs ─────────────────────────────────────────────
|
|
61
|
+
export const wallet = () => get('/api/v1/wallet')
|
|
62
|
+
export const runs = (hours = 24, limit = 50) => get(`/api/v1/runs?hours=${hours}&limit=${limit}`)
|
|
63
|
+
|
|
64
|
+
// ── Status / Download / Cancel / Logs ─────────────────────────
|
|
65
|
+
export const status = (runId) => get(`/api/v1/scrape/status/${runId}`)
|
|
66
|
+
export const cancel = (runId) => post(`/api/v1/scrape/cancel/${runId}`)
|
|
67
|
+
export const logs = (runId) => get(`/api/v1/scrape/logs/${runId}`)
|
|
68
|
+
|
|
69
|
+
export async function download(runId, outputPath) {
|
|
70
|
+
const key = getApiKey()
|
|
71
|
+
if (!key) throw new Error('No API key')
|
|
72
|
+
const res = await fetch(`${BASE}/api/downloads/${runId}`, {
|
|
73
|
+
headers: { 'Authorization': `Bearer ${key}` }
|
|
74
|
+
})
|
|
75
|
+
if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`)
|
|
76
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
77
|
+
const out = outputPath || `${runId}.csv`
|
|
78
|
+
fs.writeFileSync(out, buf)
|
|
79
|
+
return { path: out, bytes: buf.length }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Apollo ────────────────────────────────────────────────────
|
|
83
|
+
export const apollo = (url, count = 1000, fileName = '') =>
|
|
84
|
+
post('/api/v1/scrape/apollo', { url, count, fileName })
|
|
85
|
+
|
|
86
|
+
export const apolloFilters = (filters) =>
|
|
87
|
+
post('/api/v1/scrape/apollo-filters', filters)
|
|
88
|
+
|
|
89
|
+
export const apolloStatus = () => get('/api/v1/apollo-status')
|
|
90
|
+
|
|
91
|
+
// ── Maps ──────────────────────────────────────────────────────
|
|
92
|
+
export const maps = (query, location, limit = 500) =>
|
|
93
|
+
post('/api/v1/scrape/maps', {
|
|
94
|
+
searchStringsArray: Array.isArray(query) ? query : [query],
|
|
95
|
+
locationQuery: location,
|
|
96
|
+
maxCrawledPlacesPerSearch: limit
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// ── Email Validator ───────────────────────────────────────────
|
|
100
|
+
export const emailValidate = (emails) =>
|
|
101
|
+
post('/api/v1/scrape/email-validator', { emails })
|
|
102
|
+
|
|
103
|
+
// ── Email Finder ──────────────────────────────────────────────
|
|
104
|
+
export const emailFind = (contacts, opts = {}) =>
|
|
105
|
+
post('/api/v1/scrape/email-finder', {
|
|
106
|
+
contacts,
|
|
107
|
+
autoValidateEmails: opts.validate || false,
|
|
108
|
+
autoFindMobiles: opts.mobiles || false
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// ── Mobile Finder ─────────────────────────────────────────────
|
|
112
|
+
export const mobileFinder = (inputs) =>
|
|
113
|
+
post('/api/v1/scrape/mobile-finder', { inputs })
|
|
114
|
+
|
|
115
|
+
// ── People Finder / Skip Trace ────────────────────────────────
|
|
116
|
+
export const peopleFinder = (params) =>
|
|
117
|
+
post('/api/v1/scrape/people-finder', params)
|
|
118
|
+
|
|
119
|
+
// ── Store Leads (Ecommerce) ───────────────────────────────────
|
|
120
|
+
export const storeLeads = (params) =>
|
|
121
|
+
post('/api/v1/scrape/store-leads', params)
|
|
122
|
+
|
|
123
|
+
// ── BuiltWith ─────────────────────────────────────────────────
|
|
124
|
+
export const builtwith = (technology, fileName) =>
|
|
125
|
+
post('/api/v1/scrape/builtwith', { technology, fileName })
|
|
126
|
+
|
|
127
|
+
// ── Criminal Records ──────────────────────────────────────────
|
|
128
|
+
export const criminal = (name, state, dob) =>
|
|
129
|
+
post('/api/v1/scrape/criminal-records', { name, state, dob })
|
|
130
|
+
|
|
131
|
+
// ── Airbnb Email ──────────────────────────────────────────────
|
|
132
|
+
export const airbnb = (params) =>
|
|
133
|
+
post('/api/v1/scrape/airbnb-email', params)
|
|
134
|
+
|
|
135
|
+
// ── YouTube Email ─────────────────────────────────────────────
|
|
136
|
+
export const youtubeEmail = (channels) =>
|
|
137
|
+
post('/api/v1/scrape/youtube-email', { channels })
|
|
138
|
+
|
|
139
|
+
// ── Website Finder ────────────────────────────────────────────
|
|
140
|
+
export const websiteFinder = (domains, jobTitle) =>
|
|
141
|
+
post('/api/v1/scrape/website-finder', { domains, jobTitle })
|
|
142
|
+
|
|
143
|
+
// ── Yelp ──────────────────────────────────────────────────────
|
|
144
|
+
export const yelp = (params) =>
|
|
145
|
+
post('/api/v1/scrape/yelp-scraper', params)
|
|
146
|
+
|
|
147
|
+
// ── Angi (Angie's List) ──────────────────────────────────────
|
|
148
|
+
export const angi = (keyword, zipCodes, maxItems = 100) =>
|
|
149
|
+
post('/api/v1/scrape/angi-angies-list-scraper', { keyword, zipCodes, maxItems })
|
|
150
|
+
|
|
151
|
+
// ── Zillow Agents ─────────────────────────────────────────────
|
|
152
|
+
export const zillowAgents = (location, opts = {}) =>
|
|
153
|
+
post('/api/v1/scrape/zillow-agents', { location, ...opts })
|
|
154
|
+
|
|
155
|
+
// ── BizBuySell ────────────────────────────────────────────────
|
|
156
|
+
export const bizbuysell = (startUrls, maxItems = 100) =>
|
|
157
|
+
post('/api/v1/scrape/bizbuysell-scraper', { startUrls, maxItems })
|
|
158
|
+
|
|
159
|
+
// ── Crexi ─────────────────────────────────────────────────────
|
|
160
|
+
export const crexi = (startUrls) =>
|
|
161
|
+
post('/api/v1/scrape/crexi-scraper', { startUrls })
|
|
162
|
+
|
|
163
|
+
// ── Property Lookup ───────────────────────────────────────────
|
|
164
|
+
export const propertyLookup = (addresses, includeOwnerContact = false) =>
|
|
165
|
+
post('/api/v1/scrape/property-lookup', { addresses, includeOwnerContact })
|
|
166
|
+
|
|
167
|
+
// ── Generic Scraper (fallback for any slug) ───────────────────
|
|
168
|
+
export const scrape = (slug, body) =>
|
|
169
|
+
post(`/api/v1/scrape/${slug}`, body)
|
|
170
|
+
|
|
171
|
+
// ── Databases ($649 plan) ─────────────────────────────────────
|
|
172
|
+
export const dbLeads = (params) => {
|
|
173
|
+
const qs = new URLSearchParams()
|
|
174
|
+
for (const [k, v] of Object.entries(params)) {
|
|
175
|
+
if (v === undefined || v === null || v === '') continue
|
|
176
|
+
if (Array.isArray(v)) v.forEach(x => qs.append(k, x))
|
|
177
|
+
else qs.append(k, String(v))
|
|
178
|
+
}
|
|
179
|
+
return get(`/api/v1/database/leads?${qs}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const dbLocalBusinesses = (params) => {
|
|
183
|
+
const qs = new URLSearchParams()
|
|
184
|
+
for (const [k, v] of Object.entries(params)) {
|
|
185
|
+
if (v === undefined || v === null || v === '') continue
|
|
186
|
+
if (Array.isArray(v)) v.forEach(x => qs.append(k, x))
|
|
187
|
+
else qs.append(k, String(v))
|
|
188
|
+
}
|
|
189
|
+
return get(`/api/v1/database/local-businesses?${qs}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const dbEcommerce = (params) => {
|
|
193
|
+
const qs = new URLSearchParams()
|
|
194
|
+
for (const [k, v] of Object.entries(params)) {
|
|
195
|
+
if (v === undefined || v === null || v === '') continue
|
|
196
|
+
if (Array.isArray(v)) v.forEach(x => qs.append(k, x))
|
|
197
|
+
else qs.append(k, String(v))
|
|
198
|
+
}
|
|
199
|
+
return get(`/api/v1/database/ecommerce?${qs}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Poll until done ───────────────────────────────────────────
|
|
203
|
+
export async function pollUntilDone(runId, { interval = 15000, timeout = 3600000, onStatus } = {}) {
|
|
204
|
+
const start = Date.now()
|
|
205
|
+
while (Date.now() - start < timeout) {
|
|
206
|
+
const s = await status(runId)
|
|
207
|
+
if (onStatus) onStatus(s)
|
|
208
|
+
if (s.status === 'SUCCEEDED') return s
|
|
209
|
+
if (s.status === 'FAILED' || s.status === 'CANCELLED') {
|
|
210
|
+
throw new Error(`Run ${runId} ended with status: ${s.status} — ${s.statusMessage || ''}`)
|
|
211
|
+
}
|
|
212
|
+
await new Promise(r => setTimeout(r, interval))
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`Timed out after ${timeout / 1000}s waiting for run ${runId}`)
|
|
215
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scrapercity",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ScraperCity CLI & MCP Server - B2B lead generation for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"scrapercity": "./bin/cli.mjs",
|
|
8
|
+
"scrapercity-mcp": "./bin/mcp.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"SKILL.md",
|
|
14
|
+
"AGENT_DOCS.txt",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"scrapercity", "mcp", "mcp-server", "lead-generation", "b2b",
|
|
19
|
+
"scraping", "apollo", "google-maps", "email-finder", "ai-agent",
|
|
20
|
+
"claude-code", "cursor"
|
|
21
|
+
],
|
|
22
|
+
"author": "ScraperCity",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://scrapercity.com/agents",
|
|
25
|
+
"repository": { "type": "git", "url": "https://github.com/scrapercity/scrapercity-cli" },
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": { "node": ">=18" }
|
|
30
|
+
}
|