@scrappycoco/cli 0.5.0 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +8 -7
  2. package/dist/index.js +100 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,12 +17,10 @@ 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
- Before every ordinary command, the CLI compares the installed Scrappycoco skill
21
- with the digest in the hash-verified public feed. When the digest changes, it
22
- automatically refreshes the skill and prints a reload/restart notice to stderr.
23
- Release-check or installer failures do not block the requested CLI command.
24
- CLI-only users without an installed skill are not modified. Set
25
- `SCRAPPYCOCO_DISABLE_SKILL_AUTO_UPDATE=1` to opt out.
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.
26
24
 
27
25
  When the CLI runs on a remote or headless host whose `127.0.0.1` is not the
28
26
  browser's localhost, keep the command running and use the manual callback
@@ -42,6 +40,8 @@ Individual commands remain available:
42
40
 
43
41
  ```sh
44
42
  npx --yes @scrappycoco/cli@latest auth login
43
+ npx --yes @scrappycoco/cli@latest skill status
44
+ npx --yes @scrappycoco/cli@latest skill update
45
45
  npx --yes @scrappycoco/cli@latest doctor --json
46
46
  npx --yes @scrappycoco/cli@latest catalog list --available --json
47
47
  npx --yes @scrappycoco/cli@latest catalog inspect web.extract_content --json
@@ -74,7 +74,8 @@ compact execution summary remains on stdout.
74
74
 
75
75
  For a long run, add `--detach` to return a job ID immediately, then finish with
76
76
  `scrappycoco jobs wait <job-id> --output results.json`. Use
77
- `scrappycoco jobs get <job-id>` for a single status check. Set
77
+ `scrappycoco jobs get <job-id>` for a single status check or
78
+ `scrappycoco jobs cancel <job-id>` to stop it. Set
78
79
  `SCRAPPYCOCO_JOB_TIMEOUT_MS` to change the default 20-minute foreground wait.
79
80
  Run `scrappycoco doctor --json` only when authentication, connectivity, or
80
81
  installation is unclear.
package/dist/index.js CHANGED
@@ -576,6 +576,13 @@ var ApiClient = class {
576
576
  job
577
577
  );
578
578
  }
579
+ if (job.status === "cancelled") {
580
+ throw new CliError(
581
+ `Job ${job.job_id} was cancelled.`,
582
+ EXIT.cancelled,
583
+ job
584
+ );
585
+ }
579
586
  const remainingBeforeDelay = deadline - Date.now();
580
587
  if (remainingBeforeDelay <= 0) throw jobTimeoutError(job, timeoutMs);
581
588
  await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingBeforeDelay)));
@@ -869,30 +876,66 @@ function installedSkillCandidates() {
869
876
  join2(homedir2(), ".cursor", "skills", SKILL_NAME, "SKILL.md")
870
877
  ];
871
878
  }
872
- async function isSkillInstalled() {
873
- for (const path of installedSkillCandidates()) {
874
- try {
875
- await access(path);
876
- return true;
877
- } catch {
878
- }
879
+ function configuredTargetPath(agent) {
880
+ const directory = agent === "codex" ? ".codex" : agent === "claude-code" ? ".claude" : agent === "cursor" ? ".cursor" : ".agents";
881
+ return join2(homedir2(), directory, "skills", SKILL_NAME, "SKILL.md");
882
+ }
883
+ async function pathExists(path) {
884
+ try {
885
+ await access(path);
886
+ return true;
887
+ } catch {
888
+ return false;
879
889
  }
880
- return false;
881
890
  }
882
- async function loadInstalledDigest() {
891
+ async function activeSkillTarget() {
892
+ const configured = configuredTargetPath(await detectSkillInstallerAgent());
893
+ if (await pathExists(configured)) return configured;
894
+ const shared = configuredTargetPath("universal");
895
+ if (await pathExists(shared)) return shared;
896
+ return configured;
897
+ }
898
+ async function isSkillInstalled() {
899
+ return pathExists(await activeSkillTarget());
900
+ }
901
+ async function loadSkillReleaseState() {
883
902
  try {
884
903
  const state = JSON.parse(
885
904
  await readFile3(skillReleaseStatePath(), "utf8")
886
905
  );
887
- return typeof state.digest === "string" && SHA256_DIGEST.test(state.digest) ? state.digest : null;
906
+ return {
907
+ targets: Object.fromEntries(
908
+ Object.entries(state.targets || {}).filter(
909
+ ([path, value]) => Boolean(path) && typeof value?.digest === "string" && SHA256_DIGEST.test(value.digest)
910
+ )
911
+ )
912
+ };
888
913
  } catch {
889
- return null;
914
+ return { targets: {} };
890
915
  }
891
916
  }
917
+ async function loadInstalledDigest() {
918
+ const target = await activeSkillTarget();
919
+ const entry = (await loadSkillReleaseState()).targets[target];
920
+ return entry?.digest || null;
921
+ }
892
922
  async function installedSkillDiagnostics() {
923
+ const target = await activeSkillTarget();
924
+ const state = await loadSkillReleaseState();
925
+ const installedTargets = (await Promise.all(
926
+ installedSkillCandidates().map(async (path) => ({
927
+ path,
928
+ installed: await pathExists(path)
929
+ }))
930
+ )).filter((item) => item.installed);
893
931
  return {
894
- installed: await isSkillInstalled(),
895
- digest: await loadInstalledDigest()
932
+ installed: installedTargets.some((item) => item.path === target),
933
+ digest: state.targets[target]?.digest || null,
934
+ target,
935
+ targets: installedTargets.map(({ path }) => ({
936
+ path,
937
+ digest: state.targets[path]?.digest || null
938
+ }))
896
939
  };
897
940
  }
898
941
  async function writePrivateJson2(path, value) {
@@ -908,10 +951,23 @@ async function writePrivateJson2(path, value) {
908
951
  }
909
952
  }
910
953
  async function saveInstalledDigest(digest) {
911
- await writePrivateJson2(skillReleaseStatePath(), {
912
- digest,
913
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
914
- });
954
+ const target = await activeSkillTarget();
955
+ const state = await loadSkillReleaseState();
956
+ await writePrivateJson2(
957
+ skillReleaseStatePath(),
958
+ recordTargetDigest(state, target, digest)
959
+ );
960
+ }
961
+ function recordTargetDigest(state, target, digest, updatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
962
+ return {
963
+ targets: {
964
+ ...state.targets,
965
+ [target]: {
966
+ digest,
967
+ updated_at: updatedAt
968
+ }
969
+ }
970
+ };
915
971
  }
916
972
  async function fetchPublishedSkillDigest(fetcher = fetch) {
917
973
  const response = await fetcher(SKILL_INDEX_URL, {
@@ -922,7 +978,7 @@ async function fetchPublishedSkillDigest(fetcher = fetch) {
922
978
  throw new Error(`Skill release check returned HTTP ${response.status}.`);
923
979
  }
924
980
  const index = await response.json();
925
- const entry = index.skills?.find((skill) => skill.name === SKILL_NAME);
981
+ const entry = index.skills?.find((skill2) => skill2.name === SKILL_NAME);
926
982
  if (!entry || typeof entry.digest !== "string" || !SHA256_DIGEST.test(entry.digest)) {
927
983
  throw new Error("Skill release index did not contain a valid Scrappycoco digest.");
928
984
  }
@@ -993,13 +1049,7 @@ async function withSkillUpdateLock(operation) {
993
1049
  await rm2(path, { force: true });
994
1050
  }
995
1051
  }
996
- function autoUpdateDisabled() {
997
- return ["1", "true", "yes"].includes(
998
- (process.env.SCRAPPYCOCO_DISABLE_SKILL_AUTO_UPDATE || "").toLowerCase()
999
- );
1000
- }
1001
- async function autoUpdateInstalledSkill() {
1002
- if (autoUpdateDisabled()) return { status: "disabled" };
1052
+ async function updateInstalledSkill() {
1003
1053
  let result;
1004
1054
  try {
1005
1055
  result = await withSkillUpdateLock(() => checkAndInstallSkillUpdate());
@@ -1060,10 +1110,6 @@ program.command("setup").description("Authenticate, install the Scrappycoco skil
1060
1110
  await rememberInstalledSkillRelease();
1061
1111
  await emit(result, globals(command).json || false);
1062
1112
  });
1063
- program.hook("preAction", async (_thisCommand, actionCommand) => {
1064
- if (actionCommand.name() === "setup") return;
1065
- await autoUpdateInstalledSkill();
1066
- });
1067
1113
  function splitScraperId(value) {
1068
1114
  const separator = value.indexOf(".");
1069
1115
  if (separator <= 0 || separator === value.length - 1) {
@@ -1131,9 +1177,6 @@ async function emitExecution(response, options, command) {
1131
1177
  }
1132
1178
  if (Array.isArray(response.items)) {
1133
1179
  summary.item_count = response.items.length;
1134
- summary.failed_item_count = response.items.filter(
1135
- (item) => item && typeof item === "object" && item.status === "failed"
1136
- ).length;
1137
1180
  }
1138
1181
  if (Array.isArray(summary.routes)) {
1139
1182
  summary.routes = summary.routes.map((value) => {
@@ -1271,6 +1314,20 @@ auth.command("logout").action(async (_options, command) => {
1271
1314
  await clearRefreshToken2();
1272
1315
  await emit({ authenticated: false }, globals(command).json || false);
1273
1316
  });
1317
+ var skill = program.command("skill").description("Inspect or explicitly update the installed skill");
1318
+ skill.command("status").action(async (_options, command) => {
1319
+ await emit(await installedSkillDiagnostics(), globals(command).json || false);
1320
+ });
1321
+ skill.command("update").action(async (_options, command) => {
1322
+ const result = await updateInstalledSkill();
1323
+ await emit(
1324
+ result.status === "failed" ? { status: result.status, error: result.error.message } : result,
1325
+ globals(command).json || false
1326
+ );
1327
+ if (result.status === "failed" || result.status === "unavailable") {
1328
+ process.exitCode = EXIT.api;
1329
+ }
1330
+ });
1274
1331
  program.command("doctor").description("Check the CLI, authentication, installed skill, API, and live catalog").action(async (_options, command) => {
1275
1332
  const usingApiKey = Boolean(process.env.SCRAPPYCOCO_API_KEY);
1276
1333
  const usingOAuth = !usingApiKey && Boolean(await loadRefreshToken());
@@ -1287,7 +1344,7 @@ program.command("doctor").description("Check the CLI, authentication, installed
1287
1344
  catalogError = error instanceof Error ? error.message : String(error);
1288
1345
  }
1289
1346
  }
1290
- const skill = await installedSkillDiagnostics();
1347
+ const skill2 = await installedSkillDiagnostics();
1291
1348
  const ok = authentication.configured && catalogError === null;
1292
1349
  await emit(
1293
1350
  {
@@ -1306,7 +1363,7 @@ program.command("doctor").description("Check the CLI, authentication, installed
1306
1363
  available_capabilities: catalog2.length,
1307
1364
  error: catalogError
1308
1365
  },
1309
- skill,
1366
+ skill: skill2,
1310
1367
  next_action: ok ? "Scrappycoco is ready." : authentication.configured ? "Check the API connection, then run doctor again." : "Run `scrappycoco setup`."
1311
1368
  },
1312
1369
  globals(command).json || false
@@ -1421,6 +1478,15 @@ jobs.command("wait <job-id>").description("Wait for a queued job and return its
1421
1478
  command
1422
1479
  );
1423
1480
  });
1481
+ jobs.command("cancel <job-id>").description("Cancel a pending or running queued job").action(async (jobId, _options, command) => {
1482
+ await emit(
1483
+ await client(command).post(
1484
+ `/jobs/${encodeURIComponent(jobId)}/cancel`,
1485
+ {}
1486
+ ),
1487
+ globals(command).json || false
1488
+ );
1489
+ });
1424
1490
  var providers = program.command("providers", { hidden: true }).description("Legacy provider commands");
1425
1491
  providers.command("list").option("--available", "only include available provider-capability routes").action(async (options, command) => {
1426
1492
  await emit(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrappycoco/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "CLI for Scrappycoco scraper discovery and execution",
5
5
  "type": "module",
6
6
  "bin": {