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/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
+ }