@hunt-labs/bytekit-cli 0.3.0 → 0.5.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,9 +1,19 @@
1
1
  import { Command } from 'commander';
2
2
  import { buildClient } from '../client.js';
3
3
  import { printResult, handleError } from '../output.js';
4
- import { parseEnum, parseIntFlag } from '../lib/parse-flags.js';
5
- /** The `listMonitors` `status` query enum. */
4
+ import { parseEnum, parseIntFlag, parseNumberFlag, parseJsonFlag } from '../lib/parse-flags.js';
5
+ /** The `listMonitors` `status` query enum (5-value list FILTER). */
6
6
  const MONITOR_STATUSES = ['active', 'processing', 'paused', 'cancelled', 'suspended'];
7
+ /**
8
+ * The `MonitorUpdateRequest.status` enum — DISTINCT from {@link MONITOR_STATUSES}. Only `active`
9
+ * (resume) and `paused` (pause) are valid update targets (openapi.yaml MonitorUpdateRequest.status);
10
+ * the other three list-filter states are server-managed lifecycle states, not settable via PATCH.
11
+ */
12
+ const MONITOR_UPDATE_STATUSES = ['active', 'paused'];
13
+ /** The `notify_on` enum shared by MonitorCreateRequest / MonitorUpdateRequest. */
14
+ const NOTIFY_ONS = ['change', 'every'];
15
+ /** The create-only `type` enum (PATCH rejects `type` with 422 — immutable, issue #1984). */
16
+ const MONITOR_TYPES = ['screenshot', 'scrape'];
7
17
  export function makeMonitorsCommand(getGlobal) {
8
18
  const cmd = new Command('monitors').description('Page-change monitor operations');
9
19
  const capturesCmd = new Command('captures').description('Monitor capture sub-resources');
@@ -29,21 +39,42 @@ export function makeMonitorsCommand(getGlobal) {
29
39
  .requiredOption('--interval-type <type>', 'Check interval: hourly, daily, weekly, or cron')
30
40
  .requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified on change')
31
41
  .option('--cron <expr>', 'Cron expression (required when --interval-type is cron, e.g. "0 * * * *")')
42
+ // `--type` is CREATE-ONLY (a monitor's type is immutable per #1984); the PATCH builder below
43
+ // deliberately omits it.
44
+ .option('--type <type>', `Monitor type: ${MONITOR_TYPES.join(', ')} (omitted → server default screenshot)`, (v) => parseEnum('--type', MONITOR_TYPES, v))
45
+ .option('--change-threshold <pct>', 'Change percentage that triggers a notification (float; omitted → server default 5)', (v) => parseNumberFlag('--change-threshold', v))
46
+ .option('--notify-on <event>', `When to notify: ${NOTIFY_ONS.join(', ')} (omitted → server default change)`, (v) => parseEnum('--notify-on', NOTIFY_ONS, v))
47
+ .option('--webhook-secret <secret>', 'HMAC secret for webhook signature verification')
48
+ .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
+ .option('--options <json>', `Screenshot-monitor options as a JSON object (omitted → server defaults)`, (v) => parseJsonFlag('--options', v, 'object'))
50
+ .option('--scrape-options <json>', `Scrape-monitor options as a JSON object (type=scrape only; omitted → server defaults)`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
32
51
  .action(async (opts) => {
33
52
  const g = getGlobal();
34
53
  const client = buildClient(g);
35
- // Key-checked subset of the SDK's MonitorCreateRequest. The CLI intentionally omits the
36
- // spec-required-with-@default fields (type, change_threshold, notify_on) and relies on
37
- // the server-side defaults, so the final call needs an explicit assertion — type-level
38
- // only; the wire body is unchanged (pinned by __tests__/request-shapes.test.ts).
54
+ // Omit-when-absent body (same rule as scrape.ts): each key is written ONLY when its flag is
55
+ // present, so a bare create never materializes a server default (#2480 E1). Typed via an
56
+ // assertion at the call site — the wire body is pinned by request-shapes.test.ts.
39
57
  const body = {
40
58
  url: opts.url,
41
- // Commander delivers a plain string; the server validates the enum (422 on a bad value).
42
59
  interval_type: opts.intervalType,
43
60
  webhook_url: opts.webhookUrl,
44
61
  };
45
62
  if (opts.cron !== undefined)
46
- body.cron_expression = opts.cron;
63
+ body['cron_expression'] = opts.cron;
64
+ if (opts.type !== undefined)
65
+ body['type'] = opts.type;
66
+ if (opts.changeThreshold !== undefined)
67
+ body['change_threshold'] = opts.changeThreshold;
68
+ if (opts.notifyOn !== undefined)
69
+ body['notify_on'] = opts.notifyOn;
70
+ if (opts.webhookSecret !== undefined)
71
+ body['webhook_secret'] = opts.webhookSecret;
72
+ if (opts.webhookHeaders !== undefined)
73
+ body['webhook_headers'] = opts.webhookHeaders;
74
+ if (opts.options !== undefined)
75
+ body['options'] = opts.options;
76
+ if (opts.scrapeOptions !== undefined)
77
+ body['scrape_options'] = opts.scrapeOptions;
47
78
  try {
48
79
  const result = await client.monitors.create(body);
49
80
  printResult(result, g.json);
@@ -58,6 +89,9 @@ export function makeMonitorsCommand(getGlobal) {
58
89
  .option('--limit <n>', 'Maximum number of monitors to return', (v) => parseIntFlag('--limit', v))
59
90
  .option('--cursor <cursor>', 'Pagination cursor from a previous page')
60
91
  .option('--status <status>', `Filter by status: ${MONITOR_STATUSES.join(', ')} (default active)`, (v) => parseEnum('--status', MONITOR_STATUSES, v))
92
+ // `status` is the SDK's spec-derived `MonitorStatus` union, not a bare string (#2547) —
93
+ // `parseEnum` above already returns exactly that union, so this is a type-only narrowing
94
+ // with no runtime change.
61
95
  .action(async (opts) => {
62
96
  const g = getGlobal();
63
97
  const client = buildClient(g);
@@ -97,10 +131,19 @@ export function makeMonitorsCommand(getGlobal) {
97
131
  });
98
132
  cmd
99
133
  .command('update <id>')
100
- .description('Update a monitor')
134
+ .description('Update a monitor (including pause/resume via --status)')
101
135
  .option('--interval-type <type>', 'New check interval: hourly, daily, weekly, or cron')
102
136
  .option('--cron <expr>', 'New cron expression (when interval-type is cron, e.g. "0 * * * *")')
103
137
  .option('--webhook-url <url>', 'New HTTPS webhook URL')
138
+ // `--status` is the 2-value MonitorUpdateRequest.status enum — NOT the 5-value list filter.
139
+ // Deliberately NO `--type`: PATCH /v1/monitors/{id} rejects `type` with 422 (immutable, #1984).
140
+ .option('--status <status>', `Pause or resume the monitor: ${MONITOR_UPDATE_STATUSES.join(', ')}`, (v) => parseEnum('--status', MONITOR_UPDATE_STATUSES, v))
141
+ .option('--change-threshold <pct>', 'New change percentage that triggers a notification (float)', (v) => parseNumberFlag('--change-threshold', v))
142
+ .option('--notify-on <event>', `When to notify: ${NOTIFY_ONS.join(', ')}`, (v) => parseEnum('--notify-on', NOTIFY_ONS, v))
143
+ .option('--webhook-secret <secret>', 'New HMAC secret for webhook signature verification')
144
+ .option('--webhook-headers <json>', `New webhook headers as a JSON object, e.g. '{"X-Sig":"abc"}'`, (v) => parseJsonFlag('--webhook-headers', v, 'object'))
145
+ .option('--options <json>', `New screenshot-monitor options as a JSON object`, (v) => parseJsonFlag('--options', v, 'object'))
146
+ .option('--scrape-options <json>', `New scrape-monitor options as a JSON object`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
104
147
  .action(async (id, opts) => {
105
148
  const g = getGlobal();
106
149
  const client = buildClient(g);
@@ -111,6 +154,20 @@ export function makeMonitorsCommand(getGlobal) {
111
154
  body['cron_expression'] = opts.cron;
112
155
  if (opts.webhookUrl !== undefined)
113
156
  body['webhook_url'] = opts.webhookUrl;
157
+ if (opts.status !== undefined)
158
+ body['status'] = opts.status;
159
+ if (opts.changeThreshold !== undefined)
160
+ body['change_threshold'] = opts.changeThreshold;
161
+ if (opts.notifyOn !== undefined)
162
+ body['notify_on'] = opts.notifyOn;
163
+ if (opts.webhookSecret !== undefined)
164
+ body['webhook_secret'] = opts.webhookSecret;
165
+ if (opts.webhookHeaders !== undefined)
166
+ body['webhook_headers'] = opts.webhookHeaders;
167
+ if (opts.options !== undefined)
168
+ body['options'] = opts.options;
169
+ if (opts.scrapeOptions !== undefined)
170
+ body['scrape_options'] = opts.scrapeOptions;
114
171
  try {
115
172
  const result = await client.monitors.update(id, body);
116
173
  printResult(result, g.json);
@@ -2,9 +2,15 @@ import { Command } from 'commander';
2
2
  import { buildClient } from '../client.js';
3
3
  import { printResult, handleError, resolveOutputMode, emitResult } from '../output.js';
4
4
  import { parseUrlLines } from '../lib/url-lines.js';
5
- import { parseIntFlag, parseEnum, parseCsv } from '../lib/parse-flags.js';
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'];
8
+ /** The `ScrapeRequest.events` item enum. */
9
+ const SCRAPE_EVENTS = ['queued', 'completed', 'failed'];
10
+ /** The `ScrapeRequest.token_encoding` spec enum. */
11
+ const TOKEN_ENCODINGS = ['cl100k_base', 'o200k_base'];
12
+ /** The `markdown_links` / `markdown_images` render-mode enum. */
13
+ const MARKDOWN_RENDER_MODES = ['inline', 'references', 'none', 'text'];
8
14
  /**
9
15
  * Exit code when a queued (202 async) envelope reaches a non-JSON output mode.
10
16
  *
@@ -13,8 +19,11 @@ const MARKDOWN_MODES = ['article', 'raw', 'llm'];
13
19
  * that PR's symbols while the two guards still compose idempotently post-merge. Deliberately NOT 0
14
20
  * (a content-body success) and NOT 1 (a usage/API error): the job WAS accepted and billed, there
15
21
  * is simply no content body yet, and a distinct code lets a caller branch on "queued" without
16
- * parsing stderr. Human mode shares the same code (soft `process.exitCode`) so "queued, not
17
- * complete" is branchable there too, for consistency with the raw-mode signal.
22
+ * parsing stderr.
23
+ *
24
+ * BOTH modes carry it as a soft `process.exitCode` (#2544): raw mode used to hard-`process.exit`,
25
+ * which runs before Node drains an ASYNC piped stdout and could discard the id it had just
26
+ * written — the exact `$(…)` capture the raw branch exists for. Human mode was already soft.
18
27
  */
19
28
  const QUEUED_EXIT_CODE = 2;
20
29
  /**
@@ -70,10 +79,34 @@ export function makeScrapeCommand(getGlobal) {
70
79
  .command('create')
71
80
  .description('Create a scrape job')
72
81
  .requiredOption('--url <url>', 'URL to scrape')
73
- .option('--format <format>', 'Output format (e.g. markdown, html); sent as the formats array')
82
+ // Repeatable AND CSV-aware: `--format raw_html --format markdown` and `--format raw_html,markdown`
83
+ // both send formats: ['raw_html','markdown']. Sent as the `formats` array; omitted → server
84
+ // default. `--raw`/`-o` require EXACTLY ONE format (validated pre-request; see the action).
85
+ .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)
74
86
  .option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
75
87
  .option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
76
88
  .option('--markdown-mode <mode>', `Markdown extraction mode: ${MARKDOWN_MODES.join(', ')}`, (v) => parseEnum('--markdown-mode', MARKDOWN_MODES, v))
89
+ .option('--markdown-query <q>', 'BM25 query string for relevance-ranked markdown filtering (omitted → disabled)')
90
+ .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))
91
+ .option('--cookies <json>', `Cookies as a JSON array, e.g. '[{"name":"a","value":"b"}]' (omitted → none)`, (v) => parseJsonFlag('--cookies', v, 'array'))
92
+ .option('--headers <json>', `Extra request headers as a JSON object, e.g. '{"X-A":"b"}' (omitted → none)`, (v) => parseJsonFlag('--headers', v, 'object'))
93
+ .option('--delay-ms <n>', 'Post-load delay in milliseconds before capture', (v) => parseIntFlag('--delay-ms', v))
94
+ .option('--events <csv>', `Webhook events (comma-separated): ${SCRAPE_EVENTS.join(', ')} (omitted → server default)`, (v) => parseCsv(v).map((e) => parseEnum('--events', SCRAPE_EVENTS, e)))
95
+ .option('--token-encoding <enc>', `Tokenizer for --token-budget: ${TOKEN_ENCODINGS.join(', ')} (omitted → server default)`, (v) => parseEnum('--token-encoding', TOKEN_ENCODINGS, v))
96
+ .option('--custom <json>', `User payload echoed on the envelope as a JSON object, e.g. '{"job":"1"}' (omitted → none)`, (v) => parseJsonFlag('--custom', v, 'object'))
97
+ // Server default is TRUE, so this is a tri-state pair: --remove-base64-images → true,
98
+ // --no-remove-base64-images → false, neither → key omitted (never materialize the default, E6).
99
+ .option('--remove-base64-images', 'Strip base64 data: images before conversion (server default)')
100
+ .option('--no-remove-base64-images', 'Preserve base64 data: images in the pipeline output')
101
+ .option('--markdown-links <mode>', `Link rendering: ${MARKDOWN_RENDER_MODES.join(', ')}`, (v) => parseEnum('--markdown-links', MARKDOWN_RENDER_MODES, v))
102
+ .option('--markdown-images <mode>', `Image retention: ${MARKDOWN_RENDER_MODES.join(', ')}`, (v) => parseEnum('--markdown-images', MARKDOWN_RENDER_MODES, v))
103
+ .option('--with-links-summary', 'Append a Links footer to the markdown output')
104
+ .option('--with-images-summary', 'Append an Images footer to the markdown output')
105
+ .option('--markdown-compact', 'Collapse excessive whitespace in the markdown output')
106
+ .option('--markdown-filter-images', 'Filter low-signal images from the markdown output')
107
+ .option('--markdown-include-media', 'Return rich link/image objects and a tables array')
108
+ .option('--markdown-include-warnings', 'Include markdown-pipeline and tag-filter warnings')
109
+ .option('--markdown-include-stats', 'Include a top-level stats object (chars, tokens, blocks)')
77
110
  .option('--async', 'Queue the scrape and return a job envelope immediately (no wait)')
78
111
  .option('--webhook-url <url>', 'HTTPS webhook URL notified when an async scrape completes')
79
112
  .option('--token-budget <n>', 'Maximum tokens of markdown to return', (v) => parseIntFlag('--token-budget', v))
@@ -87,11 +120,12 @@ export function makeScrapeCommand(getGlobal) {
87
120
  const g = getGlobal();
88
121
  const client = buildClient(g);
89
122
  const body = { url: opts.url };
90
- // The v1 scrape validator expects `formats` (array), not `format` (string).
91
- // Omit the key entirely when --format is not provided so we never send
92
- // formats: ["undefined"].
93
- if (opts.format !== undefined)
94
- body['formats'] = [opts.format];
123
+ // The v1 scrape validator expects `formats` (array). The --format collector already
124
+ // accumulates repeated/CSV occurrences into an array; write it verbatim, omitting the key
125
+ // entirely when --format is absent so we never send formats: [] or ["undefined"].
126
+ const formats = opts.format;
127
+ if (formats !== undefined)
128
+ body['formats'] = formats;
95
129
  // Each optional flag maps 1:1 to its snake_case ScrapeRequest field and is written ONLY
96
130
  // when the flag is present — a boolean must never serialize `false` (#2480 E1).
97
131
  if (opts.country !== undefined)
@@ -100,6 +134,44 @@ export function makeScrapeCommand(getGlobal) {
100
134
  body['timeout_ms'] = opts.timeoutMs;
101
135
  if (opts.markdownMode !== undefined)
102
136
  body['markdown_mode'] = opts.markdownMode;
137
+ if (opts.markdownQuery !== undefined)
138
+ body['markdown_query'] = opts.markdownQuery;
139
+ if (opts.cacheTtl !== undefined)
140
+ body['cache_ttl'] = opts.cacheTtl;
141
+ if (opts.cookies !== undefined)
142
+ body['cookies'] = opts.cookies;
143
+ if (opts.headers !== undefined)
144
+ body['headers'] = opts.headers;
145
+ if (opts.delayMs !== undefined)
146
+ body['delay_ms'] = opts.delayMs;
147
+ if (opts.events !== undefined)
148
+ body['events'] = opts.events;
149
+ if (opts.tokenEncoding !== undefined)
150
+ body['token_encoding'] = opts.tokenEncoding;
151
+ if (opts.custom !== undefined)
152
+ body['custom'] = opts.custom;
153
+ // Tri-state boolean: commander yields `undefined` when neither --remove-base64-images nor
154
+ // --no-remove-base64-images is passed, so the key is omitted and the server default holds.
155
+ if (opts.removeBase64Images !== undefined)
156
+ body['remove_base64_images'] = opts.removeBase64Images;
157
+ if (opts.markdownLinks !== undefined)
158
+ body['markdown_links'] = opts.markdownLinks;
159
+ if (opts.markdownImages !== undefined)
160
+ body['markdown_images'] = opts.markdownImages;
161
+ if (opts.withLinksSummary === true)
162
+ body['with_links_summary'] = true;
163
+ if (opts.withImagesSummary === true)
164
+ body['with_images_summary'] = true;
165
+ if (opts.markdownCompact === true)
166
+ body['markdown_compact'] = true;
167
+ if (opts.markdownFilterImages === true)
168
+ body['markdown_filter_images'] = true;
169
+ if (opts.markdownIncludeMedia === true)
170
+ body['markdown_include_media'] = true;
171
+ if (opts.markdownIncludeWarnings === true)
172
+ body['markdown_include_warnings'] = true;
173
+ if (opts.markdownIncludeStats === true)
174
+ body['markdown_include_stats'] = true;
103
175
  if (opts.async === true)
104
176
  body['async'] = true;
105
177
  if (opts.webhookUrl !== undefined)
@@ -122,6 +194,12 @@ export function makeScrapeCommand(getGlobal) {
122
194
  // handleError()'s clean one-liner. buildClient() does no network I/O, so rejecting the
123
195
  // --json/--raw conflict here still happens before any request is issued.
124
196
  const mode = resolveOutputMode({ json: g.json, raw: opts.raw, output: opts.output });
197
+ // Raw mode (--raw or a bare -o) emits a SINGLE format body; more than one format is
198
+ // ambiguous, so reject before the request (E2). Thrown inside the try so handleError()
199
+ // prints the clean one-liner and no scrape.create call is ever recorded.
200
+ if (mode.kind === 'raw' && formats !== undefined && formats.length > 1) {
201
+ throw new Error('--raw requires exactly one --format');
202
+ }
125
203
  const result = await client.scrape.create(body);
126
204
  // Queued-envelope precedence (#2480 E4s): a `--async` scrape returns a 202
127
205
  // `{ id, status: 'queued' }` envelope with NO formats map. Detect that shape here —
@@ -131,8 +209,10 @@ export function makeScrapeCommand(getGlobal) {
131
209
  // indistinguishable from that one):
132
210
  // - raw mode (`--raw` or a bare `-o`): the bare sc_ id on STDOUT (a usable
133
211
  // `$(bytekit scrape create … --raw)` handle), a human poll note on STDERR, exit
134
- // QUEUED_EXIT_CODE. The `-o` file is deliberately NEVER written — there is no
135
- // content body yet, and a file holding a bare id would masquerade as content.
212
+ // QUEUED_EXIT_CODE via soft `process.exitCode` (#2544 — a hard `process.exit` runs
213
+ // before Node drains an ASYNC piped stdout and would discard the very id this
214
+ // branch exists to deliver). The `-o` file is deliberately NEVER written — there is
215
+ // no content body yet, and a file holding a bare id would masquerade as content.
136
216
  // - json mode (`--json`, with or without `-o`): OUTSIDE the raw rule — fall through
137
217
  // to emitResult, which emits the envelope verbatim (exit 0).
138
218
  // - human default: keep the readable id/status dump, but signal queued-not-complete
@@ -143,7 +223,8 @@ export function makeScrapeCommand(getGlobal) {
143
223
  console.error(`Note: scrape ${result.id} was queued (async) — no content body is available yet. ` +
144
224
  `Poll it with: bytekit scrape get ${result.id}`);
145
225
  process.stdout.write(`${result.id}\n`);
146
- process.exit(QUEUED_EXIT_CODE);
226
+ process.exitCode = QUEUED_EXIT_CODE;
227
+ return;
147
228
  }
148
229
  if (mode.kind === 'human') {
149
230
  printResult(result, false);
@@ -151,7 +232,9 @@ export function makeScrapeCommand(getGlobal) {
151
232
  return;
152
233
  }
153
234
  }
154
- emitResult(result, mode, { file: opts.output }, opts.format);
235
+ // At this point raw mode has at most one format (E2 guard above), so formats[0] is the
236
+ // single requested body selector; json/human modes ignore the argument.
237
+ emitResult(result, mode, { file: opts.output }, formats?.[0]);
155
238
  }
156
239
  catch (err) {
157
240
  handleError(err);
@@ -159,13 +242,22 @@ export function makeScrapeCommand(getGlobal) {
159
242
  });
160
243
  cmd
161
244
  .command('get <id>')
162
- .description('Get a scrape job by ID')
163
- .action(async (id) => {
245
+ .description('Get a scrape job by ID (use --raw/-o/--format to retrieve a completed body)')
246
+ .option('--raw', 'Print the requested format body raw, with no JSON wrapper')
247
+ .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')
249
+ .action(async (id, opts) => {
164
250
  const g = getGlobal();
165
251
  const client = buildClient(g);
166
252
  try {
253
+ // Mirror `scrape create`'s output path: resolveOutputMode FIRST inside the try (so a
254
+ // --json/--raw conflict rejects before the request), then emitResult. The request body is
255
+ // unchanged — scrape.get(id) takes no options — so the inverse-case arg-length assertion in
256
+ // request-shapes.test.ts stays green. A still-queued job flows through emitResult's
257
+ // queued-envelope branch (id + soft exit 2, #2508/#2544); a completed job emits its body.
258
+ const mode = resolveOutputMode({ json: g.json, raw: opts.raw, output: opts.output });
167
259
  const result = await client.scrape.get(id);
168
- printResult(result, g.json);
260
+ emitResult(result, mode, { file: opts.output }, opts.format);
169
261
  }
170
262
  catch (err) {
171
263
  handleError(err);
@@ -7,6 +7,8 @@ import { parseEnum, parseIntFlag } from '../lib/parse-flags.js';
7
7
  const DEVICES = ['desktop', 'mobile'];
8
8
  const FORMATS = ['jpeg', 'png'];
9
9
  const WAIT_UNTILS = ['load', 'domcontentloaded', 'networkidle'];
10
+ /** `device_scale_factor` accepts only 1 or 2 (spec enum [1, 2, null]); parsed as a string, sent numeric. */
11
+ const DEVICE_SCALE_FACTORS = ['1', '2'];
10
12
  /** Parse `--viewport WxH` (e.g. `1280x720`) into `{ width, height }`; reject malformed input. */
11
13
  export function parseViewport(value) {
12
14
  const m = /^(\d+)x(\d+)$/.exec(value);
@@ -72,6 +74,21 @@ export function makeScreenshotsCommand(getGlobal) {
72
74
  .option('--quality <n>', 'JPEG quality (1-100)', (v) => parseIntFlag('--quality', v))
73
75
  .option('--wait-until <event>', `Wait condition: ${WAIT_UNTILS.join(', ')}`, (v) => parseEnum('--wait-until', WAIT_UNTILS, v))
74
76
  .option('--clip <x,y,w,h>', 'Clip region X,Y,WIDTH,HEIGHT (e.g. 0,0,800,600)', parseClip)
77
+ // Tri-state pairs — server default is TRUE, so define BOTH --x and --no-x: commander yields
78
+ // `undefined` when neither is passed, and the key is omitted (never materialize the default, E6).
79
+ .option('--block-ads', 'Block ads before capture (server default)')
80
+ .option('--no-block-ads', 'Do not block ads')
81
+ .option('--block-cookie-banners', 'Block cookie-consent banners before capture (server default)')
82
+ .option('--no-block-cookie-banners', 'Do not block cookie-consent banners')
83
+ .option('--scroll', 'Scroll the page before capture to trigger lazy content (server default)')
84
+ .option('--no-scroll', 'Do not scroll the page before capture')
85
+ .option('--wait-for-selector <sel>', 'Wait until this CSS selector is present before capture')
86
+ .option('--delay-ms <n>', 'Post-load delay in milliseconds before capture', (v) => parseIntFlag('--delay-ms', v))
87
+ .option('--dark-mode', 'Emulate prefers-color-scheme: dark')
88
+ .option('--device-scale-factor <n>', `Device pixel ratio: ${DEVICE_SCALE_FACTORS.join(', ')}`, (v) => parseEnum('--device-scale-factor', DEVICE_SCALE_FACTORS, v))
89
+ .option('--include-html', 'Include the rendered HTML alongside the screenshot')
90
+ .option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
91
+ .option('--language <lang>', 'Accept-Language override (e.g. en-US)')
75
92
  .option('--async', 'Queue the screenshot and return a job envelope immediately (no wait)')
76
93
  .option('-o, --output <file>', 'Download the captured image to this file path (use - for stdout). Not valid with --json.')
77
94
  .action(async (opts) => {
@@ -94,6 +111,30 @@ export function makeScreenshotsCommand(getGlobal) {
94
111
  body['wait_until'] = opts.waitUntil;
95
112
  if (opts.clip !== undefined)
96
113
  body['clip'] = opts.clip;
114
+ // Tri-state booleans: commander yields `undefined` when neither --x nor --no-x is passed,
115
+ // so the key is omitted and the server default (true) holds (E6).
116
+ if (opts.blockAds !== undefined)
117
+ body['block_ads'] = opts.blockAds;
118
+ if (opts.blockCookieBanners !== undefined)
119
+ body['block_cookie_banners'] = opts.blockCookieBanners;
120
+ if (opts.scroll !== undefined)
121
+ body['scroll'] = opts.scroll;
122
+ if (opts.waitForSelector !== undefined)
123
+ body['wait_for_selector'] = opts.waitForSelector;
124
+ if (opts.delayMs !== undefined)
125
+ body['delay_ms'] = opts.delayMs;
126
+ if (opts.darkMode === true)
127
+ body['dark_mode'] = true;
128
+ // device_scale_factor is a spec integer enum [1, 2]; the parser validates the string, and
129
+ // the wire value is numeric.
130
+ if (opts.deviceScaleFactor !== undefined)
131
+ body['device_scale_factor'] = Number(opts.deviceScaleFactor);
132
+ if (opts.includeHtml === true)
133
+ body['include_html'] = true;
134
+ if (opts.country !== undefined)
135
+ body['country'] = opts.country;
136
+ if (opts.language !== undefined)
137
+ body['language'] = opts.language;
97
138
  try {
98
139
  // Reject --json + -o BEFORE issuing the request: -o downloads BINARY artifact bytes, so
99
140
  // it can never coexist with --json (which must stay a text envelope). Thrown here so no
@@ -1,22 +1,31 @@
1
1
  import { Command } from 'commander';
2
2
  import { buildClient } from '../client.js';
3
3
  import { printResult, handleError } from '../output.js';
4
+ import { parseEnum, parseIntFlag } from '../lib/parse-flags.js';
5
+ /** The `SitemapRequest.strategy` spec enum. */
6
+ const SITEMAP_STRATEGIES = ['quick', 'standard', 'deep'];
4
7
  export function makeSitemapCommand(getGlobal) {
5
8
  const cmd = new Command('sitemap').description('Sitemap crawl operations');
6
9
  cmd
7
10
  .command('create')
8
11
  .description('Start a sitemap crawl')
9
12
  .requiredOption('--url <url>', 'Root URL to crawl')
13
+ .option('--strategy <strategy>', `Crawl strategy: ${SITEMAP_STRATEGIES.join(', ')} (omitted → server default standard)`, (v) => parseEnum('--strategy', SITEMAP_STRATEGIES, v))
14
+ .option('--max-depth <n>', 'Maximum crawl depth 1-5 (omitted → server default 2)', (v) => parseIntFlag('--max-depth', v))
15
+ .option('--max-urls <n>', 'Maximum URLs to collect (omitted → server default 5000)', (v) => parseIntFlag('--max-urls', v))
10
16
  .action(async (opts) => {
11
17
  const g = getGlobal();
12
18
  const client = buildClient(g);
13
- // Key-checked subset of the SDK's SitemapCreateRequest (same pattern as monitors.ts).
14
- // The CLI intentionally sends only `url` and relies on the server-side defaults the spec
15
- // declares for the remaining SitemapRequest fields (strategy, max_depth, max_urls, …).
16
- // The generated request type marks those fields required, so the final call needs an
17
- // explicit assertion — type-level only; the wire body is unchanged (pinned by
18
- // __tests__/request-shapes.test.ts).
19
+ // Omit-when-absent body; the remaining SitemapRequest fields keep their server defaults.
20
+ // The generated request type marks defaulted fields required, so the call needs an explicit
21
+ // assertion — type-level only; the wire body is pinned by request-shapes.test.ts.
19
22
  const body = { url: opts.url };
23
+ if (opts.strategy !== undefined)
24
+ body['strategy'] = opts.strategy;
25
+ if (opts.maxDepth !== undefined)
26
+ body['max_depth'] = opts.maxDepth;
27
+ if (opts.maxUrls !== undefined)
28
+ body['max_urls'] = opts.maxUrls;
20
29
  try {
21
30
  const result = await client.sitemap.create(body);
22
31
  printResult(result, g.json);
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander';
2
+ import { type GlobalOptions } from '../client.js';
3
+ /**
4
+ * The `usage` command group (#2545) — the `x-sdk-scope: v0.1` usage endpoints, previously curl-only.
5
+ *
6
+ * `daily` / `by-endpoint` accept the same three optional query flags (`--from`, `--to`,
7
+ * `--api-key`). Each maps 1:1 to its snake_case query param and is written ONLY when present; when
8
+ * NO flag is passed the SDK method is invoked with ZERO arguments (the `monitors list` pattern), so
9
+ * the bare `/v1/usage/daily` URL is serialized unchanged.
10
+ */
11
+ export declare function makeUsageCommand(getGlobal: () => GlobalOptions & {
12
+ json: boolean;
13
+ }): Command;
@@ -0,0 +1,81 @@
1
+ import { Command } from 'commander';
2
+ import { buildClient } from '../client.js';
3
+ import { printResult, handleError } from '../output.js';
4
+ /**
5
+ * The `usage` command group (#2545) — the `x-sdk-scope: v0.1` usage endpoints, previously curl-only.
6
+ *
7
+ * `daily` / `by-endpoint` accept the same three optional query flags (`--from`, `--to`,
8
+ * `--api-key`). Each maps 1:1 to its snake_case query param and is written ONLY when present; when
9
+ * NO flag is passed the SDK method is invoked with ZERO arguments (the `monitors list` pattern), so
10
+ * the bare `/v1/usage/daily` URL is serialized unchanged.
11
+ */
12
+ export function makeUsageCommand(getGlobal) {
13
+ const cmd = new Command('usage').description('Account usage and billing-period reporting');
14
+ cmd
15
+ .command('get')
16
+ .description('Get current billing-period usage summary')
17
+ .action(async () => {
18
+ const g = getGlobal();
19
+ const client = buildClient(g);
20
+ try {
21
+ const result = await client.usage.get();
22
+ printResult(result, g.json);
23
+ }
24
+ catch (err) {
25
+ handleError(err);
26
+ }
27
+ });
28
+ cmd
29
+ .command('daily')
30
+ .description('Get daily usage breakdown (default last 30 days; max 366)')
31
+ .option('--from <date>', 'Start date YYYY-MM-DD (omitted → server default, 30 days ago)')
32
+ .option('--to <date>', 'End date YYYY-MM-DD (omitted → server default, today)')
33
+ .option('--api-key <ak_id>', 'Filter by API key prefixed ID (omitted → all keys)')
34
+ .action(async (opts) => {
35
+ const g = getGlobal();
36
+ const client = buildClient(g);
37
+ const params = {};
38
+ if (opts.from !== undefined)
39
+ params.from = opts.from;
40
+ if (opts.to !== undefined)
41
+ params.to = opts.to;
42
+ if (opts.apiKey !== undefined)
43
+ params.api_key = opts.apiKey;
44
+ try {
45
+ const result = Object.keys(params).length > 0
46
+ ? await client.usage.daily(params)
47
+ : await client.usage.daily();
48
+ printResult(result, g.json);
49
+ }
50
+ catch (err) {
51
+ handleError(err);
52
+ }
53
+ });
54
+ cmd
55
+ .command('by-endpoint')
56
+ .description('Get per-endpoint bandwidth breakdown (default last 30 days; max 366)')
57
+ .option('--from <date>', 'Start date YYYY-MM-DD (omitted → server default, 30 days ago)')
58
+ .option('--to <date>', 'End date YYYY-MM-DD (omitted → server default, today)')
59
+ .option('--api-key <ak_id>', 'Filter by API key prefixed ID (omitted → all keys)')
60
+ .action(async (opts) => {
61
+ const g = getGlobal();
62
+ const client = buildClient(g);
63
+ const params = {};
64
+ if (opts.from !== undefined)
65
+ params.from = opts.from;
66
+ if (opts.to !== undefined)
67
+ params.to = opts.to;
68
+ if (opts.apiKey !== undefined)
69
+ params.api_key = opts.apiKey;
70
+ try {
71
+ const result = Object.keys(params).length > 0
72
+ ? await client.usage.byEndpoint(params)
73
+ : await client.usage.byEndpoint();
74
+ printResult(result, g.json);
75
+ }
76
+ catch (err) {
77
+ handleError(err);
78
+ }
79
+ });
80
+ return cmd;
81
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from 'commander';
2
+ import { type GlobalOptions } from '../client.js';
3
+ /**
4
+ * The `webhook-deliveries` command group (#2545) — inspect and requeue webhook delivery attempts.
5
+ *
6
+ * `list` flags map 1:1 to their query params and are omitted-when-absent; a bare `list` calls the
7
+ * SDK with ZERO args so the plain `/v1/webhook-deliveries` URL is serialized unchanged.
8
+ */
9
+ export declare function makeWebhookDeliveriesCommand(getGlobal: () => GlobalOptions & {
10
+ json: boolean;
11
+ }): Command;
@@ -0,0 +1,59 @@
1
+ import { Command } from 'commander';
2
+ import { buildClient } from '../client.js';
3
+ import { printResult, handleError } from '../output.js';
4
+ import { parseEnum, parseIntFlag } from '../lib/parse-flags.js';
5
+ /** The `listWebhookDeliveries` `status` query enum (DISTINCT from the monitor status enum). */
6
+ const DELIVERY_STATUSES = ['pending', 'delivered', 'failed', 'exhausted'];
7
+ /**
8
+ * The `webhook-deliveries` command group (#2545) — inspect and requeue webhook delivery attempts.
9
+ *
10
+ * `list` flags map 1:1 to their query params and are omitted-when-absent; a bare `list` calls the
11
+ * SDK with ZERO args so the plain `/v1/webhook-deliveries` URL is serialized unchanged.
12
+ */
13
+ export function makeWebhookDeliveriesCommand(getGlobal) {
14
+ const cmd = new Command('webhook-deliveries').description('Webhook delivery operations');
15
+ cmd
16
+ .command('list')
17
+ .description('List webhook deliveries')
18
+ .option('--limit <n>', 'Maximum number of deliveries to return', (v) => parseIntFlag('--limit', v))
19
+ .option('--cursor <cursor>', 'Pagination cursor from a previous page')
20
+ .option('--status <status>', `Filter by delivery status: ${DELIVERY_STATUSES.join(', ')}`, (v) => parseEnum('--status', DELIVERY_STATUSES, v))
21
+ .option('--since <iso>', 'Only deliveries created after this ISO date-time')
22
+ .action(async (opts) => {
23
+ const g = getGlobal();
24
+ const client = buildClient(g);
25
+ const params = {};
26
+ if (opts.limit !== undefined)
27
+ params.limit = opts.limit;
28
+ if (opts.cursor !== undefined)
29
+ params.cursor = opts.cursor;
30
+ if (opts.status !== undefined)
31
+ params.status = opts.status;
32
+ if (opts.since !== undefined)
33
+ params.since = opts.since;
34
+ try {
35
+ const result = Object.keys(params).length > 0
36
+ ? await client.webhooks.list(params)
37
+ : await client.webhooks.list();
38
+ printResult(result, g.json);
39
+ }
40
+ catch (err) {
41
+ handleError(err);
42
+ }
43
+ });
44
+ cmd
45
+ .command('retry <id>')
46
+ .description('Requeue a failed or exhausted webhook delivery')
47
+ .action(async (id) => {
48
+ const g = getGlobal();
49
+ const client = buildClient(g);
50
+ try {
51
+ const result = await client.webhooks.retry(id);
52
+ printResult(result, g.json);
53
+ }
54
+ catch (err) {
55
+ handleError(err);
56
+ }
57
+ });
58
+ return cmd;
59
+ }
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { makeMonitorsCommand } from './commands/monitors.js';
11
11
  import { makeSitemapCommand } from './commands/sitemap.js';
12
12
  import { makeSearchCommand } from './commands/search.js';
13
13
  import { makeAccountCommand } from './commands/account.js';
14
+ import { makeUsageCommand } from './commands/usage.js';
15
+ import { makeWebhookDeliveriesCommand } from './commands/webhook-deliveries.js';
14
16
  // Resolve version from package.json at runtime (supports compiled output).
15
17
  const require = createRequire(import.meta.url);
16
18
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -44,6 +46,8 @@ export function buildProgram() {
44
46
  program.addCommand(makeSitemapCommand(getGlobal));
45
47
  program.addCommand(makeSearchCommand(getGlobal));
46
48
  program.addCommand(makeAccountCommand(getGlobal));
49
+ program.addCommand(makeUsageCommand(getGlobal));
50
+ program.addCommand(makeWebhookDeliveriesCommand(getGlobal));
47
51
  return program;
48
52
  }
49
53
  /** Resolve symlinks and `..` segments, falling back to the raw path if it cannot be resolved. */