@uipath/ixp-tool 1.204.0-preview.255 → 1.205.0-preview.256
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/dist/index.js +1 -1
- package/dist/{tool-w8jjsgra.js → tool-6j0f3qjh.js} +374 -77
- package/dist/tool.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2101,7 +2101,7 @@ var package_default = {
|
|
|
2101
2101
|
name: "@uipath/ixp-tool",
|
|
2102
2102
|
author: "UiPath",
|
|
2103
2103
|
license: "SEE LICENSE IN LICENSE.txt",
|
|
2104
|
-
version: "1.
|
|
2104
|
+
version: "1.205.0-preview.256",
|
|
2105
2105
|
description: "Manage UiPath IXP projects, prompts, predictions, and model publishing.",
|
|
2106
2106
|
private: false,
|
|
2107
2107
|
repository: {
|
|
@@ -2324,6 +2324,16 @@ class NodeFileSystem {
|
|
|
2324
2324
|
async rename(oldPath, newPath) {
|
|
2325
2325
|
await fs.rename(oldPath, newPath);
|
|
2326
2326
|
}
|
|
2327
|
+
async isSymbolicLink(filePath) {
|
|
2328
|
+
try {
|
|
2329
|
+
return (await fs.lstat(filePath)).isSymbolicLink();
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
if (this.isEnoent(error) || this.hasErrnoCode(error, "ENOTDIR")) {
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
throw error;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2327
2337
|
async realpath(filePath) {
|
|
2328
2338
|
try {
|
|
2329
2339
|
return await fs.realpath(filePath);
|
|
@@ -3355,7 +3365,7 @@ function setGlobalLogFilePath(path2) {
|
|
|
3355
3365
|
function getGlobalLogFilePath() {
|
|
3356
3366
|
return logFilePathSlot.get("");
|
|
3357
3367
|
}
|
|
3358
|
-
var DEFAULT_LOG_LEVEL =
|
|
3368
|
+
var DEFAULT_LOG_LEVEL = 2 /* WARN */;
|
|
3359
3369
|
|
|
3360
3370
|
class SimpleLogger {
|
|
3361
3371
|
__brand = "SimpleLogger";
|
|
@@ -3622,7 +3632,9 @@ var ErrorDimensions = {
|
|
|
3622
3632
|
Result: "Result",
|
|
3623
3633
|
ErrorCode: "ErrorCode",
|
|
3624
3634
|
Retry: "Retry",
|
|
3625
|
-
ErrorMessage: "ErrorMessage"
|
|
3635
|
+
ErrorMessage: "ErrorMessage",
|
|
3636
|
+
HttpStatus: "HttpStatus",
|
|
3637
|
+
UpstreamErrorCode: "UpstreamErrorCode"
|
|
3626
3638
|
};
|
|
3627
3639
|
var AuthDimensions = {
|
|
3628
3640
|
AuthenticationMethod: "AuthenticationMethod",
|
|
@@ -4616,7 +4628,9 @@ var CLI_ERROR_CODES = [
|
|
|
4616
4628
|
"timeout",
|
|
4617
4629
|
"server_error",
|
|
4618
4630
|
"method_not_allowed",
|
|
4631
|
+
"not_supported",
|
|
4619
4632
|
"configuration_error",
|
|
4633
|
+
"execution_failed",
|
|
4620
4634
|
"unknown_error"
|
|
4621
4635
|
];
|
|
4622
4636
|
var RETRY_HINTS = [
|
|
@@ -5573,7 +5587,9 @@ var OutputFormatter;
|
|
|
5573
5587
|
[ErrorDimensions.Result]: data.Result,
|
|
5574
5588
|
[ErrorDimensions.ErrorCode]: data.ErrorCode,
|
|
5575
5589
|
[ErrorDimensions.Retry]: data.Retry,
|
|
5576
|
-
[ErrorDimensions.ErrorMessage]: data.Message
|
|
5590
|
+
[ErrorDimensions.ErrorMessage]: data.Message,
|
|
5591
|
+
[ErrorDimensions.HttpStatus]: data.Context?.httpStatus?.toString(),
|
|
5592
|
+
[ErrorDimensions.UpstreamErrorCode]: data.Context?.errorCode
|
|
5577
5593
|
});
|
|
5578
5594
|
}
|
|
5579
5595
|
logOutput(normalizeOutputKeys(envelope), getOutputFormat());
|
|
@@ -6347,6 +6363,52 @@ var BOOLEAN_FLAGS = new Set(HOST_GLOBAL_FLAGS);
|
|
|
6347
6363
|
// ../common/src/interactivity-context.ts
|
|
6348
6364
|
var modeSlot = singleton("InteractivityMode");
|
|
6349
6365
|
var interactiveFlagSlot = singleton("InteractiveFlag");
|
|
6366
|
+
// ../common/src/json-option.ts
|
|
6367
|
+
function jsonFileOptionHelp(inlineFlag) {
|
|
6368
|
+
return `Path to a file holding the ${inlineFlag} JSON. Use it on Windows cmd/PowerShell, where quotes inside an inline JSON argument are stripped before the CLI sees them.`;
|
|
6369
|
+
}
|
|
6370
|
+
async function readJsonOption(source) {
|
|
6371
|
+
const read = await readJsonOptionText(source);
|
|
6372
|
+
return read === undefined ? undefined : parseJsonOptionText(read, source);
|
|
6373
|
+
}
|
|
6374
|
+
async function readJsonOptionText(source) {
|
|
6375
|
+
const { inline, file, inlineFlag, fileFlag } = source;
|
|
6376
|
+
if (file !== undefined) {
|
|
6377
|
+
const fs2 = getFileSystem();
|
|
6378
|
+
const bytes = await fs2.readFile(file);
|
|
6379
|
+
if (bytes === null) {
|
|
6380
|
+
throw new Error(`File not found: ${fs2.path.resolve(file)}`);
|
|
6381
|
+
}
|
|
6382
|
+
const text = decodeJsonFile(bytes).trim();
|
|
6383
|
+
if (text === "") {
|
|
6384
|
+
throw new Error(`Expected JSON in ${file} but the file is empty.`);
|
|
6385
|
+
}
|
|
6386
|
+
return { text, origin: "file", label: `${fileFlag} ${file}` };
|
|
6387
|
+
}
|
|
6388
|
+
if (inline === undefined) {
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
6391
|
+
return { text: inline, origin: "inline", label: inlineFlag };
|
|
6392
|
+
}
|
|
6393
|
+
function decodeJsonFile(bytes) {
|
|
6394
|
+
const utf16le = bytes[0] === 255 && bytes[1] === 254;
|
|
6395
|
+
return new TextDecoder(utf16le ? "utf-16le" : "utf-8").decode(bytes);
|
|
6396
|
+
}
|
|
6397
|
+
function parseJsonOptionText(read, flags) {
|
|
6398
|
+
try {
|
|
6399
|
+
return JSON.parse(read.text);
|
|
6400
|
+
} catch (err) {
|
|
6401
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
6402
|
+
const hint = read.origin === "inline" ? windowsJsonHint(flags) : "";
|
|
6403
|
+
throw new Error(`${read.label} is not valid JSON: ${detail}.${hint}`);
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
function windowsJsonHint(flags) {
|
|
6407
|
+
if (process.platform !== "win32") {
|
|
6408
|
+
return "";
|
|
6409
|
+
}
|
|
6410
|
+
return ` On Windows cmd/PowerShell, quotes inside an inline JSON argument are stripped and \`& | < > ^\` split the command — pass the file with \`${flags.fileFlag} <path>\` instead.`;
|
|
6411
|
+
}
|
|
6350
6412
|
// ../common/src/mime-types.ts
|
|
6351
6413
|
var MIME_TYPES_BY_EXTENSION = Object.freeze({
|
|
6352
6414
|
pdf: "application/pdf",
|
|
@@ -6386,6 +6448,35 @@ function warnDeprecatedOptionAlias(deprecatedFlag, preferredFlag) {
|
|
|
6386
6448
|
getOutputSink().writeErr(`[WARN] ${deprecatedFlag} is deprecated. Use ${preferredFlag} instead.
|
|
6387
6449
|
`);
|
|
6388
6450
|
}
|
|
6451
|
+
function addDeprecatedOptionAlias(command, deprecatedFlags, preferredLong) {
|
|
6452
|
+
const preferred = command.options.find((option) => option.long === preferredLong);
|
|
6453
|
+
if (preferred === undefined) {
|
|
6454
|
+
throw new Error(`${preferredLong} must be registered before its deprecated alias`);
|
|
6455
|
+
}
|
|
6456
|
+
const deprecated = new Option(deprecatedFlags, `Deprecated alias for ${preferredLong}`).hideHelp();
|
|
6457
|
+
const preferredKey = preferred.attributeName();
|
|
6458
|
+
const deprecatedLabel = [deprecated.short, deprecated.long].filter((flag) => flag !== undefined).join("/");
|
|
6459
|
+
let aliasUsed = false;
|
|
6460
|
+
const conflict = () => command.error(`error: option '${deprecated.flags}' cannot be used with option '${preferred.flags}'`, {
|
|
6461
|
+
code: "commander.conflictingOption",
|
|
6462
|
+
exitCode: EXIT_CODES.ValidationError
|
|
6463
|
+
});
|
|
6464
|
+
command.on(`option:${preferred.name()}`, () => {
|
|
6465
|
+
if (aliasUsed)
|
|
6466
|
+
conflict();
|
|
6467
|
+
});
|
|
6468
|
+
command.on(`option:${deprecated.name()}`, (value) => {
|
|
6469
|
+
if (!aliasUsed) {
|
|
6470
|
+
if (command.getOptionValueSource(preferredKey) === "cli") {
|
|
6471
|
+
conflict();
|
|
6472
|
+
}
|
|
6473
|
+
warnDeprecatedOptionAlias(deprecatedLabel, preferredLong);
|
|
6474
|
+
aliasUsed = true;
|
|
6475
|
+
}
|
|
6476
|
+
command.setOptionValueWithSource(preferredKey, value, "cli");
|
|
6477
|
+
});
|
|
6478
|
+
return command.addOption(deprecated);
|
|
6479
|
+
}
|
|
6389
6480
|
var TENANT_SWITCH_COMMAND = "uip login tenant set <tenant>";
|
|
6390
6481
|
function createHiddenDeprecatedTenantOption(flags = "-t, --tenant <tenant-name>") {
|
|
6391
6482
|
return new Option(flags, `Tenant name. Deprecated; use ${TENANT_SWITCH_COMMAND} to switch active tenants.`).hideHelp().argParser((tenant) => {
|
|
@@ -6398,6 +6489,30 @@ function warnDeprecatedTenantOption(tenant) {
|
|
|
6398
6489
|
return;
|
|
6399
6490
|
warnDeprecatedOptionAlias("--tenant", TENANT_SWITCH_COMMAND);
|
|
6400
6491
|
}
|
|
6492
|
+
// ../common/src/option-groups.ts
|
|
6493
|
+
function requireOneOfOptions(command, longFlags) {
|
|
6494
|
+
const options = longFlags.map((long) => {
|
|
6495
|
+
const option = command.options.find((o) => o.long === long);
|
|
6496
|
+
if (option === undefined) {
|
|
6497
|
+
throw new Error(`${long} must be registered on '${command.name()}' before requireOneOfOptions`);
|
|
6498
|
+
}
|
|
6499
|
+
return option;
|
|
6500
|
+
});
|
|
6501
|
+
const flags = options.map((option) => `'${option.flags}'`).join(" or ");
|
|
6502
|
+
command.hook("preAction", (thisCommand) => {
|
|
6503
|
+
const given = options.some((option) => {
|
|
6504
|
+
const source = thisCommand.getOptionValueSource(option.attributeName());
|
|
6505
|
+
return source !== undefined && source !== "default";
|
|
6506
|
+
});
|
|
6507
|
+
if (given) {
|
|
6508
|
+
return;
|
|
6509
|
+
}
|
|
6510
|
+
thisCommand.error(`error: required option ${flags} not specified`, {
|
|
6511
|
+
code: "commander.missingMandatoryOptionValue",
|
|
6512
|
+
exitCode: EXIT_CODES.ValidationError
|
|
6513
|
+
});
|
|
6514
|
+
});
|
|
6515
|
+
}
|
|
6401
6516
|
// ../common/src/option-validators.ts
|
|
6402
6517
|
function parseLimit(raw, _previous) {
|
|
6403
6518
|
return parseBoundedInt(raw, "--limit", { min: 1, max: 1e4 });
|
|
@@ -6661,22 +6776,12 @@ var resolveConfigAsync = async ({
|
|
|
6661
6776
|
baseUrl = DEFAULT_BASE_URL;
|
|
6662
6777
|
}
|
|
6663
6778
|
baseUrl = normalizeAndValidateBaseUrl(baseUrl);
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
clientId = fileAuth.clientId;
|
|
6667
|
-
}
|
|
6668
|
-
if (!clientId) {
|
|
6669
|
-
clientId = DEFAULT_CLIENT_ID;
|
|
6670
|
-
}
|
|
6671
|
-
let clientSecret = customClientSecret;
|
|
6672
|
-
if (!clientSecret && fileAuth.clientSecret) {
|
|
6673
|
-
clientSecret = fileAuth.clientSecret;
|
|
6674
|
-
}
|
|
6675
|
-
const isExternalAppAuth = clientId !== DEFAULT_CLIENT_ID && (Boolean(clientSecret) || Boolean(customClientAssertion));
|
|
6779
|
+
const clientId = customClientId || DEFAULT_CLIENT_ID;
|
|
6780
|
+
const isExternalAppAuth = clientId !== DEFAULT_CLIENT_ID && (Boolean(customClientSecret) || Boolean(customClientAssertion));
|
|
6676
6781
|
const scopes = resolveScopes(isExternalAppAuth, customScopes, fileAuth.scopes);
|
|
6677
6782
|
return {
|
|
6678
6783
|
clientId,
|
|
6679
|
-
clientSecret,
|
|
6784
|
+
clientSecret: customClientSecret,
|
|
6680
6785
|
scopes,
|
|
6681
6786
|
baseUrl,
|
|
6682
6787
|
authorizationEndpoint: `${baseUrl}/identity_/connect/authorize`,
|
|
@@ -6850,6 +6955,16 @@ var resolveIdentityType = (claims, authFlow, email) => {
|
|
|
6850
6955
|
}
|
|
6851
6956
|
return;
|
|
6852
6957
|
};
|
|
6958
|
+
var resolveApplicationSubtype = (claims, authFlow) => {
|
|
6959
|
+
switch (asString(claims?.sub_type)) {
|
|
6960
|
+
case "service.external":
|
|
6961
|
+
return "External";
|
|
6962
|
+
case "service.internal":
|
|
6963
|
+
return "Internal";
|
|
6964
|
+
default:
|
|
6965
|
+
return authFlow === "client_credentials" || authFlow === "federated_credentials" ? "External" : undefined;
|
|
6966
|
+
}
|
|
6967
|
+
};
|
|
6853
6968
|
var looksLikeEmail = (value) => value?.includes("@") ?? false;
|
|
6854
6969
|
var pickEmail = (claims) => {
|
|
6855
6970
|
for (const candidate of [claims?.email, claims?.preferred_username]) {
|
|
@@ -6891,6 +7006,12 @@ var resolveSessionIdentity = (accessToken, authFlow) => {
|
|
|
6891
7006
|
const clientId = asString(claims?.client_id);
|
|
6892
7007
|
if (clientId)
|
|
6893
7008
|
identity.clientId = clientId;
|
|
7009
|
+
const subType = asString(claims?.sub_type);
|
|
7010
|
+
if (subType)
|
|
7011
|
+
identity.subType = subType;
|
|
7012
|
+
const applicationSubtype = resolveApplicationSubtype(claims, authFlow);
|
|
7013
|
+
if (applicationSubtype)
|
|
7014
|
+
identity.applicationSubtype = applicationSubtype;
|
|
6894
7015
|
return identity;
|
|
6895
7016
|
};
|
|
6896
7017
|
var readTokenScopes = (accessToken) => accessToken ? pickScopes(decodeClaims(accessToken)) : undefined;
|
|
@@ -7183,6 +7304,40 @@ var tryRobotClientFallback = async (options = {}) => {
|
|
|
7183
7304
|
}
|
|
7184
7305
|
};
|
|
7185
7306
|
|
|
7307
|
+
// ../auth/src/tenantSelection.ts
|
|
7308
|
+
var LOGIN_STATUS_HINT = "Run 'uip login status' to see the organization the CLI is signed in to.";
|
|
7309
|
+
var organizationHint = (organizationId) => `The request used '${organizationId}' as the organization. ${LOGIN_STATUS_HINT}`;
|
|
7310
|
+
var IDENTIFIER_STATUSES = new Set([400, 403, 404]);
|
|
7311
|
+
var describeNonJsonResponse = (organizationId, response, parseError) => {
|
|
7312
|
+
if (response.redirected) {
|
|
7313
|
+
return `Could not find organization '${organizationId}': the request was ` + `redirected to a sign-in page instead of the organization's tenant ` + `list. ${LOGIN_STATUS_HINT}`;
|
|
7314
|
+
}
|
|
7315
|
+
const contentType = response.headers.get("content-type") ?? "unknown";
|
|
7316
|
+
return `The organization service returned a '${contentType}' response instead ` + `of JSON: ${parseError.message}. ${organizationHint(organizationId)}`;
|
|
7317
|
+
};
|
|
7318
|
+
var fetchTenantsAndOrganizations = async (baseUrl, accessToken, organizationId, signal) => {
|
|
7319
|
+
const url = `${baseUrl}/${organizationId}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo`;
|
|
7320
|
+
const response = await fetch(url, {
|
|
7321
|
+
headers: {
|
|
7322
|
+
Authorization: `Bearer ${accessToken}`
|
|
7323
|
+
},
|
|
7324
|
+
...signal ? { signal } : {}
|
|
7325
|
+
});
|
|
7326
|
+
if (!response.ok) {
|
|
7327
|
+
if (response.status === 401) {
|
|
7328
|
+
throw new Error(`Unauthorized: the access token was rejected, it was not ` + `issued for organization '${organizationId}', or ` + `'${organizationId}' was not accepted as an organization ` + `URL segment in this environment. ` + LOGIN_STATUS_HINT);
|
|
7329
|
+
}
|
|
7330
|
+
const errorText = await response.text();
|
|
7331
|
+
const message = `Failed to get tenants and organizations: ${response.status} ${errorText}`;
|
|
7332
|
+
throw new Error(IDENTIFIER_STATUSES.has(response.status) ? `${message}. ${organizationHint(organizationId)}` : message);
|
|
7333
|
+
}
|
|
7334
|
+
const [parseError, data] = await catchError2(response.json());
|
|
7335
|
+
if (parseError) {
|
|
7336
|
+
throw new Error(describeNonJsonResponse(organizationId, response, parseError));
|
|
7337
|
+
}
|
|
7338
|
+
return data;
|
|
7339
|
+
};
|
|
7340
|
+
|
|
7186
7341
|
// ../auth/src/tokenRefresh.ts
|
|
7187
7342
|
var TOKEN_REFRESH_REAUTHENTICATE_MESSAGE = "Token refresh failed. Run 'uip login' to re-authenticate.";
|
|
7188
7343
|
|
|
@@ -7395,6 +7550,8 @@ var saveEnvFileAsync = async ({
|
|
|
7395
7550
|
};
|
|
7396
7551
|
|
|
7397
7552
|
// ../auth/src/loginStatus.ts
|
|
7553
|
+
var defaultRobotTenantIdCache = new Map;
|
|
7554
|
+
var ROBOT_TENANT_LOOKUP_TIMEOUT_MS = 5000;
|
|
7398
7555
|
var getLoginStatusAsync = async (options = {}) => {
|
|
7399
7556
|
return getLoginStatusWithDeps(options);
|
|
7400
7557
|
};
|
|
@@ -7407,12 +7564,20 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
|
|
|
7407
7564
|
refreshToken: refreshTokenFn = refreshAccessToken,
|
|
7408
7565
|
resolveConfig = resolveConfigAsync,
|
|
7409
7566
|
robotFallback = tryRobotClientFallback,
|
|
7567
|
+
fetchTenantsAndOrgs = fetchTenantsAndOrganizations,
|
|
7568
|
+
robotTenantIdCache = defaultRobotTenantIdCache,
|
|
7569
|
+
robotTenantLookupTimeoutMs = ROBOT_TENANT_LOOKUP_TIMEOUT_MS,
|
|
7410
7570
|
loadBreaker = loadRefreshBreaker,
|
|
7411
7571
|
saveBreaker = saveRefreshBreaker,
|
|
7412
7572
|
clearBreaker = clearRefreshBreaker
|
|
7413
7573
|
} = deps;
|
|
7574
|
+
const robotDeps = {
|
|
7575
|
+
fetchTenantsAndOrgs,
|
|
7576
|
+
cache: robotTenantIdCache,
|
|
7577
|
+
lookupTimeoutMs: robotTenantLookupTimeoutMs
|
|
7578
|
+
};
|
|
7414
7579
|
if (isRobotAuthEnforced()) {
|
|
7415
|
-
return resolveRobotEnforcedStatus(robotFallback);
|
|
7580
|
+
return resolveRobotEnforcedStatus(robotFallback, robotDeps);
|
|
7416
7581
|
}
|
|
7417
7582
|
if (isEnvAuthEnabled()) {
|
|
7418
7583
|
return readAuthFromEnv();
|
|
@@ -7430,7 +7595,7 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
|
|
|
7430
7595
|
hint: `No credentials found for profile "${activeProfile}". Run 'uip login --profile ${activeProfile}' to authenticate this profile.`
|
|
7431
7596
|
};
|
|
7432
7597
|
}
|
|
7433
|
-
return resolveBorrowedRobotStatus(robotFallback);
|
|
7598
|
+
return resolveBorrowedRobotStatus(robotFallback, robotDeps);
|
|
7434
7599
|
}
|
|
7435
7600
|
const loaded = await loadFileCredentials(loadEnvFile, absolutePath);
|
|
7436
7601
|
if ("status" in loaded) {
|
|
@@ -7472,7 +7637,7 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
|
|
|
7472
7637
|
}
|
|
7473
7638
|
return buildFileStatus(tokens, credentials, globalHint);
|
|
7474
7639
|
};
|
|
7475
|
-
async function resolveRobotEnforcedStatus(robotFallback) {
|
|
7640
|
+
async function resolveRobotEnforcedStatus(robotFallback, deps) {
|
|
7476
7641
|
if (isEnvAuthEnabled()) {
|
|
7477
7642
|
throw new EnvAuthConfigError(`${ENV_AUTH_ENABLE_VAR}=true and ${ENFORCE_ROBOT_AUTH_VAR}=true ` + `are mutually exclusive. Unset one of them and re-run.`);
|
|
7478
7643
|
}
|
|
@@ -7483,11 +7648,11 @@ async function resolveRobotEnforcedStatus(robotFallback) {
|
|
|
7483
7648
|
hint: `${ENFORCE_ROBOT_AUTH_VAR}=true but the UiPath Robot ` + `session is unavailable. Start and sign in to the Assistant, ` + `or unset ${ENFORCE_ROBOT_AUTH_VAR} to fall back to file or ` + `env-var authentication.`
|
|
7484
7649
|
};
|
|
7485
7650
|
}
|
|
7486
|
-
return buildRobotStatus(robotCreds);
|
|
7651
|
+
return buildRobotStatus(robotCreds, deps);
|
|
7487
7652
|
}
|
|
7488
|
-
async function resolveBorrowedRobotStatus(robotFallback) {
|
|
7653
|
+
async function resolveBorrowedRobotStatus(robotFallback, deps) {
|
|
7489
7654
|
const robotCreds = await robotFallback();
|
|
7490
|
-
return robotCreds ? buildRobotStatus(robotCreds) : { loginStatus: "Not logged in" };
|
|
7655
|
+
return robotCreds ? buildRobotStatus(robotCreds, deps) : { loginStatus: "Not logged in" };
|
|
7491
7656
|
}
|
|
7492
7657
|
async function loadFileCredentials(loadEnvFile, absolutePath) {
|
|
7493
7658
|
let credentials;
|
|
@@ -7622,9 +7787,10 @@ function identityFields(accessToken, credentials) {
|
|
|
7622
7787
|
...scopes ? { scopes } : {}
|
|
7623
7788
|
};
|
|
7624
7789
|
}
|
|
7625
|
-
function buildRobotStatus(robotCreds) {
|
|
7790
|
+
async function buildRobotStatus(robotCreds, deps) {
|
|
7626
7791
|
const identity = resolveSessionIdentity(robotCreds.accessToken);
|
|
7627
7792
|
const scopes = readTokenScopes(robotCreds.accessToken);
|
|
7793
|
+
const tenant = robotCreds.tenantId ? { tenantId: robotCreds.tenantId } : await resolveRobotTenantId(robotCreds, deps);
|
|
7628
7794
|
return {
|
|
7629
7795
|
loginStatus: "Logged in",
|
|
7630
7796
|
accessToken: robotCreds.accessToken,
|
|
@@ -7632,14 +7798,62 @@ function buildRobotStatus(robotCreds) {
|
|
|
7632
7798
|
organizationName: robotCreds.organizationName,
|
|
7633
7799
|
organizationId: robotCreds.organizationId,
|
|
7634
7800
|
tenantName: robotCreds.tenantName,
|
|
7635
|
-
tenantId:
|
|
7801
|
+
tenantId: tenant.tenantId,
|
|
7636
7802
|
issuer: robotCreds.issuer,
|
|
7637
7803
|
expiration: getTokenExpiration(robotCreds.accessToken),
|
|
7638
7804
|
source: "robot" /* Robot */,
|
|
7805
|
+
...tenant.hint ? { hint: tenant.hint } : {},
|
|
7639
7806
|
...identity ? { identity } : {},
|
|
7640
7807
|
...scopes ? { scopes } : {}
|
|
7641
7808
|
};
|
|
7642
7809
|
}
|
|
7810
|
+
async function resolveRobotTenantId(robotCreds, deps) {
|
|
7811
|
+
const { baseUrl, accessToken, tenantName } = robotCreds;
|
|
7812
|
+
const organization = robotCreds.organizationName ?? robotCreds.organizationId;
|
|
7813
|
+
if (!tenantName || !organization) {
|
|
7814
|
+
return {};
|
|
7815
|
+
}
|
|
7816
|
+
const key = `${baseUrl}|${organization}|${tenantName}|${accessToken}`;
|
|
7817
|
+
let pending = deps.cache.get(key);
|
|
7818
|
+
if (!pending) {
|
|
7819
|
+
pending = catchError2(lookupRobotTenantId({ baseUrl, accessToken, organization, tenantName }, deps)).then(([error, resolution]) => {
|
|
7820
|
+
const settled = error ? { hint: tenantLookupFailureHint(tenantName, error.message) } : resolution;
|
|
7821
|
+
if (!settled.tenantId) {
|
|
7822
|
+
deps.cache.delete(key);
|
|
7823
|
+
}
|
|
7824
|
+
return settled;
|
|
7825
|
+
});
|
|
7826
|
+
deps.cache.set(key, pending);
|
|
7827
|
+
}
|
|
7828
|
+
return pending;
|
|
7829
|
+
}
|
|
7830
|
+
async function lookupRobotTenantId(target, deps) {
|
|
7831
|
+
const { baseUrl, accessToken, organization, tenantName } = target;
|
|
7832
|
+
const [error, data] = await catchError2(deps.fetchTenantsAndOrgs(baseUrl, accessToken, organization, AbortSignal.timeout(deps.lookupTimeoutMs)));
|
|
7833
|
+
if (error) {
|
|
7834
|
+
const reason = error.name === "TimeoutError" ? `the tenant list did not answer within ${deps.lookupTimeoutMs} ms.` : error.message;
|
|
7835
|
+
return { hint: tenantLookupFailureHint(tenantName, reason) };
|
|
7836
|
+
}
|
|
7837
|
+
const rows = data?.tenants;
|
|
7838
|
+
const tenants = Array.isArray(rows) ? rows.filter((tenant) => typeof tenant?.id === "string" && typeof tenant?.name === "string") : [];
|
|
7839
|
+
const wantedName = decodePathSegment(tenantName);
|
|
7840
|
+
const wanted = wantedName.toLowerCase();
|
|
7841
|
+
const match = tenants.find((tenant) => tenant.name === wantedName) ?? tenants.find((tenant) => tenant.name.toLowerCase() === wanted);
|
|
7842
|
+
if (!match) {
|
|
7843
|
+
const names = tenants.map((tenant) => tenant.name).join(", ");
|
|
7844
|
+
return {
|
|
7845
|
+
hint: `The Robot session targets tenant '${tenantName}', but ` + `organization '${organization}' lists no tenant of that name` + `${names ? ` (available: ${names})` : ""}. Commands that need ` + `the tenant id will fail until it can be resolved.`
|
|
7846
|
+
};
|
|
7847
|
+
}
|
|
7848
|
+
return { tenantId: match.id };
|
|
7849
|
+
}
|
|
7850
|
+
function tenantLookupFailureHint(tenantName, reason) {
|
|
7851
|
+
return `Could not resolve the id of tenant '${tenantName}' from the ` + `Robot session: ${reason} Commands that need the tenant id will ` + `fail until it can be resolved.`;
|
|
7852
|
+
}
|
|
7853
|
+
function decodePathSegment(segment) {
|
|
7854
|
+
const [error, decoded] = catchError2(() => decodeURIComponent(segment));
|
|
7855
|
+
return error ? segment : decoded;
|
|
7856
|
+
}
|
|
7643
7857
|
var isFileNotFoundError = (error) => {
|
|
7644
7858
|
if (!(error instanceof Object))
|
|
7645
7859
|
return false;
|
|
@@ -7875,9 +8089,6 @@ function normalizeTokenRefreshUnavailableFailure() {
|
|
|
7875
8089
|
function errorMessage(error) {
|
|
7876
8090
|
return error instanceof Error ? error.message : String(error);
|
|
7877
8091
|
}
|
|
7878
|
-
// ../auth/src/tenantSelection.ts
|
|
7879
|
-
var IDENTIFIER_STATUSES = new Set([400, 403, 404]);
|
|
7880
|
-
|
|
7881
8092
|
// ../auth/src/selectTenant.ts
|
|
7882
8093
|
var TENANT_SELECTION_REQUIRED_CODE = "TENANT_SELECTION_REQUIRED";
|
|
7883
8094
|
var INVALID_TENANT_CODE = "INVALID_TENANT";
|
|
@@ -7890,7 +8101,7 @@ var package_default2 = {
|
|
|
7890
8101
|
name: "@uipath/ixp-sdk",
|
|
7891
8102
|
author: "UiPath",
|
|
7892
8103
|
license: "SEE LICENSE IN LICENSE.txt",
|
|
7893
|
-
version: "1.
|
|
8104
|
+
version: "1.205.0-preview.256",
|
|
7894
8105
|
description: "SDK for the UiPath IXP (Intelligent eXtraction Platform) API — projects, taxonomies, prompts, predictions, and model publishing.",
|
|
7895
8106
|
repository: {
|
|
7896
8107
|
type: "git",
|
|
@@ -8329,13 +8540,8 @@ var MAX_TITLE_LENGTH = 1024;
|
|
|
8329
8540
|
var MAX_FOLDER_KEY_LENGTH = 256;
|
|
8330
8541
|
|
|
8331
8542
|
// src/commands/data-types.ts
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
try {
|
|
8335
|
-
parsed = JSON.parse(raw);
|
|
8336
|
-
} catch (e) {
|
|
8337
|
-
throw new Error(`--choices is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
8338
|
-
}
|
|
8543
|
+
var CHOICES_FLAGS = { inlineFlag: "--choices", fileFlag: "--choices-file" };
|
|
8544
|
+
function parseChoicesJson(parsed) {
|
|
8339
8545
|
if (!Array.isArray(parsed)) {
|
|
8340
8546
|
throw new Error("--choices must be a JSON array");
|
|
8341
8547
|
}
|
|
@@ -8398,6 +8604,19 @@ var DATA_TYPES_ADD_EXAMPLES = [
|
|
|
8398
8604
|
InputValue: "exact-match"
|
|
8399
8605
|
}
|
|
8400
8606
|
}
|
|
8607
|
+
},
|
|
8608
|
+
{
|
|
8609
|
+
Description: "Create a Choice data type with the choices read from a file (use this on Windows PowerShell, which strips quotes from inline JSON)",
|
|
8610
|
+
Command: 'uip ixp data-types add my-invoices --name "Citizenship" --kind choice --instructions "Borrower citizenship status." --input-value exact-match --choices-file choices.json',
|
|
8611
|
+
Output: {
|
|
8612
|
+
Code: "IxpDataTypesAdd",
|
|
8613
|
+
Data: {
|
|
8614
|
+
ProjectName: "my-invoices",
|
|
8615
|
+
Name: "Citizenship",
|
|
8616
|
+
Kind: "choice",
|
|
8617
|
+
InputValue: "exact-match"
|
|
8618
|
+
}
|
|
8619
|
+
}
|
|
8401
8620
|
}
|
|
8402
8621
|
];
|
|
8403
8622
|
var DATA_TYPES_UPDATE_INSTRUCTIONS_EXAMPLES = [
|
|
@@ -8442,9 +8661,14 @@ var DATA_TYPES_DELETE_EXAMPLES = [
|
|
|
8442
8661
|
];
|
|
8443
8662
|
var registerDataTypesCommand = (program2) => {
|
|
8444
8663
|
const dataTypes = program2.command("data-types").description("Manage data types (entity_defs) — the reusable type definitions fields reference via field_type_id");
|
|
8445
|
-
dataTypes.command("add").description("Create a new data type in the project").argument("<project-name>", "Project name").requiredOption("--name <name>", `Data type name (at most ${MAX_NAME_LENGTH} characters)`).requiredOption("--kind <kind>", "Underlying data type kind: text, date, money, number, boolean, choice").requiredOption("--instructions <text>", `Instructions describing what this type represents and where its values appear (at most ${MAX_INSTRUCTIONS_LENGTH} characters)`).option("--input-value <mode>", "Whether the value appears verbatim in the document (exact-match) or is inferred/computed (inferred). Required when --kind is text or choice; forbidden for date, money, number, and boolean (those kinds don't have this property — matches the IXP UI's radio visibility).").option("--choices <json>", 'Required when --kind choice. JSON array of choices: [{"value":"<canonical>","alternates":["<alt1>","<alt2>",...]}]. `value` (required) is the canonical display name (model output). `alternates` (optional, defaults to []) are alternate spellings the model maps to `value` — omit or set to [] when the canonical value is the only matched form.').addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(DATA_TYPES_ADD_EXAMPLES).trackedAction(processContext, async (projectName, options) => {
|
|
8664
|
+
dataTypes.command("add").description("Create a new data type in the project").argument("<project-name>", "Project name").requiredOption("--name <name>", `Data type name (at most ${MAX_NAME_LENGTH} characters)`).requiredOption("--kind <kind>", "Underlying data type kind: text, date, money, number, boolean, choice").requiredOption("--instructions <text>", `Instructions describing what this type represents and where its values appear (at most ${MAX_INSTRUCTIONS_LENGTH} characters)`).option("--input-value <mode>", "Whether the value appears verbatim in the document (exact-match) or is inferred/computed (inferred). Required when --kind is text or choice; forbidden for date, money, number, and boolean (those kinds don't have this property — matches the IXP UI's radio visibility).").option("--choices <json>", 'Required when --kind choice. JSON array of choices: [{"value":"<canonical>","alternates":["<alt1>","<alt2>",...]}]. `value` (required) is the canonical display name (model output). `alternates` (optional, defaults to []) are alternate spellings the model maps to `value` — omit or set to [] when the canonical value is the only matched form.').addOption(new Option("--choices-file <path>", jsonFileOptionHelp("--choices")).conflicts("choices")).addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(DATA_TYPES_ADD_EXAMPLES).trackedAction(processContext, async (projectName, options) => {
|
|
8446
8665
|
const [err, response] = await catchError((async () => {
|
|
8447
|
-
const
|
|
8666
|
+
const rawChoices = await readJsonOption({
|
|
8667
|
+
inline: options.choices,
|
|
8668
|
+
file: options.choicesFile,
|
|
8669
|
+
...CHOICES_FLAGS
|
|
8670
|
+
});
|
|
8671
|
+
const choices = rawChoices === undefined ? undefined : parseChoicesJson(rawChoices);
|
|
8448
8672
|
const config = await createIxpConfig({
|
|
8449
8673
|
tenant: options.tenant
|
|
8450
8674
|
});
|
|
@@ -8852,7 +9076,7 @@ var registerDocumentsCommand = (program2) => {
|
|
|
8852
9076
|
Data: page
|
|
8853
9077
|
}, { preserveDataKeys: true });
|
|
8854
9078
|
});
|
|
8855
|
-
document.command("download").description("Download the original document file (PDF/PNG/JPG/etc.) for viewing").argument("<project-name>", "Project name").argument("<document-id>", "Document ID").option("-o, --output <path>", "Output file path (extension auto-corrected to match content type)", defaultDownloadPath()).addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(DOCUMENTS_DOWNLOAD_EXAMPLES).trackedAction(processContext, async (projectName, documentUid, options) => {
|
|
9079
|
+
const download = document.command("download").description("Download the original document file (PDF/PNG/JPG/etc.) for viewing").argument("<project-name>", "Project name").argument("<document-id>", "Document ID").option("-o, --output-file <path>", "Output file path (extension auto-corrected to match content type)", defaultDownloadPath()).addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(DOCUMENTS_DOWNLOAD_EXAMPLES).trackedAction(processContext, async (projectName, documentUid, options) => {
|
|
8856
9080
|
const [err, result] = await catchError((async () => {
|
|
8857
9081
|
const config = await createIxpConfig({
|
|
8858
9082
|
tenant: options.tenant
|
|
@@ -8861,7 +9085,7 @@ var registerDocumentsCommand = (program2) => {
|
|
|
8861
9085
|
const bytes = new Uint8Array(buffer);
|
|
8862
9086
|
const detectedExt = extensionForContentType(contentType);
|
|
8863
9087
|
const fs2 = getFileSystem();
|
|
8864
|
-
const finalPath = applyDetectedExtension(options.
|
|
9088
|
+
const finalPath = applyDetectedExtension(options.outputFile, detectedExt);
|
|
8865
9089
|
await fs2.writeFile(finalPath, bytes);
|
|
8866
9090
|
return {
|
|
8867
9091
|
path: finalPath,
|
|
@@ -8884,6 +9108,7 @@ var registerDocumentsCommand = (program2) => {
|
|
|
8884
9108
|
}
|
|
8885
9109
|
});
|
|
8886
9110
|
});
|
|
9111
|
+
addDeprecatedOptionAlias(download, "--output <path>", "--output-file");
|
|
8887
9112
|
document.command("upload").description("Upload a document file to an existing project").argument("<project-name>", "Project name").argument("<file>", "Path to the document file to upload").addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(DOCUMENTS_UPLOAD_EXAMPLES).trackedAction(processContext, async (projectName, file, options) => {
|
|
8888
9113
|
const [err, response] = await catchError((async () => {
|
|
8889
9114
|
const fs2 = getFileSystem();
|
|
@@ -8940,6 +9165,7 @@ var registerDocumentsCommand = (program2) => {
|
|
|
8940
9165
|
};
|
|
8941
9166
|
|
|
8942
9167
|
// src/commands/fields.ts
|
|
9168
|
+
var UPDATES_FLAGS = { inlineFlag: "--updates", fileFlag: "--updates-file" };
|
|
8943
9169
|
var FIELDS_ADD_EXAMPLES = [
|
|
8944
9170
|
{
|
|
8945
9171
|
Description: "Add a new field to a field group",
|
|
@@ -9011,6 +9237,18 @@ var FIELDS_UPDATE_PROMPTS_EXAMPLES = [
|
|
|
9011
9237
|
Unmatched: []
|
|
9012
9238
|
}
|
|
9013
9239
|
}
|
|
9240
|
+
},
|
|
9241
|
+
{
|
|
9242
|
+
Description: "Read the updates from a file (use this on Windows PowerShell, which strips quotes from inline JSON)",
|
|
9243
|
+
Command: "uip ixp fields update-prompts my-invoices --updates-file updates.json",
|
|
9244
|
+
Output: {
|
|
9245
|
+
Code: "IxpFieldsUpdatePrompts",
|
|
9246
|
+
Data: {
|
|
9247
|
+
ProjectName: "my-invoices",
|
|
9248
|
+
FieldsUpdated: 2,
|
|
9249
|
+
Unmatched: []
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9014
9252
|
}
|
|
9015
9253
|
];
|
|
9016
9254
|
var registerFieldsCommand = (program2) => {
|
|
@@ -9095,9 +9333,13 @@ var registerFieldsCommand = (program2) => {
|
|
|
9095
9333
|
Data: response
|
|
9096
9334
|
}, { preserveDataKeys: true });
|
|
9097
9335
|
});
|
|
9098
|
-
fields.command("update-prompts").description("Bulk-update per-field extraction instructions. Match by field name within each update's field group; existing definitions preserved.").argument("<project-name>", "Project name").
|
|
9336
|
+
const updatePrompts = fields.command("update-prompts").description("Bulk-update per-field extraction instructions. Match by field name within each update's field group; existing definitions preserved.").argument("<project-name>", "Project name").option("--updates <json>", `JSON array of per-field instruction updates: [{"group":"Invoice","name":"Invoice Number","instructions":"..."}]. "group" is the field group (label_def) name, "name" is the field (moon_form) name within that group. Each \`instructions\` is at most ${MAX_INSTRUCTIONS_LENGTH} characters, each \`name\` at most ${MAX_NAME_LENGTH}. Required unless --updates-file is given.`).addOption(new Option("--updates-file <path>", jsonFileOptionHelp("--updates")).conflicts("updates")).addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(FIELDS_UPDATE_PROMPTS_EXAMPLES).trackedAction(processContext, async (projectName, options) => {
|
|
9099
9337
|
const [err, response] = await catchError((async () => {
|
|
9100
|
-
const rawUpdates =
|
|
9338
|
+
const rawUpdates = await readJsonOption({
|
|
9339
|
+
inline: options.updates,
|
|
9340
|
+
file: options.updatesFile,
|
|
9341
|
+
...UPDATES_FLAGS
|
|
9342
|
+
});
|
|
9101
9343
|
if (!Array.isArray(rawUpdates)) {
|
|
9102
9344
|
throw new Error('--updates must be a JSON array, e.g. [{"group":"Invoice","name":"Invoice Number","instructions":"..."}]');
|
|
9103
9345
|
}
|
|
@@ -9117,16 +9359,13 @@ var registerFieldsCommand = (program2) => {
|
|
|
9117
9359
|
Data: response
|
|
9118
9360
|
}, { preserveDataKeys: true });
|
|
9119
9361
|
});
|
|
9362
|
+
requireOneOfOptions(updatePrompts, ["--updates", "--updates-file"]);
|
|
9120
9363
|
};
|
|
9121
9364
|
|
|
9122
9365
|
// src/commands/groups.ts
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
parsed = JSON.parse(raw);
|
|
9127
|
-
} catch (e) {
|
|
9128
|
-
throw new Error(`--fields is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
9129
|
-
}
|
|
9366
|
+
var FIELDS_FLAGS = { inlineFlag: "--fields", fileFlag: "--fields-file" };
|
|
9367
|
+
var UPDATES_FLAGS2 = { inlineFlag: "--updates", fileFlag: "--updates-file" };
|
|
9368
|
+
function parseFieldsJson(parsed) {
|
|
9130
9369
|
if (!Array.isArray(parsed)) {
|
|
9131
9370
|
throw new Error("--fields must be a JSON array");
|
|
9132
9371
|
}
|
|
@@ -9156,13 +9395,7 @@ function parseFieldsJson(raw) {
|
|
|
9156
9395
|
}
|
|
9157
9396
|
return result;
|
|
9158
9397
|
}
|
|
9159
|
-
function parseUpdatesJson(
|
|
9160
|
-
let parsed;
|
|
9161
|
-
try {
|
|
9162
|
-
parsed = JSON.parse(raw);
|
|
9163
|
-
} catch (e) {
|
|
9164
|
-
throw new Error(`--updates is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
9165
|
-
}
|
|
9398
|
+
function parseUpdatesJson(parsed) {
|
|
9166
9399
|
if (!Array.isArray(parsed)) {
|
|
9167
9400
|
throw new Error('--updates must be a JSON array, e.g. [{"name":"Invoice","instructions":"..."}]');
|
|
9168
9401
|
}
|
|
@@ -9194,6 +9427,18 @@ var GROUPS_ADD_EXAMPLES = [
|
|
|
9194
9427
|
FieldsCreated: 3
|
|
9195
9428
|
}
|
|
9196
9429
|
}
|
|
9430
|
+
},
|
|
9431
|
+
{
|
|
9432
|
+
Description: "Create a field group with the fields read from a file (use this on Windows PowerShell, which strips quotes from inline JSON)",
|
|
9433
|
+
Command: 'uip ixp groups add my-invoices --name Invoice --instructions "Header fields" --fields-file fields.json',
|
|
9434
|
+
Output: {
|
|
9435
|
+
Code: "IxpGroupsAdd",
|
|
9436
|
+
Data: {
|
|
9437
|
+
ProjectName: "my-invoices",
|
|
9438
|
+
Group: "Invoice",
|
|
9439
|
+
FieldsCreated: 3
|
|
9440
|
+
}
|
|
9441
|
+
}
|
|
9197
9442
|
}
|
|
9198
9443
|
];
|
|
9199
9444
|
var GROUPS_DELETE_EXAMPLES = [
|
|
@@ -9235,13 +9480,29 @@ var GROUPS_UPDATE_PROMPTS_EXAMPLES = [
|
|
|
9235
9480
|
Unmatched: []
|
|
9236
9481
|
}
|
|
9237
9482
|
}
|
|
9483
|
+
},
|
|
9484
|
+
{
|
|
9485
|
+
Description: "Read the updates from a file (use this on Windows PowerShell, which strips quotes from inline JSON)",
|
|
9486
|
+
Command: "uip ixp groups update-prompts my-invoices --updates-file updates.json",
|
|
9487
|
+
Output: {
|
|
9488
|
+
Code: "IxpGroupsUpdatePrompts",
|
|
9489
|
+
Data: {
|
|
9490
|
+
ProjectName: "my-invoices",
|
|
9491
|
+
GroupsUpdated: 1,
|
|
9492
|
+
Unmatched: []
|
|
9493
|
+
}
|
|
9494
|
+
}
|
|
9238
9495
|
}
|
|
9239
9496
|
];
|
|
9240
9497
|
var registerGroupsCommand = (program2) => {
|
|
9241
9498
|
const groups = program2.command("groups").description("Manage field groups (label_defs) — the document type containers for fields");
|
|
9242
|
-
groups.command("add").description("Create a new field group with instructions and at least one field").argument("<project-name>", "Project name").requiredOption("--name <name>", `New field group name (at most ${MAX_NAME_LENGTH} characters)`).requiredOption("--instructions <text>", `Instructions for the group — describes what kind of document/section it covers (at most ${MAX_INSTRUCTIONS_LENGTH} characters)`).
|
|
9499
|
+
const add = groups.command("add").description("Create a new field group with instructions and at least one field").argument("<project-name>", "Project name").requiredOption("--name <name>", `New field group name (at most ${MAX_NAME_LENGTH} characters)`).requiredOption("--instructions <text>", `Instructions for the group — describes what kind of document/section it covers (at most ${MAX_INSTRUCTIONS_LENGTH} characters)`).option("--fields <json>", `JSON array of fields to create in the group (at least one): [{"name":"...","type":"<type-name>","instructions":"..."}]. \`type\` is the name of an entity_def in the project taxonomy. \`name\` and \`type\` are at most ${MAX_NAME_LENGTH} characters, \`instructions\` at most ${MAX_INSTRUCTIONS_LENGTH}. Required unless --fields-file is given.`).addOption(new Option("--fields-file <path>", jsonFileOptionHelp("--fields")).conflicts("fields")).addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(GROUPS_ADD_EXAMPLES).trackedAction(processContext, async (projectName, options) => {
|
|
9243
9500
|
const [err, response] = await catchError((async () => {
|
|
9244
|
-
const fields = parseFieldsJson(
|
|
9501
|
+
const fields = parseFieldsJson(await readJsonOption({
|
|
9502
|
+
inline: options.fields,
|
|
9503
|
+
file: options.fieldsFile,
|
|
9504
|
+
...FIELDS_FLAGS
|
|
9505
|
+
}));
|
|
9245
9506
|
const config = await createIxpConfig({
|
|
9246
9507
|
tenant: options.tenant
|
|
9247
9508
|
});
|
|
@@ -9263,6 +9524,7 @@ var registerGroupsCommand = (program2) => {
|
|
|
9263
9524
|
Data: response
|
|
9264
9525
|
}, { preserveDataKeys: true });
|
|
9265
9526
|
});
|
|
9527
|
+
requireOneOfOptions(add, ["--fields", "--fields-file"]);
|
|
9266
9528
|
groups.command("delete").description("Delete a field group. IRREVERSIBLE: all annotations on all fields in the group are deleted.").argument("<project-name>", "Project name").requiredOption("--name <name>", "Field group name to delete").addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).option("-y, --yes", "Confirm this irreversible operation (required; the CLI never prompts)").examples(GROUPS_DELETE_EXAMPLES).trackedAction(processContext, async (projectName, options) => {
|
|
9267
9529
|
if (!requireConfirmation(options, `delete group '${options.name}'`))
|
|
9268
9530
|
return;
|
|
@@ -9303,9 +9565,13 @@ var registerGroupsCommand = (program2) => {
|
|
|
9303
9565
|
Data: response
|
|
9304
9566
|
}, { preserveDataKeys: true });
|
|
9305
9567
|
});
|
|
9306
|
-
groups.command("update-prompts").description("Bulk-update field group (label_def) extraction instructions. Match by group name; existing fields preserved.").argument("<project-name>", "Project name").
|
|
9568
|
+
const updatePrompts = groups.command("update-prompts").description("Bulk-update field group (label_def) extraction instructions. Match by group name; existing fields preserved.").argument("<project-name>", "Project name").option("--updates <json>", `JSON array of field group instruction updates: [{"name":"Invoice","instructions":"..."}]. Match by label_def name. Each \`instructions\` is at most ${MAX_INSTRUCTIONS_LENGTH} characters, each \`name\` at most ${MAX_NAME_LENGTH}. Required unless --updates-file is given.`).addOption(new Option("--updates-file <path>", jsonFileOptionHelp("--updates")).conflicts("updates")).addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(GROUPS_UPDATE_PROMPTS_EXAMPLES).trackedAction(processContext, async (projectName, options) => {
|
|
9307
9569
|
const [err, response] = await catchError((async () => {
|
|
9308
|
-
const updates = parseUpdatesJson(
|
|
9570
|
+
const updates = parseUpdatesJson(await readJsonOption({
|
|
9571
|
+
inline: options.updates,
|
|
9572
|
+
file: options.updatesFile,
|
|
9573
|
+
...UPDATES_FLAGS2
|
|
9574
|
+
}));
|
|
9309
9575
|
const config = await createIxpConfig({
|
|
9310
9576
|
tenant: options.tenant
|
|
9311
9577
|
});
|
|
@@ -9325,14 +9591,16 @@ var registerGroupsCommand = (program2) => {
|
|
|
9325
9591
|
Data: response
|
|
9326
9592
|
}, { preserveDataKeys: true });
|
|
9327
9593
|
});
|
|
9594
|
+
requireOneOfOptions(updatePrompts, ["--updates", "--updates-file"]);
|
|
9328
9595
|
};
|
|
9329
9596
|
|
|
9330
9597
|
// src/commands/labellings.ts
|
|
9598
|
+
var UPDATES_FLAGS3 = { inlineFlag: "--updates", fileFlag: "--updates-file" };
|
|
9331
9599
|
function validateConfirmMode(documentUid, options) {
|
|
9332
|
-
if (options.updates && options.occurrence !== undefined) {
|
|
9600
|
+
if (options.updates !== undefined && options.occurrence !== undefined) {
|
|
9333
9601
|
throw new Error("--occurrence and --updates are mutually exclusive. Use --occurrence for a single occurrence; use --updates to batch multiple occurrences in one call.");
|
|
9334
9602
|
}
|
|
9335
|
-
if (options.updates) {
|
|
9603
|
+
if (options.updates !== undefined) {
|
|
9336
9604
|
if (options.fields || options.corrections) {
|
|
9337
9605
|
throw new Error("--updates cannot be combined with --fields or --corrections. Move per-field corrections into the corrections map inside --updates.");
|
|
9338
9606
|
}
|
|
@@ -9354,8 +9622,8 @@ function validateConfirmMode(documentUid, options) {
|
|
|
9354
9622
|
return;
|
|
9355
9623
|
}
|
|
9356
9624
|
}
|
|
9357
|
-
function parseConfirmUpdates(
|
|
9358
|
-
const parsed =
|
|
9625
|
+
function parseConfirmUpdates(updates) {
|
|
9626
|
+
const parsed = updates;
|
|
9359
9627
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
9360
9628
|
throw new Error("--updates must be a non-empty JSON array of {occurrence, fields?, corrections?} entries.");
|
|
9361
9629
|
}
|
|
@@ -9416,7 +9684,7 @@ function buildConfirmBody(options) {
|
|
|
9416
9684
|
return options.modelVersion === undefined ? body : { ...body, ModelVersion: options.modelVersion };
|
|
9417
9685
|
}
|
|
9418
9686
|
function buildConfirmTargeting(options) {
|
|
9419
|
-
if (options.updates && options.group) {
|
|
9687
|
+
if (options.updates !== undefined && options.group) {
|
|
9420
9688
|
return {
|
|
9421
9689
|
Group: options.group,
|
|
9422
9690
|
Updates: parseConfirmUpdates(options.updates)
|
|
@@ -9438,10 +9706,10 @@ function buildConfirmTargeting(options) {
|
|
|
9438
9706
|
};
|
|
9439
9707
|
}
|
|
9440
9708
|
function validateUnconfirmMode(options) {
|
|
9441
|
-
if (options.updates && options.occurrence !== undefined) {
|
|
9709
|
+
if (options.updates !== undefined && options.occurrence !== undefined) {
|
|
9442
9710
|
throw new Error("--occurrence and --updates are mutually exclusive. Use --occurrence for a single occurrence; use --updates to unconfirm several occurrences in one call.");
|
|
9443
9711
|
}
|
|
9444
|
-
if (options.updates) {
|
|
9712
|
+
if (options.updates !== undefined) {
|
|
9445
9713
|
if (options.fields) {
|
|
9446
9714
|
throw new Error("--updates cannot be combined with --fields. List the field IDs inside each --updates entry instead.");
|
|
9447
9715
|
}
|
|
@@ -9457,8 +9725,8 @@ function validateUnconfirmMode(options) {
|
|
|
9457
9725
|
throw new Error("Provide --fields <ids> to unconfirm specific fields, or --group <name> to unconfirm a repeatable field group (optionally narrowed with --occurrence/--updates/--fields).");
|
|
9458
9726
|
}
|
|
9459
9727
|
}
|
|
9460
|
-
function parseUnconfirmUpdates(
|
|
9461
|
-
const parsed =
|
|
9728
|
+
function parseUnconfirmUpdates(updates) {
|
|
9729
|
+
const parsed = updates;
|
|
9462
9730
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
9463
9731
|
throw new Error("--updates must be a non-empty JSON array of {occurrence, fields?} entries.");
|
|
9464
9732
|
}
|
|
@@ -9483,7 +9751,7 @@ function parseUnconfirmUpdates(updatesJson) {
|
|
|
9483
9751
|
});
|
|
9484
9752
|
}
|
|
9485
9753
|
function buildUnconfirmBody(options) {
|
|
9486
|
-
if (options.updates && options.group) {
|
|
9754
|
+
if (options.updates !== undefined && options.group) {
|
|
9487
9755
|
return {
|
|
9488
9756
|
Group: options.group,
|
|
9489
9757
|
Updates: parseUnconfirmUpdates(options.updates)
|
|
@@ -9677,6 +9945,19 @@ var LABELLINGS_UNCONFIRM_EXAMPLES = [
|
|
|
9677
9945
|
Unmatched: []
|
|
9678
9946
|
}
|
|
9679
9947
|
}
|
|
9948
|
+
},
|
|
9949
|
+
{
|
|
9950
|
+
Description: "Batched per-occurrence unconfirm with the entries read from a file (use this on Windows PowerShell, which strips quotes from inline JSON)",
|
|
9951
|
+
Command: 'uip ixp labellings unconfirm my-invoices 5f3a9c21-7e4b-4d8a-9f12-6c0b8e3d2a14.7f3c8a21 --group "Line Items" --updates-file updates.json',
|
|
9952
|
+
Output: {
|
|
9953
|
+
Code: "IxpLabellingsUnconfirm",
|
|
9954
|
+
Data: {
|
|
9955
|
+
ProjectName: "my-invoices",
|
|
9956
|
+
DocumentId: "5f3a9c21-7e4b-4d8a-9f12-6c0b8e3d2a14.7f3c8a21",
|
|
9957
|
+
FieldsUnconfirmed: 4,
|
|
9958
|
+
Unmatched: []
|
|
9959
|
+
}
|
|
9960
|
+
}
|
|
9680
9961
|
}
|
|
9681
9962
|
];
|
|
9682
9963
|
var LABELLINGS_MARK_MISSING_EXAMPLES = [
|
|
@@ -9743,7 +10024,7 @@ async function listAllDocumentIds(config, projectName) {
|
|
|
9743
10024
|
}
|
|
9744
10025
|
var registerLabellingsCommand = (program2) => {
|
|
9745
10026
|
const labelling = program2.command("labellings").description("Manage IXP labellings and predictions");
|
|
9746
|
-
labelling.command("confirm").description("Confirm IXP-generated predictions as ground truth for one document. Omitting <document-id> confirms every document in the project — deprecated, and removed in the next release. For a repeatable field group, narrow with --group: alone it confirms every occurrence; add --occurrence to confirm ONE occurrence (the ergonomic single-line form); or use --updates to confirm SEVERAL occurrences in one atomic call (one request). --updates is the superset — --occurrence <N> equals --updates with a single entry — so reach for --occurrence for one line and --updates when several lines need confirming together (or with different per-line fields/corrections).").argument("<project-name>", "Project name").argument("[document-id]", "Document ID. Omitting it confirms every document in the project — deprecated, and removed in the next release.").addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).option("-f, --fields <field-ids>", "Comma-separated field IDs to confirm (omit to confirm all predicted fields). Without --group, confirms each field_id across every occurrence it appears in. With --group + --occurrence, confirms only within that specific occurrence.").option("-c, --corrections <json>", 'Override predicted values while keeping references: [{"field_id":"...","value":"corrected"}]. Applies across every occurrence the field appears in (or just the targeted occurrence when --group + --occurrence are given).').option("--group <name>", 'Target field group (label_def). Must be the FULL label path exactly as shown in the "Name" field of `get-predictions` (e.g. "Invoice > Line Items"), not just the leaf name. On its own, confirms every occurrence of the group as-is. Add --occurrence to target a single occurrence, or --updates to batch specific occurrences in one call. Combine with --fields to limit to specific fields within the group.').option("--occurrence <index>", "Single-occurrence form: 0-based index of ONE occurrence within --group to confirm. Use --fields to limit to specific fields in that occurrence; omit --fields to confirm every predicted field there. Requires --group. Mutually exclusive with --updates — for several occurrences in one call, use --updates instead.").option("--updates <json>", 'Batched form: confirm SEVERAL occurrences of --group in one atomic call. JSON array of {"occurrence":<0-based-index>,"fields"?:["<field_id>",...],"corrections"?:{"<field_id>":"<value>"}}. Per entry: omit "fields" to confirm every predicted field in that occurrence (same default as --occurrence without --fields), or list specific ones. Un-selected fields in a selected occurrence carry forward their existing annotation; occurrences not listed are left as-is. Equivalent to running --occurrence once per entry, but in a single request. Mutually exclusive with --fields, --corrections, and --occurrence.').option("-m, --model-version <version>", "Optimistic-concurrency guard: the model version (from `get-predictions`' ModelVersion) the predictions being confirmed were reviewed against. If a retrain has produced a newer version since, the confirm is rejected instead of stamping drifted values as ground truth — re-read predictions and review again. Omit to skip the check.", (raw) => parseBoundedInt(raw, "--model-version", {
|
|
10027
|
+
labelling.command("confirm").description("Confirm IXP-generated predictions as ground truth for one document. Omitting <document-id> confirms every document in the project — deprecated, and removed in the next release. For a repeatable field group, narrow with --group: alone it confirms every occurrence; add --occurrence to confirm ONE occurrence (the ergonomic single-line form); or use --updates to confirm SEVERAL occurrences in one atomic call (one request). --updates is the superset — --occurrence <N> equals --updates with a single entry — so reach for --occurrence for one line and --updates when several lines need confirming together (or with different per-line fields/corrections).").argument("<project-name>", "Project name").argument("[document-id]", "Document ID. Omitting it confirms every document in the project — deprecated, and removed in the next release.").addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).option("-f, --fields <field-ids>", "Comma-separated field IDs to confirm (omit to confirm all predicted fields). Without --group, confirms each field_id across every occurrence it appears in. With --group + --occurrence, confirms only within that specific occurrence.").option("-c, --corrections <json>", 'Override predicted values while keeping references: [{"field_id":"...","value":"corrected"}]. Applies across every occurrence the field appears in (or just the targeted occurrence when --group + --occurrence are given).').option("--group <name>", 'Target field group (label_def). Must be the FULL label path exactly as shown in the "Name" field of `get-predictions` (e.g. "Invoice > Line Items"), not just the leaf name. On its own, confirms every occurrence of the group as-is. Add --occurrence to target a single occurrence, or --updates to batch specific occurrences in one call. Combine with --fields to limit to specific fields within the group.').option("--occurrence <index>", "Single-occurrence form: 0-based index of ONE occurrence within --group to confirm. Use --fields to limit to specific fields in that occurrence; omit --fields to confirm every predicted field there. Requires --group. Mutually exclusive with --updates — for several occurrences in one call, use --updates instead.").option("--updates <json>", 'Batched form: confirm SEVERAL occurrences of --group in one atomic call. JSON array of {"occurrence":<0-based-index>,"fields"?:["<field_id>",...],"corrections"?:{"<field_id>":"<value>"}}. Per entry: omit "fields" to confirm every predicted field in that occurrence (same default as --occurrence without --fields), or list specific ones. Un-selected fields in a selected occurrence carry forward their existing annotation; occurrences not listed are left as-is. Equivalent to running --occurrence once per entry, but in a single request. Mutually exclusive with --fields, --corrections, and --occurrence.').addOption(new Option("--updates-file <path>", jsonFileOptionHelp("--updates")).conflicts("updates")).option("-m, --model-version <version>", "Optimistic-concurrency guard: the model version (from `get-predictions`' ModelVersion) the predictions being confirmed were reviewed against. If a retrain has produced a newer version since, the confirm is rejected instead of stamping drifted values as ground truth — re-read predictions and review again. Omit to skip the check.", (raw) => parseBoundedInt(raw, "--model-version", {
|
|
9747
10028
|
min: 0,
|
|
9748
10029
|
max: Number.MAX_SAFE_INTEGER
|
|
9749
10030
|
})).examples(LABELLINGS_CONFIRM_EXAMPLES).trackedAction(processContext, async (projectName, documentUid, options) => {
|
|
@@ -9751,11 +10032,19 @@ var registerLabellingsCommand = (program2) => {
|
|
|
9751
10032
|
getOutputSink().writeErr(PROJECT_WIDE_DEPRECATION_WARNING);
|
|
9752
10033
|
}
|
|
9753
10034
|
const [err, result] = await catchError((async () => {
|
|
9754
|
-
validateConfirmMode(documentUid,
|
|
10035
|
+
validateConfirmMode(documentUid, {
|
|
10036
|
+
...options,
|
|
10037
|
+
updates: options.updates ?? options.updatesFile
|
|
10038
|
+
});
|
|
10039
|
+
const updates = await readJsonOption({
|
|
10040
|
+
inline: options.updates,
|
|
10041
|
+
file: options.updatesFile,
|
|
10042
|
+
...UPDATES_FLAGS3
|
|
10043
|
+
});
|
|
9755
10044
|
const config = await createIxpConfig({
|
|
9756
10045
|
tenant: options.tenant
|
|
9757
10046
|
});
|
|
9758
|
-
const body = buildConfirmBody(options);
|
|
10047
|
+
const body = buildConfirmBody({ ...options, updates });
|
|
9759
10048
|
if (documentUid) {
|
|
9760
10049
|
const res = await confirmLabelling(config, projectName, documentUid, body);
|
|
9761
10050
|
return {
|
|
@@ -9790,13 +10079,21 @@ var registerLabellingsCommand = (program2) => {
|
|
|
9790
10079
|
}
|
|
9791
10080
|
});
|
|
9792
10081
|
});
|
|
9793
|
-
labelling.command("unconfirm").description("Remove confirmations from a document — the targeted fields go back to un-annotated state. Use to roll back an earlier confirm or mark-missing when you realize it was a mistake. Every other annotation on the document is carried forward. With --fields alone, each field_id is unconfirmed across every occurrence it appears in. For a repeatable field group, narrow with --group: alone it unconfirms every occurrence; add --occurrence to roll back ONE occurrence (the ergonomic single-line form); or use --updates to roll back SEVERAL occurrences in one atomic call. Mirrors `confirm`'s targeting flags.").argument("<project-name>", "Project name").argument("<document-id>", "Document ID").option("-f, --fields <field-ids>", "Comma-separated field IDs to unconfirm. Without --group, unconfirms each field_id across every occurrence it appears in. With --group + --occurrence, limits to that specific occurrence; omit it to unconfirm every annotated field in the targeted occurrence(s).").option("--group <name>", 'Target field group (label_def). Must be the FULL label path exactly as shown in the "Name" field of `get-predictions` (e.g. "Invoice > Line Items"), not just the leaf name. On its own, unconfirms every occurrence of the group. Add --occurrence to target a single occurrence, or --updates to batch specific occurrences in one call. Combine with --fields to limit to specific fields within the group.').option("--occurrence <index>", "Single-occurrence form: 0-based index of ONE occurrence within --group to unconfirm. Use --fields to limit to specific fields in that occurrence; omit --fields to unconfirm every annotated field there. Requires --group. Mutually exclusive with --updates.").option("--updates <json>", 'Batched form: unconfirm SEVERAL occurrences of --group in one atomic call. JSON array of {"occurrence":<0-based-index>,"fields"?:["<field_id>",...]}. Per entry: omit "fields" to unconfirm every annotated field in that occurrence, or list specific ones. Occurrences not listed are left as-is. Mutually exclusive with --fields and --occurrence.').addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(LABELLINGS_UNCONFIRM_EXAMPLES).trackedAction(processContext, async (projectName, documentUid, options) => {
|
|
10082
|
+
labelling.command("unconfirm").description("Remove confirmations from a document — the targeted fields go back to un-annotated state. Use to roll back an earlier confirm or mark-missing when you realize it was a mistake. Every other annotation on the document is carried forward. With --fields alone, each field_id is unconfirmed across every occurrence it appears in. For a repeatable field group, narrow with --group: alone it unconfirms every occurrence; add --occurrence to roll back ONE occurrence (the ergonomic single-line form); or use --updates to roll back SEVERAL occurrences in one atomic call. Mirrors `confirm`'s targeting flags.").argument("<project-name>", "Project name").argument("<document-id>", "Document ID").option("-f, --fields <field-ids>", "Comma-separated field IDs to unconfirm. Without --group, unconfirms each field_id across every occurrence it appears in. With --group + --occurrence, limits to that specific occurrence; omit it to unconfirm every annotated field in the targeted occurrence(s).").option("--group <name>", 'Target field group (label_def). Must be the FULL label path exactly as shown in the "Name" field of `get-predictions` (e.g. "Invoice > Line Items"), not just the leaf name. On its own, unconfirms every occurrence of the group. Add --occurrence to target a single occurrence, or --updates to batch specific occurrences in one call. Combine with --fields to limit to specific fields within the group.').option("--occurrence <index>", "Single-occurrence form: 0-based index of ONE occurrence within --group to unconfirm. Use --fields to limit to specific fields in that occurrence; omit --fields to unconfirm every annotated field there. Requires --group. Mutually exclusive with --updates.").option("--updates <json>", 'Batched form: unconfirm SEVERAL occurrences of --group in one atomic call. JSON array of {"occurrence":<0-based-index>,"fields"?:["<field_id>",...]}. Per entry: omit "fields" to unconfirm every annotated field in that occurrence, or list specific ones. Occurrences not listed are left as-is. Mutually exclusive with --fields and --occurrence.').addOption(new Option("--updates-file <path>", jsonFileOptionHelp("--updates")).conflicts("updates")).addOption(createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>")).examples(LABELLINGS_UNCONFIRM_EXAMPLES).trackedAction(processContext, async (projectName, documentUid, options) => {
|
|
9794
10083
|
const [err, result] = await catchError((async () => {
|
|
9795
|
-
validateUnconfirmMode(
|
|
10084
|
+
validateUnconfirmMode({
|
|
10085
|
+
...options,
|
|
10086
|
+
updates: options.updates ?? options.updatesFile
|
|
10087
|
+
});
|
|
10088
|
+
const updates = await readJsonOption({
|
|
10089
|
+
inline: options.updates,
|
|
10090
|
+
file: options.updatesFile,
|
|
10091
|
+
...UPDATES_FLAGS3
|
|
10092
|
+
});
|
|
9796
10093
|
const config = await createIxpConfig({
|
|
9797
10094
|
tenant: options.tenant
|
|
9798
10095
|
});
|
|
9799
|
-
return await unconfirmLabelling(config, projectName, documentUid, buildUnconfirmBody(options));
|
|
10096
|
+
return await unconfirmLabelling(config, projectName, documentUid, buildUnconfirmBody({ ...options, updates }));
|
|
9800
10097
|
})());
|
|
9801
10098
|
if (err) {
|
|
9802
10099
|
await emitError(err, "Failed to unconfirm fields");
|
|
@@ -10750,4 +11047,4 @@ var registerCommands = async (program2) => {
|
|
|
10750
11047
|
|
|
10751
11048
|
export { package_default, Command, metadata, registerCommands };
|
|
10752
11049
|
|
|
10753
|
-
//# debugId=
|
|
11050
|
+
//# debugId=2CB8F8700FFEB6D264756E2164756E21
|
package/dist/tool.js
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@uipath/ixp-tool",
|
|
3
3
|
"author": "UiPath",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.205.0-preview.256",
|
|
6
6
|
"description": "Manage UiPath IXP projects, prompts, predictions, and model publishing.",
|
|
7
7
|
"private": false,
|
|
8
8
|
"repository": {
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "63740c6cbb7ff282fa9118cc16a148eb030b245f"
|
|
31
31
|
}
|