@hunt-labs/bytekit-cli 0.5.0 → 0.7.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.
@@ -1,11 +1,13 @@
1
1
  import { Command, InvalidArgumentError } from 'commander';
2
2
  import { buildClient } from '../client.js';
3
3
  import { printResult, emitFetchResult, handleError } from '../output.js';
4
- import { parseUrlLines } from '../lib/url-lines.js';
5
- import { parseIntFlag, parseEnum, parseCacheTtl } from '../lib/parse-flags.js';
4
+ import { parseBulkLines } from '../lib/url-lines.js';
5
+ import { parseIntFlag, parseEnum, parseCacheTtl, parseJsonFlag } from '../lib/parse-flags.js';
6
6
  const FETCH_FORMATS = ['markdown', 'html'];
7
7
  /** The `FetchRequest.markdown_mode` spec enum (create/POST only — excluded from the GET query). */
8
8
  const MARKDOWN_MODES = ['article', 'raw', 'llm'];
9
+ /** The `markdown_links` / `markdown_images` render-mode enum (shared with `scrape create`). */
10
+ const MARKDOWN_RENDER_MODES = ['inline', 'references', 'none', 'text'];
9
11
  /**
10
12
  * Validate `--format` locally, before any HTTP request is issued.
11
13
  *
@@ -28,14 +30,32 @@ export function makeFetchCommand(getGlobal) {
28
30
  bulkCmd
29
31
  .command('create')
30
32
  .description('Create a bulk fetch job')
31
- .requiredOption('--file <path>', 'Path to newline-delimited file of URLs (plain URL or {"url":"..."} per line)')
33
+ .requiredOption('--file <path>', 'Path to a newline-delimited file. Each line is a plain URL, or a JSON object carrying "url" plus per-item config. A line whose only key is "url" behaves exactly as before (sent as a bare URL string); any line with additional keys switches the payload to the per-item shape.')
32
34
  .requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified when the job completes')
35
+ .option('--webhook-secret <secret>', 'HMAC secret used to sign the completion webhook (omitted → unsigned)')
36
+ .option('--defaults <json>', 'Defaults applied to every entry, as a JSON object (omitted → none). Unknown keys are rejected by /v1/bulk (422) and ignored by scrape bulk / fetch bulk; fetch bulk additionally rejects browser-only keys (cookies, wait_until, wait_for_selector, delay_ms) with a 400.', (v) => parseJsonFlag('--defaults', v, 'object'))
37
+ .option('--metadata <json>', `Arbitrary metadata echoed on the job envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
38
+ // Deliberately NO --custom: /v1/fetch/bulk has no top-level `custom` property
39
+ // (validators/fetch-bulk.ts:52-62), so the flag would be silently stripped (#2596 drift D2).
40
+ // FetchBulkBodySchema itself stays non-strict (issue #2648 only makes the per-item
41
+ // FetchBulkItemOverrideSchema reject unknown keys), so this top-level behavior is unchanged.
33
42
  .action(async (opts) => {
34
43
  const g = getGlobal();
35
- const client = buildClient(g);
36
44
  try {
37
- const urls = parseUrlLines(opts.file);
38
- const result = await client.fetch.bulk.create({ urls, webhook_url: opts.webhookUrl });
45
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
46
+ const client = buildClient(g);
47
+ // /v1/fetch/bulk has NO `items` property — its `urls` is a
48
+ // `string | FetchBulkItemOverride` union (validators/fetch-bulk.ts:57), so both line
49
+ // shapes ride there directly and no promotion step is needed (#2596 drift D3).
50
+ const urls = parseBulkLines(opts.file);
51
+ const body = { urls, webhook_url: opts.webhookUrl };
52
+ if (opts.webhookSecret !== undefined)
53
+ body['webhook_secret'] = opts.webhookSecret;
54
+ if (opts.defaults !== undefined)
55
+ body['defaults'] = opts.defaults;
56
+ if (opts.metadata !== undefined)
57
+ body['metadata'] = opts.metadata;
58
+ const result = await client.fetch.bulk.create(body);
39
59
  printResult(result, g.json);
40
60
  }
41
61
  catch (err) {
@@ -47,8 +67,9 @@ export function makeFetchCommand(getGlobal) {
47
67
  .description('Get a bulk fetch job')
48
68
  .action(async (id) => {
49
69
  const g = getGlobal();
50
- const client = buildClient(g);
51
70
  try {
71
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
72
+ const client = buildClient(g);
52
73
  const result = await client.fetch.bulk.get(id);
53
74
  printResult(result, g.json);
54
75
  }
@@ -65,23 +86,64 @@ export function makeFetchCommand(getGlobal) {
65
86
  .option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
66
87
  .option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
67
88
  .option('--cache-ttl <ttl>', 'Cache freshness: 0, Nh (hours), or Nd (days), e.g. 48h, 2d, 0 (omitted → server default)', (v) => parseCacheTtl('--cache-ttl', v))
68
- // `--markdown-mode` is POST-only: FetchGetOpts excludes it, so adding it to `get` below would be
69
- // a compile error — the create/get asymmetry is correct by construction.
89
+ // The markdown surface below is POST-only: FetchGetOpts excludes it, so adding any of it to
90
+ // `get` would be a compile error — the create/get asymmetry is correct by construction.
70
91
  .option('--markdown-mode <mode>', `Markdown mode: ${MARKDOWN_MODES.join(', ')}`, (v) => parseEnum('--markdown-mode', MARKDOWN_MODES, v))
92
+ .option('--markdown-query <q>', 'BM25 query string for relevance-ranked markdown filtering (omitted → disabled)')
93
+ .option('--markdown-links <mode>', `Link rendering: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-links', MARKDOWN_RENDER_MODES, v))
94
+ .option('--markdown-images <mode>', `Image retention: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-images', MARKDOWN_RENDER_MODES, v))
95
+ .option('--with-links-summary', 'Append a Links footer to the markdown output (omitted → off)')
96
+ .option('--with-images-summary', 'Append an Images footer to the markdown output (omitted → off)')
97
+ .option('--markdown-compact', 'Collapse excessive whitespace in the markdown output (omitted → off)')
98
+ .option('--markdown-filter-images', 'Filter low-signal images from the markdown output (omitted → off)')
99
+ .option('--markdown-include-media', 'Return rich link/image objects and a tables array (omitted → off)')
100
+ .option('--markdown-include-warnings', 'Include markdown-pipeline and tag-filter warnings (omitted → off)')
101
+ .option('--markdown-include-stats', 'Include a top-level stats object (chars, tokens, blocks) (omitted → off)')
102
+ .option('--custom <json>', `User payload echoed on the envelope as a JSON object, e.g. '{"job":"1"}' (omitted → none)`, (v) => parseJsonFlag('--custom', v, 'object'))
71
103
  .option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
72
104
  .action(async (opts) => {
73
105
  const g = getGlobal();
74
- const client = buildClient(g);
106
+ // Omit-when-absent body, in the `body: Record<string, unknown>` form every other command
107
+ // file uses (scrape.ts:230-268 is the exemplar). The five pre-existing fields keep the
108
+ // exact same wire output they had as inline spreads; a boolean is written ONLY on
109
+ // `=== true`, so it never serializes `false` (#2480 E1).
110
+ const body = { url: opts.url };
111
+ if (opts.format !== undefined)
112
+ body['format'] = opts.format;
113
+ if (opts.country !== undefined)
114
+ body['country'] = opts.country;
115
+ if (opts.timeoutMs !== undefined)
116
+ body['timeout_ms'] = opts.timeoutMs;
117
+ if (opts.cacheTtl !== undefined)
118
+ body['cache_ttl'] = opts.cacheTtl;
119
+ if (opts.markdownMode !== undefined)
120
+ body['markdown_mode'] = opts.markdownMode;
121
+ if (opts.markdownQuery !== undefined)
122
+ body['markdown_query'] = opts.markdownQuery;
123
+ if (opts.markdownLinks !== undefined)
124
+ body['markdown_links'] = opts.markdownLinks;
125
+ if (opts.markdownImages !== undefined)
126
+ body['markdown_images'] = opts.markdownImages;
127
+ if (opts.withLinksSummary === true)
128
+ body['with_links_summary'] = true;
129
+ if (opts.withImagesSummary === true)
130
+ body['with_images_summary'] = true;
131
+ if (opts.markdownCompact === true)
132
+ body['markdown_compact'] = true;
133
+ if (opts.markdownFilterImages === true)
134
+ body['markdown_filter_images'] = true;
135
+ if (opts.markdownIncludeMedia === true)
136
+ body['markdown_include_media'] = true;
137
+ if (opts.markdownIncludeWarnings === true)
138
+ body['markdown_include_warnings'] = true;
139
+ if (opts.markdownIncludeStats === true)
140
+ body['markdown_include_stats'] = true;
141
+ if (opts.custom !== undefined)
142
+ body['custom'] = opts.custom;
75
143
  try {
76
- const result = await client.fetch.create({
77
- url: opts.url,
78
- // Each optional field is spread ONLY when its flag is present (#2480 E1).
79
- ...(opts.format !== undefined ? { format: opts.format } : {}),
80
- ...(opts.country !== undefined ? { country: opts.country } : {}),
81
- ...(opts.timeoutMs !== undefined ? { timeout_ms: opts.timeoutMs } : {}),
82
- ...(opts.cacheTtl !== undefined ? { cache_ttl: opts.cacheTtl } : {}),
83
- ...(opts.markdownMode !== undefined ? { markdown_mode: opts.markdownMode } : {}),
84
- });
144
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
145
+ const client = buildClient(g);
146
+ const result = await client.fetch.create(body);
85
147
  emitFetchResult(result, g.json, { file: opts.output });
86
148
  }
87
149
  catch (err) {
@@ -99,8 +161,9 @@ export function makeFetchCommand(getGlobal) {
99
161
  .option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
100
162
  .action(async (opts) => {
101
163
  const g = getGlobal();
102
- const client = buildClient(g);
103
164
  try {
165
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
166
+ const client = buildClient(g);
104
167
  const result = await client.fetch.get({
105
168
  url: opts.url,
106
169
  ...(opts.format !== undefined ? { format: opts.format } : {}),
@@ -20,11 +20,25 @@ export function makeMonitorsCommand(getGlobal) {
20
20
  capturesCmd
21
21
  .command('list <id>')
22
22
  .description('List captures for a monitor')
23
- .action(async (id) => {
23
+ .option('--limit <n>', 'Maximum number of captures to return', (v) => parseIntFlag('--limit', v))
24
+ .option('--cursor <cursor>', 'Pagination cursor from a previous page')
25
+ .action(async (id, opts) => {
24
26
  const g = getGlobal();
25
- const client = buildClient(g);
27
+ // Same omit-when-absent rule as `monitors list` (#2480): build the params object with
28
+ // ONLY the flags the caller passed, and call captures.list with a single argument (never
29
+ // captures.list(id, {}) / captures.list(id, undefined)) when no query flag is passed, so
30
+ // the inverse-case request-shape assertion stays byte-identical to today (#2594 DoD-4).
31
+ const params = {};
32
+ if (opts.limit !== undefined)
33
+ params.limit = opts.limit;
34
+ if (opts.cursor !== undefined)
35
+ params.cursor = opts.cursor;
26
36
  try {
27
- const result = await client.monitors.captures.list(id);
37
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
38
+ const client = buildClient(g);
39
+ const result = Object.keys(params).length > 0
40
+ ? await client.monitors.captures.list(id, params)
41
+ : await client.monitors.captures.list(id);
28
42
  printResult(result, g.json);
29
43
  }
30
44
  catch (err) {
@@ -36,6 +50,14 @@ export function makeMonitorsCommand(getGlobal) {
36
50
  .command('create')
37
51
  .description('Create a page-change monitor')
38
52
  .requiredOption('--url <url>', 'URL to monitor')
53
+ // `--interval-type` is passed through VERBATIM and validated by the API, not locally — a
54
+ // deliberate choice recorded for #2686 item 5's open question, matching what `search`'s
55
+ // `--type`/`--date-range` already do. A local enum list is a second source of truth that
56
+ // drifts from the spec silently: the CLI would start rejecting a value the API has begun
57
+ // accepting, and the error text would come from a stale copy rather than the server. The
58
+ // siblings that DO validate locally (`--type`, `--markdown-mode`, `--device`, …) are the
59
+ // enums this package chose to pin; nothing here should be read as those being wrong, only as
60
+ // the pass-through being intentional rather than an oversight.
39
61
  .requiredOption('--interval-type <type>', 'Check interval: hourly, daily, weekly, or cron')
40
62
  .requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified on change')
41
63
  .option('--cron <expr>', 'Cron expression (required when --interval-type is cron, e.g. "0 * * * *")')
@@ -48,9 +70,9 @@ export function makeMonitorsCommand(getGlobal) {
48
70
  .option('--webhook-headers <json>', `Extra webhook headers as a JSON object, e.g. '{"X-Sig":"abc"}' (omitted → none)`, (v) => parseJsonFlag('--webhook-headers', v, 'object'))
49
71
  .option('--options <json>', `Screenshot-monitor options as a JSON object (omitted → server defaults)`, (v) => parseJsonFlag('--options', v, 'object'))
50
72
  .option('--scrape-options <json>', `Scrape-monitor options as a JSON object (type=scrape only; omitted → server defaults)`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
73
+ .option('--metadata <json>', `Arbitrary metadata echoed on the monitor envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
51
74
  .action(async (opts) => {
52
75
  const g = getGlobal();
53
- const client = buildClient(g);
54
76
  // Omit-when-absent body (same rule as scrape.ts): each key is written ONLY when its flag is
55
77
  // present, so a bare create never materializes a server default (#2480 E1). Typed via an
56
78
  // assertion at the call site — the wire body is pinned by request-shapes.test.ts.
@@ -75,7 +97,11 @@ export function makeMonitorsCommand(getGlobal) {
75
97
  body['options'] = opts.options;
76
98
  if (opts.scrapeOptions !== undefined)
77
99
  body['scrape_options'] = opts.scrapeOptions;
100
+ if (opts.metadata !== undefined)
101
+ body['metadata'] = opts.metadata;
78
102
  try {
103
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
104
+ const client = buildClient(g);
79
105
  const result = await client.monitors.create(body);
80
106
  printResult(result, g.json);
81
107
  }
@@ -94,7 +120,6 @@ export function makeMonitorsCommand(getGlobal) {
94
120
  // with no runtime change.
95
121
  .action(async (opts) => {
96
122
  const g = getGlobal();
97
- const client = buildClient(g);
98
123
  // Build the params object with the same omit-when-absent rule as the body builders. When NO
99
124
  // query flag is passed, call list() with ZERO arguments (never list({}) / list(undefined)) so
100
125
  // the inverse-case request-shape assertion `monitors.list` args length 0 stays green (#2480).
@@ -106,6 +131,8 @@ export function makeMonitorsCommand(getGlobal) {
106
131
  if (opts.status !== undefined)
107
132
  params.status = opts.status;
108
133
  try {
134
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
135
+ const client = buildClient(g);
109
136
  const result = Object.keys(params).length > 0
110
137
  ? await client.monitors.list(params)
111
138
  : await client.monitors.list();
@@ -120,8 +147,9 @@ export function makeMonitorsCommand(getGlobal) {
120
147
  .description('Get a monitor by ID')
121
148
  .action(async (id) => {
122
149
  const g = getGlobal();
123
- const client = buildClient(g);
124
150
  try {
151
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
152
+ const client = buildClient(g);
125
153
  const result = await client.monitors.get(id);
126
154
  printResult(result, g.json);
127
155
  }
@@ -132,6 +160,7 @@ export function makeMonitorsCommand(getGlobal) {
132
160
  cmd
133
161
  .command('update <id>')
134
162
  .description('Update a monitor (including pause/resume via --status)')
163
+ .option('--url <url>', 'New URL to monitor (omitted → unchanged)')
135
164
  .option('--interval-type <type>', 'New check interval: hourly, daily, weekly, or cron')
136
165
  .option('--cron <expr>', 'New cron expression (when interval-type is cron, e.g. "0 * * * *")')
137
166
  .option('--webhook-url <url>', 'New HTTPS webhook URL')
@@ -144,10 +173,14 @@ export function makeMonitorsCommand(getGlobal) {
144
173
  .option('--webhook-headers <json>', `New webhook headers as a JSON object, e.g. '{"X-Sig":"abc"}'`, (v) => parseJsonFlag('--webhook-headers', v, 'object'))
145
174
  .option('--options <json>', `New screenshot-monitor options as a JSON object`, (v) => parseJsonFlag('--options', v, 'object'))
146
175
  .option('--scrape-options <json>', `New scrape-monitor options as a JSON object`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
176
+ .option('--metadata <json>', `New metadata as a JSON object, e.g. '{"run":"7"}' (omitted → unchanged)`, (v) => parseJsonFlag('--metadata', v, 'object'))
147
177
  .action(async (id, opts) => {
148
178
  const g = getGlobal();
149
- const client = buildClient(g);
150
179
  const body = {};
180
+ // `url` IS settable on PATCH (openapi.yaml MonitorUpdateRequest) — unlike `type`, which
181
+ // stays immutable and deliberately has no flag (#1984).
182
+ if (opts.url !== undefined)
183
+ body['url'] = opts.url;
151
184
  if (opts.intervalType !== undefined)
152
185
  body['interval_type'] = opts.intervalType;
153
186
  if (opts.cron !== undefined)
@@ -168,7 +201,11 @@ export function makeMonitorsCommand(getGlobal) {
168
201
  body['options'] = opts.options;
169
202
  if (opts.scrapeOptions !== undefined)
170
203
  body['scrape_options'] = opts.scrapeOptions;
204
+ if (opts.metadata !== undefined)
205
+ body['metadata'] = opts.metadata;
171
206
  try {
207
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
208
+ const client = buildClient(g);
172
209
  const result = await client.monitors.update(id, body);
173
210
  printResult(result, g.json);
174
211
  }
@@ -181,8 +218,9 @@ export function makeMonitorsCommand(getGlobal) {
181
218
  .description('Delete a monitor')
182
219
  .action(async (id) => {
183
220
  const g = getGlobal();
184
- const client = buildClient(g);
185
221
  try {
222
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
223
+ const client = buildClient(g);
186
224
  const result = await client.monitors.delete(id);
187
225
  printResult(result, g.json);
188
226
  }
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { buildClient } from '../client.js';
3
3
  import { printResult, handleError, resolveOutputMode, emitResult } from '../output.js';
4
- import { parseUrlLines } from '../lib/url-lines.js';
4
+ import { parseBulkLines, toBulkItems } from '../lib/url-lines.js';
5
5
  import { parseIntFlag, parseEnum, parseCsv, parseCacheTtl, parseJsonFlag, collectFormats, } from '../lib/parse-flags.js';
6
6
  /** The `ScrapeRequest.markdown_mode` spec enum. */
7
7
  const MARKDOWN_MODES = ['article', 'raw', 'llm'];
@@ -45,15 +45,35 @@ export function makeScrapeCommand(getGlobal) {
45
45
  bulkCmd
46
46
  .command('create')
47
47
  .description('Create a bulk scrape job')
48
- .requiredOption('--file <path>', 'Path to newline-delimited file of URLs (plain URL or {"url":"..."} per line)')
48
+ .requiredOption('--file <path>', 'Path to a newline-delimited file. Each line is a plain URL, or a JSON object carrying "url" plus per-item config. A line whose only key is "url" behaves exactly as before (sent as a bare URL string); any line with additional keys switches the payload to the per-item shape.')
49
49
  .requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified when the job completes')
50
+ .option('--webhook-secret <secret>', 'HMAC secret used to sign the completion webhook (omitted → unsigned)')
51
+ .option('--defaults <json>', 'Defaults applied to every entry, as a JSON object (omitted → none). Unknown keys are rejected by /v1/bulk (422) and ignored by scrape bulk / fetch bulk; fetch bulk additionally rejects browser-only keys (cookies, wait_until, wait_for_selector, delay_ms) with a 400.', (v) => parseJsonFlag('--defaults', v, 'object'))
52
+ .option('--metadata <json>', `Arbitrary metadata echoed on the job envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
53
+ // /v1/scrape/bulk is the ONE bulk endpoint with a top-level `custom`
54
+ // (validators/scrape-bulk.ts:88). Per-entry `custom` arrives through --file lines instead.
55
+ .option('--custom <json>', `User payload echoed on every scrape entry, as a JSON object, e.g. '{"job":"1"}' (omitted → none; a per-entry value in --file wins)`, (v) => parseJsonFlag('--custom', v, 'object'))
50
56
  .action(async (opts) => {
51
57
  const g = getGlobal();
52
- const client = buildClient(g);
53
58
  try {
59
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
60
+ const client = buildClient(g);
54
61
  // readFileSync throws ENOENT on missing file — caught by handleError below
55
- const urls = parseUrlLines(opts.file);
56
- const result = await client.scrape.bulk.create({ urls, webhook_url: opts.webhookUrl });
62
+ const lines = parseBulkLines(opts.file);
63
+ // `urls` XOR `items` (validators/scrape-bulk.ts:91-98) — send exactly one, keeping the
64
+ // pre-#2596 `{ urls, webhook_url }` body byte-identical for url-only files (E2).
65
+ const body = lines.every((l) => typeof l === 'string')
66
+ ? { urls: lines, webhook_url: opts.webhookUrl }
67
+ : { items: toBulkItems(lines), webhook_url: opts.webhookUrl };
68
+ if (opts.webhookSecret !== undefined)
69
+ body['webhook_secret'] = opts.webhookSecret;
70
+ if (opts.defaults !== undefined)
71
+ body['defaults'] = opts.defaults;
72
+ if (opts.metadata !== undefined)
73
+ body['metadata'] = opts.metadata;
74
+ if (opts.custom !== undefined)
75
+ body['custom'] = opts.custom;
76
+ const result = await client.scrape.bulk.create(body);
57
77
  printResult(result, g.json);
58
78
  }
59
79
  catch (err) {
@@ -65,8 +85,9 @@ export function makeScrapeCommand(getGlobal) {
65
85
  .description('Get a bulk scrape job')
66
86
  .action(async (id) => {
67
87
  const g = getGlobal();
68
- const client = buildClient(g);
69
88
  try {
89
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
90
+ const client = buildClient(g);
70
91
  const result = await client.scrape.bulk.get(id);
71
92
  printResult(result, g.json);
72
93
  }
@@ -82,6 +103,12 @@ export function makeScrapeCommand(getGlobal) {
82
103
  // Repeatable AND CSV-aware: `--format raw_html --format markdown` and `--format raw_html,markdown`
83
104
  // both send formats: ['raw_html','markdown']. Sent as the `formats` array; omitted → server
84
105
  // default. `--raw`/`-o` require EXACTLY ONE format (validated pre-request; see the action).
106
+ //
107
+ // The format VALUES travel to the server unvalidated — deliberate, and recorded here for
108
+ // #2686 item 5's open question (same rationale as `monitors --interval-type` and `search`'s
109
+ // `--type`/`--date-range`). The persisted-format vocabulary grows with the spec, and a local
110
+ // enum list is a second source of truth that drifts silently: the CLI would reject a format
111
+ // the API has begun serving, and the error would come from a stale copy rather than the API.
85
112
  .option('--format <format>', 'Output format; repeat or comma-separate for multiple (e.g. --format raw_html --format markdown, or --format raw_html,markdown). Sent as the formats array; omitted → server default.', collectFormats)
86
113
  .option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
87
114
  .option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
@@ -118,7 +145,6 @@ export function makeScrapeCommand(getGlobal) {
118
145
  .option('-o, --output <file>', 'Write the output to the given file path (implies --raw unless --json is set)')
119
146
  .action(async (opts) => {
120
147
  const g = getGlobal();
121
- const client = buildClient(g);
122
148
  const body = { url: opts.url };
123
149
  // The v1 scrape validator expects `formats` (array). The --format collector already
124
150
  // accumulates repeated/CSV occurrences into an array; write it verbatim, omitting the key
@@ -187,6 +213,8 @@ export function makeScrapeCommand(getGlobal) {
187
213
  if (opts.mobile === true)
188
214
  body['mobile'] = true;
189
215
  try {
216
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
217
+ const client = buildClient(g);
190
218
  // FIRST statement inside the try — deliberately NOT hoisted next to buildClient() where
191
219
  // the other handlers put pre-request logic. index.ts drives commander with .parse(), not
192
220
  // .parseAsync(), so nothing awaits this action's promise: a throw raised above the try
@@ -245,11 +273,12 @@ export function makeScrapeCommand(getGlobal) {
245
273
  .description('Get a scrape job by ID (use --raw/-o/--format to retrieve a completed body)')
246
274
  .option('--raw', 'Print the requested format body raw, with no JSON wrapper')
247
275
  .option('-o, --output <file>', 'Write the output to the given file path (implies --raw unless --json is set)')
248
- .option('--format <format>', 'Which persisted format to emit raw when several are present')
276
+ .option('--format <format>', 'Which persisted format to emit — only takes effect with --raw/-o; ignored otherwise')
249
277
  .action(async (id, opts) => {
250
278
  const g = getGlobal();
251
- const client = buildClient(g);
252
279
  try {
280
+ // Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
281
+ const client = buildClient(g);
253
282
  // Mirror `scrape create`'s output path: resolveOutputMode FIRST inside the try (so a
254
283
  // --json/--raw conflict rejects before the request), then emitResult. The request body is
255
284
  // unchanged — scrape.get(id) takes no options — so the inverse-case arg-length assertion in