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 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] GET ${maskKey(url, params.KEY || params.key)}\n`);
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 body;
110
+ let resBody;
98
111
  try {
99
- body = await res.json();
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 = body && body.Errors
106
- ? body.Errors.map(e => e.Message || e).join('; ')
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 (body && body.Errors && body.Errors.length > 0) {
113
- const msg = body.Errors.map(e => e.Message || e).join('; ');
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(body);
120
- return body;
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "builtwith-official-cli",
3
- "version": "1.2.0",
3
+ "version": "1.5.1",
4
4
  "description": "Non-interactive, scriptable CLI for the BuiltWith API",
5
5
  "main": "lib/cli.js",
6
6
  "bin": {