@scrappycoco/cli 0.4.1 → 0.4.2
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 +20 -10
- package/dist/index.js +193 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ judgment.
|
|
|
7
7
|
Run the published package directly with `npx`:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npx --yes @scrappycoco/cli setup
|
|
10
|
+
npx --yes @scrappycoco/cli@latest setup
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
`setup` opens OAuth in the browser. New users can create an account in that
|
|
@@ -16,12 +16,19 @@ the live catalog, and tells you when to reload or restart your agent. Browser
|
|
|
16
16
|
authorization is not complete until the terminal confirms that the credential
|
|
17
17
|
was saved and the catalog check succeeded.
|
|
18
18
|
|
|
19
|
+
Before every ordinary command, the CLI compares the installed Scrappycoco skill
|
|
20
|
+
with the digest in the hash-verified public feed. When the digest changes, it
|
|
21
|
+
automatically refreshes the skill and prints a reload/restart notice to stderr.
|
|
22
|
+
Release-check or installer failures do not block the requested CLI command.
|
|
23
|
+
CLI-only users without an installed skill are not modified. Set
|
|
24
|
+
`SCRAPPYCOCO_DISABLE_SKILL_AUTO_UPDATE=1` to opt out.
|
|
25
|
+
|
|
19
26
|
When the CLI runs on a remote or headless host whose `127.0.0.1` is not the
|
|
20
27
|
browser's localhost, keep the command running and use the manual callback
|
|
21
28
|
flow:
|
|
22
29
|
|
|
23
30
|
```sh
|
|
24
|
-
npx --yes @scrappycoco/cli setup --no-browser --manual-callback
|
|
31
|
+
npx --yes @scrappycoco/cli@latest setup --no-browser --manual-callback
|
|
25
32
|
```
|
|
26
33
|
|
|
27
34
|
Open the printed authorization URL, then paste the browser's final callback
|
|
@@ -33,14 +40,14 @@ URL.
|
|
|
33
40
|
Individual commands remain available:
|
|
34
41
|
|
|
35
42
|
```sh
|
|
36
|
-
npx --yes @scrappycoco/cli auth login
|
|
37
|
-
npx --yes @scrappycoco/cli catalog list --available --json
|
|
38
|
-
npx --yes @scrappycoco/cli catalog inspect web.extract_content --json
|
|
39
|
-
npx --yes @scrappycoco/cli run web.extract_content --file request.json --json
|
|
40
|
-
npx --yes @scrappycoco/cli discover --file discovery.json --json
|
|
41
|
-
npx --yes @scrappycoco/cli discover --id DISCOVERY_ID --test --input '{"url":"https://example.com"}' --json
|
|
42
|
-
npx --yes @scrappycoco/cli discover --id DISCOVERY_ID --finalize --json
|
|
43
|
-
npx --yes @scrappycoco/cli run --config DISCOVERY_ID --input '{}' --json
|
|
43
|
+
npx --yes @scrappycoco/cli@latest auth login
|
|
44
|
+
npx --yes @scrappycoco/cli@latest catalog list --available --json
|
|
45
|
+
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
|
|
47
|
+
npx --yes @scrappycoco/cli@latest discover --file discovery.json --json
|
|
48
|
+
npx --yes @scrappycoco/cli@latest discover --id DISCOVERY_ID --test --input '{"url":"https://example.com"}' --json
|
|
49
|
+
npx --yes @scrappycoco/cli@latest discover --id DISCOVERY_ID --finalize --json
|
|
50
|
+
npx --yes @scrappycoco/cli@latest run --config DISCOVERY_ID --input '{}' --json
|
|
44
51
|
```
|
|
45
52
|
|
|
46
53
|
From a repository checkout, use `npm ci`, `npm run build`, and
|
|
@@ -70,6 +77,9 @@ and `options`, so one discovery can test both `zyte-http` with
|
|
|
70
77
|
`{"browser_html":false}` and `zyte-browser` with
|
|
71
78
|
`{"browser_html":true}` against the same input.
|
|
72
79
|
|
|
80
|
+
The distributed skill includes a Discovery workbook with complete direct-Run,
|
|
81
|
+
comparison, update, finalization, fallback, and audit-handoff examples.
|
|
82
|
+
|
|
73
83
|
Use `scrappycoco --help` for the complete command reference. See the
|
|
74
84
|
[Scrappycoco API documentation](https://scrappycoco.ai/docs) for the public
|
|
75
85
|
contract.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { randomUUID as
|
|
4
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
5
5
|
import { readFileSync } from "fs";
|
|
6
6
|
import { Command, CommanderError, Option } from "commander";
|
|
7
7
|
|
|
@@ -671,7 +671,7 @@ import { determineAgent } from "@vercel/detect-agent";
|
|
|
671
671
|
var SKILL_SOURCE = "https://scrappycoco.ai";
|
|
672
672
|
var SKILL_INSTALL_ARGS = [
|
|
673
673
|
"--yes",
|
|
674
|
-
"skills",
|
|
674
|
+
"skills@latest",
|
|
675
675
|
"add",
|
|
676
676
|
SKILL_SOURCE,
|
|
677
677
|
"--skill",
|
|
@@ -710,7 +710,7 @@ function skillInstallInvocation(agent = "universal", npmExecPath = process.env.n
|
|
|
710
710
|
args: [
|
|
711
711
|
npmExecPath,
|
|
712
712
|
"dlx",
|
|
713
|
-
"skills",
|
|
713
|
+
"skills@latest",
|
|
714
714
|
...SKILL_INSTALL_ARGS.slice(2),
|
|
715
715
|
"--agent",
|
|
716
716
|
agent
|
|
@@ -839,6 +839,185 @@ async function performSetup(options, dependencies = defaultDependencies(options.
|
|
|
839
839
|
};
|
|
840
840
|
}
|
|
841
841
|
|
|
842
|
+
// src/skill-update.ts
|
|
843
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
844
|
+
import { access, chmod as chmod2, mkdir as mkdir2, open as open3, readFile as readFile3, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
845
|
+
import { homedir as homedir2 } from "os";
|
|
846
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
847
|
+
var SKILL_NAME = "scrappycoco";
|
|
848
|
+
var SKILL_INDEX_URL = `${SKILL_SOURCE}/.well-known/agent-skills/index.json`;
|
|
849
|
+
var SKILL_UPDATE_TIMEOUT_MS = 3e3;
|
|
850
|
+
var SKILL_UPDATE_LOCK_STALE_MS = 5 * 6e4;
|
|
851
|
+
var SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/i;
|
|
852
|
+
function skillReleaseStatePath() {
|
|
853
|
+
return join2(dirname2(fallbackCredentialPath()), "skill-release.json");
|
|
854
|
+
}
|
|
855
|
+
function skillUpdateLockPath() {
|
|
856
|
+
return `${skillReleaseStatePath()}.lock`;
|
|
857
|
+
}
|
|
858
|
+
function installedSkillCandidates() {
|
|
859
|
+
return [
|
|
860
|
+
join2(homedir2(), ".agents", "skills", SKILL_NAME, "SKILL.md"),
|
|
861
|
+
join2(homedir2(), ".codex", "skills", SKILL_NAME, "SKILL.md"),
|
|
862
|
+
join2(homedir2(), ".claude", "skills", SKILL_NAME, "SKILL.md"),
|
|
863
|
+
join2(homedir2(), ".cursor", "skills", SKILL_NAME, "SKILL.md")
|
|
864
|
+
];
|
|
865
|
+
}
|
|
866
|
+
async function isSkillInstalled() {
|
|
867
|
+
for (const path of installedSkillCandidates()) {
|
|
868
|
+
try {
|
|
869
|
+
await access(path);
|
|
870
|
+
return true;
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
async function loadInstalledDigest() {
|
|
877
|
+
try {
|
|
878
|
+
const state = JSON.parse(
|
|
879
|
+
await readFile3(skillReleaseStatePath(), "utf8")
|
|
880
|
+
);
|
|
881
|
+
return typeof state.digest === "string" && SHA256_DIGEST.test(state.digest) ? state.digest : null;
|
|
882
|
+
} catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async function writePrivateJson2(path, value) {
|
|
887
|
+
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
888
|
+
const temporary = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
889
|
+
try {
|
|
890
|
+
await writeFile3(temporary, JSON.stringify(value), { mode: 384 });
|
|
891
|
+
if (process.platform !== "win32") await chmod2(temporary, 384);
|
|
892
|
+
await rename2(temporary, path);
|
|
893
|
+
if (process.platform !== "win32") await chmod2(path, 384);
|
|
894
|
+
} finally {
|
|
895
|
+
await rm2(temporary, { force: true });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function saveInstalledDigest(digest) {
|
|
899
|
+
await writePrivateJson2(skillReleaseStatePath(), {
|
|
900
|
+
digest,
|
|
901
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
async function fetchPublishedSkillDigest(fetcher = fetch) {
|
|
905
|
+
const response = await fetcher(SKILL_INDEX_URL, {
|
|
906
|
+
headers: { accept: "application/json" },
|
|
907
|
+
signal: AbortSignal.timeout(SKILL_UPDATE_TIMEOUT_MS)
|
|
908
|
+
});
|
|
909
|
+
if (!response.ok) {
|
|
910
|
+
throw new Error(`Skill release check returned HTTP ${response.status}.`);
|
|
911
|
+
}
|
|
912
|
+
const index = await response.json();
|
|
913
|
+
const entry = index.skills?.find((skill) => skill.name === SKILL_NAME);
|
|
914
|
+
if (!entry || typeof entry.digest !== "string" || !SHA256_DIGEST.test(entry.digest)) {
|
|
915
|
+
throw new Error("Skill release index did not contain a valid Scrappycoco digest.");
|
|
916
|
+
}
|
|
917
|
+
return entry.digest.toLowerCase();
|
|
918
|
+
}
|
|
919
|
+
function defaultDependencies2() {
|
|
920
|
+
return {
|
|
921
|
+
isInstalled: isSkillInstalled,
|
|
922
|
+
loadInstalledDigest,
|
|
923
|
+
fetchPublishedDigest: fetchPublishedSkillDigest,
|
|
924
|
+
install: installSkill,
|
|
925
|
+
saveInstalledDigest
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
async function checkAndInstallSkillUpdate(dependencies = defaultDependencies2()) {
|
|
929
|
+
if (!await dependencies.isInstalled()) return { status: "not_installed" };
|
|
930
|
+
let publishedDigest;
|
|
931
|
+
try {
|
|
932
|
+
publishedDigest = await dependencies.fetchPublishedDigest();
|
|
933
|
+
} catch {
|
|
934
|
+
return { status: "unavailable" };
|
|
935
|
+
}
|
|
936
|
+
if (await dependencies.loadInstalledDigest() === publishedDigest) {
|
|
937
|
+
return { status: "current" };
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
await dependencies.install();
|
|
941
|
+
} catch (error) {
|
|
942
|
+
return {
|
|
943
|
+
status: "failed",
|
|
944
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
let stateSaved = true;
|
|
948
|
+
try {
|
|
949
|
+
await dependencies.saveInstalledDigest(publishedDigest);
|
|
950
|
+
} catch {
|
|
951
|
+
stateSaved = false;
|
|
952
|
+
}
|
|
953
|
+
return { status: "updated", digest: publishedDigest, state_saved: stateSaved };
|
|
954
|
+
}
|
|
955
|
+
async function withSkillUpdateLock(operation) {
|
|
956
|
+
const path = skillUpdateLockPath();
|
|
957
|
+
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
958
|
+
let handle;
|
|
959
|
+
try {
|
|
960
|
+
handle = await open3(path, "wx", 384);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
if (error.code !== "EEXIST") {
|
|
963
|
+
return { status: "unavailable" };
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
const lock = await stat2(path);
|
|
967
|
+
if (Date.now() - lock.mtimeMs <= SKILL_UPDATE_LOCK_STALE_MS) {
|
|
968
|
+
return { status: "busy" };
|
|
969
|
+
}
|
|
970
|
+
await rm2(path, { force: true });
|
|
971
|
+
handle = await open3(path, "wx", 384);
|
|
972
|
+
} catch {
|
|
973
|
+
return { status: "busy" };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, created_at: Date.now() }));
|
|
978
|
+
return await operation();
|
|
979
|
+
} finally {
|
|
980
|
+
await handle.close();
|
|
981
|
+
await rm2(path, { force: true });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function autoUpdateDisabled() {
|
|
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" };
|
|
991
|
+
let result;
|
|
992
|
+
try {
|
|
993
|
+
result = await withSkillUpdateLock(() => checkAndInstallSkillUpdate());
|
|
994
|
+
} catch {
|
|
995
|
+
return { status: "unavailable" };
|
|
996
|
+
}
|
|
997
|
+
if (result.status === "updated") {
|
|
998
|
+
process.stderr.write(
|
|
999
|
+
"Updated the Scrappycoco skill. Reload or restart the agent to use the new instructions.\n"
|
|
1000
|
+
);
|
|
1001
|
+
if (!result.state_saved) {
|
|
1002
|
+
process.stderr.write(
|
|
1003
|
+
"Warning: the skill update succeeded, but its release state could not be saved.\n"
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
} else if (result.status === "failed") {
|
|
1007
|
+
process.stderr.write(
|
|
1008
|
+
`Warning: could not update the Scrappycoco skill; continuing with the installed copy: ${result.error.message}
|
|
1009
|
+
`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
async function rememberInstalledSkillRelease() {
|
|
1015
|
+
try {
|
|
1016
|
+
await saveInstalledDigest(await fetchPublishedSkillDigest());
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
842
1021
|
// src/index.ts
|
|
843
1022
|
var packageMetadata = JSON.parse(
|
|
844
1023
|
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
@@ -860,8 +1039,13 @@ program.command("setup").description("Authenticate, install the Scrappycoco skil
|
|
|
860
1039
|
noBrowser: options.browser === false,
|
|
861
1040
|
manualCallback: options.manualCallback
|
|
862
1041
|
});
|
|
1042
|
+
await rememberInstalledSkillRelease();
|
|
863
1043
|
await emit(result, globals(command).json || false);
|
|
864
1044
|
});
|
|
1045
|
+
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
1046
|
+
if (actionCommand.name() === "setup") return;
|
|
1047
|
+
await autoUpdateInstalledSkill();
|
|
1048
|
+
});
|
|
865
1049
|
function splitScraperId(value) {
|
|
866
1050
|
const separator = value.indexOf(".");
|
|
867
1051
|
if (separator <= 0 || separator === value.length - 1) {
|
|
@@ -985,7 +1169,7 @@ function executionCommand(name) {
|
|
|
985
1169
|
const response = await client(command).postJob(
|
|
986
1170
|
name === "run" ? "/scrapers/jobs" : "/scrapers/compare/jobs",
|
|
987
1171
|
payload,
|
|
988
|
-
options.idempotencyKey ||
|
|
1172
|
+
options.idempotencyKey || randomUUID4()
|
|
989
1173
|
);
|
|
990
1174
|
await emitExecution(response, options, command);
|
|
991
1175
|
});
|
|
@@ -1000,7 +1184,7 @@ program.command("run [capability-id]").description("Run a capability directly; d
|
|
|
1000
1184
|
const response2 = await client(command).postJob(
|
|
1001
1185
|
`/runs/${encodeURIComponent(options.retryFailed)}/retry-failed`,
|
|
1002
1186
|
{},
|
|
1003
|
-
options.idempotencyKey ||
|
|
1187
|
+
options.idempotencyKey || randomUUID4()
|
|
1004
1188
|
);
|
|
1005
1189
|
await emitExecution(response2, options, command);
|
|
1006
1190
|
return;
|
|
@@ -1018,7 +1202,7 @@ program.command("run [capability-id]").description("Run a capability directly; d
|
|
|
1018
1202
|
input,
|
|
1019
1203
|
limit: Number(options.limit ?? fromFile.limit ?? 25)
|
|
1020
1204
|
},
|
|
1021
|
-
options.idempotencyKey ||
|
|
1205
|
+
options.idempotencyKey || randomUUID4()
|
|
1022
1206
|
);
|
|
1023
1207
|
await emitExecution(response2, options, command);
|
|
1024
1208
|
return;
|
|
@@ -1031,7 +1215,7 @@ program.command("run [capability-id]").description("Run a capability directly; d
|
|
|
1031
1215
|
const response = await client(command).postJob(
|
|
1032
1216
|
"/scrapers/jobs",
|
|
1033
1217
|
payload,
|
|
1034
|
-
options.idempotencyKey ||
|
|
1218
|
+
options.idempotencyKey || randomUUID4()
|
|
1035
1219
|
);
|
|
1036
1220
|
await emitExecution(response, options, command);
|
|
1037
1221
|
});
|
|
@@ -1086,7 +1270,7 @@ discoveries.command("run <discovery-id>").option("-f, --file <path>", "request J
|
|
|
1086
1270
|
const response = await client(command).postJob(
|
|
1087
1271
|
`/discoveries/${encodeURIComponent(discoveryId)}/jobs`,
|
|
1088
1272
|
payload,
|
|
1089
|
-
options.idempotencyKey ||
|
|
1273
|
+
options.idempotencyKey || randomUUID4()
|
|
1090
1274
|
);
|
|
1091
1275
|
await emitExecution(response, options, command);
|
|
1092
1276
|
});
|
|
@@ -1134,7 +1318,7 @@ program.command("discover").description("Save, sample-test, or finalize an agent
|
|
|
1134
1318
|
const response = await client(command).postJob(
|
|
1135
1319
|
`/discoveries/${encodeURIComponent(options.id)}/jobs`,
|
|
1136
1320
|
{ input, limit: 25 },
|
|
1137
|
-
options.idempotencyKey ||
|
|
1321
|
+
options.idempotencyKey || randomUUID4()
|
|
1138
1322
|
);
|
|
1139
1323
|
await emit(response, globals(command).json || false);
|
|
1140
1324
|
});
|