@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 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.readme.io) for the public
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).post(
401
- `/scrapers/${name === "run" ? "execute" : "compare"}`,
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).post(
452
- `/discoveries/${encodeURIComponent(discoveryId)}/run`,
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.1.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": {