@scrappycoco/cli 0.1.0 → 0.2.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/README.md +5 -2
- package/dist/index.js +73 -8
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -25,8 +25,11 @@ Node.js 20 or newer is required. Interactive use authenticates with Clerk OAuth
|
|
|
25
25
|
|
|
26
26
|
Use `--json` for machine-readable responses. Execution commands support
|
|
27
27
|
`--format json|jsonl|csv` with `--output`, repeatable `--provider` flags, and an
|
|
28
|
-
explicit `--idempotency-key` for safe identical retries.
|
|
28
|
+
explicit `--idempotency-key` for safe identical retries. They submit durable
|
|
29
|
+
jobs and poll for completion; set `SCRAPPYCOCO_JOB_TIMEOUT_MS` to change the
|
|
30
|
+
20-minute local wait. If a command times out while its job continues, inspect
|
|
31
|
+
it with `scrappycoco jobs get <job-id>`.
|
|
29
32
|
|
|
30
33
|
Use `scrappycoco --help` for the complete command reference. See the
|
|
31
|
-
[Scrappycoco API documentation](https://scrappycoco.
|
|
34
|
+
[Scrappycoco API documentation](https://scrappycoco.ai/docs) for the public
|
|
32
35
|
contract.
|
package/dist/index.js
CHANGED
|
@@ -202,6 +202,21 @@ async function accessToken(apiUrl) {
|
|
|
202
202
|
// src/client.ts
|
|
203
203
|
import { randomUUID } from "crypto";
|
|
204
204
|
var DEFAULT_API_URL = process.env.SCRAPPYCOCO_API_URL || "https://api.scrappycoco.ai";
|
|
205
|
+
var DEFAULT_JOB_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
206
|
+
var DEFAULT_JOB_POLL_INITIAL_MS = 250;
|
|
207
|
+
var DEFAULT_JOB_POLL_MAX_MS = 5e3;
|
|
208
|
+
function positiveInteger(value, fallback) {
|
|
209
|
+
if (!value) return fallback;
|
|
210
|
+
const parsed = Number(value);
|
|
211
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
212
|
+
}
|
|
213
|
+
function jobTimeoutError(job, timeoutMs) {
|
|
214
|
+
return new CliError(
|
|
215
|
+
`Job ${job.job_id} did not finish within ${Math.ceil(timeoutMs / 1e3)} seconds.`,
|
|
216
|
+
EXIT.timeout,
|
|
217
|
+
job
|
|
218
|
+
);
|
|
219
|
+
}
|
|
205
220
|
var ApiClient = class {
|
|
206
221
|
constructor(baseUrl = DEFAULT_API_URL) {
|
|
207
222
|
this.baseUrl = baseUrl;
|
|
@@ -218,11 +233,12 @@ var ApiClient = class {
|
|
|
218
233
|
...extra
|
|
219
234
|
};
|
|
220
235
|
}
|
|
221
|
-
async request(method, path, body, headers) {
|
|
236
|
+
async request(method, path, body, headers, signal) {
|
|
222
237
|
const response = await fetch(`${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`, {
|
|
223
238
|
method,
|
|
224
239
|
headers: await this.headers(headers),
|
|
225
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
240
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
241
|
+
signal
|
|
226
242
|
});
|
|
227
243
|
if (response.status === 204) return void 0;
|
|
228
244
|
const payload = await response.json().catch(() => ({}));
|
|
@@ -233,12 +249,54 @@ var ApiClient = class {
|
|
|
233
249
|
}
|
|
234
250
|
return payload;
|
|
235
251
|
}
|
|
236
|
-
get(path) {
|
|
237
|
-
return this.request("GET", path);
|
|
252
|
+
get(path, signal) {
|
|
253
|
+
return this.request("GET", path, void 0, void 0, signal);
|
|
238
254
|
}
|
|
239
255
|
post(path, body, key = randomUUID()) {
|
|
240
256
|
return this.request("POST", path, body, { "Idempotency-Key": key });
|
|
241
257
|
}
|
|
258
|
+
async postJob(path, body, key = randomUUID()) {
|
|
259
|
+
const submitted = await this.post(path, body, key);
|
|
260
|
+
const timeoutMs = positiveInteger(process.env.SCRAPPYCOCO_JOB_TIMEOUT_MS, DEFAULT_JOB_TIMEOUT_MS);
|
|
261
|
+
const initialDelayMs = positiveInteger(
|
|
262
|
+
process.env.SCRAPPYCOCO_JOB_POLL_INITIAL_MS,
|
|
263
|
+
DEFAULT_JOB_POLL_INITIAL_MS
|
|
264
|
+
);
|
|
265
|
+
const maxDelayMs = positiveInteger(process.env.SCRAPPYCOCO_JOB_POLL_MAX_MS, DEFAULT_JOB_POLL_MAX_MS);
|
|
266
|
+
const deadline = Date.now() + timeoutMs;
|
|
267
|
+
let delayMs = Math.min(initialDelayMs, maxDelayMs);
|
|
268
|
+
let job = submitted;
|
|
269
|
+
while (job.status !== "completed") {
|
|
270
|
+
if (job.status === "failed") {
|
|
271
|
+
throw new CliError(
|
|
272
|
+
job.error || `Job ${job.job_id} failed.`,
|
|
273
|
+
EXIT.api,
|
|
274
|
+
job
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const remainingBeforeDelay = deadline - Date.now();
|
|
278
|
+
if (remainingBeforeDelay <= 0) throw jobTimeoutError(job, timeoutMs);
|
|
279
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingBeforeDelay)));
|
|
280
|
+
const remainingBeforeRequest = deadline - Date.now();
|
|
281
|
+
if (remainingBeforeRequest <= 0) throw jobTimeoutError(job, timeoutMs);
|
|
282
|
+
try {
|
|
283
|
+
job = await this.get(
|
|
284
|
+
`/jobs/${encodeURIComponent(job.job_id)}`,
|
|
285
|
+
AbortSignal.timeout(remainingBeforeRequest)
|
|
286
|
+
);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (Date.now() >= deadline || error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) {
|
|
289
|
+
throw jobTimeoutError(job, timeoutMs);
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
294
|
+
}
|
|
295
|
+
if (!job.result || typeof job.result !== "object" || Array.isArray(job.result)) {
|
|
296
|
+
throw new CliError(`Job ${job.job_id} completed without a result.`, EXIT.api, job);
|
|
297
|
+
}
|
|
298
|
+
return job.result;
|
|
299
|
+
}
|
|
242
300
|
patch(path, body) {
|
|
243
301
|
return this.request("PATCH", path, body);
|
|
244
302
|
}
|
|
@@ -397,8 +455,8 @@ function executionCommand(name) {
|
|
|
397
455
|
return scrapers.command(`${name} <scraper-id>`).description(name === "run" ? "Run one capability through a provider waterfall" : "Compare providers for one capability").option("-f, --file <path>", "canonical request JSON file").option("--input <json>", "capability input JSON").option("--provider <id>", "provider ID; repeat to choose and order providers", collect, []).option("--limit <number>", "maximum records").option("--idempotency-key <key>", "stable retry key").addOption(new Option("--format <format>", "output record format").choices(["json", "jsonl", "csv"]).default("json")).option("-o, --output <path>", "write records to a file").action(async (scraperId, options, command) => {
|
|
398
456
|
const payload = await requestPayload(options, scraperId);
|
|
399
457
|
if (payload.limit === void 0) payload.limit = 10;
|
|
400
|
-
const response = await client(command).
|
|
401
|
-
|
|
458
|
+
const response = await client(command).postJob(
|
|
459
|
+
name === "run" ? "/scrapers/jobs" : "/scrapers/compare/jobs",
|
|
402
460
|
payload,
|
|
403
461
|
options.idempotencyKey || randomUUID2()
|
|
404
462
|
);
|
|
@@ -407,6 +465,13 @@ function executionCommand(name) {
|
|
|
407
465
|
}
|
|
408
466
|
executionCommand("run");
|
|
409
467
|
executionCommand("compare");
|
|
468
|
+
var jobs = program.command("jobs").description("Inspect durable queued jobs");
|
|
469
|
+
jobs.command("get <job-id>").action(async (jobId, _options, command) => {
|
|
470
|
+
await emit(
|
|
471
|
+
await client(command).get(`/jobs/${encodeURIComponent(jobId)}`),
|
|
472
|
+
globals(command).json || false
|
|
473
|
+
);
|
|
474
|
+
});
|
|
410
475
|
var providers = program.command("providers").description("Inspect integrated scraper providers");
|
|
411
476
|
providers.command("list").option("--available", "only include available provider-capability routes").action(async (options, command) => {
|
|
412
477
|
await emit(
|
|
@@ -448,8 +513,8 @@ discoveries.command("run <discovery-id>").option("-f, --file <path>", "request J
|
|
|
448
513
|
input: input || {},
|
|
449
514
|
limit: Number(options.limit ?? fromFile.limit ?? 25)
|
|
450
515
|
};
|
|
451
|
-
const response = await client(command).
|
|
452
|
-
`/discoveries/${encodeURIComponent(discoveryId)}/
|
|
516
|
+
const response = await client(command).postJob(
|
|
517
|
+
`/discoveries/${encodeURIComponent(discoveryId)}/jobs`,
|
|
453
518
|
payload,
|
|
454
519
|
options.idempotencyKey || randomUUID2()
|
|
455
520
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scrappycoco/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for Scrappycoco scraper discovery, execution, and provider comparison",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"build": "tsup src/index.ts --format esm --platform node --target node20 --clean && node scripts/make-executable.mjs",
|
|
19
19
|
"test": "npm run build && vitest run",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"release:check": "npm run build && node scripts/check-release-readiness.mjs",
|
|
21
22
|
"prepack": "npm run build"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|