@scrappycoco/cli 0.8.2 → 0.8.3

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.
Files changed (3) hide show
  1. package/README.md +9 -4
  2. package/dist/index.js +96 -91
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,10 +17,15 @@ the live catalog, and tells you when to reload or restart your agent. Browser
17
17
  authorization is not complete until the terminal confirms that the credential
18
18
  was saved and the catalog check succeeded.
19
19
 
20
- Ordinary commands never mutate installed agent instructions. Setup installs
21
- the matching skill release. Run `scrappycoco skill status` to inspect the
22
- active target and `scrappycoco skill update` to explicitly install a newer
23
- hash-verified release, then reload or restart the agent.
20
+ Setup installs the matching skill release. On the first ordinary CLI command
21
+ after the 24-hour cooldown, Scrappycoco compares the installed release with the
22
+ hash-verified public feed and installs a changed skill automatically. The data
23
+ command continues when the check or installer is unavailable. Reload or restart
24
+ the agent after an update notice so it reads the new instructions. Run
25
+ `scrappycoco skill status` to inspect the active target and update state, or
26
+ `scrappycoco skill update` to check immediately. Set
27
+ `SCRAPPYCOCO_SKILL_AUTO_UPDATE=0` to keep the installed copy pinned and use only
28
+ explicit updates.
24
29
 
25
30
  When the CLI runs on a remote or headless host whose `127.0.0.1` is not the
26
31
  browser's localhost, keep the command running and use the manual callback
package/dist/index.js CHANGED
@@ -861,7 +861,10 @@ var SKILL_NAME = "scrappycoco";
861
861
  var SKILL_INDEX_URL = `${SKILL_SOURCE}/.well-known/agent-skills/index.json`;
862
862
  var SKILL_UPDATE_TIMEOUT_MS = 3e3;
863
863
  var SKILL_UPDATE_LOCK_STALE_MS = 5 * 6e4;
864
+ var SKILL_AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 6e4;
864
865
  var SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/i;
866
+ var DISABLED_AUTO_UPDATE_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
867
+ var ENABLED_AUTO_UPDATE_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
865
868
  function skillReleaseStatePath() {
866
869
  return join2(dirname2(fallbackCredentialPath()), "skill-release.json");
867
870
  }
@@ -919,9 +922,21 @@ async function loadInstalledDigest() {
919
922
  const entry = (await loadSkillReleaseState()).targets[target];
920
923
  return entry?.digest || null;
921
924
  }
925
+ async function loadLastCheckedAt() {
926
+ const target = await activeSkillTarget();
927
+ const entry = (await loadSkillReleaseState()).targets[target];
928
+ return entry?.checked_at || entry?.updated_at || null;
929
+ }
930
+ function skillAutoUpdateEnabled(environment = process.env) {
931
+ const configured = environment.SCRAPPYCOCO_SKILL_AUTO_UPDATE?.trim().toLowerCase();
932
+ if (configured && ENABLED_AUTO_UPDATE_VALUES.has(configured)) return true;
933
+ if (configured && DISABLED_AUTO_UPDATE_VALUES.has(configured)) return false;
934
+ return environment.NODE_ENV !== "test" && !environment.VITEST && !ENABLED_AUTO_UPDATE_VALUES.has(environment.CI?.trim().toLowerCase() || "");
935
+ }
922
936
  async function installedSkillDiagnostics() {
923
937
  const target = await activeSkillTarget();
924
938
  const state = await loadSkillReleaseState();
939
+ const activeRelease = state.targets[target];
925
940
  const installedTargets = (await Promise.all(
926
941
  installedSkillCandidates().map(async (path) => ({
927
942
  path,
@@ -930,8 +945,13 @@ async function installedSkillDiagnostics() {
930
945
  )).filter((item) => item.installed);
931
946
  return {
932
947
  installed: installedTargets.some((item) => item.path === target),
933
- digest: state.targets[target]?.digest || null,
948
+ digest: activeRelease?.digest || null,
934
949
  target,
950
+ auto_update: {
951
+ enabled: skillAutoUpdateEnabled(),
952
+ interval_hours: SKILL_AUTO_UPDATE_INTERVAL_MS / (60 * 6e4),
953
+ last_checked_at: activeRelease?.checked_at || activeRelease?.updated_at || null
954
+ },
935
955
  targets: installedTargets.map(({ path }) => ({
936
956
  path,
937
957
  digest: state.targets[path]?.digest || null
@@ -964,11 +984,38 @@ function recordTargetDigest(state, target, digest, updatedAt = (/* @__PURE__ */
964
984
  ...state.targets,
965
985
  [target]: {
966
986
  digest,
967
- updated_at: updatedAt
987
+ updated_at: updatedAt,
988
+ checked_at: updatedAt
968
989
  }
969
990
  }
970
991
  };
971
992
  }
993
+ function recordTargetCheck(state, target, checkedAt = (/* @__PURE__ */ new Date()).toISOString()) {
994
+ const release = state.targets[target];
995
+ if (!release) return state;
996
+ return {
997
+ targets: {
998
+ ...state.targets,
999
+ [target]: {
1000
+ ...release,
1001
+ checked_at: checkedAt
1002
+ }
1003
+ }
1004
+ };
1005
+ }
1006
+ async function saveLastCheckedAt(checkedAt) {
1007
+ const target = await activeSkillTarget();
1008
+ const state = await loadSkillReleaseState();
1009
+ await writePrivateJson2(
1010
+ skillReleaseStatePath(),
1011
+ recordTargetCheck(state, target, checkedAt)
1012
+ );
1013
+ }
1014
+ function automaticSkillUpdateDue(lastCheckedAt, now = /* @__PURE__ */ new Date(), intervalMs = SKILL_AUTO_UPDATE_INTERVAL_MS) {
1015
+ if (!lastCheckedAt) return true;
1016
+ const elapsed = now.getTime() - Date.parse(lastCheckedAt);
1017
+ return !Number.isFinite(elapsed) || elapsed < 0 || elapsed >= intervalMs;
1018
+ }
972
1019
  async function fetchPublishedSkillDigest(fetcher = fetch) {
973
1020
  const response = await fetcher(SKILL_INDEX_URL, {
974
1021
  headers: { accept: "application/json" },
@@ -1071,8 +1118,46 @@ async function updateInstalledSkill() {
1071
1118
  `
1072
1119
  );
1073
1120
  }
1121
+ if (result.status === "current" || result.status === "updated" && result.state_saved) {
1122
+ try {
1123
+ await saveLastCheckedAt((/* @__PURE__ */ new Date()).toISOString());
1124
+ } catch {
1125
+ }
1126
+ }
1074
1127
  return result;
1075
1128
  }
1129
+ function defaultAutomaticDependencies() {
1130
+ return {
1131
+ enabled: skillAutoUpdateEnabled,
1132
+ now: () => /* @__PURE__ */ new Date(),
1133
+ loadLastCheckedAt,
1134
+ update: updateInstalledSkill,
1135
+ saveLastCheckedAt
1136
+ };
1137
+ }
1138
+ async function maybeUpdateInstalledSkill(dependencies = defaultAutomaticDependencies(), intervalMs = SKILL_AUTO_UPDATE_INTERVAL_MS) {
1139
+ if (!dependencies.enabled()) return { status: "disabled" };
1140
+ try {
1141
+ const now = dependencies.now();
1142
+ if (!automaticSkillUpdateDue(
1143
+ await dependencies.loadLastCheckedAt(),
1144
+ now,
1145
+ intervalMs
1146
+ )) {
1147
+ return { status: "throttled" };
1148
+ }
1149
+ const result = await dependencies.update();
1150
+ if (result.status === "current" || result.status === "updated") {
1151
+ try {
1152
+ await dependencies.saveLastCheckedAt(now.toISOString());
1153
+ } catch {
1154
+ }
1155
+ }
1156
+ return result;
1157
+ } catch {
1158
+ return { status: "unavailable" };
1159
+ }
1160
+ }
1076
1161
  async function rememberInstalledSkillRelease() {
1077
1162
  try {
1078
1163
  await saveInstalledDigest(await fetchPublishedSkillDigest());
@@ -1140,7 +1225,7 @@ async function requestPayload(options, scraperId) {
1140
1225
  };
1141
1226
  }
1142
1227
  async function emitExecution(response, options, command) {
1143
- const records = "records" in response && Array.isArray(response.records) ? response.records : response.items;
1228
+ const records = response.records;
1144
1229
  const jsonMode = globals(command).json || false;
1145
1230
  if (options.output) {
1146
1231
  await emit(response, jsonMode, options.output, formatRecords(records, options.format));
@@ -1158,10 +1243,9 @@ async function emitExecution(response, options, command) {
1158
1243
  await emit(response, jsonMode);
1159
1244
  }
1160
1245
  function executionSummary(response) {
1161
- const records = "records" in response && Array.isArray(response.records) ? response.records : response.items;
1246
+ const records = response.records;
1162
1247
  const summary = { ...response };
1163
1248
  delete summary.records;
1164
- delete summary.items;
1165
1249
  delete summary.normalized_schema;
1166
1250
  summary.truncated_count = records.filter((record) => {
1167
1251
  const metadata = record.metadata;
@@ -1196,7 +1280,7 @@ function executionSummary(response) {
1196
1280
  if (Object.keys(counts).length) summary.provider_result_counts = counts;
1197
1281
  delete summary.provider_results;
1198
1282
  }
1199
- if (Array.isArray(response.items)) summary.item_count = response.items.length;
1283
+ summary.item_count = records.length;
1200
1284
  if (Array.isArray(summary.routes)) {
1201
1285
  summary.routes = summary.routes.map((value) => {
1202
1286
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
@@ -1340,7 +1424,7 @@ auth.command("logout").action(async (_options, command) => {
1340
1424
  await clearRefreshToken2();
1341
1425
  await emit({ authenticated: false }, globals(command).json || false);
1342
1426
  });
1343
- var skill = program.command("skill").description("Inspect or explicitly update the installed skill");
1427
+ var skill = program.command("skill").description("Inspect or update the installed skill");
1344
1428
  skill.command("status").action(async (_options, command) => {
1345
1429
  await emit(await installedSkillDiagnostics(), globals(command).json || false);
1346
1430
  });
@@ -1396,24 +1480,6 @@ program.command("doctor").description("Check the CLI, authentication, installed
1396
1480
  );
1397
1481
  if (!ok) process.exitCode = authentication.configured ? EXIT.api : EXIT.auth;
1398
1482
  });
1399
- var scrapers = program.command("scrapers", { hidden: true }).description("Legacy scraper commands");
1400
- scrapers.command("list").option("--source <source>", "filter by web, x, reddit, instagram, tiktok, or filings").option("--provider <provider>", "filter by provider implementation").option("--available", "only include available providers").action(async (options, command) => {
1401
- const query = new URLSearchParams();
1402
- if (options.source) query.set("source", options.source);
1403
- if (options.provider) query.set("provider", options.provider);
1404
- if (options.available) query.set("available_only", "true");
1405
- const suffix = query.size ? `?${query}` : "";
1406
- await emit(await client(command).get(`/scrapers${suffix}`), globals(command).json || false);
1407
- });
1408
- scrapers.command("inspect <scraper-id>").action(async (scraperId, _options, command) => {
1409
- const { source, capability } = splitScraperId(scraperId);
1410
- await emit(
1411
- await client(command).get(
1412
- `/scrapers/${encodeURIComponent(source)}/${encodeURIComponent(capability)}`
1413
- ),
1414
- globals(command).json || false
1415
- );
1416
- });
1417
1483
  var catalog = program.command("catalog").description("Inspect capabilities, provider schemas, options, and pricing");
1418
1484
  catalog.command("list").option("--source <source>", "filter by web, x, reddit, instagram, tiktok, or filings").option("--provider <provider>", "filter by provider implementation").option("--available", "only include configured providers; this is not a live health check").option("--full", "include input/output and provider option schemas in JSON output").action(async (options, command) => {
1419
1485
  const query = new URLSearchParams();
@@ -1433,20 +1499,6 @@ catalog.command("inspect <capability-id>").action(async (capabilityId, _options,
1433
1499
  globals(command).json || false
1434
1500
  );
1435
1501
  });
1436
- function executionCommand(name) {
1437
- 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("--provider-options <json>", "provider-native options keyed by provider ID").option("--concurrency <number>", "batch concurrency (default 3, maximum 10)").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) => {
1438
- const payload = await requestPayload(options, scraperId);
1439
- if (payload.limit === void 0) payload.limit = 10;
1440
- const response = await client(command).postJob(
1441
- name === "run" ? "/scrapers/jobs" : "/scrapers/compare/jobs",
1442
- payload,
1443
- options.idempotencyKey || randomUUID4()
1444
- );
1445
- await emitExecution(response, options, command);
1446
- });
1447
- }
1448
- executionCommand("run");
1449
- executionCommand("compare");
1450
1502
  program.command("run [capability-id]").description("Run a capability directly; use Discover only when configuration is uncertain").option("--config <discovery-id>", "run a finalized multi-step configuration").option("-f, --file <path>", "canonical request JSON file").option("--input <json>", "capability input JSON; use url or urls for web.extract_content").option("--provider <id>", "provider ID; repeat for an ordered fallback waterfall", collect, []).option("--provider-options <json>", "provider-native options keyed by provider ID").option("--concurrency <number>", "batch concurrency (default 3, maximum 10)").option("--limit <number>", "maximum records").option("--idempotency-key <key>", "stable retry key").option("--retry-failed <run-id>", "retry only failed URLs from a partial batch run").option("--detach", "queue the job and return immediately").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 (capabilityId, options, command) => {
1451
1503
  if (options.retryFailed) {
1452
1504
  if (capabilityId || options.config) {
@@ -1513,58 +1565,6 @@ jobs.command("cancel <job-id>").description("Cancel a pending or running queued
1513
1565
  globals(command).json || false
1514
1566
  );
1515
1567
  });
1516
- var providers = program.command("providers", { hidden: true }).description("Legacy provider commands");
1517
- providers.command("list").option("--available", "only include available provider-capability routes").action(async (options, command) => {
1518
- await emit(
1519
- await client(command).get(`/providers${options.available ? "?available_only=true" : ""}`),
1520
- globals(command).json || false
1521
- );
1522
- });
1523
- var discoveries = program.command("discoveries", { hidden: true }).description("Legacy discovery commands");
1524
- discoveries.command("create").requiredOption("-f, --file <path>", "agent-authored discovery JSON with goal and configuration").action(async (options, command) => {
1525
- await emit(
1526
- await client(command).post("/discoveries", await readJsonFile(options.file)),
1527
- globals(command).json || false
1528
- );
1529
- });
1530
- discoveries.command("list").action(async (_options, command) => {
1531
- await emit(await client(command).get("/discoveries"), globals(command).json || false);
1532
- });
1533
- discoveries.command("get <discovery-id>").action(async (discoveryId, _options, command) => {
1534
- await emit(
1535
- await client(command).get(`/discoveries/${encodeURIComponent(discoveryId)}`),
1536
- globals(command).json || false
1537
- );
1538
- });
1539
- discoveries.command("update <discovery-id>").option("-f, --file <path>", "update JSON containing name, priority, or configuration").option("--name <name>", "saved discovery name").option("--priority <priority>", "balanced, quality, coverage, cost, or speed").action(async (discoveryId, options, command) => {
1540
- const payload = options.file ? await readJsonFile(options.file) : {};
1541
- if (options.name) payload.name = options.name;
1542
- if (options.priority) payload.priority = options.priority;
1543
- if (!Object.keys(payload).length) throw new CliError("Provide --file, --name, or --priority.", EXIT.usage);
1544
- await emit(
1545
- await client(command).patch(`/discoveries/${encodeURIComponent(discoveryId)}`, payload),
1546
- globals(command).json || false
1547
- );
1548
- });
1549
- discoveries.command("run <discovery-id>").option("-f, --file <path>", "request JSON containing input and optional limit").option("--input <json>", "runtime parameter JSON").option("--limit <number>", "maximum records per route").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 (discoveryId, options, command) => {
1550
- const fromFile = options.file ? await readJsonFile(options.file) : {};
1551
- const input = options.input ? parseJsonObject(options.input, "input JSON") : fromFile.input;
1552
- const payload = {
1553
- ...fromFile,
1554
- input: input || {},
1555
- limit: Number(options.limit ?? fromFile.limit ?? 25)
1556
- };
1557
- const response = await client(command).postJob(
1558
- `/discoveries/${encodeURIComponent(discoveryId)}/jobs`,
1559
- payload,
1560
- options.idempotencyKey || randomUUID4()
1561
- );
1562
- await emitExecution(response, options, command);
1563
- });
1564
- discoveries.command("delete <discovery-id>").requiredOption("--yes", "confirm permanent deletion").action(async (discoveryId, _options, command) => {
1565
- await client(command).delete(`/discoveries/${encodeURIComponent(discoveryId)}`);
1566
- await emit({ deleted: true, discovery_id: discoveryId }, globals(command).json || false);
1567
- });
1568
1568
  program.command("discover").description("Save, sample-test, or finalize an agent-authored configuration").option("-f, --file <path>", "create from agent-authored discovery JSON").option("--id <discovery-id>", "existing discovery ID").option("--test", "run a representative provider sample test").option("--input <json>", "sample runtime input JSON").option("--update <path>", "replace fields or configuration from agent-authored JSON").option("--finalize", "mark the current explicit configuration finalized").option("--idempotency-key <key>", "stable sample retry key").option("-o, --output <path>", "write complete sample-test evidence to a JSON file").action(async (options, command) => {
1569
1569
  const selected = Number(Boolean(options.file)) + Number(Boolean(options.test)) + Number(Boolean(options.update)) + Number(Boolean(options.finalize));
1570
1570
  if (selected !== 1) {
@@ -1632,6 +1632,11 @@ program.command("discover").description("Save, sample-test, or finalize an agent
1632
1632
  await emit(response, globals(command).json || false);
1633
1633
  });
1634
1634
  program.configureOutput({ writeErr: (text) => process.stderr.write(text) });
1635
+ program.hook("preAction", async (_command, actionCommand) => {
1636
+ const parentName = actionCommand.parent?.name();
1637
+ if (actionCommand.name() === "setup" || parentName === "skill") return;
1638
+ await maybeUpdateInstalledSkill();
1639
+ });
1635
1640
  program.parseAsync(process.argv).catch(async (error) => {
1636
1641
  if (error instanceof CommanderError) {
1637
1642
  process.exitCode = error.exitCode === 0 ? EXIT.ok : EXIT.usage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrappycoco/cli",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "CLI for Scrappycoco scraper discovery and execution",
5
5
  "type": "module",
6
6
  "bin": {