@hunt-labs/bytekit-cli 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,27 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.10.0] - 2026-09-14
14
+
15
+ ### Added
16
+
17
+ - `--solve-challenge` on `scrape create` (#4449). It sends `solve_challenge: true`, which asks
18
+ the scrape to attempt to clear a Cloudflare challenge; success is not guaranteed. It is billed
19
+ as a normal browser render. Without the flag the request body is unchanged and carries no
20
+ `solve_challenge` key.
21
+
22
+ ## [0.9.4] - 2026-09-10
23
+
24
+ ### Added
25
+
26
+ - `--cursor`, `--limit` and `--status` on the three bulk recovery commands (#3717):
27
+ `bulk screenshots list <id>`, `scrape bulk get <id>` and `fetch bulk get <id>`.
28
+ They forward the cursor pagination the API already exposed, so a job whose webhook
29
+ manifest came back incomplete can be enumerated page by page from the CLI.
30
+ `--limit` is bounded 1-500 and `--status` accepts only the documented result
31
+ statuses; both are rejected locally before a request is made. No new command and no
32
+ `--all` flag — the existing commands gained options, nothing else moved.
33
+
13
34
  ## [0.9.3] - 2026-09-03
14
35
 
15
36
  ### Changed
package/README.md CHANGED
@@ -129,6 +129,7 @@ materialized):
129
129
  | `--include-tags <csv>` | Comma-separated HTML tags to keep. |
130
130
  | `--exclude-tags <csv>` | Comma-separated HTML tags to drop. |
131
131
  | `--mobile` | Render with a mobile viewport/user-agent. |
132
+ | `--solve-challenge` | Attempt to clear a Cloudflare challenge; success is not guaranteed. Billed as a normal browser render. |
132
133
  | `--async` | Queue the scrape and return a job envelope immediately. |
133
134
  | `--webhook-url <url>` | Webhook notified when an async scrape completes. |
134
135
 
@@ -276,8 +277,10 @@ bytekit bulk screenshots list "$bulk_id"
276
277
  `urls.txt` is newline-delimited: one plain URL (or `{"url":"https://..."}`) per
277
278
  line. A line whose only key is `url` is sent as a bare URL string; any line with
278
279
  additional keys switches that entry to the per-item shape. `bulk cancel <id>`
279
- stops an in-flight job; `bulk get <id>` and `bulk screenshots list <id>` take no
280
- flags.
280
+ stops an in-flight job. Recover another result page with `--cursor <cursor>`,
281
+ bound it with `--limit <n>`, or filter it with `--status <status>` on
282
+ `bulk screenshots list <id>`. The same recovery options are available on
283
+ `scrape bulk get <id>` and `fetch bulk get <id>`.
281
284
 
282
285
  The three bulk-job creators — `bulk create`, `scrape bulk create` and
283
286
  `fetch bulk create` — share one flag set:
@@ -2,19 +2,34 @@ import { Command } from 'commander';
2
2
  import { buildClient } from '../client.js';
3
3
  import { printResult, handleError } from '../output.js';
4
4
  import { parseBulkLines, toBulkItems } from '../lib/url-lines.js';
5
- import { parseJsonFlag } from '../lib/parse-flags.js';
5
+ import { parseEnum, parseIntFlag, parseJsonFlag } from '../lib/parse-flags.js';
6
+ const BULK_RESULT_STATUSES = ['pending', 'processing', 'completed', 'failed', 'cancelled'];
7
+ // Mirrors the `limit` query schema (`minimum: 1, maximum: 500`) on this job's recovery route in
8
+ // docs/api/openapi.yaml, so the range the `--limit` help text advertises is enforced locally.
9
+ const PAGE_LIMIT_BOUNDS = { min: 1, max: 500 };
6
10
  export function makeBulkCommand(getGlobal) {
7
11
  const cmd = new Command('bulk').description('Bulk screenshot operations');
8
12
  const screenshotsCmd = new Command('screenshots').description('Bulk screenshot sub-resources');
9
13
  screenshotsCmd
10
14
  .command('list <id>')
11
- .description('List screenshots in a bulk job')
12
- .action(async (id) => {
15
+ .description('List items in a mixed bulk job')
16
+ .option('--cursor <cursor>', 'Continue from a cursor returned by the preceding page')
17
+ .option('--limit <n>', 'Maximum items per type to return (1-500)', (v) => parseIntFlag('--limit', v, PAGE_LIMIT_BOUNDS))
18
+ .option('--status <status>', `Filter items by status: ${BULK_RESULT_STATUSES.join(', ')}`, (v) => parseEnum('--status', BULK_RESULT_STATUSES, v))
19
+ .action(async (id, opts) => {
13
20
  const g = getGlobal();
14
21
  try {
15
22
  // 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.
16
23
  const client = buildClient(g);
17
- const result = await client.bulk.screenshots.list(id);
24
+ const bulkId = id;
25
+ const pageParams = {
26
+ ...(opts.cursor !== undefined ? { cursor: opts.cursor } : {}),
27
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
28
+ ...(opts.status !== undefined ? { status: opts.status } : {}),
29
+ };
30
+ const result = Object.keys(pageParams).length === 0
31
+ ? await client.bulk.screenshots.list(bulkId)
32
+ : await client.bulk.screenshots.list(bulkId, pageParams);
18
33
  printResult(result, g.json);
19
34
  }
20
35
  catch (err) {
@@ -2,7 +2,11 @@ import { Command } from 'commander';
2
2
  import { buildClient } from '../client.js';
3
3
  import { printResult, emitFetchResult, handleError } from '../output.js';
4
4
  import { parseBulkLines } from '../lib/url-lines.js';
5
- import { parseIntFlag, parseCacheTtl, parseJsonFlag } from '../lib/parse-flags.js';
5
+ import { parseIntFlag, parseCacheTtl, parseJsonFlag, parseEnum } from '../lib/parse-flags.js';
6
+ const BULK_RESULT_STATUSES = ['pending', 'processing', 'completed', 'failed', 'cancelled'];
7
+ // Mirrors the `limit` query schema (`minimum: 1, maximum: 500`) on this job's recovery route in
8
+ // docs/api/openapi.yaml, so the range the `--limit` help text advertises is enforced locally.
9
+ const PAGE_LIMIT_BOUNDS = { min: 1, max: 500 };
6
10
  export function makeFetchCommand(getGlobal) {
7
11
  const cmd = new Command('fetch').description('Fetch URL content operations');
8
12
  const bulkCmd = new Command('bulk').description('Bulk fetch operations');
@@ -44,12 +48,23 @@ export function makeFetchCommand(getGlobal) {
44
48
  bulkCmd
45
49
  .command('get <id>')
46
50
  .description('Get a bulk fetch job')
47
- .action(async (id) => {
51
+ .option('--cursor <cursor>', 'Continue from a cursor returned by the preceding page')
52
+ .option('--limit <n>', 'Maximum items to return (1-500)', (v) => parseIntFlag('--limit', v, PAGE_LIMIT_BOUNDS))
53
+ .option('--status <status>', `Filter items by status: ${BULK_RESULT_STATUSES.join(', ')}`, (v) => parseEnum('--status', BULK_RESULT_STATUSES, v))
54
+ .action(async (id, opts) => {
48
55
  const g = getGlobal();
49
56
  try {
50
57
  // 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.
51
58
  const client = buildClient(g);
52
- const result = await client.fetch.bulk.get(id);
59
+ const bulkId = id;
60
+ const pageParams = {
61
+ ...(opts.cursor !== undefined ? { cursor: opts.cursor } : {}),
62
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
63
+ ...(opts.status !== undefined ? { status: opts.status } : {}),
64
+ };
65
+ const result = Object.keys(pageParams).length === 0
66
+ ? await client.fetch.bulk.get(bulkId)
67
+ : await client.fetch.bulk.get(bulkId, pageParams);
53
68
  printResult(result, g.json);
54
69
  }
55
70
  catch (err) {
@@ -11,6 +11,10 @@ const SCRAPE_EVENTS = ['queued', 'completed', 'failed'];
11
11
  const TOKEN_ENCODINGS = ['cl100k_base', 'o200k_base'];
12
12
  /** The `markdown_links` / `markdown_images` render-mode enum. */
13
13
  const MARKDOWN_RENDER_MODES = ['inline', 'references', 'none', 'text'];
14
+ const BULK_RESULT_STATUSES = ['pending', 'processing', 'completed', 'failed', 'cancelled'];
15
+ // Mirrors the `limit` query schema (`minimum: 1, maximum: 500`) on this job's recovery route in
16
+ // docs/api/openapi.yaml, so the range the `--limit` help text advertises is enforced locally.
17
+ const PAGE_LIMIT_BOUNDS = { min: 1, max: 500 };
14
18
  /**
15
19
  * Exit code when a queued (202 async) envelope reaches a non-JSON output mode.
16
20
  *
@@ -83,12 +87,23 @@ export function makeScrapeCommand(getGlobal) {
83
87
  bulkCmd
84
88
  .command('get <id>')
85
89
  .description('Get a bulk scrape job')
86
- .action(async (id) => {
90
+ .option('--cursor <cursor>', 'Continue from a cursor returned by the preceding page')
91
+ .option('--limit <n>', 'Maximum items to return (1-500)', (v) => parseIntFlag('--limit', v, PAGE_LIMIT_BOUNDS))
92
+ .option('--status <status>', `Filter items by status: ${BULK_RESULT_STATUSES.join(', ')}`, (v) => parseEnum('--status', BULK_RESULT_STATUSES, v))
93
+ .action(async (id, opts) => {
87
94
  const g = getGlobal();
88
95
  try {
89
96
  // 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
97
  const client = buildClient(g);
91
- const result = await client.scrape.bulk.get(id);
98
+ const bulkId = id;
99
+ const pageParams = {
100
+ ...(opts.cursor !== undefined ? { cursor: opts.cursor } : {}),
101
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
102
+ ...(opts.status !== undefined ? { status: opts.status } : {}),
103
+ };
104
+ const result = Object.keys(pageParams).length === 0
105
+ ? await client.scrape.bulk.get(bulkId)
106
+ : await client.scrape.bulk.get(bulkId, pageParams);
92
107
  printResult(result, g.json);
93
108
  }
94
109
  catch (err) {
@@ -139,6 +154,7 @@ export function makeScrapeCommand(getGlobal) {
139
154
  .option('--include-tags <csv>', 'Comma-separated HTML tags to keep (e.g. article,main)')
140
155
  .option('--exclude-tags <csv>', 'Comma-separated HTML tags to drop (e.g. nav,footer)')
141
156
  .option('--mobile', 'Render with a mobile viewport/user-agent')
157
+ .option('--solve-challenge', 'Attempt to clear a Cloudflare challenge; success is not guaranteed. Billed as a normal browser render.')
142
158
  .option('--raw', 'Print the requested format body raw, with no JSON wrapper')
143
159
  .option('-o, --output <file>', 'Write the output to the given file path (implies --raw unless --json is set)')
144
160
  .action(async (opts) => {
@@ -206,6 +222,8 @@ export function makeScrapeCommand(getGlobal) {
206
222
  body['exclude_tags'] = parseCsv(opts.excludeTags);
207
223
  if (opts.mobile === true)
208
224
  body['mobile'] = true;
225
+ if (opts.solveChallenge === true)
226
+ body['solve_challenge'] = true;
209
227
  try {
210
228
  // 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.
211
229
  const client = buildClient(g);
@@ -8,8 +8,16 @@
8
8
  * Parse a whole-number flag value. The strict `/^\d+$/` test deterministically rejects `abc`, ``,
9
9
  * `1.5`, `-5`, `1e2`, so an off-spec value fails at parse time with an actionable message instead
10
10
  * of a server 422 or a NaN on the wire.
11
+ *
12
+ * `bounds` extends that guarantee to flags whose spec schema declares a range (#3717): a
13
+ * well-formed integer outside `[min, max]` is just as certain to 422 as `abc` is, so it is
14
+ * rejected here rather than spending a round trip to be told so. Pass it wherever the flag's help
15
+ * text states a range; omit it where the spec declares none.
11
16
  */
12
- export declare function parseIntFlag(flag: string, value: string): number;
17
+ export declare function parseIntFlag(flag: string, value: string, bounds?: {
18
+ min: number;
19
+ max: number;
20
+ }): number;
13
21
  /** Reject an off-spec enum value at parse time with the allowed set in the message. */
14
22
  export declare function parseEnum<T extends string>(flag: string, allowed: readonly T[], value: string): T;
15
23
  /** Split a comma-separated flag value into a trimmed, non-empty string array. */
@@ -9,12 +9,21 @@ import { InvalidArgumentError } from 'commander';
9
9
  * Parse a whole-number flag value. The strict `/^\d+$/` test deterministically rejects `abc`, ``,
10
10
  * `1.5`, `-5`, `1e2`, so an off-spec value fails at parse time with an actionable message instead
11
11
  * of a server 422 or a NaN on the wire.
12
+ *
13
+ * `bounds` extends that guarantee to flags whose spec schema declares a range (#3717): a
14
+ * well-formed integer outside `[min, max]` is just as certain to 422 as `abc` is, so it is
15
+ * rejected here rather than spending a round trip to be told so. Pass it wherever the flag's help
16
+ * text states a range; omit it where the spec declares none.
12
17
  */
13
- export function parseIntFlag(flag, value) {
18
+ export function parseIntFlag(flag, value, bounds) {
14
19
  if (!/^\d+$/.test(value)) {
15
20
  throw new InvalidArgumentError(`${flag} must be a whole number.`);
16
21
  }
17
- return Number(value);
22
+ const parsed = Number(value);
23
+ if (bounds && (parsed < bounds.min || parsed > bounds.max)) {
24
+ throw new InvalidArgumentError(`${flag} must be between ${bounds.min} and ${bounds.max}.`);
25
+ }
26
+ return parsed;
18
27
  }
19
28
  /** Reject an off-spec enum value at parse time with the allowed set in the message. */
20
29
  export function parseEnum(flag, allowed, value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunt-labs/bytekit-cli",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "ByteKit CLI — wraps @hunt-labs/bytekit-sdk as shell subcommands",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "commander": "^12.0.0",
44
- "@hunt-labs/bytekit-sdk": "^0.9.2"
44
+ "@hunt-labs/bytekit-sdk": "^0.11.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc",