builtwith-official-cli 1.2.0 → 1.5.1
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/README.md +45 -0
- package/lib/cli.js +3 -0
- package/lib/client.js +27 -14
- package/lib/commands/auth.js +81 -0
- package/lib/commands/keyword-search.js +32 -0
- package/lib/commands/payment.js +67 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,6 +170,45 @@ bw products search "running shoes" --page 2 --limit 50
|
|
|
170
170
|
bw trust lookup <domain>
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
### 🔎 Vector Search
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bw vector search <query> [--limit <n>]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
bw vector search "react framework"
|
|
181
|
+
bw vector search "ecommerce platform" --limit 20
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 🔐 Auth
|
|
185
|
+
|
|
186
|
+
Obtain a temporary `bw-` prefixed API token via browser approval — no API key needed to run this command.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
bw auth login
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Flow:
|
|
193
|
+
1. Prints a `builtwith.com` URL — open it in your browser and click **Approve**
|
|
194
|
+
2. Polls automatically every 5 seconds
|
|
195
|
+
3. Prints the `access_token` (`bw-...`) on approval — use it as `BW_API_KEY`
|
|
196
|
+
|
|
197
|
+
### 💳 Payment
|
|
198
|
+
|
|
199
|
+
Manage API credits autonomously.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bw payment balance # current credit balance
|
|
203
|
+
bw payment config # limits, pricing, monthly usage
|
|
204
|
+
bw payment purchase <credits> # purchase credits (minimum 2000)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bw payment balance
|
|
209
|
+
bw payment purchase 2000
|
|
210
|
+
```
|
|
211
|
+
|
|
173
212
|
### 👤 Account
|
|
174
213
|
|
|
175
214
|
```bash
|
|
@@ -359,8 +398,14 @@ If your API key isn't in an env var or `.builtwithrc`, pass it inline:
|
|
|
359
398
|
| `trends_tech` | 📈 Historical adoption trend for a technology |
|
|
360
399
|
| `products_search` | 🛍️ Search ecommerce products across indexed stores |
|
|
361
400
|
| `trust_lookup` | 🛡️ Trust/quality score for a domain |
|
|
401
|
+
| `vector_search` | 🔎 Semantic search across technologies and categories |
|
|
402
|
+
| `payment_balance` | 💳 Get current Agent Payment API credit balance |
|
|
403
|
+
| `payment_config` | ⚙️ Retrieve payment limits and pricing configuration |
|
|
404
|
+
| `payment_purchase` | 🛒 Purchase API credits (minimum 2000) |
|
|
362
405
|
| `account_whoami` | 👤 Authenticated account identity |
|
|
363
406
|
| `account_usage` | 📊 API usage statistics |
|
|
407
|
+
| `agent-auth-start` | 🔐 Start Device-Code Authorization (no API key required) |
|
|
408
|
+
| `agent-auth-token` | 🔐 Poll for authorization result and access token (no API key required) |
|
|
364
409
|
|
|
365
410
|
### 🔬 Implementation note
|
|
366
411
|
|
package/lib/cli.js
CHANGED
|
@@ -36,10 +36,13 @@ function run() {
|
|
|
36
36
|
require('./commands/products')(program);
|
|
37
37
|
require('./commands/trust')(program);
|
|
38
38
|
require('./commands/vector')(program);
|
|
39
|
+
require('./commands/keyword-search')(program);
|
|
40
|
+
require('./commands/payment')(program);
|
|
39
41
|
require('./commands/account')(program);
|
|
40
42
|
require('./commands/live')(program);
|
|
41
43
|
require('./commands/mcp')(program);
|
|
42
44
|
require('./commands/schema')(program);
|
|
45
|
+
require('./commands/auth')(program);
|
|
43
46
|
|
|
44
47
|
program.parseAsync(process.argv).catch(err => {
|
|
45
48
|
if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
|
package/lib/client.js
CHANGED
|
@@ -37,8 +37,14 @@ const BASE_URLS = {
|
|
|
37
37
|
products: 'https://api.builtwith.com/productv1/api.json',
|
|
38
38
|
trust: 'https://api.builtwith.com/trustv1/api.json',
|
|
39
39
|
vector: 'https://api.builtwith.com/vector/v1/api.json',
|
|
40
|
+
'keyword-search': 'https://api.builtwith.com/kws1/api.json',
|
|
40
41
|
whoami: 'https://api.builtwith.com/whoamiv1/api.json',
|
|
41
42
|
usage: 'https://api.builtwith.com/usagev2/api.json',
|
|
43
|
+
'payment-balance': 'https://payments.builtwith.com/v1/billing/api-discovery',
|
|
44
|
+
'payment-config': 'https://payments.builtwith.com/v1/billing/api-configuration',
|
|
45
|
+
'payment-purchase': 'https://payments.builtwith.com/v1/billing/api-purchase',
|
|
46
|
+
'agent-auth-start': 'https://api.builtwith.com/agent-auth-start',
|
|
47
|
+
'agent-auth-token': 'https://api.builtwith.com/agent-auth-token',
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -62,7 +68,7 @@ function maskKey(url, key) {
|
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
async function request(endpoint, params, opts = {}) {
|
|
65
|
-
const { dryRun, debug, spinner } = opts;
|
|
71
|
+
const { dryRun, debug, spinner, method = 'GET', body } = opts;
|
|
66
72
|
const url = buildUrl(endpoint, params);
|
|
67
73
|
|
|
68
74
|
if (dryRun) {
|
|
@@ -72,17 +78,24 @@ async function request(endpoint, params, opts = {}) {
|
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
if (debug) {
|
|
75
|
-
process.stderr.write(`[debug]
|
|
81
|
+
process.stderr.write(`[debug] ${method} ${maskKey(url, params.KEY || params.key)}\n`);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
if (spinner) spinner.start();
|
|
79
85
|
|
|
86
|
+
const fetchOpts = {
|
|
87
|
+
method,
|
|
88
|
+
headers: { 'User-Agent': 'builtwith-cli/1.0.0' },
|
|
89
|
+
timeout: 30000,
|
|
90
|
+
};
|
|
91
|
+
if (body !== undefined) {
|
|
92
|
+
fetchOpts.body = JSON.stringify(body);
|
|
93
|
+
fetchOpts.headers['Content-Type'] = 'application/json';
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
let res;
|
|
81
97
|
try {
|
|
82
|
-
res = await fetch(url,
|
|
83
|
-
headers: { 'User-Agent': 'builtwith-cli/1.0.0' },
|
|
84
|
-
timeout: 30000,
|
|
85
|
-
});
|
|
98
|
+
res = await fetch(url, fetchOpts);
|
|
86
99
|
} catch (err) {
|
|
87
100
|
if (spinner) spinner.stop();
|
|
88
101
|
throw new NetworkError(`Network request failed: ${err.message}`);
|
|
@@ -94,30 +107,30 @@ async function request(endpoint, params, opts = {}) {
|
|
|
94
107
|
|
|
95
108
|
if (spinner) spinner.stop();
|
|
96
109
|
|
|
97
|
-
let
|
|
110
|
+
let resBody;
|
|
98
111
|
try {
|
|
99
|
-
|
|
112
|
+
resBody = await res.json();
|
|
100
113
|
} catch (_) {
|
|
101
114
|
throw new ApiError(`Invalid JSON response (HTTP ${res.status})`, res.status);
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
if (!res.ok) {
|
|
105
|
-
const msg =
|
|
106
|
-
?
|
|
118
|
+
const msg = resBody && resBody.Errors
|
|
119
|
+
? resBody.Errors.map(e => e.Message || e).join('; ')
|
|
107
120
|
: `HTTP ${res.status} ${res.statusText}`;
|
|
108
121
|
throw new ApiError(msg, res.status);
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
// BuiltWith API sometimes returns error info in the body with HTTP 200
|
|
112
|
-
if (
|
|
113
|
-
const msg =
|
|
125
|
+
if (resBody && resBody.Errors && resBody.Errors.length > 0) {
|
|
126
|
+
const msg = resBody.Errors.map(e => e.Message || e).join('; ');
|
|
114
127
|
// Treat auth-related messages as auth errors
|
|
115
128
|
const isAuth = /key|auth|unauthori/i.test(msg);
|
|
116
129
|
throw new ApiError(msg, isAuth ? 403 : 400);
|
|
117
130
|
}
|
|
118
131
|
|
|
119
|
-
scanForInjection(
|
|
120
|
-
return
|
|
132
|
+
scanForInjection(resBody);
|
|
133
|
+
return resBody;
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
module.exports = { buildUrl, maskKey, request, scanForInjection, BASE_URLS };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch');
|
|
4
|
+
const output = require('../output');
|
|
5
|
+
|
|
6
|
+
const AUTH_START_URL = 'https://api.builtwith.com/agent-auth-start';
|
|
7
|
+
const AUTH_TOKEN_URL = 'https://api.builtwith.com/agent-auth-token';
|
|
8
|
+
const POLL_INTERVAL_MS = 5000;
|
|
9
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
module.exports = function registerAuth(program) {
|
|
12
|
+
const auth = program.command('auth').description('Agent Device-Code Authorization – obtain a temporary API token');
|
|
13
|
+
|
|
14
|
+
auth
|
|
15
|
+
.command('login')
|
|
16
|
+
.description('Authorize this agent to use the BuiltWith API via browser approval')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
const opts = program.opts();
|
|
19
|
+
if (opts.noColor) output.setNoColor(true);
|
|
20
|
+
|
|
21
|
+
// Step 1: start the device-code flow
|
|
22
|
+
let startRes;
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(AUTH_START_URL, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: '{}',
|
|
28
|
+
});
|
|
29
|
+
startRes = await res.json();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
output.error(`Failed to start authorization: ${err.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { device_code, verification_uri } = startRes;
|
|
36
|
+
if (!device_code || !verification_uri) {
|
|
37
|
+
output.error('Unexpected response from authorization server.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 2: prompt user to open the browser
|
|
42
|
+
process.stderr.write(`\nOpen this URL in your browser to authorize access:\n\n ${verification_uri}\n\nWaiting for approval`);
|
|
43
|
+
|
|
44
|
+
// Step 3: poll every 5 seconds
|
|
45
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
46
|
+
while (Date.now() < deadline) {
|
|
47
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
48
|
+
process.stderr.write('.');
|
|
49
|
+
|
|
50
|
+
let tokenRes;
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(AUTH_TOKEN_URL, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ device_code }),
|
|
56
|
+
});
|
|
57
|
+
tokenRes = await res.json();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
output.error(`\nPolling error: ${err.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (tokenRes.access_token) {
|
|
64
|
+
process.stderr.write('\n\n');
|
|
65
|
+
output.print({ access_token: tokenRes.access_token, message: 'Authorization approved. Use this token as your BW_API_KEY.' }, { format: opts.format });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (tokenRes.error === 'access_denied') {
|
|
70
|
+
process.stderr.write('\n');
|
|
71
|
+
output.error('Authorization was denied by the user.');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
// error === 'authorization_pending' — keep polling
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
process.stderr.write('\n');
|
|
78
|
+
output.error('Authorization timed out after 5 minutes.');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireKey } = require('../config');
|
|
4
|
+
const { request } = require('../client');
|
|
5
|
+
const output = require('../output');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
|
|
8
|
+
module.exports = function registerKeywordSearch(program) {
|
|
9
|
+
const kws = program.command('keyword-search').description('Find websites containing a specific keyword');
|
|
10
|
+
|
|
11
|
+
kws
|
|
12
|
+
.command('search <keyword>')
|
|
13
|
+
.description('Search for websites by keyword')
|
|
14
|
+
.option('--limit <n>', 'Results per page (16-1000)', parseInt)
|
|
15
|
+
.option('--offset <offset>', 'Pagination offset (NextOffset from previous response)')
|
|
16
|
+
.action(async (keywordArg, cmdOpts) => {
|
|
17
|
+
const opts = program.opts();
|
|
18
|
+
if (opts.noColor) output.setNoColor(true);
|
|
19
|
+
const key = requireKey(opts.key);
|
|
20
|
+
const params = { KEY: key, KEYWORD: keywordArg };
|
|
21
|
+
if (cmdOpts.limit) params.LIMIT = cmdOpts.limit;
|
|
22
|
+
if (cmdOpts.offset) params.OFFSET = cmdOpts.offset;
|
|
23
|
+
const spinner = opts.quiet ? null : ora({ text: `Searching for "${keywordArg}"...`, stream: process.stderr }).start();
|
|
24
|
+
try {
|
|
25
|
+
const data = await request('keyword-search', params, { dryRun: opts.dryRun, debug: opts.debug, spinner });
|
|
26
|
+
output.print(data, { format: opts.format });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (spinner) spinner.stop();
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireKey } = require('../config');
|
|
4
|
+
const { request } = require('../client');
|
|
5
|
+
const { InputError } = require('../errors');
|
|
6
|
+
const output = require('../output');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
|
|
9
|
+
module.exports = function registerPayment(program) {
|
|
10
|
+
const payment = program.command('payment').description('Agent Payment API – manage and purchase API credits');
|
|
11
|
+
|
|
12
|
+
payment
|
|
13
|
+
.command('balance')
|
|
14
|
+
.description('Get current credit balance')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
const opts = program.opts();
|
|
17
|
+
if (opts.noColor) output.setNoColor(true);
|
|
18
|
+
const key = requireKey(opts.key);
|
|
19
|
+
const params = { KEY: key };
|
|
20
|
+
const spinner = opts.quiet ? null : ora({ text: 'Fetching credit balance...', stream: process.stderr }).start();
|
|
21
|
+
try {
|
|
22
|
+
const data = await request('payment-balance', params, { dryRun: opts.dryRun, debug: opts.debug, spinner });
|
|
23
|
+
output.print(data, { format: opts.format, fields: opts.fields });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (spinner) spinner.stop();
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
payment
|
|
31
|
+
.command('config')
|
|
32
|
+
.description('Retrieve payment configuration (limits, pricing)')
|
|
33
|
+
.action(async () => {
|
|
34
|
+
const opts = program.opts();
|
|
35
|
+
if (opts.noColor) output.setNoColor(true);
|
|
36
|
+
const key = requireKey(opts.key);
|
|
37
|
+
const params = { KEY: key };
|
|
38
|
+
const spinner = opts.quiet ? null : ora({ text: 'Fetching payment config...', stream: process.stderr }).start();
|
|
39
|
+
try {
|
|
40
|
+
const data = await request('payment-config', params, { dryRun: opts.dryRun, debug: opts.debug, spinner });
|
|
41
|
+
output.print(data, { format: opts.format, fields: opts.fields });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (spinner) spinner.stop();
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
payment
|
|
49
|
+
.command('purchase <credits>')
|
|
50
|
+
.description('Purchase API credits (minimum 2000)')
|
|
51
|
+
.action(async (creditsArg) => {
|
|
52
|
+
const opts = program.opts();
|
|
53
|
+
if (opts.noColor) output.setNoColor(true);
|
|
54
|
+
const key = requireKey(opts.key);
|
|
55
|
+
const credits = parseInt(creditsArg, 10);
|
|
56
|
+
if (isNaN(credits) || credits < 2000) throw new InputError('credits must be an integer of at least 2000');
|
|
57
|
+
const params = { KEY: key };
|
|
58
|
+
const spinner = opts.quiet ? null : ora({ text: `Purchasing ${credits} credits...`, stream: process.stderr }).start();
|
|
59
|
+
try {
|
|
60
|
+
const data = await request('payment-purchase', params, { dryRun: opts.dryRun, debug: opts.debug, spinner, method: 'POST', body: { credits } });
|
|
61
|
+
output.print(data, { format: opts.format, fields: opts.fields });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (spinner) spinner.stop();
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|