@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 CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  Discover, run, and compare scraper capabilities from a terminal or automation environment.
4
4
 
5
- The npm package must be published before using `npx`. Verify availability with
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.readme.io) for the public
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("0.1.0").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();
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).post(
401
- `/scrapers/${name === "run" ? "execute" : "compare"}`,
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).post(
452
- `/discoveries/${encodeURIComponent(discoveryId)}/run`,
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.0",
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": {