@scrappycoco/cli 0.4.2 → 0.5.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 +22 -11
- package/dist/index.js +213 -18
- 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
|
|
|
@@ -41,9 +42,10 @@ Individual commands remain available:
|
|
|
41
42
|
|
|
42
43
|
```sh
|
|
43
44
|
npx --yes @scrappycoco/cli@latest auth login
|
|
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,22 @@ 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. Set
|
|
78
|
+
`SCRAPPYCOCO_JOB_TIMEOUT_MS` to change the default 20-minute foreground wait.
|
|
79
|
+
Run `scrappycoco doctor --json` only when authentication, connectivity, or
|
|
80
|
+
installation is unclear.
|
|
81
|
+
|
|
82
|
+
When provider or configuration choice is uncertain, define at least one named
|
|
83
|
+
candidate for each special provider configuration under `candidates` in a
|
|
84
|
+
Discover route. Scrappycoco automatically adds one default candidate for every
|
|
85
|
+
available provider omitted from the draft, so the comparison remains
|
|
86
|
+
exhaustive. Each candidate has its own `id`, `provider`, and `options`, so one
|
|
87
|
+
discovery can test both `zyte-http` with
|
|
77
88
|
`{"browser_html":false}` and `zyte-browser` with
|
|
78
89
|
`{"browser_html":true}` against the same input.
|
|
79
90
|
|
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,
|
|
@@ -594,7 +597,10 @@ var ApiClient = class {
|
|
|
594
597
|
if (!job.result || typeof job.result !== "object" || Array.isArray(job.result)) {
|
|
595
598
|
throw new CliError(`Job ${job.job_id} completed without a result.`, EXIT.api, job);
|
|
596
599
|
}
|
|
597
|
-
return job.result;
|
|
600
|
+
return { ...job.result, job_id: job.job_id };
|
|
601
|
+
}
|
|
602
|
+
async postJob(path, body, key = randomUUID2()) {
|
|
603
|
+
return this.waitForJob(await this.submitJob(path, body, key));
|
|
598
604
|
}
|
|
599
605
|
patch(path, body) {
|
|
600
606
|
return this.request("PATCH", path, body);
|
|
@@ -883,6 +889,12 @@ async function loadInstalledDigest() {
|
|
|
883
889
|
return null;
|
|
884
890
|
}
|
|
885
891
|
}
|
|
892
|
+
async function installedSkillDiagnostics() {
|
|
893
|
+
return {
|
|
894
|
+
installed: await isSkillInstalled(),
|
|
895
|
+
digest: await loadInstalledDigest()
|
|
896
|
+
};
|
|
897
|
+
}
|
|
886
898
|
async function writePrivateJson2(path, value) {
|
|
887
899
|
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
888
900
|
const temporary = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
@@ -1033,6 +1045,12 @@ function client(command) {
|
|
|
1033
1045
|
function collect(value, previous) {
|
|
1034
1046
|
return [...previous, value];
|
|
1035
1047
|
}
|
|
1048
|
+
function selectFields(value, fields) {
|
|
1049
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1050
|
+
return Object.fromEntries(
|
|
1051
|
+
fields.filter((field) => field in value).map((field) => [field, value[field]])
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1036
1054
|
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
1055
|
const result = await performSetup({
|
|
1038
1056
|
apiUrl: globals(command).apiUrl,
|
|
@@ -1074,12 +1092,141 @@ async function emitExecution(response, options, command) {
|
|
|
1074
1092
|
const jsonMode = globals(command).json || false;
|
|
1075
1093
|
if (options.output) {
|
|
1076
1094
|
await emit(response, jsonMode, options.output, formatRecords(records, options.format));
|
|
1095
|
+
const summary = { ...response };
|
|
1096
|
+
delete summary.records;
|
|
1097
|
+
delete summary.items;
|
|
1098
|
+
delete summary.normalized_schema;
|
|
1099
|
+
summary.truncated_count = records.filter((record) => {
|
|
1100
|
+
const metadata = record.metadata;
|
|
1101
|
+
return metadata && typeof metadata === "object" && !Array.isArray(metadata) && metadata.truncated === true;
|
|
1102
|
+
}).length;
|
|
1103
|
+
if (summary.usage) {
|
|
1104
|
+
summary.usage = selectFields(summary.usage, [
|
|
1105
|
+
"billing_status",
|
|
1106
|
+
"payg_charge_usd_exact",
|
|
1107
|
+
"provider_cost_usd_exact",
|
|
1108
|
+
"unresolved_cost_count"
|
|
1109
|
+
]);
|
|
1110
|
+
}
|
|
1111
|
+
if (Array.isArray(summary.attempts)) {
|
|
1112
|
+
summary.attempts = summary.attempts.map((attempt) => selectFields(attempt, [
|
|
1113
|
+
"provider",
|
|
1114
|
+
"status",
|
|
1115
|
+
"result_count",
|
|
1116
|
+
"latency_ms",
|
|
1117
|
+
"estimated_cost_usd",
|
|
1118
|
+
"error"
|
|
1119
|
+
]));
|
|
1120
|
+
}
|
|
1121
|
+
const providerResults = summary.provider_results;
|
|
1122
|
+
if (providerResults && typeof providerResults === "object" && !Array.isArray(providerResults)) {
|
|
1123
|
+
const counts = Object.fromEntries(
|
|
1124
|
+
Object.entries(providerResults).map(([provider, values]) => [
|
|
1125
|
+
provider,
|
|
1126
|
+
Array.isArray(values) ? values.length : 0
|
|
1127
|
+
])
|
|
1128
|
+
);
|
|
1129
|
+
if (Object.keys(counts).length) summary.provider_result_counts = counts;
|
|
1130
|
+
delete summary.provider_results;
|
|
1131
|
+
}
|
|
1132
|
+
if (Array.isArray(response.items)) {
|
|
1133
|
+
summary.item_count = response.items.length;
|
|
1134
|
+
summary.failed_item_count = response.items.filter(
|
|
1135
|
+
(item) => item && typeof item === "object" && item.status === "failed"
|
|
1136
|
+
).length;
|
|
1137
|
+
}
|
|
1138
|
+
if (Array.isArray(summary.routes)) {
|
|
1139
|
+
summary.routes = summary.routes.map((value) => {
|
|
1140
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1141
|
+
const route = { ...value };
|
|
1142
|
+
const routeResults = route.provider_results;
|
|
1143
|
+
if (routeResults && typeof routeResults === "object" && !Array.isArray(routeResults)) {
|
|
1144
|
+
const counts = Object.fromEntries(
|
|
1145
|
+
Object.entries(routeResults).map(([provider, values]) => [
|
|
1146
|
+
provider,
|
|
1147
|
+
Array.isArray(values) ? values.length : 0
|
|
1148
|
+
])
|
|
1149
|
+
);
|
|
1150
|
+
if (Object.keys(counts).length) route.provider_result_counts = counts;
|
|
1151
|
+
delete route.provider_results;
|
|
1152
|
+
}
|
|
1153
|
+
if (Array.isArray(route.attempts)) {
|
|
1154
|
+
route.attempts = route.attempts.map((attempt) => selectFields(attempt, [
|
|
1155
|
+
"provider",
|
|
1156
|
+
"status",
|
|
1157
|
+
"result_count",
|
|
1158
|
+
"latency_ms",
|
|
1159
|
+
"estimated_cost_usd",
|
|
1160
|
+
"error"
|
|
1161
|
+
]));
|
|
1162
|
+
}
|
|
1163
|
+
return route;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
for (const field of ["cursor", "monitor"]) {
|
|
1167
|
+
if (summary[field] === null) delete summary[field];
|
|
1168
|
+
}
|
|
1169
|
+
summary.output = {
|
|
1170
|
+
path: options.output,
|
|
1171
|
+
format: options.format,
|
|
1172
|
+
record_count: records.length
|
|
1173
|
+
};
|
|
1174
|
+
await emit(summary, jsonMode);
|
|
1077
1175
|
process.stderr.write(`Saved ${records.length} records to ${options.output}
|
|
1078
1176
|
`);
|
|
1079
1177
|
return;
|
|
1080
1178
|
}
|
|
1081
1179
|
await emit(response, jsonMode);
|
|
1082
1180
|
}
|
|
1181
|
+
async function executeQueuedJob(path, payload, options, command) {
|
|
1182
|
+
if (options.detach && options.output) {
|
|
1183
|
+
throw new CliError(
|
|
1184
|
+
"Do not combine --detach with --output. Use `scrappycoco jobs wait JOB_ID --output PATH`.",
|
|
1185
|
+
EXIT.usage
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
const apiClient = client(command);
|
|
1189
|
+
const idempotencyKey = options.idempotencyKey || randomUUID4();
|
|
1190
|
+
if (options.detach) {
|
|
1191
|
+
const job = await apiClient.submitJob(path, payload, idempotencyKey);
|
|
1192
|
+
await emit(
|
|
1193
|
+
{
|
|
1194
|
+
...job,
|
|
1195
|
+
next_command: `scrappycoco jobs wait ${job.job_id}`
|
|
1196
|
+
},
|
|
1197
|
+
globals(command).json || false
|
|
1198
|
+
);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
await emitExecution(
|
|
1202
|
+
await apiClient.postJob(
|
|
1203
|
+
path,
|
|
1204
|
+
payload,
|
|
1205
|
+
idempotencyKey
|
|
1206
|
+
),
|
|
1207
|
+
options,
|
|
1208
|
+
command
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
function formatCatalog(items) {
|
|
1212
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1213
|
+
for (const item of items) {
|
|
1214
|
+
const source = String(item.source || item.id.split(".", 1)[0] || "other");
|
|
1215
|
+
grouped.set(source, [...grouped.get(source) || [], item]);
|
|
1216
|
+
}
|
|
1217
|
+
const lines = [];
|
|
1218
|
+
for (const [source, capabilities] of grouped) {
|
|
1219
|
+
lines.push(`${source.toUpperCase()} (${capabilities.length})`);
|
|
1220
|
+
for (const capability of capabilities) {
|
|
1221
|
+
const label = typeof capability.label === "string" ? capability.label : capability.id;
|
|
1222
|
+
const description = typeof capability.description === "string" ? ` \u2014 ${capability.description}` : "";
|
|
1223
|
+
lines.push(` ${capability.id} ${label}${description}`);
|
|
1224
|
+
}
|
|
1225
|
+
lines.push("");
|
|
1226
|
+
}
|
|
1227
|
+
return `${lines.join("\n").trimEnd()}
|
|
1228
|
+
`;
|
|
1229
|
+
}
|
|
1083
1230
|
var auth = program.command("auth").description("Manage Clerk OAuth credentials");
|
|
1084
1231
|
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
1232
|
const result = await login({
|
|
@@ -1124,6 +1271,48 @@ auth.command("logout").action(async (_options, command) => {
|
|
|
1124
1271
|
await clearRefreshToken2();
|
|
1125
1272
|
await emit({ authenticated: false }, globals(command).json || false);
|
|
1126
1273
|
});
|
|
1274
|
+
program.command("doctor").description("Check the CLI, authentication, installed skill, API, and live catalog").action(async (_options, command) => {
|
|
1275
|
+
const usingApiKey = Boolean(process.env.SCRAPPYCOCO_API_KEY);
|
|
1276
|
+
const usingOAuth = !usingApiKey && Boolean(await loadRefreshToken());
|
|
1277
|
+
const authentication = {
|
|
1278
|
+
configured: usingApiKey || usingOAuth,
|
|
1279
|
+
method: usingApiKey ? "api_key" : usingOAuth ? "oauth" : null
|
|
1280
|
+
};
|
|
1281
|
+
let catalog2 = [];
|
|
1282
|
+
let catalogError = null;
|
|
1283
|
+
if (authentication.configured) {
|
|
1284
|
+
try {
|
|
1285
|
+
catalog2 = await client(command).get("/scrapers?available_only=true");
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
catalogError = error instanceof Error ? error.message : String(error);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const skill = await installedSkillDiagnostics();
|
|
1291
|
+
const ok = authentication.configured && catalogError === null;
|
|
1292
|
+
await emit(
|
|
1293
|
+
{
|
|
1294
|
+
ok,
|
|
1295
|
+
cli: {
|
|
1296
|
+
version: packageMetadata.version,
|
|
1297
|
+
node: process.version
|
|
1298
|
+
},
|
|
1299
|
+
authentication,
|
|
1300
|
+
api: {
|
|
1301
|
+
url: globals(command).apiUrl,
|
|
1302
|
+
reachable: authentication.configured && catalogError === null
|
|
1303
|
+
},
|
|
1304
|
+
catalog: {
|
|
1305
|
+
reachable: authentication.configured && catalogError === null,
|
|
1306
|
+
available_capabilities: catalog2.length,
|
|
1307
|
+
error: catalogError
|
|
1308
|
+
},
|
|
1309
|
+
skill,
|
|
1310
|
+
next_action: ok ? "Scrappycoco is ready." : authentication.configured ? "Check the API connection, then run doctor again." : "Run `scrappycoco setup`."
|
|
1311
|
+
},
|
|
1312
|
+
globals(command).json || false
|
|
1313
|
+
);
|
|
1314
|
+
if (!ok) process.exitCode = authentication.configured ? EXIT.api : EXIT.auth;
|
|
1315
|
+
});
|
|
1127
1316
|
var scrapers = program.command("scrapers", { hidden: true }).description("Legacy scraper commands");
|
|
1128
1317
|
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
1318
|
const query = new URLSearchParams();
|
|
@@ -1148,10 +1337,9 @@ catalog.command("list").option("--source <source>", "filter by web, x, reddit, o
|
|
|
1148
1337
|
if (options.source) query.set("source", options.source);
|
|
1149
1338
|
if (options.provider) query.set("provider", options.provider);
|
|
1150
1339
|
if (options.available) query.set("available_only", "true");
|
|
1151
|
-
await
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
);
|
|
1340
|
+
const items = await client(command).get(`/scrapers${query.size ? `?${query}` : ""}`);
|
|
1341
|
+
if (globals(command).json) await emit(items, true);
|
|
1342
|
+
else process.stdout.write(formatCatalog(items));
|
|
1155
1343
|
});
|
|
1156
1344
|
catalog.command("inspect <capability-id>").action(async (capabilityId, _options, command) => {
|
|
1157
1345
|
const { source, capability } = splitScraperId(capabilityId);
|
|
@@ -1176,17 +1364,17 @@ function executionCommand(name) {
|
|
|
1176
1364
|
}
|
|
1177
1365
|
executionCommand("run");
|
|
1178
1366
|
executionCommand("compare");
|
|
1179
|
-
program.command("run [capability-id]").description("Run a capability directly;
|
|
1367
|
+
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
1368
|
if (options.retryFailed) {
|
|
1181
1369
|
if (capabilityId || options.config) {
|
|
1182
1370
|
throw new CliError("Do not combine --retry-failed with a capability ID or --config.", EXIT.usage);
|
|
1183
1371
|
}
|
|
1184
|
-
|
|
1372
|
+
await executeQueuedJob(
|
|
1185
1373
|
`/runs/${encodeURIComponent(options.retryFailed)}/retry-failed`,
|
|
1186
1374
|
{},
|
|
1187
|
-
options
|
|
1375
|
+
options,
|
|
1376
|
+
command
|
|
1188
1377
|
);
|
|
1189
|
-
await emitExecution(response2, options, command);
|
|
1190
1378
|
return;
|
|
1191
1379
|
}
|
|
1192
1380
|
if (options.config) {
|
|
@@ -1195,16 +1383,16 @@ program.command("run [capability-id]").description("Run a capability directly; d
|
|
|
1195
1383
|
}
|
|
1196
1384
|
const fromFile = options.file ? await readJsonFile(options.file) : {};
|
|
1197
1385
|
const input = options.input ? parseJsonObject(options.input, "runtime input JSON") : fromFile.input || {};
|
|
1198
|
-
|
|
1386
|
+
await executeQueuedJob(
|
|
1199
1387
|
`/discoveries/${encodeURIComponent(options.config)}/jobs`,
|
|
1200
1388
|
{
|
|
1201
1389
|
...fromFile,
|
|
1202
1390
|
input,
|
|
1203
1391
|
limit: Number(options.limit ?? fromFile.limit ?? 25)
|
|
1204
1392
|
},
|
|
1205
|
-
options
|
|
1393
|
+
options,
|
|
1394
|
+
command
|
|
1206
1395
|
);
|
|
1207
|
-
await emitExecution(response2, options, command);
|
|
1208
1396
|
return;
|
|
1209
1397
|
}
|
|
1210
1398
|
if (!capabilityId) {
|
|
@@ -1212,12 +1400,12 @@ program.command("run [capability-id]").description("Run a capability directly; d
|
|
|
1212
1400
|
}
|
|
1213
1401
|
const payload = await requestPayload(options, capabilityId);
|
|
1214
1402
|
if (payload.limit === void 0) payload.limit = 10;
|
|
1215
|
-
|
|
1403
|
+
await executeQueuedJob(
|
|
1216
1404
|
"/scrapers/jobs",
|
|
1217
1405
|
payload,
|
|
1218
|
-
options
|
|
1406
|
+
options,
|
|
1407
|
+
command
|
|
1219
1408
|
);
|
|
1220
|
-
await emitExecution(response, options, command);
|
|
1221
1409
|
});
|
|
1222
1410
|
var jobs = program.command("jobs").description("Inspect durable queued jobs");
|
|
1223
1411
|
jobs.command("get <job-id>").action(async (jobId, _options, command) => {
|
|
@@ -1226,6 +1414,13 @@ jobs.command("get <job-id>").action(async (jobId, _options, command) => {
|
|
|
1226
1414
|
globals(command).json || false
|
|
1227
1415
|
);
|
|
1228
1416
|
});
|
|
1417
|
+
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) => {
|
|
1418
|
+
await emitExecution(
|
|
1419
|
+
await client(command).waitForJob(jobId),
|
|
1420
|
+
options,
|
|
1421
|
+
command
|
|
1422
|
+
);
|
|
1423
|
+
});
|
|
1229
1424
|
var providers = program.command("providers", { hidden: true }).description("Legacy provider commands");
|
|
1230
1425
|
providers.command("list").option("--available", "only include available provider-capability routes").action(async (options, command) => {
|
|
1231
1426
|
await emit(
|
|
@@ -1278,7 +1473,7 @@ discoveries.command("delete <discovery-id>").requiredOption("--yes", "confirm pe
|
|
|
1278
1473
|
await client(command).delete(`/discoveries/${encodeURIComponent(discoveryId)}`);
|
|
1279
1474
|
await emit({ deleted: true, discovery_id: discoveryId }, globals(command).json || false);
|
|
1280
1475
|
});
|
|
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", "
|
|
1476
|
+
program.command("discover").description("Save, sample-test, or finalize an agent-authored configuration").option("-f, --file <path>", "create from agent-authored discovery JSON").option("--id <discovery-id>", "existing discovery ID").option("--test", "run a representative provider sample test").option("--input <json>", "sample runtime input JSON").option("--update <path>", "replace fields or configuration from agent-authored JSON").option("--finalize", "mark the current explicit configuration finalized").option("--idempotency-key <key>", "stable sample retry key").action(async (options, command) => {
|
|
1282
1477
|
const selected = Number(Boolean(options.file)) + Number(Boolean(options.test)) + Number(Boolean(options.update)) + Number(Boolean(options.finalize));
|
|
1283
1478
|
if (selected !== 1) {
|
|
1284
1479
|
throw new CliError(
|