@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.
- package/CHANGELOG.md +203 -3
- package/README.md +74 -6
- package/dist/client.d.ts +126 -6
- package/dist/client.js +248 -30
- package/dist/commands/account.js +2 -1
- package/dist/commands/bulk.js +30 -8
- package/dist/commands/fetch.js +83 -20
- package/dist/commands/monitors.js +46 -8
- package/dist/commands/scrape.js +38 -9
- package/dist/commands/screenshots.js +184 -7
- package/dist/commands/search.js +2 -1
- package/dist/commands/sitemap.js +28 -3
- package/dist/commands/usage.js +6 -3
- package/dist/commands/webhook-deliveries.js +4 -2
- package/dist/lib/download-artifact.d.ts +30 -1
- package/dist/lib/download-artifact.js +90 -5
- package/dist/lib/parse-flags.js +3 -1
- package/dist/lib/url-lines.d.ts +46 -8
- package/dist/lib/url-lines.js +58 -19
- package/dist/output.d.ts +33 -2
- package/dist/output.js +113 -5
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@ import { Command, InvalidArgumentError } from 'commander';
|
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, handleError } from '../output.js';
|
|
4
4
|
import { downloadArtifact } from '../lib/download-artifact.js';
|
|
5
|
-
import { parseEnum, parseIntFlag } from '../lib/parse-flags.js';
|
|
5
|
+
import { parseEnum, parseIntFlag, parseJsonFlag } from '../lib/parse-flags.js';
|
|
6
6
|
/** `ScreenshotRequest` spec enums. */
|
|
7
7
|
const DEVICES = ['desktop', 'mobile'];
|
|
8
8
|
const FORMATS = ['jpeg', 'png'];
|
|
@@ -23,12 +23,122 @@ export function parseClip(value) {
|
|
|
23
23
|
throw new InvalidArgumentError('--clip must be X,Y,WIDTH,HEIGHT (e.g. 0,0,800,600).');
|
|
24
24
|
return { x: Number(m[1]), y: Number(m[2]), width: Number(m[3]), height: Number(m[4]) };
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Exit code when a queued (202 async) envelope reaches human output (#2596).
|
|
28
|
+
*
|
|
29
|
+
* Mirrors `scrape create`'s own `QUEUED_EXIT_CODE` (`commands/scrape.ts:40`) EXACTLY, and is
|
|
30
|
+
* likewise a LOCAL constant rather than an import — the two commands agree on the value, not on a
|
|
31
|
+
* shared symbol, and the guard stays where the ticket pins it (the screenshots async branch)
|
|
32
|
+
* instead of migrating into the shared printer, where it would silently set exit 2 for every
|
|
33
|
+
* command that prints a result.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT 0 (a completed capture) and NOT 1 (a usage/API error): the job WAS accepted and
|
|
36
|
+
* billed, the artifact simply does not exist yet, and a distinct code lets a caller branch on
|
|
37
|
+
* "queued" without parsing stderr.
|
|
38
|
+
*
|
|
39
|
+
* BREAKING (#2596): `screenshots create --async` previously exited 0. See the README exit-code
|
|
40
|
+
* table and the CHANGELOG `[0.6.0]` note (`[Unreleased]` is kept deliberately empty per
|
|
41
|
+
* `packaging.test.ts:203`; see #2620).
|
|
42
|
+
*
|
|
43
|
+
* Also carried by a NOT-READY `screenshots get -o` poll (#2686 item 1) — same meaning, same value:
|
|
44
|
+
* accepted and billed, artifact not materialized yet.
|
|
45
|
+
*/
|
|
46
|
+
const QUEUED_EXIT_CODE = 2;
|
|
47
|
+
/**
|
|
48
|
+
* The ONLY statuses a screenshot can still reach `completed` from, and therefore the only ones
|
|
49
|
+
* worth telling a caller to poll (#2792). Every other value of `status` — the terminal ones, and
|
|
50
|
+
* anything the server may add later — is a state polling cannot improve.
|
|
51
|
+
*/
|
|
52
|
+
const POLLABLE_STATUSES = ['pending', 'processing'];
|
|
53
|
+
/**
|
|
54
|
+
* A still-pollable screenshot poll that can hand its `ss_` handle back, or undefined.
|
|
55
|
+
*
|
|
56
|
+
* "Not ready" is `status === 'pending' || status === 'processing'` and nothing else, plus a usable
|
|
57
|
+
* `ss_` handle and no materialized `image_url` (#2792). Before that gate existed the predicate
|
|
58
|
+
* decided from `error`/`image_url`/`id` alone and read `status` only to LABEL the message, so a
|
|
59
|
+
* `completed` envelope with a null `image_url`, a status outside the enum, and the FLAT
|
|
60
|
+
* `error_code`/`error_message` failed shape that `serializeScreenshot` emits all received "poll
|
|
61
|
+
* again" advice for a job that would never complete — the exact loop the exclusion exists to
|
|
62
|
+
* prevent.
|
|
63
|
+
*
|
|
64
|
+
* Two shapes are excluded deliberately rather than incidentally:
|
|
65
|
+
*
|
|
66
|
+
* - **No `status` at all (#2792 E1).** Not treated as pending. `serializeScreenshot`
|
|
67
|
+
* (`gateway/src/routes/screenshots.ts:365`) always writes `status`, so a statusless envelope is
|
|
68
|
+
* not something the server produces; and an envelope carrying no evidence that work is in
|
|
69
|
+
* progress is exactly what must not be advertised as pollable. It takes the error path, which
|
|
70
|
+
* names the state instead of promising an artifact.
|
|
71
|
+
* - **A status outside the enum (#2792 E3).** Same reasoning, stated as the closed-set gate: a new
|
|
72
|
+
* server status is unknown to this build, so it is reported, never polled and never fallen
|
|
73
|
+
* through into the download path.
|
|
74
|
+
*
|
|
75
|
+
* And one shape is deliberately NOT excluded: a pollable status carrying a flat `error_code`. That
|
|
76
|
+
* is the ordinary RETRY envelope, not a verdict — see {@link flatScreenshotFailure}. Excluding it
|
|
77
|
+
* would abandon a job the server is actively retrying (#2792 B1).
|
|
78
|
+
*
|
|
79
|
+
* An envelope with no string `id` is excluded too — there is no handle to emit, so the ordinary
|
|
80
|
+
* error path (which at least names the status) is the honest outcome.
|
|
81
|
+
*/
|
|
82
|
+
function notReadyScreenshot(result) {
|
|
83
|
+
if (typeof result !== 'object' || result === null)
|
|
84
|
+
return undefined;
|
|
85
|
+
const obj = result;
|
|
86
|
+
// The gate, not merely the label: `status` decides, and the closed list is the whole decision.
|
|
87
|
+
const status = POLLABLE_STATUSES.find((s) => s === obj.status);
|
|
88
|
+
if (status === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
// The NESTED error envelope stays disqualifying, exactly as before this ticket. A flat
|
|
91
|
+
// `error_code` is NOT consulted here: on a pollable status it is a retry error (#2792 B1), and
|
|
92
|
+
// the nested shape can never co-occur with a pollable status to disagree — it is built only by
|
|
93
|
+
// `buildFailedScreenshotEnvelope`, under `status === 'failed'` (`routes/screenshots.ts:267`).
|
|
94
|
+
if (obj.error !== undefined && obj.error !== null)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (typeof obj.image_url === 'string' && obj.image_url.length > 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (typeof obj.id !== 'string' || obj.id.length === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
return { id: obj.id, status };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The failure carried by the FLAT `error_code`/`error_message` fields, or undefined (#2792).
|
|
104
|
+
*
|
|
105
|
+
* `serializeScreenshot` (gateway `routes/screenshots.ts:365`) — and therefore the spec's own
|
|
106
|
+
* `ScreenshotResponse` — reports a failure as top-level `error_code`/`error_message`, while
|
|
107
|
+
* `buildFailedScreenshotEnvelope` (`:295`, #2027) reports it NESTED as `{error:{code,message}}`.
|
|
108
|
+
* Only the nested one was ever read, so a flat failure surfaced neither code nor message.
|
|
109
|
+
*
|
|
110
|
+
* These two are NOT interchangeable, which is why this helper reads only one of them:
|
|
111
|
+
*
|
|
112
|
+
* - The nested envelope is built only under `status === 'failed'` (`routes/screenshots.ts:267`), so
|
|
113
|
+
* it is always terminal.
|
|
114
|
+
* - The flat columns are ALSO written by the non-terminal retry path — `job-queue.ts:346-356` sets
|
|
115
|
+
* `error_code`/`error_message` and `status = 'pending'` in one UPDATE, and `claim()` (`:276-284`)
|
|
116
|
+
* carries them into the next `processing` attempt. On a pollable status they describe the attempt
|
|
117
|
+
* that just failed, not the job's outcome (#2792 B1).
|
|
118
|
+
*
|
|
119
|
+
* So the caller consults this only once the status is no longer pollable.
|
|
120
|
+
*/
|
|
121
|
+
function flatScreenshotFailure(obj) {
|
|
122
|
+
const detail = {};
|
|
123
|
+
if (typeof obj.error_code === 'string' && obj.error_code.length > 0)
|
|
124
|
+
detail.code = obj.error_code;
|
|
125
|
+
if (typeof obj.error_message === 'string' && obj.error_message.length > 0)
|
|
126
|
+
detail.message = obj.error_message;
|
|
127
|
+
return detail.code === undefined && detail.message === undefined ? undefined : detail;
|
|
128
|
+
}
|
|
26
129
|
/**
|
|
27
130
|
* Extract a non-empty string `image_url` from a screenshot envelope, or throw an actionable error.
|
|
28
131
|
*
|
|
29
|
-
* A pending/processing envelope has no `image_url` yet; a terminal-`failed` screenshot
|
|
30
|
-
*
|
|
31
|
-
*
|
|
132
|
+
* A pending/processing envelope has no `image_url` yet; a terminal-`failed` screenshot reports its
|
|
133
|
+
* failure either nested or flat. Either way the artifact was never materialized, so we throw rather
|
|
134
|
+
* than fetching `undefined` (#2480 E5).
|
|
135
|
+
*
|
|
136
|
+
* The NESTED shape's message is untouched by #2792 — the ticket's Specification pins it as an
|
|
137
|
+
* inverse case ("the nested-error failed shape keeps its current exit-1 message"), and it is now
|
|
138
|
+
* pinned by an exact-equality test rather than the substring assertions that would not have
|
|
139
|
+
* noticed. The FLAT shape, which previously reported nothing at all, names both of its fields as
|
|
140
|
+
* `<message> (<code>)` — `extractRawBody`'s format for a failed scrape, so the two poll commands
|
|
141
|
+
* read alike — degrading to whichever field exists.
|
|
32
142
|
*/
|
|
33
143
|
function extractImageUrl(result) {
|
|
34
144
|
if (typeof result === 'object' && result !== null) {
|
|
@@ -40,8 +150,28 @@ function extractImageUrl(result) {
|
|
|
40
150
|
return obj.image_url;
|
|
41
151
|
}
|
|
42
152
|
const status = typeof obj.status === 'string' ? obj.status : 'unknown';
|
|
43
|
-
|
|
153
|
+
// "not ready yet" is reserved for a genuinely pollable state (#2792). It is still reachable
|
|
154
|
+
// here for a pending/processing envelope carrying no `ss_` id — `notReadyScreenshot` rejects
|
|
155
|
+
// those for want of a handle, and this message stays their wording. Reached BEFORE the flat
|
|
156
|
+
// failure below, because on a pollable status those columns describe the attempt that just
|
|
157
|
+
// failed and is being retried, not the job's outcome (#2792 B1).
|
|
158
|
+
if (POLLABLE_STATUSES.includes(status)) {
|
|
159
|
+
throw new Error(`not ready yet: screenshot is ${status}`);
|
|
160
|
+
}
|
|
161
|
+
const flat = flatScreenshotFailure(obj);
|
|
162
|
+
if (flat !== undefined) {
|
|
163
|
+
const reason = flat.message !== undefined && flat.code !== undefined
|
|
164
|
+
? `${flat.message} (${flat.code})`
|
|
165
|
+
: (flat.message ?? flat.code ?? 'unknown error');
|
|
166
|
+
throw new Error(`screenshot failed: ${reason}`);
|
|
167
|
+
}
|
|
168
|
+
// Every other artifact-less state says so rather than implying the artifact soon will exist: a
|
|
169
|
+
// `completed` row with a null `image_url`, and any status this build does not know.
|
|
170
|
+
throw new Error(`no image artifact available: screenshot is ${status}`);
|
|
44
171
|
}
|
|
172
|
+
// The non-object arm keeps its original wording, pinned by #2480 cr-5
|
|
173
|
+
// (`screenshot-download.test.ts:287`): this is not a screenshot envelope at all, so neither the
|
|
174
|
+
// status gate above nor its vocabulary applies.
|
|
45
175
|
throw new Error('not ready yet: screenshot is unknown');
|
|
46
176
|
}
|
|
47
177
|
/**
|
|
@@ -53,9 +183,43 @@ function extractImageUrl(result) {
|
|
|
53
183
|
async function emitScreenshot(result, opts) {
|
|
54
184
|
if (opts.isAsync === true) {
|
|
55
185
|
printResult(result, opts.json);
|
|
186
|
+
// Discriminate on the --async FLAG, never on the response body. The screenshot 202 reports a
|
|
187
|
+
// PENDING status (gateway src/routes/screenshots.ts:138) — not the queued status that
|
|
188
|
+
// `scrape create`'s envelope guard (commands/scrape.ts:49-53) tests for. Reusing that guard
|
|
189
|
+
// here would never fire in production and this branch would silently no-op. Grep gate G11
|
|
190
|
+
// (#2596) enforces the absence of a response-status discriminator in this file, which is why
|
|
191
|
+
// neither the symbol nor the comparison is spelled out above.
|
|
192
|
+
//
|
|
193
|
+
// The assignment sits INSIDE this branch, ahead of the `-o` return below, so `--async -o`
|
|
194
|
+
// carries the same exit code as bare `--async` (#2596 DoD 15 / E20). `--json` keeps exit 0:
|
|
195
|
+
// the envelope is a complete machine-readable payload, identical to scrape's split
|
|
196
|
+
// (scrape.ts:313-317). Soft `process.exitCode`, never `process.exit` — a hard exit runs
|
|
197
|
+
// before Node drains an async piped stdout and would truncate the envelope just written
|
|
198
|
+
// (#2544).
|
|
199
|
+
if (!opts.json)
|
|
200
|
+
process.exitCode = QUEUED_EXIT_CODE;
|
|
56
201
|
return;
|
|
57
202
|
}
|
|
58
203
|
if (opts.output !== undefined) {
|
|
204
|
+
// A not-ready poll has no artifact to download — but the job WAS accepted and billed, so
|
|
205
|
+
// losing the handle behind a generic exit 1 is the defect (#2686 item 1). Mirror
|
|
206
|
+
// `scrape get --raw` on a queued envelope EXACTLY: the bare id on STDOUT (so
|
|
207
|
+
// `ss=$(bytekit screenshots get … -o -)` still captures a usable handle), a human note on
|
|
208
|
+
// STDERR, and QUEUED_EXIT_CODE. The `-o` file is deliberately never written — there is no
|
|
209
|
+
// image yet, and a file holding a bare id would masquerade as an artifact.
|
|
210
|
+
//
|
|
211
|
+
// Soft `process.exitCode`, never `process.exit`: a hard exit runs before Node drains an async
|
|
212
|
+
// piped stdout and would discard the very id this branch exists to deliver (#2544). Execution
|
|
213
|
+
// therefore continues past the assignment, so this branch must `return` explicitly — falling
|
|
214
|
+
// through to `extractImageUrl` would throw `not ready yet` and clobber the signal with exit 1.
|
|
215
|
+
const notReady = notReadyScreenshot(result);
|
|
216
|
+
if (notReady !== undefined) {
|
|
217
|
+
console.error(`Note: screenshot ${notReady.id} is ${notReady.status} — no image artifact is available ` +
|
|
218
|
+
`yet. Poll it with: bytekit screenshots get ${notReady.id}`);
|
|
219
|
+
process.stdout.write(`${notReady.id}\n`);
|
|
220
|
+
process.exitCode = QUEUED_EXIT_CODE;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
59
223
|
await downloadArtifact(extractImageUrl(result), opts.output);
|
|
60
224
|
return;
|
|
61
225
|
}
|
|
@@ -89,11 +253,13 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
89
253
|
.option('--include-html', 'Include the rendered HTML alongside the screenshot')
|
|
90
254
|
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
91
255
|
.option('--language <lang>', 'Accept-Language override (e.g. en-US)')
|
|
256
|
+
.option('--headers <json>', `Extra request headers as a JSON object, e.g. '{"X-A":"b"}' (omitted → none)`, (v) => parseJsonFlag('--headers', v, 'object'))
|
|
257
|
+
.option('--cookies <json>', `Cookies as a JSON array of {name,value,domain}, e.g. '[{"name":"a","value":"b","domain":"example.com"}]' (omitted → none)`, (v) => parseJsonFlag('--cookies', v, 'array'))
|
|
258
|
+
.option('--metadata <json>', `Arbitrary metadata echoed on the screenshot envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
92
259
|
.option('--async', 'Queue the screenshot and return a job envelope immediately (no wait)')
|
|
93
260
|
.option('-o, --output <file>', 'Download the captured image to this file path (use - for stdout). Not valid with --json.')
|
|
94
261
|
.action(async (opts) => {
|
|
95
262
|
const g = getGlobal();
|
|
96
|
-
const client = buildClient(g);
|
|
97
263
|
const body = { url: opts.url };
|
|
98
264
|
// Each optional flag maps 1:1 to its snake_case ScreenshotRequest field, written ONLY when
|
|
99
265
|
// present — a boolean never serializes `false` (#2480 E1).
|
|
@@ -135,7 +301,17 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
135
301
|
body['country'] = opts.country;
|
|
136
302
|
if (opts.language !== undefined)
|
|
137
303
|
body['language'] = opts.language;
|
|
304
|
+
// The server defaults headers/cookies/metadata to {} / [] / {}; writing an empty
|
|
305
|
+
// container on a bare create would materialize those defaults (#2480 E1).
|
|
306
|
+
if (opts.headers !== undefined)
|
|
307
|
+
body['headers'] = opts.headers;
|
|
308
|
+
if (opts.cookies !== undefined)
|
|
309
|
+
body['cookies'] = opts.cookies;
|
|
310
|
+
if (opts.metadata !== undefined)
|
|
311
|
+
body['metadata'] = opts.metadata;
|
|
138
312
|
try {
|
|
313
|
+
// 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.
|
|
314
|
+
const client = buildClient(g);
|
|
139
315
|
// Reject --json + -o BEFORE issuing the request: -o downloads BINARY artifact bytes, so
|
|
140
316
|
// it can never coexist with --json (which must stay a text envelope). Thrown here so no
|
|
141
317
|
// request — and therefore no download fetch — is ever issued (#2480 E2).
|
|
@@ -164,8 +340,9 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
164
340
|
.option('-o, --output <file>', 'Download the captured image to this file path (use - for stdout). Not valid with --json.')
|
|
165
341
|
.action(async (id, opts) => {
|
|
166
342
|
const g = getGlobal();
|
|
167
|
-
const client = buildClient(g);
|
|
168
343
|
try {
|
|
344
|
+
// 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.
|
|
345
|
+
const client = buildClient(g);
|
|
169
346
|
if (g.json && opts.output !== undefined) {
|
|
170
347
|
throw new Error('-o cannot be used with --json');
|
|
171
348
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -49,7 +49,6 @@ export function makeSearchCommand(getGlobal) {
|
|
|
49
49
|
.option('--date-range <window>', 'Restrict results to a recency window: any (default), hour, day, week, month, or year')
|
|
50
50
|
.action(async (query, opts) => {
|
|
51
51
|
const g = getGlobal();
|
|
52
|
-
const client = buildClient(g);
|
|
53
52
|
// Build SearchOpts from the positional arg plus any provided flags.
|
|
54
53
|
// Omit absent optionals so we never send undefined/NaN over the wire.
|
|
55
54
|
// type/date_range are passed through verbatim; the API validates the
|
|
@@ -66,6 +65,8 @@ export function makeSearchCommand(getGlobal) {
|
|
|
66
65
|
if (opts.dateRange !== undefined)
|
|
67
66
|
body.date_range = opts.dateRange;
|
|
68
67
|
try {
|
|
68
|
+
// 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.
|
|
69
|
+
const client = buildClient(g);
|
|
69
70
|
const result = await client.search.query(body);
|
|
70
71
|
if (g.json) {
|
|
71
72
|
printResult(result, true);
|
package/dist/commands/sitemap.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
4
|
+
import { parseEnum, parseIntFlag, parseCacheTtl, parseJsonFlag } from '../lib/parse-flags.js';
|
|
5
5
|
/** The `SitemapRequest.strategy` spec enum. */
|
|
6
6
|
const SITEMAP_STRATEGIES = ['quick', 'standard', 'deep'];
|
|
7
7
|
export function makeSitemapCommand(getGlobal) {
|
|
@@ -13,9 +13,17 @@ export function makeSitemapCommand(getGlobal) {
|
|
|
13
13
|
.option('--strategy <strategy>', `Crawl strategy: ${SITEMAP_STRATEGIES.join(', ')} (omitted → server default standard)`, (v) => parseEnum('--strategy', SITEMAP_STRATEGIES, v))
|
|
14
14
|
.option('--max-depth <n>', 'Maximum crawl depth 1-5 (omitted → server default 2)', (v) => parseIntFlag('--max-depth', v))
|
|
15
15
|
.option('--max-urls <n>', 'Maximum URLs to collect (omitted → server default 5000)', (v) => parseIntFlag('--max-urls', v))
|
|
16
|
+
.option('--webhook-url <url>', 'HTTPS webhook URL notified when the crawl completes (omitted → poll-only, no delivery)')
|
|
17
|
+
.option('--webhook-secret <secret>', 'HMAC secret used to sign the completion webhook (omitted → unsigned)')
|
|
18
|
+
// Grammar-only validation, deliberately: sitemap caps cache_ttl at 90 DAYS
|
|
19
|
+
// (validators/sitemap.ts MAX_CACHE_TTL_SECONDS), not the 7d used by scrape/fetch. Mirroring
|
|
20
|
+
// the cap client-side would fork a second source of truth for a number only the server knows.
|
|
21
|
+
.option('--cache-ttl <ttl>', 'Cache freshness: 0, Nh (hours), or Nd (days), e.g. 48h, 30d, 0 (omitted → server default 30d; max 90d, enforced server-side)', (v) => parseCacheTtl('--cache-ttl', v))
|
|
22
|
+
.option('--compact', 'Return a compact result payload (omitted → server default off)')
|
|
23
|
+
.option('--process <json>', `Post-crawl processing directive as a JSON object, e.g. '{"action":"summarize"}' (omitted → none)`, (v) => parseJsonFlag('--process', v, 'object'))
|
|
24
|
+
.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'))
|
|
16
25
|
.action(async (opts) => {
|
|
17
26
|
const g = getGlobal();
|
|
18
|
-
const client = buildClient(g);
|
|
19
27
|
// Omit-when-absent body; the remaining SitemapRequest fields keep their server defaults.
|
|
20
28
|
// The generated request type marks defaulted fields required, so the call needs an explicit
|
|
21
29
|
// assertion — type-level only; the wire body is pinned by request-shapes.test.ts.
|
|
@@ -26,7 +34,23 @@ export function makeSitemapCommand(getGlobal) {
|
|
|
26
34
|
body['max_depth'] = opts.maxDepth;
|
|
27
35
|
if (opts.maxUrls !== undefined)
|
|
28
36
|
body['max_urls'] = opts.maxUrls;
|
|
37
|
+
if (opts.webhookUrl !== undefined)
|
|
38
|
+
body['webhook_url'] = opts.webhookUrl;
|
|
39
|
+
if (opts.webhookSecret !== undefined)
|
|
40
|
+
body['webhook_secret'] = opts.webhookSecret;
|
|
41
|
+
if (opts.cacheTtl !== undefined)
|
|
42
|
+
body['cache_ttl'] = opts.cacheTtl;
|
|
43
|
+
// `compact` server-defaults to FALSE, so it is written ONLY on `=== true` — writing
|
|
44
|
+
// `false` on a bare create would materialize a default (#2480 E1).
|
|
45
|
+
if (opts.compact === true)
|
|
46
|
+
body['compact'] = true;
|
|
47
|
+
if (opts.process !== undefined)
|
|
48
|
+
body['process'] = opts.process;
|
|
49
|
+
if (opts.metadata !== undefined)
|
|
50
|
+
body['metadata'] = opts.metadata;
|
|
29
51
|
try {
|
|
52
|
+
// 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.
|
|
53
|
+
const client = buildClient(g);
|
|
30
54
|
const result = await client.sitemap.create(body);
|
|
31
55
|
printResult(result, g.json);
|
|
32
56
|
}
|
|
@@ -39,8 +63,9 @@ export function makeSitemapCommand(getGlobal) {
|
|
|
39
63
|
.description('Get a sitemap job by ID')
|
|
40
64
|
.action(async (id) => {
|
|
41
65
|
const g = getGlobal();
|
|
42
|
-
const client = buildClient(g);
|
|
43
66
|
try {
|
|
67
|
+
// 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.
|
|
68
|
+
const client = buildClient(g);
|
|
44
69
|
const result = await client.sitemap.get(id);
|
|
45
70
|
printResult(result, g.json);
|
|
46
71
|
}
|
package/dist/commands/usage.js
CHANGED
|
@@ -16,8 +16,9 @@ export function makeUsageCommand(getGlobal) {
|
|
|
16
16
|
.description('Get current billing-period usage summary')
|
|
17
17
|
.action(async () => {
|
|
18
18
|
const g = getGlobal();
|
|
19
|
-
const client = buildClient(g);
|
|
20
19
|
try {
|
|
20
|
+
// 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.
|
|
21
|
+
const client = buildClient(g);
|
|
21
22
|
const result = await client.usage.get();
|
|
22
23
|
printResult(result, g.json);
|
|
23
24
|
}
|
|
@@ -33,7 +34,6 @@ export function makeUsageCommand(getGlobal) {
|
|
|
33
34
|
.option('--api-key <ak_id>', 'Filter by API key prefixed ID (omitted → all keys)')
|
|
34
35
|
.action(async (opts) => {
|
|
35
36
|
const g = getGlobal();
|
|
36
|
-
const client = buildClient(g);
|
|
37
37
|
const params = {};
|
|
38
38
|
if (opts.from !== undefined)
|
|
39
39
|
params.from = opts.from;
|
|
@@ -42,6 +42,8 @@ export function makeUsageCommand(getGlobal) {
|
|
|
42
42
|
if (opts.apiKey !== undefined)
|
|
43
43
|
params.api_key = opts.apiKey;
|
|
44
44
|
try {
|
|
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);
|
|
45
47
|
const result = Object.keys(params).length > 0
|
|
46
48
|
? await client.usage.daily(params)
|
|
47
49
|
: await client.usage.daily();
|
|
@@ -59,7 +61,6 @@ export function makeUsageCommand(getGlobal) {
|
|
|
59
61
|
.option('--api-key <ak_id>', 'Filter by API key prefixed ID (omitted → all keys)')
|
|
60
62
|
.action(async (opts) => {
|
|
61
63
|
const g = getGlobal();
|
|
62
|
-
const client = buildClient(g);
|
|
63
64
|
const params = {};
|
|
64
65
|
if (opts.from !== undefined)
|
|
65
66
|
params.from = opts.from;
|
|
@@ -68,6 +69,8 @@ export function makeUsageCommand(getGlobal) {
|
|
|
68
69
|
if (opts.apiKey !== undefined)
|
|
69
70
|
params.api_key = opts.apiKey;
|
|
70
71
|
try {
|
|
72
|
+
// 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.
|
|
73
|
+
const client = buildClient(g);
|
|
71
74
|
const result = Object.keys(params).length > 0
|
|
72
75
|
? await client.usage.byEndpoint(params)
|
|
73
76
|
: await client.usage.byEndpoint();
|
|
@@ -21,7 +21,6 @@ export function makeWebhookDeliveriesCommand(getGlobal) {
|
|
|
21
21
|
.option('--since <iso>', 'Only deliveries created after this ISO date-time')
|
|
22
22
|
.action(async (opts) => {
|
|
23
23
|
const g = getGlobal();
|
|
24
|
-
const client = buildClient(g);
|
|
25
24
|
const params = {};
|
|
26
25
|
if (opts.limit !== undefined)
|
|
27
26
|
params.limit = opts.limit;
|
|
@@ -32,6 +31,8 @@ export function makeWebhookDeliveriesCommand(getGlobal) {
|
|
|
32
31
|
if (opts.since !== undefined)
|
|
33
32
|
params.since = opts.since;
|
|
34
33
|
try {
|
|
34
|
+
// 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.
|
|
35
|
+
const client = buildClient(g);
|
|
35
36
|
const result = Object.keys(params).length > 0
|
|
36
37
|
? await client.webhooks.list(params)
|
|
37
38
|
: await client.webhooks.list();
|
|
@@ -46,8 +47,9 @@ export function makeWebhookDeliveriesCommand(getGlobal) {
|
|
|
46
47
|
.description('Requeue a failed or exhausted webhook delivery')
|
|
47
48
|
.action(async (id) => {
|
|
48
49
|
const g = getGlobal();
|
|
49
|
-
const client = buildClient(g);
|
|
50
50
|
try {
|
|
51
|
+
// 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.
|
|
52
|
+
const client = buildClient(g);
|
|
51
53
|
const result = await client.webhooks.retry(id);
|
|
52
54
|
printResult(result, g.json);
|
|
53
55
|
}
|
|
@@ -9,5 +9,34 @@
|
|
|
9
9
|
* test that stubs `globalThis.fetch` in a `beforeEach` — after this module was imported — still
|
|
10
10
|
* sees its stub (G-049). A non-2xx response throws with the HTTP status so `handleError` renders a
|
|
11
11
|
* clean one-liner and exits 1.
|
|
12
|
+
*
|
|
13
|
+
* TIMEOUT (#2587): this path sat entirely outside the transport seam — no abort signal, no
|
|
14
|
+
* connection-error taxonomy — so a stalled artifact host (headers, then a body that never
|
|
15
|
+
* completes) hung the CLI forever, even though the 0.5.0 CHANGELOG's "a stalled body hung the CLI
|
|
16
|
+
* forever" note (#2546) reads as if every response body were covered. #2546 only covered API-call
|
|
17
|
+
* bodies read through `sendWithRetry`; this download never rode that seam. The fix REUSES
|
|
18
|
+
* `sendWithRetry`'s own per-attempt abort budget and connection-error taxonomy
|
|
19
|
+
* (`createAttemptBudget` / `toConnectionError`, exported from `../client.js`) rather than a second
|
|
20
|
+
* timeout implementation, and mirrors the same default (`DEFAULT_TIMEOUT_MS`, 120 s per attempt) —
|
|
21
|
+
* there is no retry here, since artifact GETs are not the API's 429 domain.
|
|
22
|
+
*
|
|
23
|
+
* PARTIAL-FILE POLICY (#2587 dod-1 / E1, decided + recorded, scoped to the DOWNLOAD phase only —
|
|
24
|
+
* cr-4): the response is buffered COMPLETELY in memory (`res.arrayBuffer()`) before either write
|
|
25
|
+
* path (`writeFileSync` / `process.stdout.write`) runs, so an ABORT — whether from the timeout or
|
|
26
|
+
* any other connection failure — always fails BEFORE a single byte is written; no partial file is
|
|
27
|
+
* ever left at `dest` from a download-side failure. This does NOT cover a failure DURING the write
|
|
28
|
+
* itself: `writeFileSync` opens `dest` with `O_TRUNC` and writes in a loop, so e.g. an `ENOSPC`
|
|
29
|
+
* partway through a large write leaves a created, truncated file at `dest` (any pre-existing file
|
|
30
|
+
* there already clobbered by the `O_TRUNC` open). That failure mode is accepted as-is for now (no
|
|
31
|
+
* write-to-temp-then-`renameSync` here) — the CLI still exits 1, and the write phase's tiny
|
|
32
|
+
* (post-buffering) failure window is a separate, unaddressed edge from the abort guarantee above.
|
|
33
|
+
*
|
|
34
|
+
* @param timeoutMs Per-attempt abort budget in ms; defaults to {@link DEFAULT_TIMEOUT_MS} (120 s).
|
|
35
|
+
* `<= 0` DISABLES the budget entirely (no `AbortSignal` is attached, restoring the exact
|
|
36
|
+
* unbounded-hang behavior this ticket fixes) rather than meaning "no override" — there is no
|
|
37
|
+
* sentinel for that. #2587 cr-8: no CLI command reaches this parameter today
|
|
38
|
+
* (`commands/screenshots.ts` always calls the 2-arg form), so a future caller — e.g. an
|
|
39
|
+
* anticipated `--timeout` flag threaded through here — must NOT pass `0` to mean "use the
|
|
40
|
+
* default"; it must omit the argument or pass `DEFAULT_TIMEOUT_MS` explicitly.
|
|
12
41
|
*/
|
|
13
|
-
export declare function downloadArtifact(url: string, dest: string): Promise<void>;
|
|
42
|
+
export declare function downloadArtifact(url: string, dest: string, timeoutMs?: number): Promise<void>;
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { createAttemptBudget, toConnectionError, DEFAULT_TIMEOUT_MS } from '../client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Redact a URL for error-message rendering: origin + pathname only, query string dropped.
|
|
5
|
+
*
|
|
6
|
+
* #2587 cr-2: `image_url` for a completed screenshot is an AWS SigV4 PRESIGNED URL
|
|
7
|
+
* (`X-Amz-Credential=…&X-Amz-Signature=…`, `gateway/src/lib/signed-url.ts`), unlike every other
|
|
8
|
+
* `toConnectionError` call site in `client.ts` which passes a bare relative API path with no
|
|
9
|
+
* credential material. Rendering the full URL in `handleError`'s unmodified `err.message` would
|
|
10
|
+
* print a live 600s-valid signature to stderr on every new failure path this diff adds. On a
|
|
11
|
+
* malformed URL (should not happen — `image_url` is always an absolute URL from the API) fall
|
|
12
|
+
* back to the original string rather than throwing out of an error-handling path.
|
|
13
|
+
*/
|
|
14
|
+
function redactedPath(url) {
|
|
15
|
+
try {
|
|
16
|
+
const u = new URL(url);
|
|
17
|
+
return `${u.origin}${u.pathname}`;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
2
23
|
/**
|
|
3
24
|
* Download a screenshot artifact from its `image_url` and write the raw bytes to `dest`.
|
|
4
25
|
*
|
|
@@ -10,13 +31,77 @@ import { writeFileSync } from 'node:fs';
|
|
|
10
31
|
* test that stubs `globalThis.fetch` in a `beforeEach` — after this module was imported — still
|
|
11
32
|
* sees its stub (G-049). A non-2xx response throws with the HTTP status so `handleError` renders a
|
|
12
33
|
* clean one-liner and exits 1.
|
|
34
|
+
*
|
|
35
|
+
* TIMEOUT (#2587): this path sat entirely outside the transport seam — no abort signal, no
|
|
36
|
+
* connection-error taxonomy — so a stalled artifact host (headers, then a body that never
|
|
37
|
+
* completes) hung the CLI forever, even though the 0.5.0 CHANGELOG's "a stalled body hung the CLI
|
|
38
|
+
* forever" note (#2546) reads as if every response body were covered. #2546 only covered API-call
|
|
39
|
+
* bodies read through `sendWithRetry`; this download never rode that seam. The fix REUSES
|
|
40
|
+
* `sendWithRetry`'s own per-attempt abort budget and connection-error taxonomy
|
|
41
|
+
* (`createAttemptBudget` / `toConnectionError`, exported from `../client.js`) rather than a second
|
|
42
|
+
* timeout implementation, and mirrors the same default (`DEFAULT_TIMEOUT_MS`, 120 s per attempt) —
|
|
43
|
+
* there is no retry here, since artifact GETs are not the API's 429 domain.
|
|
44
|
+
*
|
|
45
|
+
* PARTIAL-FILE POLICY (#2587 dod-1 / E1, decided + recorded, scoped to the DOWNLOAD phase only —
|
|
46
|
+
* cr-4): the response is buffered COMPLETELY in memory (`res.arrayBuffer()`) before either write
|
|
47
|
+
* path (`writeFileSync` / `process.stdout.write`) runs, so an ABORT — whether from the timeout or
|
|
48
|
+
* any other connection failure — always fails BEFORE a single byte is written; no partial file is
|
|
49
|
+
* ever left at `dest` from a download-side failure. This does NOT cover a failure DURING the write
|
|
50
|
+
* itself: `writeFileSync` opens `dest` with `O_TRUNC` and writes in a loop, so e.g. an `ENOSPC`
|
|
51
|
+
* partway through a large write leaves a created, truncated file at `dest` (any pre-existing file
|
|
52
|
+
* there already clobbered by the `O_TRUNC` open). That failure mode is accepted as-is for now (no
|
|
53
|
+
* write-to-temp-then-`renameSync` here) — the CLI still exits 1, and the write phase's tiny
|
|
54
|
+
* (post-buffering) failure window is a separate, unaddressed edge from the abort guarantee above.
|
|
55
|
+
*
|
|
56
|
+
* @param timeoutMs Per-attempt abort budget in ms; defaults to {@link DEFAULT_TIMEOUT_MS} (120 s).
|
|
57
|
+
* `<= 0` DISABLES the budget entirely (no `AbortSignal` is attached, restoring the exact
|
|
58
|
+
* unbounded-hang behavior this ticket fixes) rather than meaning "no override" — there is no
|
|
59
|
+
* sentinel for that. #2587 cr-8: no CLI command reaches this parameter today
|
|
60
|
+
* (`commands/screenshots.ts` always calls the 2-arg form), so a future caller — e.g. an
|
|
61
|
+
* anticipated `--timeout` flag threaded through here — must NOT pass `0` to mean "use the
|
|
62
|
+
* default"; it must omit the argument or pass `DEFAULT_TIMEOUT_MS` explicitly.
|
|
13
63
|
*/
|
|
14
|
-
export async function downloadArtifact(url, dest) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
64
|
+
export async function downloadArtifact(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
65
|
+
const budget = timeoutMs > 0 ? createAttemptBudget(timeoutMs) : undefined;
|
|
66
|
+
const safePath = redactedPath(url);
|
|
67
|
+
let bytes;
|
|
68
|
+
try {
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
res = await globalThis.fetch(url, budget ? { signal: budget.signal } : {});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
// No HTTP response was produced (timeout, abort, socket/DNS/TLS) — re-throw as the SDK's own
|
|
75
|
+
// ByteKitConnectionError, exactly like the API transport, so a hung/broken artifact host can
|
|
76
|
+
// never hang the CLI and `handleError` renders the same taxonomy either way. `safePath` (not
|
|
77
|
+
// `url`) is what gets rendered — see {@link redactedPath}.
|
|
78
|
+
throw toConnectionError(err, 'GET', safePath, budget?.signal.aborted === true);
|
|
79
|
+
}
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
// #2587 cr2-1: no drain here, unlike client.ts's 429 path. cr-11 originally added one
|
|
82
|
+
// (mirroring client.ts's corrected 429-drain mechanism, cr-3), reasoning that a full
|
|
83
|
+
// `text()` read lets undici return the connection to the pool. That reasoning does not
|
|
84
|
+
// apply on THIS path: `commands/screenshots.ts` throws this error straight into
|
|
85
|
+
// `handleError`, which is the last statement of the action's catch — the command then ends
|
|
86
|
+
// (signalling exit 1 via `process.exitCode` since #2686 item 4), so there is no next request
|
|
87
|
+
// in this process for a reclaimed socket to serve. A drain here was bounded only by the FULL
|
|
88
|
+
// per-attempt abort budget (or, under `timeoutMs <= 0`, by nothing at all) for zero benefit,
|
|
89
|
+
// so it is removed outright rather than merely capped.
|
|
90
|
+
throw new Error(`artifact download failed: HTTP ${res.status}`);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
bytes = Buffer.from(await res.arrayBuffer());
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// A body read that never completes is aborted by the same budget; undici then rejects the
|
|
97
|
+
// stream (mirrors #2546's body-read guard). Map it into the same taxonomy as above.
|
|
98
|
+
throw toConnectionError(err, 'GET', safePath, budget?.signal.aborted === true);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
// Clear the timer on every exit path so a healthy download leaves no dangling budget timer.
|
|
103
|
+
budget?.cleanup();
|
|
18
104
|
}
|
|
19
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
20
105
|
if (dest === '-') {
|
|
21
106
|
process.stdout.write(bytes);
|
|
22
107
|
return;
|
package/dist/lib/parse-flags.js
CHANGED
|
@@ -69,7 +69,9 @@ export function parseJsonFlag(flag, value, expect) {
|
|
|
69
69
|
parsed = JSON.parse(value);
|
|
70
70
|
}
|
|
71
71
|
catch {
|
|
72
|
-
|
|
72
|
+
// "an", not "a": both shape words (`object`, `array`) begin with a vowel, so a hardcoded
|
|
73
|
+
// article read `(a object)` / `(a array)` (#2686 item 5).
|
|
74
|
+
throw new InvalidArgumentError(`${flag} must be valid JSON (an ${expect}), e.g. '${example}'.`);
|
|
73
75
|
}
|
|
74
76
|
const matches = expect === 'array'
|
|
75
77
|
? Array.isArray(parsed)
|
package/dist/lib/url-lines.d.ts
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* One parsed `--file` line: a bare URL string, or a per-item config object carrying `url`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
* the
|
|
7
|
-
|
|
4
|
+
* The two arms are not interchangeable on the wire. `/v1/bulk` and `/v1/scrape/bulk` accept
|
|
5
|
+
* `urls` (strings) XOR `items` (objects); `/v1/fetch/bulk` has no `items` property at all and
|
|
6
|
+
* takes a heterogeneous `urls` union. Callers pick the right shape — see {@link toBulkItems}.
|
|
7
|
+
*/
|
|
8
|
+
export type BulkLine = string | Record<string, unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Read a newline-delimited `--file` and return one {@link BulkLine} per non-blank line.
|
|
11
|
+
*
|
|
12
|
+
* Line grammar:
|
|
13
|
+
* - a non-JSON line -> the raw line (a plain URL string)
|
|
14
|
+
* - a JSON object whose ONLY key is `url` -> the bare url string
|
|
15
|
+
* - a JSON object carrying other keys too -> the object verbatim (per-item config)
|
|
16
|
+
* - a JSON object with a missing/non-string url -> `InvalidArgumentError` (rejected locally)
|
|
17
|
+
* - a JSON non-object (number, array, string) -> the raw line
|
|
18
|
+
*
|
|
19
|
+
* The "only key is `url`" arm is what keeps the inverse case BYTE-IDENTICAL rather than merely
|
|
20
|
+
* equivalent: a file of url-only lines produces the exact same `{ urls, webhook_url }` body as
|
|
21
|
+
* before per-item config existed (#2596 E2).
|
|
22
|
+
*
|
|
23
|
+
* Rejecting a malformed `url` is a deliberate behavior change (#2596 DoD 8). The previous parser
|
|
24
|
+
* fell through to the raw line, so `{"url": 123}` was sent to the server AS a URL — a confusing
|
|
25
|
+
* 422 for what is a local input mistake. `InvalidArgumentError` surfaces through the same
|
|
26
|
+
* local-usage-error path as every argParser rejection (exit 1), before any request is issued.
|
|
27
|
+
*
|
|
28
|
+
* Throws on a missing file (ENOENT from readFileSync) — callers surface this via their
|
|
29
|
+
* handleError path.
|
|
30
|
+
*
|
|
31
|
+
* The rejection message for a malformed line (#2596 cr-1 / E12) names the 1-based PHYSICAL line
|
|
32
|
+
* NUMBER (i.e. the line's own position in the file, counting blank lines) and the line's own KEY
|
|
33
|
+
* NAMES only — never the raw line or any value. A `--file` line is where per-item `cookies`,
|
|
34
|
+
* `headers`, and auth-bearing `metadata` now live (#2596), so echoing the raw line back would
|
|
35
|
+
* leak session tokens and similar secrets to stderr, CI logs, and shell transcripts. Because the
|
|
36
|
+
* raw line is deliberately never echoed, the line number is the ONLY locator left in the message —
|
|
37
|
+
* it must be computed BEFORE blank lines are filtered out, or it silently mis-locates the bad
|
|
38
|
+
* entry whenever the file contains blank lines above it (#2596 cr-2).
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseBulkLines(file: string): BulkLine[];
|
|
41
|
+
/**
|
|
42
|
+
* Promote every {@link BulkLine} to the `items[]` object shape used by `/v1/bulk` and
|
|
43
|
+
* `/v1/scrape/bulk`.
|
|
8
44
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
45
|
+
* Deliberately stamps NOTHING beyond `url`. `/v1/bulk` owns capture-kind precedence server-side
|
|
46
|
+
* (per-item value, then `defaults`, then the endpoint default) since #2641 made the per-item
|
|
47
|
+
* field optional; a CLI-side default would silently override a caller's `--defaults` and is
|
|
48
|
+
* grep-gated out (#2596 G7).
|
|
11
49
|
*/
|
|
12
|
-
export declare function
|
|
50
|
+
export declare function toBulkItems(lines: BulkLine[]): Array<Record<string, unknown>>;
|