@scrappycoco/cli 0.8.2 → 0.8.4
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 +12 -4
- package/dist/index.js +140 -93
- 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
|
|
@@ -61,6 +66,9 @@ agent, general workflow, or scheduler commands. For repeated checks, callers
|
|
|
61
66
|
schedule ordinary Runs and pass each returned cursor into the next Run.
|
|
62
67
|
|
|
63
68
|
Node.js 20 or newer is required. Interactive use authenticates with Clerk OAuth Authorization Code + PKCE. CI can set `SCRAPPYCOCO_API_KEY`.
|
|
69
|
+
API keys have no refresh-token rotation step and are preferred for unattended
|
|
70
|
+
concurrent application workloads. OAuth refresh is serialized across CLI
|
|
71
|
+
processes and recovers if another process rotates the stored token first.
|
|
64
72
|
|
|
65
73
|
OAuth and API requests have bounded timeouts. Set
|
|
66
74
|
`SCRAPPYCOCO_AUTH_TIMEOUT_MS` or `SCRAPPYCOCO_API_TIMEOUT_MS` only when a slow
|
package/dist/index.js
CHANGED
|
@@ -60,17 +60,23 @@ function wait(milliseconds) {
|
|
|
60
60
|
}
|
|
61
61
|
async function withCredentialRefreshLock(operation) {
|
|
62
62
|
const path = credentialRefreshLockPath();
|
|
63
|
+
const owner = randomUUID();
|
|
63
64
|
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
64
65
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
65
66
|
while (true) {
|
|
66
67
|
try {
|
|
67
68
|
const handle = await open(path, "wx", 384);
|
|
68
69
|
try {
|
|
69
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, created_at: Date.now() }));
|
|
70
|
+
await handle.writeFile(JSON.stringify({ owner, pid: process.pid, created_at: Date.now() }));
|
|
70
71
|
return await operation();
|
|
71
72
|
} finally {
|
|
72
73
|
await handle.close();
|
|
73
|
-
|
|
74
|
+
try {
|
|
75
|
+
const current = JSON.parse(await readFile(path, "utf8"));
|
|
76
|
+
if (current.owner === owner) await rm(path, { force: true });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code !== "ENOENT") throw error;
|
|
79
|
+
}
|
|
74
80
|
}
|
|
75
81
|
} catch (error) {
|
|
76
82
|
const code = error.code;
|
|
@@ -199,6 +205,8 @@ async function clearRefreshToken() {
|
|
|
199
205
|
var cachedAccessToken;
|
|
200
206
|
var refreshesInFlight = /* @__PURE__ */ new Map();
|
|
201
207
|
var DEFAULT_AUTH_HTTP_TIMEOUT_MS = 15e3;
|
|
208
|
+
var ROTATED_TOKEN_RELOAD_ATTEMPTS = 10;
|
|
209
|
+
var ROTATED_TOKEN_RELOAD_DELAY_MS = 100;
|
|
202
210
|
function positiveInteger(value, fallback) {
|
|
203
211
|
if (!value) return fallback;
|
|
204
212
|
const parsed = Number(value);
|
|
@@ -213,6 +221,12 @@ function networkMessage(error) {
|
|
|
213
221
|
}
|
|
214
222
|
return "failed to reach the authentication service";
|
|
215
223
|
}
|
|
224
|
+
function isInvalidGrant(error) {
|
|
225
|
+
return error instanceof CliError && error.exitCode === EXIT.auth && typeof error.details === "object" && error.details !== null && "oauth_error" in error.details && error.details.oauth_error === "invalid_grant";
|
|
226
|
+
}
|
|
227
|
+
function wait2(milliseconds) {
|
|
228
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
229
|
+
}
|
|
216
230
|
async function oauthFetch(url, init, phase, exitCode) {
|
|
217
231
|
try {
|
|
218
232
|
return await fetch(url, {
|
|
@@ -436,7 +450,9 @@ async function accessToken(apiUrl) {
|
|
|
436
450
|
let tokens;
|
|
437
451
|
let acceptedRefreshToken;
|
|
438
452
|
let lastAuthError;
|
|
453
|
+
const attemptedRefreshTokens = /* @__PURE__ */ new Set();
|
|
439
454
|
for (const candidate of candidates) {
|
|
455
|
+
attemptedRefreshTokens.add(candidate.value);
|
|
440
456
|
try {
|
|
441
457
|
tokens = await tokenRequest(config.issuer, new URLSearchParams({
|
|
442
458
|
grant_type: "refresh_token",
|
|
@@ -452,6 +468,28 @@ async function accessToken(apiUrl) {
|
|
|
452
468
|
lastAuthError = error;
|
|
453
469
|
}
|
|
454
470
|
}
|
|
471
|
+
if (!tokens && isInvalidGrant(lastAuthError)) {
|
|
472
|
+
for (let attempt = 0; attempt < ROTATED_TOKEN_RELOAD_ATTEMPTS && !tokens; attempt += 1) {
|
|
473
|
+
await wait2(ROTATED_TOKEN_RELOAD_DELAY_MS);
|
|
474
|
+
const rotatedCandidates = await loadRefreshTokenCandidates();
|
|
475
|
+
const rotated = rotatedCandidates.find(
|
|
476
|
+
(candidate) => !attemptedRefreshTokens.has(candidate.value)
|
|
477
|
+
);
|
|
478
|
+
if (!rotated) continue;
|
|
479
|
+
attemptedRefreshTokens.add(rotated.value);
|
|
480
|
+
try {
|
|
481
|
+
tokens = await tokenRequest(config.issuer, new URLSearchParams({
|
|
482
|
+
grant_type: "refresh_token",
|
|
483
|
+
client_id: config.client_id,
|
|
484
|
+
refresh_token: rotated.value
|
|
485
|
+
}), "token refresh");
|
|
486
|
+
acceptedRefreshToken = rotated.value;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (!isInvalidGrant(error)) throw error;
|
|
489
|
+
lastAuthError = error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
455
493
|
if (!tokens || !acceptedRefreshToken) throw lastAuthError || new CliError("Stored OAuth login is no longer valid.", EXIT.auth);
|
|
456
494
|
await saveRefreshToken(tokens.refresh_token || acceptedRefreshToken);
|
|
457
495
|
cachedAccessToken = {
|
|
@@ -861,7 +899,10 @@ var SKILL_NAME = "scrappycoco";
|
|
|
861
899
|
var SKILL_INDEX_URL = `${SKILL_SOURCE}/.well-known/agent-skills/index.json`;
|
|
862
900
|
var SKILL_UPDATE_TIMEOUT_MS = 3e3;
|
|
863
901
|
var SKILL_UPDATE_LOCK_STALE_MS = 5 * 6e4;
|
|
902
|
+
var SKILL_AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 6e4;
|
|
864
903
|
var SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/i;
|
|
904
|
+
var DISABLED_AUTO_UPDATE_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
905
|
+
var ENABLED_AUTO_UPDATE_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
865
906
|
function skillReleaseStatePath() {
|
|
866
907
|
return join2(dirname2(fallbackCredentialPath()), "skill-release.json");
|
|
867
908
|
}
|
|
@@ -919,9 +960,21 @@ async function loadInstalledDigest() {
|
|
|
919
960
|
const entry = (await loadSkillReleaseState()).targets[target];
|
|
920
961
|
return entry?.digest || null;
|
|
921
962
|
}
|
|
963
|
+
async function loadLastCheckedAt() {
|
|
964
|
+
const target = await activeSkillTarget();
|
|
965
|
+
const entry = (await loadSkillReleaseState()).targets[target];
|
|
966
|
+
return entry?.checked_at || entry?.updated_at || null;
|
|
967
|
+
}
|
|
968
|
+
function skillAutoUpdateEnabled(environment = process.env) {
|
|
969
|
+
const configured = environment.SCRAPPYCOCO_SKILL_AUTO_UPDATE?.trim().toLowerCase();
|
|
970
|
+
if (configured && ENABLED_AUTO_UPDATE_VALUES.has(configured)) return true;
|
|
971
|
+
if (configured && DISABLED_AUTO_UPDATE_VALUES.has(configured)) return false;
|
|
972
|
+
return environment.NODE_ENV !== "test" && !environment.VITEST && !ENABLED_AUTO_UPDATE_VALUES.has(environment.CI?.trim().toLowerCase() || "");
|
|
973
|
+
}
|
|
922
974
|
async function installedSkillDiagnostics() {
|
|
923
975
|
const target = await activeSkillTarget();
|
|
924
976
|
const state = await loadSkillReleaseState();
|
|
977
|
+
const activeRelease = state.targets[target];
|
|
925
978
|
const installedTargets = (await Promise.all(
|
|
926
979
|
installedSkillCandidates().map(async (path) => ({
|
|
927
980
|
path,
|
|
@@ -930,8 +983,13 @@ async function installedSkillDiagnostics() {
|
|
|
930
983
|
)).filter((item) => item.installed);
|
|
931
984
|
return {
|
|
932
985
|
installed: installedTargets.some((item) => item.path === target),
|
|
933
|
-
digest:
|
|
986
|
+
digest: activeRelease?.digest || null,
|
|
934
987
|
target,
|
|
988
|
+
auto_update: {
|
|
989
|
+
enabled: skillAutoUpdateEnabled(),
|
|
990
|
+
interval_hours: SKILL_AUTO_UPDATE_INTERVAL_MS / (60 * 6e4),
|
|
991
|
+
last_checked_at: activeRelease?.checked_at || activeRelease?.updated_at || null
|
|
992
|
+
},
|
|
935
993
|
targets: installedTargets.map(({ path }) => ({
|
|
936
994
|
path,
|
|
937
995
|
digest: state.targets[path]?.digest || null
|
|
@@ -964,11 +1022,38 @@ function recordTargetDigest(state, target, digest, updatedAt = (/* @__PURE__ */
|
|
|
964
1022
|
...state.targets,
|
|
965
1023
|
[target]: {
|
|
966
1024
|
digest,
|
|
967
|
-
updated_at: updatedAt
|
|
1025
|
+
updated_at: updatedAt,
|
|
1026
|
+
checked_at: updatedAt
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function recordTargetCheck(state, target, checkedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1032
|
+
const release = state.targets[target];
|
|
1033
|
+
if (!release) return state;
|
|
1034
|
+
return {
|
|
1035
|
+
targets: {
|
|
1036
|
+
...state.targets,
|
|
1037
|
+
[target]: {
|
|
1038
|
+
...release,
|
|
1039
|
+
checked_at: checkedAt
|
|
968
1040
|
}
|
|
969
1041
|
}
|
|
970
1042
|
};
|
|
971
1043
|
}
|
|
1044
|
+
async function saveLastCheckedAt(checkedAt) {
|
|
1045
|
+
const target = await activeSkillTarget();
|
|
1046
|
+
const state = await loadSkillReleaseState();
|
|
1047
|
+
await writePrivateJson2(
|
|
1048
|
+
skillReleaseStatePath(),
|
|
1049
|
+
recordTargetCheck(state, target, checkedAt)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
function automaticSkillUpdateDue(lastCheckedAt, now = /* @__PURE__ */ new Date(), intervalMs = SKILL_AUTO_UPDATE_INTERVAL_MS) {
|
|
1053
|
+
if (!lastCheckedAt) return true;
|
|
1054
|
+
const elapsed = now.getTime() - Date.parse(lastCheckedAt);
|
|
1055
|
+
return !Number.isFinite(elapsed) || elapsed < 0 || elapsed >= intervalMs;
|
|
1056
|
+
}
|
|
972
1057
|
async function fetchPublishedSkillDigest(fetcher = fetch) {
|
|
973
1058
|
const response = await fetcher(SKILL_INDEX_URL, {
|
|
974
1059
|
headers: { accept: "application/json" },
|
|
@@ -1071,8 +1156,46 @@ async function updateInstalledSkill() {
|
|
|
1071
1156
|
`
|
|
1072
1157
|
);
|
|
1073
1158
|
}
|
|
1159
|
+
if (result.status === "current" || result.status === "updated" && result.state_saved) {
|
|
1160
|
+
try {
|
|
1161
|
+
await saveLastCheckedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1074
1165
|
return result;
|
|
1075
1166
|
}
|
|
1167
|
+
function defaultAutomaticDependencies() {
|
|
1168
|
+
return {
|
|
1169
|
+
enabled: skillAutoUpdateEnabled,
|
|
1170
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
1171
|
+
loadLastCheckedAt,
|
|
1172
|
+
update: updateInstalledSkill,
|
|
1173
|
+
saveLastCheckedAt
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
async function maybeUpdateInstalledSkill(dependencies = defaultAutomaticDependencies(), intervalMs = SKILL_AUTO_UPDATE_INTERVAL_MS) {
|
|
1177
|
+
if (!dependencies.enabled()) return { status: "disabled" };
|
|
1178
|
+
try {
|
|
1179
|
+
const now = dependencies.now();
|
|
1180
|
+
if (!automaticSkillUpdateDue(
|
|
1181
|
+
await dependencies.loadLastCheckedAt(),
|
|
1182
|
+
now,
|
|
1183
|
+
intervalMs
|
|
1184
|
+
)) {
|
|
1185
|
+
return { status: "throttled" };
|
|
1186
|
+
}
|
|
1187
|
+
const result = await dependencies.update();
|
|
1188
|
+
if (result.status === "current" || result.status === "updated") {
|
|
1189
|
+
try {
|
|
1190
|
+
await dependencies.saveLastCheckedAt(now.toISOString());
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return result;
|
|
1195
|
+
} catch {
|
|
1196
|
+
return { status: "unavailable" };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1076
1199
|
async function rememberInstalledSkillRelease() {
|
|
1077
1200
|
try {
|
|
1078
1201
|
await saveInstalledDigest(await fetchPublishedSkillDigest());
|
|
@@ -1140,7 +1263,7 @@ async function requestPayload(options, scraperId) {
|
|
|
1140
1263
|
};
|
|
1141
1264
|
}
|
|
1142
1265
|
async function emitExecution(response, options, command) {
|
|
1143
|
-
const records =
|
|
1266
|
+
const records = response.records;
|
|
1144
1267
|
const jsonMode = globals(command).json || false;
|
|
1145
1268
|
if (options.output) {
|
|
1146
1269
|
await emit(response, jsonMode, options.output, formatRecords(records, options.format));
|
|
@@ -1153,15 +1276,16 @@ async function emitExecution(response, options, command) {
|
|
|
1153
1276
|
await emit(summary, jsonMode);
|
|
1154
1277
|
process.stderr.write(`Saved ${records.length} records to ${options.output}
|
|
1155
1278
|
`);
|
|
1279
|
+
if (response.status === "failed") process.exitCode = EXIT.api;
|
|
1156
1280
|
return;
|
|
1157
1281
|
}
|
|
1158
1282
|
await emit(response, jsonMode);
|
|
1283
|
+
if (response.status === "failed") process.exitCode = EXIT.api;
|
|
1159
1284
|
}
|
|
1160
1285
|
function executionSummary(response) {
|
|
1161
|
-
const records =
|
|
1286
|
+
const records = response.records;
|
|
1162
1287
|
const summary = { ...response };
|
|
1163
1288
|
delete summary.records;
|
|
1164
|
-
delete summary.items;
|
|
1165
1289
|
delete summary.normalized_schema;
|
|
1166
1290
|
summary.truncated_count = records.filter((record) => {
|
|
1167
1291
|
const metadata = record.metadata;
|
|
@@ -1182,6 +1306,7 @@ function executionSummary(response) {
|
|
|
1182
1306
|
"result_count",
|
|
1183
1307
|
"latency_ms",
|
|
1184
1308
|
"estimated_cost_usd",
|
|
1309
|
+
"provider_http_status",
|
|
1185
1310
|
"error"
|
|
1186
1311
|
]));
|
|
1187
1312
|
}
|
|
@@ -1196,7 +1321,7 @@ function executionSummary(response) {
|
|
|
1196
1321
|
if (Object.keys(counts).length) summary.provider_result_counts = counts;
|
|
1197
1322
|
delete summary.provider_results;
|
|
1198
1323
|
}
|
|
1199
|
-
|
|
1324
|
+
summary.item_count = records.length;
|
|
1200
1325
|
if (Array.isArray(summary.routes)) {
|
|
1201
1326
|
summary.routes = summary.routes.map((value) => {
|
|
1202
1327
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
@@ -1219,6 +1344,7 @@ function executionSummary(response) {
|
|
|
1219
1344
|
"result_count",
|
|
1220
1345
|
"latency_ms",
|
|
1221
1346
|
"estimated_cost_usd",
|
|
1347
|
+
"provider_http_status",
|
|
1222
1348
|
"error"
|
|
1223
1349
|
]));
|
|
1224
1350
|
}
|
|
@@ -1340,7 +1466,7 @@ auth.command("logout").action(async (_options, command) => {
|
|
|
1340
1466
|
await clearRefreshToken2();
|
|
1341
1467
|
await emit({ authenticated: false }, globals(command).json || false);
|
|
1342
1468
|
});
|
|
1343
|
-
var skill = program.command("skill").description("Inspect or
|
|
1469
|
+
var skill = program.command("skill").description("Inspect or update the installed skill");
|
|
1344
1470
|
skill.command("status").action(async (_options, command) => {
|
|
1345
1471
|
await emit(await installedSkillDiagnostics(), globals(command).json || false);
|
|
1346
1472
|
});
|
|
@@ -1396,24 +1522,6 @@ program.command("doctor").description("Check the CLI, authentication, installed
|
|
|
1396
1522
|
);
|
|
1397
1523
|
if (!ok) process.exitCode = authentication.configured ? EXIT.api : EXIT.auth;
|
|
1398
1524
|
});
|
|
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
1525
|
var catalog = program.command("catalog").description("Inspect capabilities, provider schemas, options, and pricing");
|
|
1418
1526
|
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
1527
|
const query = new URLSearchParams();
|
|
@@ -1433,20 +1541,6 @@ catalog.command("inspect <capability-id>").action(async (capabilityId, _options,
|
|
|
1433
1541
|
globals(command).json || false
|
|
1434
1542
|
);
|
|
1435
1543
|
});
|
|
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
1544
|
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
1545
|
if (options.retryFailed) {
|
|
1452
1546
|
if (capabilityId || options.config) {
|
|
@@ -1513,58 +1607,6 @@ jobs.command("cancel <job-id>").description("Cancel a pending or running queued
|
|
|
1513
1607
|
globals(command).json || false
|
|
1514
1608
|
);
|
|
1515
1609
|
});
|
|
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
1610
|
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
1611
|
const selected = Number(Boolean(options.file)) + Number(Boolean(options.test)) + Number(Boolean(options.update)) + Number(Boolean(options.finalize));
|
|
1570
1612
|
if (selected !== 1) {
|
|
@@ -1632,6 +1674,11 @@ program.command("discover").description("Save, sample-test, or finalize an agent
|
|
|
1632
1674
|
await emit(response, globals(command).json || false);
|
|
1633
1675
|
});
|
|
1634
1676
|
program.configureOutput({ writeErr: (text) => process.stderr.write(text) });
|
|
1677
|
+
program.hook("preAction", async (_command, actionCommand) => {
|
|
1678
|
+
const parentName = actionCommand.parent?.name();
|
|
1679
|
+
if (actionCommand.name() === "setup" || parentName === "skill") return;
|
|
1680
|
+
await maybeUpdateInstalledSkill();
|
|
1681
|
+
});
|
|
1635
1682
|
program.parseAsync(process.argv).catch(async (error) => {
|
|
1636
1683
|
if (error instanceof CommanderError) {
|
|
1637
1684
|
process.exitCode = error.exitCode === 0 ? EXIT.ok : EXIT.usage;
|