@scrappycoco/cli 0.5.0 → 0.7.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 +216 -108
  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) {
@@ -1092,80 +1138,7 @@ async function emitExecution(response, options, command) {
1092
1138
  const jsonMode = globals(command).json || false;
1093
1139
  if (options.output) {
1094
1140
  await emit(response, jsonMode, options.output, formatRecords(records, options.format));
1095
- const summary = { ...response };
1096
- delete summary.records;
1097
- delete summary.items;
1098
- delete summary.normalized_schema;
1099
- summary.truncated_count = records.filter((record) => {
1100
- const metadata = record.metadata;
1101
- return metadata && typeof metadata === "object" && !Array.isArray(metadata) && metadata.truncated === true;
1102
- }).length;
1103
- if (summary.usage) {
1104
- summary.usage = selectFields(summary.usage, [
1105
- "billing_status",
1106
- "payg_charge_usd_exact",
1107
- "provider_cost_usd_exact",
1108
- "unresolved_cost_count"
1109
- ]);
1110
- }
1111
- if (Array.isArray(summary.attempts)) {
1112
- summary.attempts = summary.attempts.map((attempt) => selectFields(attempt, [
1113
- "provider",
1114
- "status",
1115
- "result_count",
1116
- "latency_ms",
1117
- "estimated_cost_usd",
1118
- "error"
1119
- ]));
1120
- }
1121
- const providerResults = summary.provider_results;
1122
- if (providerResults && typeof providerResults === "object" && !Array.isArray(providerResults)) {
1123
- const counts = Object.fromEntries(
1124
- Object.entries(providerResults).map(([provider, values]) => [
1125
- provider,
1126
- Array.isArray(values) ? values.length : 0
1127
- ])
1128
- );
1129
- if (Object.keys(counts).length) summary.provider_result_counts = counts;
1130
- delete summary.provider_results;
1131
- }
1132
- if (Array.isArray(response.items)) {
1133
- 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
- }
1138
- if (Array.isArray(summary.routes)) {
1139
- summary.routes = summary.routes.map((value) => {
1140
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
1141
- const route = { ...value };
1142
- const routeResults = route.provider_results;
1143
- if (routeResults && typeof routeResults === "object" && !Array.isArray(routeResults)) {
1144
- const counts = Object.fromEntries(
1145
- Object.entries(routeResults).map(([provider, values]) => [
1146
- provider,
1147
- Array.isArray(values) ? values.length : 0
1148
- ])
1149
- );
1150
- if (Object.keys(counts).length) route.provider_result_counts = counts;
1151
- delete route.provider_results;
1152
- }
1153
- if (Array.isArray(route.attempts)) {
1154
- route.attempts = route.attempts.map((attempt) => selectFields(attempt, [
1155
- "provider",
1156
- "status",
1157
- "result_count",
1158
- "latency_ms",
1159
- "estimated_cost_usd",
1160
- "error"
1161
- ]));
1162
- }
1163
- return route;
1164
- });
1165
- }
1166
- for (const field of ["cursor", "monitor"]) {
1167
- if (summary[field] === null) delete summary[field];
1168
- }
1141
+ const summary = executionSummary(response);
1169
1142
  summary.output = {
1170
1143
  path: options.output,
1171
1144
  format: options.format,
@@ -1178,6 +1151,96 @@ async function emitExecution(response, options, command) {
1178
1151
  }
1179
1152
  await emit(response, jsonMode);
1180
1153
  }
1154
+ function executionSummary(response) {
1155
+ const records = "records" in response && Array.isArray(response.records) ? response.records : response.items;
1156
+ const summary = { ...response };
1157
+ delete summary.records;
1158
+ delete summary.items;
1159
+ delete summary.normalized_schema;
1160
+ summary.truncated_count = records.filter((record) => {
1161
+ const metadata = record.metadata;
1162
+ return metadata && typeof metadata === "object" && !Array.isArray(metadata) && metadata.truncated === true;
1163
+ }).length;
1164
+ if (summary.usage) {
1165
+ summary.usage = selectFields(summary.usage, [
1166
+ "billing_status",
1167
+ "payg_charge_usd_exact",
1168
+ "provider_cost_usd_exact",
1169
+ "unresolved_cost_count"
1170
+ ]);
1171
+ }
1172
+ if (Array.isArray(summary.attempts)) {
1173
+ summary.attempts = summary.attempts.map((attempt) => selectFields(attempt, [
1174
+ "provider",
1175
+ "status",
1176
+ "result_count",
1177
+ "latency_ms",
1178
+ "estimated_cost_usd",
1179
+ "error"
1180
+ ]));
1181
+ }
1182
+ const providerResults = summary.provider_results;
1183
+ if (providerResults && typeof providerResults === "object" && !Array.isArray(providerResults)) {
1184
+ const counts = Object.fromEntries(
1185
+ Object.entries(providerResults).map(([provider, values]) => [
1186
+ provider,
1187
+ Array.isArray(values) ? values.length : 0
1188
+ ])
1189
+ );
1190
+ if (Object.keys(counts).length) summary.provider_result_counts = counts;
1191
+ delete summary.provider_results;
1192
+ }
1193
+ if (Array.isArray(response.items)) summary.item_count = response.items.length;
1194
+ if (Array.isArray(summary.routes)) {
1195
+ summary.routes = summary.routes.map((value) => {
1196
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
1197
+ const route = { ...value };
1198
+ const routeResults = route.provider_results;
1199
+ if (routeResults && typeof routeResults === "object" && !Array.isArray(routeResults)) {
1200
+ const counts = Object.fromEntries(
1201
+ Object.entries(routeResults).map(([provider, values]) => [
1202
+ provider,
1203
+ Array.isArray(values) ? values.length : 0
1204
+ ])
1205
+ );
1206
+ if (Object.keys(counts).length) route.provider_result_counts = counts;
1207
+ delete route.provider_results;
1208
+ }
1209
+ if (Array.isArray(route.attempts)) {
1210
+ route.attempts = route.attempts.map((attempt) => selectFields(attempt, [
1211
+ "provider",
1212
+ "status",
1213
+ "result_count",
1214
+ "latency_ms",
1215
+ "estimated_cost_usd",
1216
+ "error"
1217
+ ]));
1218
+ }
1219
+ return route;
1220
+ });
1221
+ }
1222
+ for (const field of ["cursor", "monitor"]) {
1223
+ if (summary[field] === null) delete summary[field];
1224
+ }
1225
+ return summary;
1226
+ }
1227
+ function compactCatalog(items) {
1228
+ return items.map((item) => ({
1229
+ id: item.id,
1230
+ source: item.source,
1231
+ capability: item.capability,
1232
+ label: item.label,
1233
+ description: item.description,
1234
+ execution_mode: item.execution_mode,
1235
+ providers: Array.isArray(item.providers) ? item.providers.map((provider) => selectFields(provider, [
1236
+ "id",
1237
+ "label",
1238
+ "available",
1239
+ "reason",
1240
+ "pricing"
1241
+ ])) : []
1242
+ }));
1243
+ }
1181
1244
  async function executeQueuedJob(path, payload, options, command) {
1182
1245
  if (options.detach && options.output) {
1183
1246
  throw new CliError(
@@ -1271,6 +1334,20 @@ auth.command("logout").action(async (_options, command) => {
1271
1334
  await clearRefreshToken2();
1272
1335
  await emit({ authenticated: false }, globals(command).json || false);
1273
1336
  });
1337
+ var skill = program.command("skill").description("Inspect or explicitly update the installed skill");
1338
+ skill.command("status").action(async (_options, command) => {
1339
+ await emit(await installedSkillDiagnostics(), globals(command).json || false);
1340
+ });
1341
+ skill.command("update").action(async (_options, command) => {
1342
+ const result = await updateInstalledSkill();
1343
+ await emit(
1344
+ result.status === "failed" ? { status: result.status, error: result.error.message } : result,
1345
+ globals(command).json || false
1346
+ );
1347
+ if (result.status === "failed" || result.status === "unavailable") {
1348
+ process.exitCode = EXIT.api;
1349
+ }
1350
+ });
1274
1351
  program.command("doctor").description("Check the CLI, authentication, installed skill, API, and live catalog").action(async (_options, command) => {
1275
1352
  const usingApiKey = Boolean(process.env.SCRAPPYCOCO_API_KEY);
1276
1353
  const usingOAuth = !usingApiKey && Boolean(await loadRefreshToken());
@@ -1287,7 +1364,7 @@ program.command("doctor").description("Check the CLI, authentication, installed
1287
1364
  catalogError = error instanceof Error ? error.message : String(error);
1288
1365
  }
1289
1366
  }
1290
- const skill = await installedSkillDiagnostics();
1367
+ const skill2 = await installedSkillDiagnostics();
1291
1368
  const ok = authentication.configured && catalogError === null;
1292
1369
  await emit(
1293
1370
  {
@@ -1306,7 +1383,7 @@ program.command("doctor").description("Check the CLI, authentication, installed
1306
1383
  available_capabilities: catalog2.length,
1307
1384
  error: catalogError
1308
1385
  },
1309
- skill,
1386
+ skill: skill2,
1310
1387
  next_action: ok ? "Scrappycoco is ready." : authentication.configured ? "Check the API connection, then run doctor again." : "Run `scrappycoco setup`."
1311
1388
  },
1312
1389
  globals(command).json || false
@@ -1332,13 +1409,13 @@ scrapers.command("inspect <scraper-id>").action(async (scraperId, _options, comm
1332
1409
  );
1333
1410
  });
1334
1411
  var catalog = program.command("catalog").description("Inspect capabilities, provider schemas, options, and pricing");
1335
- catalog.command("list").option("--source <source>", "filter by web, x, reddit, or filings").option("--provider <provider>", "filter by provider implementation").option("--available", "only include available providers").action(async (options, command) => {
1412
+ catalog.command("list").option("--source <source>", "filter by web, x, reddit, 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) => {
1336
1413
  const query = new URLSearchParams();
1337
1414
  if (options.source) query.set("source", options.source);
1338
1415
  if (options.provider) query.set("provider", options.provider);
1339
1416
  if (options.available) query.set("available_only", "true");
1340
1417
  const items = await client(command).get(`/scrapers${query.size ? `?${query}` : ""}`);
1341
- if (globals(command).json) await emit(items, true);
1418
+ if (globals(command).json) await emit(options.full ? items : compactCatalog(items), true);
1342
1419
  else process.stdout.write(formatCatalog(items));
1343
1420
  });
1344
1421
  catalog.command("inspect <capability-id>").action(async (capabilityId, _options, command) => {
@@ -1421,6 +1498,15 @@ jobs.command("wait <job-id>").description("Wait for a queued job and return its
1421
1498
  command
1422
1499
  );
1423
1500
  });
1501
+ jobs.command("cancel <job-id>").description("Cancel a pending or running queued job").action(async (jobId, _options, command) => {
1502
+ await emit(
1503
+ await client(command).post(
1504
+ `/jobs/${encodeURIComponent(jobId)}/cancel`,
1505
+ {}
1506
+ ),
1507
+ globals(command).json || false
1508
+ );
1509
+ });
1424
1510
  var providers = program.command("providers", { hidden: true }).description("Legacy provider commands");
1425
1511
  providers.command("list").option("--available", "only include available provider-capability routes").action(async (options, command) => {
1426
1512
  await emit(
@@ -1473,7 +1559,7 @@ discoveries.command("delete <discovery-id>").requiredOption("--yes", "confirm pe
1473
1559
  await client(command).delete(`/discoveries/${encodeURIComponent(discoveryId)}`);
1474
1560
  await emit({ deleted: true, discovery_id: discoveryId }, globals(command).json || false);
1475
1561
  });
1476
- 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").action(async (options, command) => {
1562
+ 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) => {
1477
1563
  const selected = Number(Boolean(options.file)) + Number(Boolean(options.test)) + Number(Boolean(options.update)) + Number(Boolean(options.finalize));
1478
1564
  if (selected !== 1) {
1479
1565
  throw new CliError(
@@ -1481,6 +1567,9 @@ program.command("discover").description("Save, sample-test, or finalize an agent
1481
1567
  EXIT.usage
1482
1568
  );
1483
1569
  }
1570
+ if (options.output && !options.test) {
1571
+ throw new CliError("--output is supported only with --test.", EXIT.usage);
1572
+ }
1484
1573
  if (options.file) {
1485
1574
  await emit(
1486
1575
  await client(command).post("/discoveries", await readJsonFile(options.file)),
@@ -1515,6 +1604,25 @@ program.command("discover").description("Save, sample-test, or finalize an agent
1515
1604
  { input, limit: 25 },
1516
1605
  options.idempotencyKey || randomUUID4()
1517
1606
  );
1607
+ if (options.output) {
1608
+ await emit(
1609
+ response,
1610
+ true,
1611
+ options.output,
1612
+ `${JSON.stringify(response, null, 2)}
1613
+ `
1614
+ );
1615
+ const summary = executionSummary(response);
1616
+ summary.output = {
1617
+ path: options.output,
1618
+ format: "json",
1619
+ kind: "discovery_evidence"
1620
+ };
1621
+ await emit(summary, globals(command).json || false);
1622
+ process.stderr.write(`Saved complete Discovery evidence to ${options.output}
1623
+ `);
1624
+ return;
1625
+ }
1518
1626
  await emit(response, globals(command).json || false);
1519
1627
  });
1520
1628
  program.configureOutput({ writeErr: (text) => process.stderr.write(text) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrappycoco/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for Scrappycoco scraper discovery and execution",
5
5
  "type": "module",
6
6
  "bin": {