@scrappycoco/cli 0.4.2 → 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.
- package/README.md +29 -17
- package/dist/index.js +306 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Scrappycoco CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
judgment
|
|
3
|
+
Run external-data capabilities from a terminal or automation environment.
|
|
4
|
+
Scrappycoco is deterministic infrastructure, not an AI agent: all LLM
|
|
5
|
+
reasoning, provider decisions, and result judgment stay in the user's agentic
|
|
6
|
+
client.
|
|
6
7
|
|
|
7
8
|
Run the published package directly with `npx`:
|
|
8
9
|
|
|
@@ -16,12 +17,10 @@ the live catalog, and tells you when to reload or restart your agent. Browser
|
|
|
16
17
|
authorization is not complete until the terminal confirms that the credential
|
|
17
18
|
was saved and the catalog check succeeded.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
CLI-only users without an installed skill are not modified. Set
|
|
24
|
-
`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.
|
|
25
24
|
|
|
26
25
|
When the CLI runs on a remote or headless host whose `127.0.0.1` is not the
|
|
27
26
|
browser's localhost, keep the command running and use the manual callback
|
|
@@ -41,9 +40,12 @@ Individual commands remain available:
|
|
|
41
40
|
|
|
42
41
|
```sh
|
|
43
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
|
+
npx --yes @scrappycoco/cli@latest doctor --json
|
|
44
46
|
npx --yes @scrappycoco/cli@latest catalog list --available --json
|
|
45
47
|
npx --yes @scrappycoco/cli@latest catalog inspect web.extract_content --json
|
|
46
|
-
npx --yes @scrappycoco/cli@latest run web.extract_content --file request.json --json
|
|
48
|
+
npx --yes @scrappycoco/cli@latest run web.extract_content --file request.json --output results.json --json
|
|
47
49
|
npx --yes @scrappycoco/cli@latest discover --file discovery.json --json
|
|
48
50
|
npx --yes @scrappycoco/cli@latest discover --id DISCOVERY_ID --test --input '{"url":"https://example.com"}' --json
|
|
49
51
|
npx --yes @scrappycoco/cli@latest discover --id DISCOVERY_ID --finalize --json
|
|
@@ -67,13 +69,23 @@ Use `--json` for machine-readable responses. Execution commands support
|
|
|
67
69
|
`--format json|jsonl|csv` with `--output`, provider-native
|
|
68
70
|
`--provider-options`, batch `--concurrency`, and an explicit
|
|
69
71
|
`--idempotency-key` for safe identical retries. They submit durable jobs and
|
|
70
|
-
poll for completion
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`
|
|
76
|
-
|
|
72
|
+
poll for completion. When `--output` is used, records go to the file and a
|
|
73
|
+
compact execution summary remains on stdout.
|
|
74
|
+
|
|
75
|
+
For a long run, add `--detach` to return a job ID immediately, then finish with
|
|
76
|
+
`scrappycoco jobs wait <job-id> --output results.json`. Use
|
|
77
|
+
`scrappycoco jobs get <job-id>` for a single status check or
|
|
78
|
+
`scrappycoco jobs cancel <job-id>` to stop it. Set
|
|
79
|
+
`SCRAPPYCOCO_JOB_TIMEOUT_MS` to change the default 20-minute foreground wait.
|
|
80
|
+
Run `scrappycoco doctor --json` only when authentication, connectivity, or
|
|
81
|
+
installation is unclear.
|
|
82
|
+
|
|
83
|
+
When provider or configuration choice is uncertain, define at least one named
|
|
84
|
+
candidate for each special provider configuration under `candidates` in a
|
|
85
|
+
Discover route. Scrappycoco automatically adds one default candidate for every
|
|
86
|
+
available provider omitted from the draft, so the comparison remains
|
|
87
|
+
exhaustive. Each candidate has its own `id`, `provider`, and `options`, so one
|
|
88
|
+
discovery can test both `zyte-http` with
|
|
77
89
|
`{"browser_html":false}` and `zyte-browser` with
|
|
78
90
|
`{"browser_html":true}` against the same input.
|
|
79
91
|
|
package/dist/index.js
CHANGED
|
@@ -554,8 +554,11 @@ var ApiClient = class {
|
|
|
554
554
|
post(path, body, key = randomUUID2()) {
|
|
555
555
|
return this.request("POST", path, body, { "Idempotency-Key": key });
|
|
556
556
|
}
|
|
557
|
-
|
|
558
|
-
|
|
557
|
+
submitJob(path, body, key = randomUUID2()) {
|
|
558
|
+
return this.post(path, body, key);
|
|
559
|
+
}
|
|
560
|
+
async waitForJob(submittedOrJobId) {
|
|
561
|
+
const submitted = typeof submittedOrJobId === "string" ? await this.get(`/jobs/${encodeURIComponent(submittedOrJobId)}`) : submittedOrJobId;
|
|
559
562
|
const timeoutMs = positiveInteger2(process.env.SCRAPPYCOCO_JOB_TIMEOUT_MS, DEFAULT_JOB_TIMEOUT_MS);
|
|
560
563
|
const initialDelayMs = positiveInteger2(
|
|
561
564
|
process.env.SCRAPPYCOCO_JOB_POLL_INITIAL_MS,
|
|
@@ -573,6 +576,13 @@ var ApiClient = class {
|
|
|
573
576
|
job
|
|
574
577
|
);
|
|
575
578
|
}
|
|
579
|
+
if (job.status === "cancelled") {
|
|
580
|
+
throw new CliError(
|
|
581
|
+
`Job ${job.job_id} was cancelled.`,
|
|
582
|
+
EXIT.cancelled,
|
|
583
|
+
job
|
|
584
|
+
);
|
|
585
|
+
}
|
|
576
586
|
const remainingBeforeDelay = deadline - Date.now();
|
|
577
587
|
if (remainingBeforeDelay <= 0) throw jobTimeoutError(job, timeoutMs);
|
|
578
588
|
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingBeforeDelay)));
|
|
@@ -594,7 +604,10 @@ var ApiClient = class {
|
|
|
594
604
|
if (!job.result || typeof job.result !== "object" || Array.isArray(job.result)) {
|
|
595
605
|
throw new CliError(`Job ${job.job_id} completed without a result.`, EXIT.api, job);
|
|
596
606
|
}
|
|
597
|
-
return job.result;
|
|
607
|
+
return { ...job.result, job_id: job.job_id };
|
|
608
|
+
}
|
|
609
|
+
async postJob(path, body, key = randomUUID2()) {
|
|
610
|
+
return this.waitForJob(await this.submitJob(path, body, key));
|
|
598
611
|
}
|
|
599
612
|
patch(path, body) {
|
|
600
613
|
return this.request("PATCH", path, body);
|
|
@@ -863,26 +876,68 @@ function installedSkillCandidates() {
|
|
|
863
876
|
join2(homedir2(), ".cursor", "skills", SKILL_NAME, "SKILL.md")
|
|
864
877
|
];
|
|
865
878
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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;
|
|
873
889
|
}
|
|
874
|
-
return false;
|
|
875
890
|
}
|
|
876
|
-
async function
|
|
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() {
|
|
877
902
|
try {
|
|
878
903
|
const state = JSON.parse(
|
|
879
904
|
await readFile3(skillReleaseStatePath(), "utf8")
|
|
880
905
|
);
|
|
881
|
-
return
|
|
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
|
+
};
|
|
882
913
|
} catch {
|
|
883
|
-
return
|
|
914
|
+
return { targets: {} };
|
|
884
915
|
}
|
|
885
916
|
}
|
|
917
|
+
async function loadInstalledDigest() {
|
|
918
|
+
const target = await activeSkillTarget();
|
|
919
|
+
const entry = (await loadSkillReleaseState()).targets[target];
|
|
920
|
+
return entry?.digest || null;
|
|
921
|
+
}
|
|
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);
|
|
931
|
+
return {
|
|
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
|
+
}))
|
|
939
|
+
};
|
|
940
|
+
}
|
|
886
941
|
async function writePrivateJson2(path, value) {
|
|
887
942
|
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
888
943
|
const temporary = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
@@ -896,10 +951,23 @@ async function writePrivateJson2(path, value) {
|
|
|
896
951
|
}
|
|
897
952
|
}
|
|
898
953
|
async function saveInstalledDigest(digest) {
|
|
899
|
-
await
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
+
};
|
|
903
971
|
}
|
|
904
972
|
async function fetchPublishedSkillDigest(fetcher = fetch) {
|
|
905
973
|
const response = await fetcher(SKILL_INDEX_URL, {
|
|
@@ -910,7 +978,7 @@ async function fetchPublishedSkillDigest(fetcher = fetch) {
|
|
|
910
978
|
throw new Error(`Skill release check returned HTTP ${response.status}.`);
|
|
911
979
|
}
|
|
912
980
|
const index = await response.json();
|
|
913
|
-
const entry = index.skills?.find((
|
|
981
|
+
const entry = index.skills?.find((skill2) => skill2.name === SKILL_NAME);
|
|
914
982
|
if (!entry || typeof entry.digest !== "string" || !SHA256_DIGEST.test(entry.digest)) {
|
|
915
983
|
throw new Error("Skill release index did not contain a valid Scrappycoco digest.");
|
|
916
984
|
}
|
|
@@ -981,13 +1049,7 @@ async function withSkillUpdateLock(operation) {
|
|
|
981
1049
|
await rm2(path, { force: true });
|
|
982
1050
|
}
|
|
983
1051
|
}
|
|
984
|
-
function
|
|
985
|
-
return ["1", "true", "yes"].includes(
|
|
986
|
-
(process.env.SCRAPPYCOCO_DISABLE_SKILL_AUTO_UPDATE || "").toLowerCase()
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
async function autoUpdateInstalledSkill() {
|
|
990
|
-
if (autoUpdateDisabled()) return { status: "disabled" };
|
|
1052
|
+
async function updateInstalledSkill() {
|
|
991
1053
|
let result;
|
|
992
1054
|
try {
|
|
993
1055
|
result = await withSkillUpdateLock(() => checkAndInstallSkillUpdate());
|
|
@@ -1033,6 +1095,12 @@ function client(command) {
|
|
|
1033
1095
|
function collect(value, previous) {
|
|
1034
1096
|
return [...previous, value];
|
|
1035
1097
|
}
|
|
1098
|
+
function selectFields(value, fields) {
|
|
1099
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1100
|
+
return Object.fromEntries(
|
|
1101
|
+
fields.filter((field) => field in value).map((field) => [field, value[field]])
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1036
1104
|
program.command("setup").description("Authenticate, install the Scrappycoco skill, and verify the connection").option("--no-browser", "print the authorization URL without opening it").option("--manual-callback", "paste the final callback URL in this terminal for remote/headless login").action(async (options, command) => {
|
|
1037
1105
|
const result = await performSetup({
|
|
1038
1106
|
apiUrl: globals(command).apiUrl,
|
|
@@ -1042,10 +1110,6 @@ program.command("setup").description("Authenticate, install the Scrappycoco skil
|
|
|
1042
1110
|
await rememberInstalledSkillRelease();
|
|
1043
1111
|
await emit(result, globals(command).json || false);
|
|
1044
1112
|
});
|
|
1045
|
-
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
1046
|
-
if (actionCommand.name() === "setup") return;
|
|
1047
|
-
await autoUpdateInstalledSkill();
|
|
1048
|
-
});
|
|
1049
1113
|
function splitScraperId(value) {
|
|
1050
1114
|
const separator = value.indexOf(".");
|
|
1051
1115
|
if (separator <= 0 || separator === value.length - 1) {
|
|
@@ -1074,12 +1138,138 @@ async function emitExecution(response, options, command) {
|
|
|
1074
1138
|
const jsonMode = globals(command).json || false;
|
|
1075
1139
|
if (options.output) {
|
|
1076
1140
|
await emit(response, jsonMode, options.output, formatRecords(records, options.format));
|
|
1141
|
+
const summary = { ...response };
|
|
1142
|
+
delete summary.records;
|
|
1143
|
+
delete summary.items;
|
|
1144
|
+
delete summary.normalized_schema;
|
|
1145
|
+
summary.truncated_count = records.filter((record) => {
|
|
1146
|
+
const metadata = record.metadata;
|
|
1147
|
+
return metadata && typeof metadata === "object" && !Array.isArray(metadata) && metadata.truncated === true;
|
|
1148
|
+
}).length;
|
|
1149
|
+
if (summary.usage) {
|
|
1150
|
+
summary.usage = selectFields(summary.usage, [
|
|
1151
|
+
"billing_status",
|
|
1152
|
+
"payg_charge_usd_exact",
|
|
1153
|
+
"provider_cost_usd_exact",
|
|
1154
|
+
"unresolved_cost_count"
|
|
1155
|
+
]);
|
|
1156
|
+
}
|
|
1157
|
+
if (Array.isArray(summary.attempts)) {
|
|
1158
|
+
summary.attempts = summary.attempts.map((attempt) => selectFields(attempt, [
|
|
1159
|
+
"provider",
|
|
1160
|
+
"status",
|
|
1161
|
+
"result_count",
|
|
1162
|
+
"latency_ms",
|
|
1163
|
+
"estimated_cost_usd",
|
|
1164
|
+
"error"
|
|
1165
|
+
]));
|
|
1166
|
+
}
|
|
1167
|
+
const providerResults = summary.provider_results;
|
|
1168
|
+
if (providerResults && typeof providerResults === "object" && !Array.isArray(providerResults)) {
|
|
1169
|
+
const counts = Object.fromEntries(
|
|
1170
|
+
Object.entries(providerResults).map(([provider, values]) => [
|
|
1171
|
+
provider,
|
|
1172
|
+
Array.isArray(values) ? values.length : 0
|
|
1173
|
+
])
|
|
1174
|
+
);
|
|
1175
|
+
if (Object.keys(counts).length) summary.provider_result_counts = counts;
|
|
1176
|
+
delete summary.provider_results;
|
|
1177
|
+
}
|
|
1178
|
+
if (Array.isArray(response.items)) {
|
|
1179
|
+
summary.item_count = response.items.length;
|
|
1180
|
+
}
|
|
1181
|
+
if (Array.isArray(summary.routes)) {
|
|
1182
|
+
summary.routes = summary.routes.map((value) => {
|
|
1183
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1184
|
+
const route = { ...value };
|
|
1185
|
+
const routeResults = route.provider_results;
|
|
1186
|
+
if (routeResults && typeof routeResults === "object" && !Array.isArray(routeResults)) {
|
|
1187
|
+
const counts = Object.fromEntries(
|
|
1188
|
+
Object.entries(routeResults).map(([provider, values]) => [
|
|
1189
|
+
provider,
|
|
1190
|
+
Array.isArray(values) ? values.length : 0
|
|
1191
|
+
])
|
|
1192
|
+
);
|
|
1193
|
+
if (Object.keys(counts).length) route.provider_result_counts = counts;
|
|
1194
|
+
delete route.provider_results;
|
|
1195
|
+
}
|
|
1196
|
+
if (Array.isArray(route.attempts)) {
|
|
1197
|
+
route.attempts = route.attempts.map((attempt) => selectFields(attempt, [
|
|
1198
|
+
"provider",
|
|
1199
|
+
"status",
|
|
1200
|
+
"result_count",
|
|
1201
|
+
"latency_ms",
|
|
1202
|
+
"estimated_cost_usd",
|
|
1203
|
+
"error"
|
|
1204
|
+
]));
|
|
1205
|
+
}
|
|
1206
|
+
return route;
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
for (const field of ["cursor", "monitor"]) {
|
|
1210
|
+
if (summary[field] === null) delete summary[field];
|
|
1211
|
+
}
|
|
1212
|
+
summary.output = {
|
|
1213
|
+
path: options.output,
|
|
1214
|
+
format: options.format,
|
|
1215
|
+
record_count: records.length
|
|
1216
|
+
};
|
|
1217
|
+
await emit(summary, jsonMode);
|
|
1077
1218
|
process.stderr.write(`Saved ${records.length} records to ${options.output}
|
|
1078
1219
|
`);
|
|
1079
1220
|
return;
|
|
1080
1221
|
}
|
|
1081
1222
|
await emit(response, jsonMode);
|
|
1082
1223
|
}
|
|
1224
|
+
async function executeQueuedJob(path, payload, options, command) {
|
|
1225
|
+
if (options.detach && options.output) {
|
|
1226
|
+
throw new CliError(
|
|
1227
|
+
"Do not combine --detach with --output. Use `scrappycoco jobs wait JOB_ID --output PATH`.",
|
|
1228
|
+
EXIT.usage
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
const apiClient = client(command);
|
|
1232
|
+
const idempotencyKey = options.idempotencyKey || randomUUID4();
|
|
1233
|
+
if (options.detach) {
|
|
1234
|
+
const job = await apiClient.submitJob(path, payload, idempotencyKey);
|
|
1235
|
+
await emit(
|
|
1236
|
+
{
|
|
1237
|
+
...job,
|
|
1238
|
+
next_command: `scrappycoco jobs wait ${job.job_id}`
|
|
1239
|
+
},
|
|
1240
|
+
globals(command).json || false
|
|
1241
|
+
);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
await emitExecution(
|
|
1245
|
+
await apiClient.postJob(
|
|
1246
|
+
path,
|
|
1247
|
+
payload,
|
|
1248
|
+
idempotencyKey
|
|
1249
|
+
),
|
|
1250
|
+
options,
|
|
1251
|
+
command
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
function formatCatalog(items) {
|
|
1255
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1256
|
+
for (const item of items) {
|
|
1257
|
+
const source = String(item.source || item.id.split(".", 1)[0] || "other");
|
|
1258
|
+
grouped.set(source, [...grouped.get(source) || [], item]);
|
|
1259
|
+
}
|
|
1260
|
+
const lines = [];
|
|
1261
|
+
for (const [source, capabilities] of grouped) {
|
|
1262
|
+
lines.push(`${source.toUpperCase()} (${capabilities.length})`);
|
|
1263
|
+
for (const capability of capabilities) {
|
|
1264
|
+
const label = typeof capability.label === "string" ? capability.label : capability.id;
|
|
1265
|
+
const description = typeof capability.description === "string" ? ` \u2014 ${capability.description}` : "";
|
|
1266
|
+
lines.push(` ${capability.id} ${label}${description}`);
|
|
1267
|
+
}
|
|
1268
|
+
lines.push("");
|
|
1269
|
+
}
|
|
1270
|
+
return `${lines.join("\n").trimEnd()}
|
|
1271
|
+
`;
|
|
1272
|
+
}
|
|
1083
1273
|
var auth = program.command("auth").description("Manage Clerk OAuth credentials");
|
|
1084
1274
|
auth.command("login").option("--no-browser", "print the authorization URL without opening it").option("--manual-callback", "paste the final callback URL in this terminal for remote/headless login").action(async (options, command) => {
|
|
1085
1275
|
const result = await login({
|
|
@@ -1124,6 +1314,62 @@ auth.command("logout").action(async (_options, command) => {
|
|
|
1124
1314
|
await clearRefreshToken2();
|
|
1125
1315
|
await emit({ authenticated: false }, globals(command).json || false);
|
|
1126
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
|
+
});
|
|
1331
|
+
program.command("doctor").description("Check the CLI, authentication, installed skill, API, and live catalog").action(async (_options, command) => {
|
|
1332
|
+
const usingApiKey = Boolean(process.env.SCRAPPYCOCO_API_KEY);
|
|
1333
|
+
const usingOAuth = !usingApiKey && Boolean(await loadRefreshToken());
|
|
1334
|
+
const authentication = {
|
|
1335
|
+
configured: usingApiKey || usingOAuth,
|
|
1336
|
+
method: usingApiKey ? "api_key" : usingOAuth ? "oauth" : null
|
|
1337
|
+
};
|
|
1338
|
+
let catalog2 = [];
|
|
1339
|
+
let catalogError = null;
|
|
1340
|
+
if (authentication.configured) {
|
|
1341
|
+
try {
|
|
1342
|
+
catalog2 = await client(command).get("/scrapers?available_only=true");
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
catalogError = error instanceof Error ? error.message : String(error);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const skill2 = await installedSkillDiagnostics();
|
|
1348
|
+
const ok = authentication.configured && catalogError === null;
|
|
1349
|
+
await emit(
|
|
1350
|
+
{
|
|
1351
|
+
ok,
|
|
1352
|
+
cli: {
|
|
1353
|
+
version: packageMetadata.version,
|
|
1354
|
+
node: process.version
|
|
1355
|
+
},
|
|
1356
|
+
authentication,
|
|
1357
|
+
api: {
|
|
1358
|
+
url: globals(command).apiUrl,
|
|
1359
|
+
reachable: authentication.configured && catalogError === null
|
|
1360
|
+
},
|
|
1361
|
+
catalog: {
|
|
1362
|
+
reachable: authentication.configured && catalogError === null,
|
|
1363
|
+
available_capabilities: catalog2.length,
|
|
1364
|
+
error: catalogError
|
|
1365
|
+
},
|
|
1366
|
+
skill: skill2,
|
|
1367
|
+
next_action: ok ? "Scrappycoco is ready." : authentication.configured ? "Check the API connection, then run doctor again." : "Run `scrappycoco setup`."
|
|
1368
|
+
},
|
|
1369
|
+
globals(command).json || false
|
|
1370
|
+
);
|
|
1371
|
+
if (!ok) process.exitCode = authentication.configured ? EXIT.api : EXIT.auth;
|
|
1372
|
+
});
|
|
1127
1373
|
var scrapers = program.command("scrapers", { hidden: true }).description("Legacy scraper commands");
|
|
1128
1374
|
scrapers.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) => {
|
|
1129
1375
|
const query = new URLSearchParams();
|
|
@@ -1148,10 +1394,9 @@ catalog.command("list").option("--source <source>", "filter by web, x, reddit, o
|
|
|
1148
1394
|
if (options.source) query.set("source", options.source);
|
|
1149
1395
|
if (options.provider) query.set("provider", options.provider);
|
|
1150
1396
|
if (options.available) query.set("available_only", "true");
|
|
1151
|
-
await
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
);
|
|
1397
|
+
const items = await client(command).get(`/scrapers${query.size ? `?${query}` : ""}`);
|
|
1398
|
+
if (globals(command).json) await emit(items, true);
|
|
1399
|
+
else process.stdout.write(formatCatalog(items));
|
|
1155
1400
|
});
|
|
1156
1401
|
catalog.command("inspect <capability-id>").action(async (capabilityId, _options, command) => {
|
|
1157
1402
|
const { source, capability } = splitScraperId(capabilityId);
|
|
@@ -1176,17 +1421,17 @@ function executionCommand(name) {
|
|
|
1176
1421
|
}
|
|
1177
1422
|
executionCommand("run");
|
|
1178
1423
|
executionCommand("compare");
|
|
1179
|
-
program.command("run [capability-id]").description("Run a capability directly;
|
|
1424
|
+
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) => {
|
|
1180
1425
|
if (options.retryFailed) {
|
|
1181
1426
|
if (capabilityId || options.config) {
|
|
1182
1427
|
throw new CliError("Do not combine --retry-failed with a capability ID or --config.", EXIT.usage);
|
|
1183
1428
|
}
|
|
1184
|
-
|
|
1429
|
+
await executeQueuedJob(
|
|
1185
1430
|
`/runs/${encodeURIComponent(options.retryFailed)}/retry-failed`,
|
|
1186
1431
|
{},
|
|
1187
|
-
options
|
|
1432
|
+
options,
|
|
1433
|
+
command
|
|
1188
1434
|
);
|
|
1189
|
-
await emitExecution(response2, options, command);
|
|
1190
1435
|
return;
|
|
1191
1436
|
}
|
|
1192
1437
|
if (options.config) {
|
|
@@ -1195,16 +1440,16 @@ program.command("run [capability-id]").description("Run a capability directly; d
|
|
|
1195
1440
|
}
|
|
1196
1441
|
const fromFile = options.file ? await readJsonFile(options.file) : {};
|
|
1197
1442
|
const input = options.input ? parseJsonObject(options.input, "runtime input JSON") : fromFile.input || {};
|
|
1198
|
-
|
|
1443
|
+
await executeQueuedJob(
|
|
1199
1444
|
`/discoveries/${encodeURIComponent(options.config)}/jobs`,
|
|
1200
1445
|
{
|
|
1201
1446
|
...fromFile,
|
|
1202
1447
|
input,
|
|
1203
1448
|
limit: Number(options.limit ?? fromFile.limit ?? 25)
|
|
1204
1449
|
},
|
|
1205
|
-
options
|
|
1450
|
+
options,
|
|
1451
|
+
command
|
|
1206
1452
|
);
|
|
1207
|
-
await emitExecution(response2, options, command);
|
|
1208
1453
|
return;
|
|
1209
1454
|
}
|
|
1210
1455
|
if (!capabilityId) {
|
|
@@ -1212,12 +1457,12 @@ program.command("run [capability-id]").description("Run a capability directly; d
|
|
|
1212
1457
|
}
|
|
1213
1458
|
const payload = await requestPayload(options, capabilityId);
|
|
1214
1459
|
if (payload.limit === void 0) payload.limit = 10;
|
|
1215
|
-
|
|
1460
|
+
await executeQueuedJob(
|
|
1216
1461
|
"/scrapers/jobs",
|
|
1217
1462
|
payload,
|
|
1218
|
-
options
|
|
1463
|
+
options,
|
|
1464
|
+
command
|
|
1219
1465
|
);
|
|
1220
|
-
await emitExecution(response, options, command);
|
|
1221
1466
|
});
|
|
1222
1467
|
var jobs = program.command("jobs").description("Inspect durable queued jobs");
|
|
1223
1468
|
jobs.command("get <job-id>").action(async (jobId, _options, command) => {
|
|
@@ -1226,6 +1471,22 @@ jobs.command("get <job-id>").action(async (jobId, _options, command) => {
|
|
|
1226
1471
|
globals(command).json || false
|
|
1227
1472
|
);
|
|
1228
1473
|
});
|
|
1474
|
+
jobs.command("wait <job-id>").description("Wait for a queued job and return its result").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 (jobId, options, command) => {
|
|
1475
|
+
await emitExecution(
|
|
1476
|
+
await client(command).waitForJob(jobId),
|
|
1477
|
+
options,
|
|
1478
|
+
command
|
|
1479
|
+
);
|
|
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
|
+
});
|
|
1229
1490
|
var providers = program.command("providers", { hidden: true }).description("Legacy provider commands");
|
|
1230
1491
|
providers.command("list").option("--available", "only include available provider-capability routes").action(async (options, command) => {
|
|
1231
1492
|
await emit(
|
|
@@ -1278,7 +1539,7 @@ discoveries.command("delete <discovery-id>").requiredOption("--yes", "confirm pe
|
|
|
1278
1539
|
await client(command).delete(`/discoveries/${encodeURIComponent(discoveryId)}`);
|
|
1279
1540
|
await emit({ deleted: true, discovery_id: discoveryId }, globals(command).json || false);
|
|
1280
1541
|
});
|
|
1281
|
-
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", "
|
|
1542
|
+
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) => {
|
|
1282
1543
|
const selected = Number(Boolean(options.file)) + Number(Boolean(options.test)) + Number(Boolean(options.update)) + Number(Boolean(options.finalize));
|
|
1283
1544
|
if (selected !== 1) {
|
|
1284
1545
|
throw new CliError(
|