@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.
- package/README.md +9 -4
- package/dist/index.js +96 -91
- 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
|
-
|
|
21
|
-
the
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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;
|