@scrappycoco/cli 0.1.0 → 0.2.1
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 +6 -4
- package/dist/index.js +78 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Discover, run, and compare scraper capabilities from a terminal or automation environment.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`npm view @scrappycoco/cli version`, then run:
|
|
5
|
+
Run the published package directly with `npx`:
|
|
7
6
|
|
|
8
7
|
```sh
|
|
9
8
|
npx @scrappycoco/cli auth login
|
|
@@ -25,8 +24,11 @@ Node.js 20 or newer is required. Interactive use authenticates with Clerk OAuth
|
|
|
25
24
|
|
|
26
25
|
Use `--json` for machine-readable responses. Execution commands support
|
|
27
26
|
`--format json|jsonl|csv` with `--output`, repeatable `--provider` flags, and an
|
|
28
|
-
explicit `--idempotency-key` for safe identical retries.
|
|
27
|
+
explicit `--idempotency-key` for safe identical retries. They submit durable
|
|
28
|
+
jobs and poll for completion; set `SCRAPPYCOCO_JOB_TIMEOUT_MS` to change the
|
|
29
|
+
20-minute local wait. If a command times out while its job continues, inspect
|
|
30
|
+
it with `scrappycoco jobs get <job-id>`.
|
|
29
31
|
|
|
30
32
|
Use `scrappycoco --help` for the complete command reference. See the
|
|
31
|
-
[Scrappycoco API documentation](https://scrappycoco.
|
|
33
|
+
[Scrappycoco API documentation](https://scrappycoco.ai/docs) for the public
|
|
32
34
|
contract.
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
5
6
|
import { Command, CommanderError, Option } from "commander";
|
|
6
7
|
|
|
7
8
|
// src/auth.ts
|
|
@@ -202,6 +203,21 @@ async function accessToken(apiUrl) {
|
|
|
202
203
|
// src/client.ts
|
|
203
204
|
import { randomUUID } from "crypto";
|
|
204
205
|
var DEFAULT_API_URL = process.env.SCRAPPYCOCO_API_URL || "https://api.scrappycoco.ai";
|
|
206
|
+
var DEFAULT_JOB_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
207
|
+
var DEFAULT_JOB_POLL_INITIAL_MS = 250;
|
|
208
|
+
var DEFAULT_JOB_POLL_MAX_MS = 5e3;
|
|
209
|
+
function positiveInteger(value, fallback) {
|
|
210
|
+
if (!value) return fallback;
|
|
211
|
+
const parsed = Number(value);
|
|
212
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
213
|
+
}
|
|
214
|
+
function jobTimeoutError(job, timeoutMs) {
|
|
215
|
+
return new CliError(
|
|
216
|
+
`Job ${job.job_id} did not finish within ${Math.ceil(timeoutMs / 1e3)} seconds.`,
|
|
217
|
+
EXIT.timeout,
|
|
218
|
+
job
|
|
219
|
+
);
|
|
220
|
+
}
|
|
205
221
|
var ApiClient = class {
|
|
206
222
|
constructor(baseUrl = DEFAULT_API_URL) {
|
|
207
223
|
this.baseUrl = baseUrl;
|
|
@@ -218,11 +234,12 @@ var ApiClient = class {
|
|
|
218
234
|
...extra
|
|
219
235
|
};
|
|
220
236
|
}
|
|
221
|
-
async request(method, path, body, headers) {
|
|
237
|
+
async request(method, path, body, headers, signal) {
|
|
222
238
|
const response = await fetch(`${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`, {
|
|
223
239
|
method,
|
|
224
240
|
headers: await this.headers(headers),
|
|
225
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
241
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
242
|
+
signal
|
|
226
243
|
});
|
|
227
244
|
if (response.status === 204) return void 0;
|
|
228
245
|
const payload = await response.json().catch(() => ({}));
|
|
@@ -233,12 +250,54 @@ var ApiClient = class {
|
|
|
233
250
|
}
|
|
234
251
|
return payload;
|
|
235
252
|
}
|
|
236
|
-
get(path) {
|
|
237
|
-
return this.request("GET", path);
|
|
253
|
+
get(path, signal) {
|
|
254
|
+
return this.request("GET", path, void 0, void 0, signal);
|
|
238
255
|
}
|
|
239
256
|
post(path, body, key = randomUUID()) {
|
|
240
257
|
return this.request("POST", path, body, { "Idempotency-Key": key });
|
|
241
258
|
}
|
|
259
|
+
async postJob(path, body, key = randomUUID()) {
|
|
260
|
+
const submitted = await this.post(path, body, key);
|
|
261
|
+
const timeoutMs = positiveInteger(process.env.SCRAPPYCOCO_JOB_TIMEOUT_MS, DEFAULT_JOB_TIMEOUT_MS);
|
|
262
|
+
const initialDelayMs = positiveInteger(
|
|
263
|
+
process.env.SCRAPPYCOCO_JOB_POLL_INITIAL_MS,
|
|
264
|
+
DEFAULT_JOB_POLL_INITIAL_MS
|
|
265
|
+
);
|
|
266
|
+
const maxDelayMs = positiveInteger(process.env.SCRAPPYCOCO_JOB_POLL_MAX_MS, DEFAULT_JOB_POLL_MAX_MS);
|
|
267
|
+
const deadline = Date.now() + timeoutMs;
|
|
268
|
+
let delayMs = Math.min(initialDelayMs, maxDelayMs);
|
|
269
|
+
let job = submitted;
|
|
270
|
+
while (job.status !== "completed") {
|
|
271
|
+
if (job.status === "failed") {
|
|
272
|
+
throw new CliError(
|
|
273
|
+
job.error || `Job ${job.job_id} failed.`,
|
|
274
|
+
EXIT.api,
|
|
275
|
+
job
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const remainingBeforeDelay = deadline - Date.now();
|
|
279
|
+
if (remainingBeforeDelay <= 0) throw jobTimeoutError(job, timeoutMs);
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingBeforeDelay)));
|
|
281
|
+
const remainingBeforeRequest = deadline - Date.now();
|
|
282
|
+
if (remainingBeforeRequest <= 0) throw jobTimeoutError(job, timeoutMs);
|
|
283
|
+
try {
|
|
284
|
+
job = await this.get(
|
|
285
|
+
`/jobs/${encodeURIComponent(job.job_id)}`,
|
|
286
|
+
AbortSignal.timeout(remainingBeforeRequest)
|
|
287
|
+
);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (Date.now() >= deadline || error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) {
|
|
290
|
+
throw jobTimeoutError(job, timeoutMs);
|
|
291
|
+
}
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
295
|
+
}
|
|
296
|
+
if (!job.result || typeof job.result !== "object" || Array.isArray(job.result)) {
|
|
297
|
+
throw new CliError(`Job ${job.job_id} completed without a result.`, EXIT.api, job);
|
|
298
|
+
}
|
|
299
|
+
return job.result;
|
|
300
|
+
}
|
|
242
301
|
patch(path, body) {
|
|
243
302
|
return this.request("PATCH", path, body);
|
|
244
303
|
}
|
|
@@ -309,8 +368,11 @@ function errorPayload(error) {
|
|
|
309
368
|
}
|
|
310
369
|
|
|
311
370
|
// src/index.ts
|
|
371
|
+
var packageMetadata = JSON.parse(
|
|
372
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
373
|
+
);
|
|
312
374
|
var program = new Command();
|
|
313
|
-
program.name("scrappycoco").description("Discover, run, and compare scraper capabilities through one API").version(
|
|
375
|
+
program.name("scrappycoco").description("Discover, run, and compare scraper capabilities through one API").version(packageMetadata.version).option("--json", "emit machine-readable JSON to stdout").option("--api-url <url>", "API base URL", process.env.SCRAPPYCOCO_API_URL || "https://api.scrappycoco.ai").showHelpAfterError().exitOverride();
|
|
314
376
|
function globals(command) {
|
|
315
377
|
return command.optsWithGlobals();
|
|
316
378
|
}
|
|
@@ -397,8 +459,8 @@ function executionCommand(name) {
|
|
|
397
459
|
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
460
|
const payload = await requestPayload(options, scraperId);
|
|
399
461
|
if (payload.limit === void 0) payload.limit = 10;
|
|
400
|
-
const response = await client(command).
|
|
401
|
-
|
|
462
|
+
const response = await client(command).postJob(
|
|
463
|
+
name === "run" ? "/scrapers/jobs" : "/scrapers/compare/jobs",
|
|
402
464
|
payload,
|
|
403
465
|
options.idempotencyKey || randomUUID2()
|
|
404
466
|
);
|
|
@@ -407,6 +469,13 @@ function executionCommand(name) {
|
|
|
407
469
|
}
|
|
408
470
|
executionCommand("run");
|
|
409
471
|
executionCommand("compare");
|
|
472
|
+
var jobs = program.command("jobs").description("Inspect durable queued jobs");
|
|
473
|
+
jobs.command("get <job-id>").action(async (jobId, _options, command) => {
|
|
474
|
+
await emit(
|
|
475
|
+
await client(command).get(`/jobs/${encodeURIComponent(jobId)}`),
|
|
476
|
+
globals(command).json || false
|
|
477
|
+
);
|
|
478
|
+
});
|
|
410
479
|
var providers = program.command("providers").description("Inspect integrated scraper providers");
|
|
411
480
|
providers.command("list").option("--available", "only include available provider-capability routes").action(async (options, command) => {
|
|
412
481
|
await emit(
|
|
@@ -448,8 +517,8 @@ discoveries.command("run <discovery-id>").option("-f, --file <path>", "request J
|
|
|
448
517
|
input: input || {},
|
|
449
518
|
limit: Number(options.limit ?? fromFile.limit ?? 25)
|
|
450
519
|
};
|
|
451
|
-
const response = await client(command).
|
|
452
|
-
`/discoveries/${encodeURIComponent(discoveryId)}/
|
|
520
|
+
const response = await client(command).postJob(
|
|
521
|
+
`/discoveries/${encodeURIComponent(discoveryId)}/jobs`,
|
|
453
522
|
payload,
|
|
454
523
|
options.idempotencyKey || randomUUID2()
|
|
455
524
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scrappycoco/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|