@hunt-labs/bytekit-cli 0.2.2 → 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.
- package/CHANGELOG.md +172 -0
- package/README.md +193 -15
- package/dist/client.d.ts +39 -18
- package/dist/client.js +230 -39
- package/dist/commands/account.d.ts +11 -0
- package/dist/commands/account.js +12 -80
- package/dist/commands/bulk.js +14 -0
- package/dist/commands/fetch.d.ts +14 -0
- package/dist/commands/fetch.js +45 -6
- package/dist/commands/monitors.js +83 -3
- package/dist/commands/scrape.js +190 -10
- package/dist/commands/screenshots.d.ts +12 -0
- package/dist/commands/screenshots.js +146 -6
- package/dist/commands/search.js +5 -1
- package/dist/commands/sitemap.js +17 -1
- package/dist/commands/usage.d.ts +13 -0
- package/dist/commands/usage.js +81 -0
- package/dist/commands/webhook-deliveries.d.ts +11 -0
- package/dist/commands/webhook-deliveries.js +59 -0
- package/dist/index.js +4 -0
- package/dist/lib/download-artifact.d.ts +13 -0
- package/dist/lib/download-artifact.js +25 -0
- package/dist/lib/parse-flags.d.ts +46 -0
- package/dist/lib/parse-flags.js +90 -0
- package/dist/output.d.ts +88 -0
- package/dist/output.js +188 -7
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ByteKit, ByteKitError, ByteKitConnectionError } from '@hunt-labs/bytekit-sdk';
|
|
2
2
|
/**
|
|
3
3
|
* The API host the SDK itself defaults to. The SDK's copy is module-private, so the CLI declares
|
|
4
4
|
* its own and always passes an explicit `baseUrl` to the constructor — that way the SDK client and
|
|
@@ -18,6 +18,92 @@ export const MIN_RETRY_AFTER_MS = 1_000;
|
|
|
18
18
|
export const MAX_RETRY_AFTER_MS = 60_000;
|
|
19
19
|
/** Exactly one retry. A second 429 is terminal — no loop. */
|
|
20
20
|
export const MAX_RETRIES = 1;
|
|
21
|
+
/**
|
|
22
|
+
* Default CLIENT-SIDE abort budget for a single fetch attempt, in ms.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors the SDK's own `DEFAULT_TIMEOUT_MS` (120s). Overriding `client.request` with this
|
|
25
|
+
* transport had discarded the SDK's default, so a hung server hung the CLI forever; this restores
|
|
26
|
+
* the same bound. The budget is **per attempt**, not per command: the single 429 retry (and its
|
|
27
|
+
* backoff sleep) gets a fresh budget, so the retry can never be cut short by time already spent on
|
|
28
|
+
* the first attempt. A future `--timeout` flag would override this default; today only the default
|
|
29
|
+
* exists.
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
32
|
+
/** The abort reason used when a per-attempt budget elapses; `name` maps it to `code: 'timeout'`. */
|
|
33
|
+
function timeoutReason(ms) {
|
|
34
|
+
const reason = new Error(`ByteKit: request timed out after ${ms}ms`);
|
|
35
|
+
reason.name = 'TimeoutError';
|
|
36
|
+
return reason;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A per-attempt abort budget: an `AbortController` fired by a `setTimeout`.
|
|
40
|
+
*
|
|
41
|
+
* The budget spans the WHOLE attempt — time-to-headers **and** the response body read (#2546).
|
|
42
|
+
* Arming it only until headers arrive left `response.text()` / `response.arrayBuffer()` unbounded,
|
|
43
|
+
* so a server that answered with headers and then stalled its body hung the CLI forever.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately NOT `AbortSignal.timeout` (which the SDK prefers): that schedules on a timer the
|
|
46
|
+
* test runner's fake timers cannot advance, so the transport's timeout would be untestable under
|
|
47
|
+
* `vi.useFakeTimers()`. CLI requests are short-lived and single-shot, so the GC advantage
|
|
48
|
+
* `AbortSignal.timeout` buys the long-running SDK does not apply here. The timer is `unref`'d so a
|
|
49
|
+
* pending budget never keeps the process alive, and `cleanup()` clears it on every exit path.
|
|
50
|
+
*/
|
|
51
|
+
function createAttemptBudget(ms) {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timer = setTimeout(() => controller.abort(timeoutReason(ms)), ms);
|
|
54
|
+
timer.unref?.();
|
|
55
|
+
return {
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
cleanup: () => clearTimeout(timer),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The `name` of an error-shaped value, or `undefined` when it carries none. */
|
|
61
|
+
const nameOf = (e) => typeof e === 'object' && e !== null && 'name' in e ? String(e.name) : undefined;
|
|
62
|
+
/** The `cause` of an error-shaped value, or `undefined` when it carries none. */
|
|
63
|
+
const causeOf = (e) => typeof e === 'object' && e !== null && 'cause' in e ? e.cause : undefined;
|
|
64
|
+
/**
|
|
65
|
+
* The abort-taxonomy name of a rejection, seeing through undici's mid-body wrapper.
|
|
66
|
+
*
|
|
67
|
+
* When a request is aborted while its BODY is streaming, undici does not reject with the abort
|
|
68
|
+
* reason itself — it rejects with `TypeError: terminated` carrying the reason on `.cause` (#2546).
|
|
69
|
+
* A name-only check would read that as a plain connection failure. Nothing that reached this
|
|
70
|
+
* function before #2546 carried a `.cause`, so every previously-asserted classification is
|
|
71
|
+
* unchanged; a `.cause` whose own name is not abort-shaped still falls through to `'connection'`.
|
|
72
|
+
*/
|
|
73
|
+
function abortNameOf(err) {
|
|
74
|
+
const top = nameOf(err);
|
|
75
|
+
if (top === 'TimeoutError' || top === 'AbortError')
|
|
76
|
+
return top;
|
|
77
|
+
return nameOf(causeOf(err));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Map a `fetch` or body-read rejection (no usable HTTP payload was produced) onto the SDK's
|
|
81
|
+
* `ByteKitConnectionError` taxonomy, so a CLI failure carries the same `{name, code, cause}` a
|
|
82
|
+
* direct SDK call would.
|
|
83
|
+
*
|
|
84
|
+
* `timedOut` is true when this attempt's own budget fired the abort, which is the reliable
|
|
85
|
+
* discriminator (undici rejects with the signal's reason — or a wrapper carrying it, see
|
|
86
|
+
* {@link abortNameOf} — whose `name` we also inspect as a fallback for a caller-style AbortError).
|
|
87
|
+
*/
|
|
88
|
+
function toConnectionError(err, method, path, timedOut) {
|
|
89
|
+
const name = abortNameOf(err) ?? '';
|
|
90
|
+
let code;
|
|
91
|
+
if (timedOut || name === 'TimeoutError') {
|
|
92
|
+
code = 'timeout';
|
|
93
|
+
}
|
|
94
|
+
else if (name === 'AbortError') {
|
|
95
|
+
code = 'aborted';
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
code = 'connection';
|
|
99
|
+
}
|
|
100
|
+
const detail = code === 'timeout'
|
|
101
|
+
? 'request timed out'
|
|
102
|
+
: code === 'aborted'
|
|
103
|
+
? 'request aborted'
|
|
104
|
+
: 'connection failed before a response was received';
|
|
105
|
+
return new ByteKitConnectionError(code, `ByteKit: ${method} ${path} — ${detail}`, { cause: err });
|
|
106
|
+
}
|
|
21
107
|
/**
|
|
22
108
|
* Parse a `Retry-After` header into a clamped millisecond delay.
|
|
23
109
|
*
|
|
@@ -44,7 +130,7 @@ function sleep(ms) {
|
|
|
44
130
|
/**
|
|
45
131
|
* Shape a failed response into the SDK's OWN error class, using the SDK's own body parsing.
|
|
46
132
|
*
|
|
47
|
-
* Reusing `
|
|
133
|
+
* Reusing `ByteKitError` keeps `{status, code, message}` identical to what a direct SDK call
|
|
48
134
|
* would throw, so `handleError()`'s `'status' in err` branch still renders
|
|
49
135
|
* `Error: <message> (status: <n>)` and exits 1.
|
|
50
136
|
*/
|
|
@@ -59,69 +145,174 @@ async function toApiError(response) {
|
|
|
59
145
|
catch {
|
|
60
146
|
// leave defaults
|
|
61
147
|
}
|
|
62
|
-
return new
|
|
148
|
+
return new ByteKitError(response.status, code, message);
|
|
63
149
|
}
|
|
64
150
|
/**
|
|
65
|
-
*
|
|
151
|
+
* Perform the authenticated round-trip with a single bounded retry on 429, returning whatever the
|
|
152
|
+
* caller's `readBody` continuation makes of the successful response.
|
|
153
|
+
*
|
|
154
|
+
* This is the CLI's one transport core: it owns auth, the per-attempt abort budget, the 429 retry,
|
|
155
|
+
* and the error taxonomy — mirroring the SDK's own private `send()`. Taking the body reader as a
|
|
156
|
+
* continuation (rather than handing the `Response` back with its body unread) is what lets the two
|
|
157
|
+
* public transports built on it ({@link createRetryingRequest} and {@link createRetryingRequestRaw})
|
|
158
|
+
* differ ONLY in how they decode a successful body, so no endpoint can ever ride one behavior and
|
|
159
|
+
* miss the other (#2535: `/v1/fetch` silently bypassed the retry because only `request` was
|
|
160
|
+
* overridden) — while keeping the abort budget armed ACROSS that read (#2546).
|
|
161
|
+
*
|
|
162
|
+
* The budget spans headers **and** body. Returning the `Response` used to end the guarded region
|
|
163
|
+
* at the headers boundary, leaving `response.text()` / `response.arrayBuffer()` — and
|
|
164
|
+
* `toApiError`'s error-body `response.json()` — unbounded: a server that sent headers and then
|
|
165
|
+
* stalled its body hung the CLI forever. `readBody` therefore MUST only consume the stream;
|
|
166
|
+
* `JSON.parse` / `TextDecoder` run in the callers, OUTSIDE the guard, so a malformed body still
|
|
167
|
+
* surfaces its own parse error instead of being remapped to a connection error.
|
|
66
168
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
169
|
+
* The budget stays PER ATTEMPT: `cleanup()` runs in an attempt-wide `finally`, and the 429 retry
|
|
170
|
+
* sleep happens after that `finally` — so a retry can never be cut short by time already spent on
|
|
171
|
+
* the first attempt.
|
|
172
|
+
*
|
|
173
|
+
* Why the CLI owns the `fetch` instead of wrapping the SDK's transports: the SDK throws a
|
|
174
|
+
* header-less `ByteKitError` and discards the `Response`, so a delegating wrapper can see
|
|
69
175
|
* `status === 429` but can never read `Retry-After`. Owning the fetch is the only way to honor the
|
|
70
176
|
* server's advertised backoff without editing the SDK.
|
|
71
177
|
*
|
|
72
|
-
* Every SDK resource dereferences `this.client.request` at call time, so installing this as an own
|
|
73
|
-
* property of the client (see {@link buildClient}) routes *all* commands through it from one seam.
|
|
74
|
-
*
|
|
75
178
|
* Retries apply to 429 only. Non-429 statuses and network rejections propagate untouched.
|
|
76
179
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
180
|
+
async function sendWithRetry(apiKey, baseUrl, timeoutMs, method, path, body, readBody) {
|
|
181
|
+
for (let attempt = 0;; attempt++) {
|
|
182
|
+
const init = {
|
|
183
|
+
method,
|
|
184
|
+
headers: {
|
|
185
|
+
Authorization: `Bearer ${apiKey}`,
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
if (body !== undefined) {
|
|
190
|
+
init.body = JSON.stringify(body);
|
|
191
|
+
}
|
|
192
|
+
// Per-attempt abort budget: a fresh timer for THIS attempt. A budget of 0 disables it.
|
|
193
|
+
const budget = timeoutMs > 0 ? createAttemptBudget(timeoutMs) : undefined;
|
|
194
|
+
if (budget !== undefined) {
|
|
195
|
+
init.signal = budget.signal;
|
|
196
|
+
}
|
|
197
|
+
// Set only by the 429-retry branch below — the one path that falls out of the `try` rather
|
|
198
|
+
// than returning or throwing. Left `undefined` on every other path, which never reaches the
|
|
199
|
+
// `sleep` after the `finally`.
|
|
200
|
+
let retryAfterMs;
|
|
201
|
+
try {
|
|
202
|
+
let response;
|
|
203
|
+
try {
|
|
204
|
+
response = await fetch(`${baseUrl}${path}`, init);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
// No HTTP response was produced (timeout, abort, socket/DNS/TLS). Re-throw as the SDK's
|
|
208
|
+
// own ByteKitConnectionError so a hung server can never hang the CLI and a network failure
|
|
209
|
+
// no longer surfaces as a bare `Error: fetch failed`.
|
|
210
|
+
throw toConnectionError(err, method, path, budget?.signal.aborted === true);
|
|
89
211
|
}
|
|
90
|
-
const response = await fetch(`${baseUrl}${path}`, init);
|
|
91
212
|
if (response.ok) {
|
|
92
|
-
|
|
213
|
+
try {
|
|
214
|
+
return await readBody(response);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
// A body read that never completes is aborted by the same budget; undici then rejects
|
|
218
|
+
// the stream (#2546). Map it into the same taxonomy the headers phase uses.
|
|
219
|
+
throw toConnectionError(err, method, path, budget?.signal.aborted === true);
|
|
220
|
+
}
|
|
93
221
|
}
|
|
94
222
|
if (response.status === 429 && attempt < MAX_RETRIES) {
|
|
95
|
-
|
|
96
|
-
|
|
223
|
+
retryAfterMs = parseRetryAfterMs(response.headers.get('Retry-After'));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// `toApiError` reads the error body, so it too runs inside the budget.
|
|
227
|
+
throw await toApiError(response);
|
|
97
228
|
}
|
|
98
|
-
throw await toApiError(response);
|
|
99
229
|
}
|
|
230
|
+
finally {
|
|
231
|
+
// Clear the timer on every exit path — success (body read included), HTTP error, or
|
|
232
|
+
// abort — so a healthy request leaves no dangling 120s timer.
|
|
233
|
+
budget?.cleanup();
|
|
234
|
+
}
|
|
235
|
+
// OUTSIDE the budget's `finally`: the backoff is not spent against any attempt's budget, and
|
|
236
|
+
// the next attempt arms a fresh one. The `??` is unreachable in practice (only the branch
|
|
237
|
+
// that assigns `retryAfterMs` falls through to here) and exists solely to keep the delay a
|
|
238
|
+
// `number` without an assertion; it uses the same value `parseRetryAfterMs(null)` returns.
|
|
239
|
+
await sleep(retryAfterMs ?? DEFAULT_RETRY_AFTER_MS);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Build the CLI's JSON transport: the SDK's `request` contract, plus the single bounded 429 retry.
|
|
244
|
+
*
|
|
245
|
+
* Every SDK resource dereferences `this.client.request` at call time, so installing this as an own
|
|
246
|
+
* property of the client (see {@link buildClient}) routes all JSON commands through it from one
|
|
247
|
+
* seam. `/v1/fetch` does NOT ride this transport — it rides `requestRaw` (see
|
|
248
|
+
* {@link createRetryingRequestRaw}); `/v1/fetch/bulk` is a JSON job envelope and does.
|
|
249
|
+
*/
|
|
250
|
+
export function createRetryingRequest(apiKey, baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
251
|
+
return async function request(method, path, body) {
|
|
252
|
+
// The reader consumes the stream and nothing else, so the abort budget covers the read
|
|
253
|
+
// (#2546) while the 204/empty guard and `JSON.parse` stay OUTSIDE the guarded region —
|
|
254
|
+
// a malformed 200 body must still surface its own SyntaxError, never a connection error.
|
|
255
|
+
// Read-first ordering is preserved verbatim from before #2546: `.text()` is called
|
|
256
|
+
// unconditionally, and only then is the status inspected.
|
|
257
|
+
const { status, text } = await sendWithRetry(apiKey, baseUrl, timeoutMs, method, path, body, async (response) => ({ status: response.status, text: await response.text() }));
|
|
258
|
+
if (status === 204 || text.trim().length === 0) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
return JSON.parse(text);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Build the CLI's RAW transport: the SDK's `requestRaw` contract, plus the same bounded 429 retry.
|
|
266
|
+
*
|
|
267
|
+
* `/v1/fetch` returns the fetched content AS the body (HTML, markdown, or true binary) with its
|
|
268
|
+
* metadata in `X-Fetch-*` headers, so the SDK routes it through `requestRaw` rather than the JSON
|
|
269
|
+
* `request`. The CLI must override BOTH seams or fetch commands silently bypass the retry and the
|
|
270
|
+
* CLI's error taxonomy — the #2535 defect, where only `request` was replaced.
|
|
271
|
+
*
|
|
272
|
+
* The returned shape is the SDK's `requestRaw` contract verbatim (`{body, bytes, headers, status}`)
|
|
273
|
+
* so `FetchResource` builds an ordinary `FetchResult` from it and the CLI never forks fetch
|
|
274
|
+
* handling away from the SDK. `bytes` is byte-exact (binary-safe); `body` is the best-effort UTF-8
|
|
275
|
+
* decode, matching the SDK's own semantics.
|
|
276
|
+
*/
|
|
277
|
+
export function createRetryingRequestRaw(apiKey, baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
278
|
+
return async function requestRaw(method, path, body) {
|
|
279
|
+
// As in `createRetryingRequest`: the reader only drains the stream (so the abort budget
|
|
280
|
+
// covers it, #2546); the UTF-8 decode happens outside the guarded region.
|
|
281
|
+
const { bytes, headers, status } = await sendWithRetry(apiKey, baseUrl, timeoutMs, method, path, body, async (response) => ({
|
|
282
|
+
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
283
|
+
headers: response.headers,
|
|
284
|
+
status: response.status,
|
|
285
|
+
}));
|
|
286
|
+
return {
|
|
287
|
+
body: new TextDecoder('utf-8').decode(bytes),
|
|
288
|
+
bytes,
|
|
289
|
+
headers,
|
|
290
|
+
status,
|
|
291
|
+
};
|
|
100
292
|
};
|
|
101
293
|
}
|
|
102
294
|
/**
|
|
103
295
|
* Build an authenticated SDK client from global CLI options.
|
|
104
296
|
*
|
|
105
|
-
* Resolution order (explicit input beats environment
|
|
106
|
-
* apiKey: --key flag → BYTEKIT_API_KEY
|
|
107
|
-
* baseUrl: --base-url flag → BYTEKIT_BASE_URL →
|
|
108
|
-
*
|
|
109
|
-
* descriptive error when no key is available.
|
|
297
|
+
* Resolution order (explicit input beats environment):
|
|
298
|
+
* apiKey: --key flag → BYTEKIT_API_KEY.
|
|
299
|
+
* baseUrl: --base-url flag → BYTEKIT_BASE_URL → DEFAULT_BASE_URL.
|
|
300
|
+
* Exits with a descriptive error when no key is available.
|
|
110
301
|
*
|
|
111
|
-
* The returned client's `request`
|
|
112
|
-
* the 429 retry
|
|
302
|
+
* The returned client's `request` AND `requestRaw` are both replaced by the retrying transports, so
|
|
303
|
+
* every command inherits the 429 retry — JSON commands via `request`, the `/v1/fetch` content
|
|
304
|
+
* commands via `requestRaw`. Overriding only `request` is the #2535 defect: the SDK's fetch
|
|
305
|
+
* resource rides `requestRaw`, so fetch bypassed the retry and the CLI's error taxonomy entirely.
|
|
113
306
|
*/
|
|
114
307
|
export function buildClient(opts) {
|
|
115
|
-
const apiKey = opts.key ?? process.env['BYTEKIT_API_KEY']
|
|
308
|
+
const apiKey = opts.key ?? process.env['BYTEKIT_API_KEY'];
|
|
116
309
|
if (!apiKey) {
|
|
117
310
|
console.error('Error: API key required. Pass --key or set BYTEKIT_API_KEY.');
|
|
118
311
|
process.exit(1);
|
|
119
312
|
}
|
|
120
|
-
const baseUrl = opts.baseUrl ??
|
|
121
|
-
|
|
122
|
-
process.env['RAPIDCRAWL_BASE_URL'] ??
|
|
123
|
-
DEFAULT_BASE_URL;
|
|
124
|
-
const client = new RapidCrawl({ apiKey, baseUrl });
|
|
313
|
+
const baseUrl = opts.baseUrl ?? process.env['BYTEKIT_BASE_URL'] ?? DEFAULT_BASE_URL;
|
|
314
|
+
const client = new ByteKit({ apiKey, baseUrl });
|
|
125
315
|
client.request = createRetryingRequest(apiKey, baseUrl);
|
|
316
|
+
client.requestRaw = createRetryingRequestRaw(apiKey, baseUrl);
|
|
126
317
|
return client;
|
|
127
318
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type GlobalOptions } from '../client.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `account` command group.
|
|
5
|
+
*
|
|
6
|
+
* Key management is DELIBERATELY absent (#2536). Those operations are mounted behind
|
|
7
|
+
* `clerkSession(db)` in the gateway and are annotated `x-internal: true` / `x-sdk-scope: internal`
|
|
8
|
+
* in the OpenAPI spec, so a bearer `sk_` key — the CLI's only credential — can never satisfy them.
|
|
9
|
+
* Keys are managed from the dashboard instead. Widening the gateway to accept bearer auth was
|
|
10
|
+
* rejected on security grounds: the reveal operation returns the live plaintext of any key on the
|
|
11
|
+
* account and there is no per-key scope model, so any leaked key would become a full
|
|
12
|
+
* account-credential factory. `internal-scope-guard.test.ts` mechanically enforces this boundary.
|
|
13
|
+
*/
|
|
3
14
|
export declare function makeAccountCommand(getGlobal: () => GlobalOptions & {
|
|
4
15
|
json: boolean;
|
|
5
16
|
}): Command;
|
package/dist/commands/account.js
CHANGED
|
@@ -1,87 +1,19 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, handleError } from '../output.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `account` command group.
|
|
6
|
+
*
|
|
7
|
+
* Key management is DELIBERATELY absent (#2536). Those operations are mounted behind
|
|
8
|
+
* `clerkSession(db)` in the gateway and are annotated `x-internal: true` / `x-sdk-scope: internal`
|
|
9
|
+
* in the OpenAPI spec, so a bearer `sk_` key — the CLI's only credential — can never satisfy them.
|
|
10
|
+
* Keys are managed from the dashboard instead. Widening the gateway to accept bearer auth was
|
|
11
|
+
* rejected on security grounds: the reveal operation returns the live plaintext of any key on the
|
|
12
|
+
* account and there is no per-key scope model, so any leaked key would become a full
|
|
13
|
+
* account-credential factory. `internal-scope-guard.test.ts` mechanically enforces this boundary.
|
|
14
|
+
*/
|
|
4
15
|
export function makeAccountCommand(getGlobal) {
|
|
5
|
-
const cmd = new Command('account').description('Account
|
|
6
|
-
const apiKeysCmd = new Command('api-keys').description('API key management');
|
|
7
|
-
apiKeysCmd
|
|
8
|
-
.command('create')
|
|
9
|
-
.description('Create a new API key')
|
|
10
|
-
.requiredOption('--name <name>', 'Display name for the key')
|
|
11
|
-
.action(async (opts) => {
|
|
12
|
-
const g = getGlobal();
|
|
13
|
-
const client = buildClient(g);
|
|
14
|
-
try {
|
|
15
|
-
// The API's field is `label`, not `name` — sending `{ name }` created an
|
|
16
|
-
// UNLABELLED key, because the gateway ignored the unknown field (#2426:
|
|
17
|
-
// the SDK's newly derived request type is what surfaced this).
|
|
18
|
-
const result = await client.account.apiKeys.create({ label: opts.name });
|
|
19
|
-
printResult(result, g.json);
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
handleError(err);
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
apiKeysCmd
|
|
26
|
-
.command('list')
|
|
27
|
-
.description('List all API keys')
|
|
28
|
-
.action(async () => {
|
|
29
|
-
const g = getGlobal();
|
|
30
|
-
const client = buildClient(g);
|
|
31
|
-
try {
|
|
32
|
-
const result = await client.account.apiKeys.list();
|
|
33
|
-
printResult(result, g.json);
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
handleError(err);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
apiKeysCmd
|
|
40
|
-
.command('get <id>')
|
|
41
|
-
.description('Get an API key by ID')
|
|
42
|
-
.action(async (id) => {
|
|
43
|
-
const g = getGlobal();
|
|
44
|
-
const client = buildClient(g);
|
|
45
|
-
try {
|
|
46
|
-
const result = await client.account.apiKeys.get(id);
|
|
47
|
-
printResult(result, g.json);
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
handleError(err);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
apiKeysCmd
|
|
54
|
-
.command('update <id>')
|
|
55
|
-
.description('Update an API key')
|
|
56
|
-
.requiredOption('--name <name>', 'New display name')
|
|
57
|
-
.action(async (id, opts) => {
|
|
58
|
-
const g = getGlobal();
|
|
59
|
-
const client = buildClient(g);
|
|
60
|
-
try {
|
|
61
|
-
// As above: the API's field is `label`, so `--name` never actually
|
|
62
|
-
// renamed the key.
|
|
63
|
-
const result = await client.account.apiKeys.update(id, { label: opts.name });
|
|
64
|
-
printResult(result, g.json);
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
handleError(err);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
apiKeysCmd
|
|
71
|
-
.command('revoke <id>')
|
|
72
|
-
.description('Revoke an API key (idempotent)')
|
|
73
|
-
.action(async (id) => {
|
|
74
|
-
const g = getGlobal();
|
|
75
|
-
const client = buildClient(g);
|
|
76
|
-
try {
|
|
77
|
-
const result = await client.account.apiKeys.revoke(id);
|
|
78
|
-
printResult(result, g.json);
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
handleError(err);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
cmd.addCommand(apiKeysCmd);
|
|
16
|
+
const cmd = new Command('account').description('Account operations');
|
|
85
17
|
cmd
|
|
86
18
|
.command('get')
|
|
87
19
|
.description('Get the authenticated account')
|
package/dist/commands/bulk.js
CHANGED
|
@@ -51,5 +51,19 @@ export function makeBulkCommand(getGlobal) {
|
|
|
51
51
|
handleError(err);
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
+
cmd
|
|
55
|
+
.command('cancel <id>')
|
|
56
|
+
.description('Cancel an in-flight bulk job')
|
|
57
|
+
.action(async (id) => {
|
|
58
|
+
const g = getGlobal();
|
|
59
|
+
const client = buildClient(g);
|
|
60
|
+
try {
|
|
61
|
+
const result = await client.bulk.cancel(id);
|
|
62
|
+
printResult(result, g.json);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
handleError(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
54
68
|
return cmd;
|
|
55
69
|
}
|
package/dist/commands/fetch.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type GlobalOptions } from '../client.js';
|
|
3
|
+
/** The `/v1/fetch` spec union for `format` — mirrors `FetchOpts['format']` in the SDK. */
|
|
4
|
+
type FetchFormat = 'markdown' | 'html';
|
|
5
|
+
/**
|
|
6
|
+
* Validate `--format` locally, before any HTTP request is issued.
|
|
7
|
+
*
|
|
8
|
+
* The SDK's `FetchOpts.format` is narrowed to the spec union `'markdown' | 'html'`
|
|
9
|
+
* (#2475), so an off-spec value like `pdf` is a compile-time error at the call site.
|
|
10
|
+
* Commander runs an option's argParser during parsing — ahead of the action callback —
|
|
11
|
+
* so rejecting here means the client is never built and no request is issued, and the
|
|
12
|
+
* caller gets an actionable message instead of a server 422 (same convention as
|
|
13
|
+
* `parseLimit` in search.ts).
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseFormat(value: string): FetchFormat;
|
|
3
16
|
export declare function makeFetchCommand(getGlobal: () => GlobalOptions & {
|
|
4
17
|
json: boolean;
|
|
5
18
|
}): Command;
|
|
19
|
+
export {};
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
1
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
|
-
import { printResult, handleError } from '../output.js';
|
|
3
|
+
import { printResult, emitFetchResult, handleError } from '../output.js';
|
|
4
4
|
import { parseUrlLines } from '../lib/url-lines.js';
|
|
5
|
+
import { parseIntFlag, parseEnum, parseCacheTtl } from '../lib/parse-flags.js';
|
|
6
|
+
const FETCH_FORMATS = ['markdown', 'html'];
|
|
7
|
+
/** The `FetchRequest.markdown_mode` spec enum (create/POST only — excluded from the GET query). */
|
|
8
|
+
const MARKDOWN_MODES = ['article', 'raw', 'llm'];
|
|
9
|
+
/**
|
|
10
|
+
* Validate `--format` locally, before any HTTP request is issued.
|
|
11
|
+
*
|
|
12
|
+
* The SDK's `FetchOpts.format` is narrowed to the spec union `'markdown' | 'html'`
|
|
13
|
+
* (#2475), so an off-spec value like `pdf` is a compile-time error at the call site.
|
|
14
|
+
* Commander runs an option's argParser during parsing — ahead of the action callback —
|
|
15
|
+
* so rejecting here means the client is never built and no request is issued, and the
|
|
16
|
+
* caller gets an actionable message instead of a server 422 (same convention as
|
|
17
|
+
* `parseLimit` in search.ts).
|
|
18
|
+
*/
|
|
19
|
+
export function parseFormat(value) {
|
|
20
|
+
if (!FETCH_FORMATS.includes(value)) {
|
|
21
|
+
throw new InvalidArgumentError(`--format must be one of: ${FETCH_FORMATS.join(', ')}.`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
5
25
|
export function makeFetchCommand(getGlobal) {
|
|
6
26
|
const cmd = new Command('fetch').description('Fetch URL content operations');
|
|
7
27
|
const bulkCmd = new Command('bulk').description('Bulk fetch operations');
|
|
@@ -41,16 +61,28 @@ export function makeFetchCommand(getGlobal) {
|
|
|
41
61
|
.command('create')
|
|
42
62
|
.description('Fetch URL content (POST)')
|
|
43
63
|
.requiredOption('--url <url>', 'URL to fetch')
|
|
44
|
-
.option('--format <format>', 'Response format')
|
|
64
|
+
.option('--format <format>', 'Response format (markdown or html)', parseFormat)
|
|
65
|
+
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
66
|
+
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
67
|
+
.option('--cache-ttl <ttl>', 'Cache freshness: 0, Nh (hours), or Nd (days), e.g. 48h, 2d, 0 (omitted → server default)', (v) => parseCacheTtl('--cache-ttl', v))
|
|
68
|
+
// `--markdown-mode` is POST-only: FetchGetOpts excludes it, so adding it to `get` below would be
|
|
69
|
+
// a compile error — the create/get asymmetry is correct by construction.
|
|
70
|
+
.option('--markdown-mode <mode>', `Markdown mode: ${MARKDOWN_MODES.join(', ')}`, (v) => parseEnum('--markdown-mode', MARKDOWN_MODES, v))
|
|
71
|
+
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
45
72
|
.action(async (opts) => {
|
|
46
73
|
const g = getGlobal();
|
|
47
74
|
const client = buildClient(g);
|
|
48
75
|
try {
|
|
49
76
|
const result = await client.fetch.create({
|
|
50
77
|
url: opts.url,
|
|
78
|
+
// Each optional field is spread ONLY when its flag is present (#2480 E1).
|
|
51
79
|
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
80
|
+
...(opts.country !== undefined ? { country: opts.country } : {}),
|
|
81
|
+
...(opts.timeoutMs !== undefined ? { timeout_ms: opts.timeoutMs } : {}),
|
|
82
|
+
...(opts.cacheTtl !== undefined ? { cache_ttl: opts.cacheTtl } : {}),
|
|
83
|
+
...(opts.markdownMode !== undefined ? { markdown_mode: opts.markdownMode } : {}),
|
|
52
84
|
});
|
|
53
|
-
|
|
85
|
+
emitFetchResult(result, g.json, { file: opts.output });
|
|
54
86
|
}
|
|
55
87
|
catch (err) {
|
|
56
88
|
handleError(err);
|
|
@@ -60,7 +92,11 @@ export function makeFetchCommand(getGlobal) {
|
|
|
60
92
|
.command('get')
|
|
61
93
|
.description('Fetch URL content (GET via query params)')
|
|
62
94
|
.requiredOption('--url <url>', 'URL to fetch')
|
|
63
|
-
.option('--format <format>', 'Response format')
|
|
95
|
+
.option('--format <format>', 'Response format (markdown or html)', parseFormat)
|
|
96
|
+
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
97
|
+
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
98
|
+
.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))
|
|
99
|
+
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
64
100
|
.action(async (opts) => {
|
|
65
101
|
const g = getGlobal();
|
|
66
102
|
const client = buildClient(g);
|
|
@@ -68,8 +104,11 @@ export function makeFetchCommand(getGlobal) {
|
|
|
68
104
|
const result = await client.fetch.get({
|
|
69
105
|
url: opts.url,
|
|
70
106
|
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
107
|
+
...(opts.country !== undefined ? { country: opts.country } : {}),
|
|
108
|
+
...(opts.timeoutMs !== undefined ? { timeout_ms: opts.timeoutMs } : {}),
|
|
109
|
+
...(opts.cacheTtl !== undefined ? { cache_ttl: opts.cacheTtl } : {}),
|
|
71
110
|
});
|
|
72
|
-
|
|
111
|
+
emitFetchResult(result, g.json, { file: opts.output });
|
|
73
112
|
}
|
|
74
113
|
catch (err) {
|
|
75
114
|
handleError(err);
|