agent-inspect 1.2.0 → 1.3.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/CHANGELOG.md +25 -3
- package/README.md +6 -5
- package/docs/ADAPTERS.md +22 -1
- package/docs/API.md +11 -3
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CLI.md +9 -1
- package/docs/EXPORTS.md +14 -0
- package/docs/LIMITATIONS.md +11 -0
- package/docs/SCHEMA.md +1 -0
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +271 -14
- package/packages/cli/dist/index.cjs.map +1 -1
- package/packages/cli/dist/index.mjs +271 -14
- package/packages/cli/dist/index.mjs.map +1 -1
- package/packages/core/dist/index.cjs +315 -14
- package/packages/core/dist/index.cjs.map +1 -1
- package/packages/core/dist/index.d.cts +51 -3
- package/packages/core/dist/index.d.ts +51 -3
- package/packages/core/dist/index.mjs +313 -15
- package/packages/core/dist/index.mjs.map +1 -1
|
@@ -467,7 +467,7 @@ function stableHash(value) {
|
|
|
467
467
|
const h = crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
468
468
|
return h.slice(0, 8);
|
|
469
469
|
}
|
|
470
|
-
function compileRules(rules) {
|
|
470
|
+
function compileRules(rules, extraKeys) {
|
|
471
471
|
const out = /* @__PURE__ */ new Map();
|
|
472
472
|
const set = (r) => {
|
|
473
473
|
const k = toKey(r.key);
|
|
@@ -476,6 +476,11 @@ function compileRules(rules) {
|
|
|
476
476
|
for (const k of DEFAULT_REDACT_KEYS) {
|
|
477
477
|
set({ key: k, strategy: "full" });
|
|
478
478
|
}
|
|
479
|
+
for (const k of extraKeys ?? []) {
|
|
480
|
+
if (typeof k === "string" && k.length > 0) {
|
|
481
|
+
set({ key: k, strategy: "full" });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
479
484
|
for (const r of rules ?? []) {
|
|
480
485
|
if (typeof r === "string") {
|
|
481
486
|
set({ key: r, strategy: "full" });
|
|
@@ -493,7 +498,7 @@ function compileRules(rules) {
|
|
|
493
498
|
var Redactor = class {
|
|
494
499
|
#rules;
|
|
495
500
|
constructor(options) {
|
|
496
|
-
this.#rules = compileRules(options?.rules);
|
|
501
|
+
this.#rules = compileRules(options?.rules, options?.extraKeys);
|
|
497
502
|
}
|
|
498
503
|
redactValue(key, value) {
|
|
499
504
|
const k = toKey(key);
|
|
@@ -1195,6 +1200,91 @@ function warn(message, error) {
|
|
|
1195
1200
|
}
|
|
1196
1201
|
console.warn(`${base}: ${formatError(error).message}`);
|
|
1197
1202
|
}
|
|
1203
|
+
|
|
1204
|
+
// packages/core/src/redaction-profiles.ts
|
|
1205
|
+
var SHARE_PROFILE_EXTRA_KEYS = [
|
|
1206
|
+
"userEmail",
|
|
1207
|
+
"customerEmail",
|
|
1208
|
+
"phone",
|
|
1209
|
+
"phoneNumber",
|
|
1210
|
+
"address",
|
|
1211
|
+
"ip",
|
|
1212
|
+
"ipAddress",
|
|
1213
|
+
"sessionId",
|
|
1214
|
+
"requestId",
|
|
1215
|
+
"correlationId",
|
|
1216
|
+
"decisionId",
|
|
1217
|
+
"groupId",
|
|
1218
|
+
"customerId",
|
|
1219
|
+
"userId",
|
|
1220
|
+
"accountId",
|
|
1221
|
+
"tenantId",
|
|
1222
|
+
"orgId",
|
|
1223
|
+
"organizationId",
|
|
1224
|
+
"traceId",
|
|
1225
|
+
"spanId",
|
|
1226
|
+
"parentSpanId"
|
|
1227
|
+
];
|
|
1228
|
+
var STRICT_PROFILE_EXTRA_KEYS = [
|
|
1229
|
+
"prompt",
|
|
1230
|
+
"completion",
|
|
1231
|
+
"input",
|
|
1232
|
+
"output",
|
|
1233
|
+
"inputPreview",
|
|
1234
|
+
"outputPreview",
|
|
1235
|
+
"message",
|
|
1236
|
+
"messages",
|
|
1237
|
+
"transcript",
|
|
1238
|
+
"context",
|
|
1239
|
+
"document",
|
|
1240
|
+
"documents",
|
|
1241
|
+
"chunk",
|
|
1242
|
+
"chunks",
|
|
1243
|
+
"retrieval",
|
|
1244
|
+
"query"
|
|
1245
|
+
];
|
|
1246
|
+
function resolveRedactionProfile(profile = "local") {
|
|
1247
|
+
switch (profile) {
|
|
1248
|
+
case "local":
|
|
1249
|
+
return { profile: "local", extraKeys: [] };
|
|
1250
|
+
case "share":
|
|
1251
|
+
return {
|
|
1252
|
+
profile: "share",
|
|
1253
|
+
extraKeys: SHARE_PROFILE_EXTRA_KEYS,
|
|
1254
|
+
maxMetadataValueLengthCap: 500,
|
|
1255
|
+
maxPreviewLengthCap: 200
|
|
1256
|
+
};
|
|
1257
|
+
case "strict":
|
|
1258
|
+
return {
|
|
1259
|
+
profile: "strict",
|
|
1260
|
+
extraKeys: [...SHARE_PROFILE_EXTRA_KEYS, ...STRICT_PROFILE_EXTRA_KEYS],
|
|
1261
|
+
maxMetadataValueLengthCap: 200,
|
|
1262
|
+
maxPreviewLengthCap: 80
|
|
1263
|
+
};
|
|
1264
|
+
default:
|
|
1265
|
+
return { profile: "local", extraKeys: [] };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function isPreviewKey(key) {
|
|
1269
|
+
return key.toLowerCase().includes("preview");
|
|
1270
|
+
}
|
|
1271
|
+
function applyProfileMetadataCaps(maxMetadataValueLength, maxPreviewLength, resolved) {
|
|
1272
|
+
let meta = maxMetadataValueLength;
|
|
1273
|
+
let preview = maxPreviewLength;
|
|
1274
|
+
if (resolved.maxMetadataValueLengthCap !== void 0) {
|
|
1275
|
+
meta = Math.min(meta, resolved.maxMetadataValueLengthCap);
|
|
1276
|
+
}
|
|
1277
|
+
if (resolved.maxPreviewLengthCap !== void 0) {
|
|
1278
|
+
preview = Math.min(preview, resolved.maxPreviewLengthCap);
|
|
1279
|
+
}
|
|
1280
|
+
return { maxMetadataValueLength: meta, maxPreviewLength: preview };
|
|
1281
|
+
}
|
|
1282
|
+
function truncateStringForProfile(value, key, maxMetadataValueLength, maxPreviewLength) {
|
|
1283
|
+
const max = isPreviewKey(key) ? maxPreviewLength : maxMetadataValueLength;
|
|
1284
|
+
if (max <= 0) return "\u2026";
|
|
1285
|
+
if (value.length <= max) return value;
|
|
1286
|
+
return `${value.slice(0, max)}\u2026`;
|
|
1287
|
+
}
|
|
1198
1288
|
function isRecord6(value) {
|
|
1199
1289
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1200
1290
|
}
|
|
@@ -1345,7 +1435,7 @@ var DEFAULT_MAX_EVENT_BYTES = 65536;
|
|
|
1345
1435
|
function isRecord7(value) {
|
|
1346
1436
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1347
1437
|
}
|
|
1348
|
-
function
|
|
1438
|
+
function isPreviewKey2(key) {
|
|
1349
1439
|
return key.toLowerCase().includes("preview");
|
|
1350
1440
|
}
|
|
1351
1441
|
function truncateString(value, maxLen) {
|
|
@@ -1362,11 +1452,28 @@ function resolveTraceSafetyOptions(options) {
|
|
|
1362
1452
|
{
|
|
1363
1453
|
redactEnabled = true;
|
|
1364
1454
|
}
|
|
1455
|
+
const profile = options?.redactionProfile ?? "local";
|
|
1456
|
+
const resolvedProfile = resolveRedactionProfile(profile);
|
|
1457
|
+
const userMaxMetadata = "undefined" === "number" && Number.isFinite(options.maxMetadataValueLength) && options.maxMetadataValueLength >= 0 ? Math.floor(options.maxMetadataValueLength) : void 0;
|
|
1458
|
+
const userMaxPreview = "undefined" === "number" && Number.isFinite(options.maxPreviewLength) && options.maxPreviewLength >= 0 ? Math.floor(options.maxPreviewLength) : void 0;
|
|
1459
|
+
let maxMetadataValueLength = userMaxMetadata ?? DEFAULT_MAX_METADATA_VALUE_LENGTH;
|
|
1460
|
+
let maxPreviewLength = userMaxPreview ?? DEFAULT_MAX_PREVIEW_LENGTH;
|
|
1461
|
+
if (redactEnabled && profile !== "local") {
|
|
1462
|
+
const capped = applyProfileMetadataCaps(
|
|
1463
|
+
maxMetadataValueLength,
|
|
1464
|
+
maxPreviewLength,
|
|
1465
|
+
resolvedProfile
|
|
1466
|
+
);
|
|
1467
|
+
maxMetadataValueLength = capped.maxMetadataValueLength;
|
|
1468
|
+
maxPreviewLength = capped.maxPreviewLength;
|
|
1469
|
+
}
|
|
1365
1470
|
return {
|
|
1366
1471
|
redactEnabled,
|
|
1367
1472
|
redactionRules,
|
|
1368
|
-
|
|
1369
|
-
|
|
1473
|
+
redactionProfile: profile,
|
|
1474
|
+
profileExtraKeys: redactEnabled ? resolvedProfile.extraKeys : [],
|
|
1475
|
+
maxMetadataValueLength,
|
|
1476
|
+
maxPreviewLength,
|
|
1370
1477
|
maxEventBytes: "undefined" === "number" && Number.isFinite(options.maxEventBytes) && options.maxEventBytes > 0 ? Math.floor(options.maxEventBytes) : DEFAULT_MAX_EVENT_BYTES
|
|
1371
1478
|
};
|
|
1372
1479
|
}
|
|
@@ -1374,7 +1481,7 @@ function boundMetadataValue(key, value, opts, seen, depth) {
|
|
|
1374
1481
|
if (depth > 32) return "[MaxDepth]";
|
|
1375
1482
|
if (value === null || typeof value !== "object") {
|
|
1376
1483
|
if (typeof value === "string") {
|
|
1377
|
-
const max =
|
|
1484
|
+
const max = isPreviewKey2(key) ? opts.maxPreviewLength : opts.maxMetadataValueLength;
|
|
1378
1485
|
return truncateString(value, max);
|
|
1379
1486
|
}
|
|
1380
1487
|
return value;
|
|
@@ -1400,7 +1507,10 @@ function boundMetadataValue(key, value, opts, seen, depth) {
|
|
|
1400
1507
|
}
|
|
1401
1508
|
function redactMetadata(metadata, opts) {
|
|
1402
1509
|
if (!opts.redactEnabled) return { ...metadata };
|
|
1403
|
-
const redactor = new Redactor({
|
|
1510
|
+
const redactor = new Redactor({
|
|
1511
|
+
rules: opts.redactionRules,
|
|
1512
|
+
extraKeys: opts.profileExtraKeys
|
|
1513
|
+
});
|
|
1404
1514
|
return redactor.redactRecord(metadata);
|
|
1405
1515
|
}
|
|
1406
1516
|
function prepareMetadataForDisk(metadata, opts) {
|
|
@@ -3207,6 +3317,134 @@ Object.assign(stepImpl, {
|
|
|
3207
3317
|
// packages/core/src/exporters/types.ts
|
|
3208
3318
|
var EXPORT_PAYLOAD_VERSION = "0.1.2";
|
|
3209
3319
|
|
|
3320
|
+
// packages/core/src/exporters/redact-export.ts
|
|
3321
|
+
function isRecord9(value) {
|
|
3322
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3323
|
+
}
|
|
3324
|
+
function deepClone(value) {
|
|
3325
|
+
if (value === null || typeof value !== "object") {
|
|
3326
|
+
return value;
|
|
3327
|
+
}
|
|
3328
|
+
if (Array.isArray(value)) {
|
|
3329
|
+
return value.map((item) => deepClone(item));
|
|
3330
|
+
}
|
|
3331
|
+
const out = {};
|
|
3332
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3333
|
+
out[k] = deepClone(v);
|
|
3334
|
+
}
|
|
3335
|
+
return out;
|
|
3336
|
+
}
|
|
3337
|
+
function boundAttributeValues(record, maxMetadataValueLength, maxPreviewLength, seen, depth) {
|
|
3338
|
+
if (depth > 32) {
|
|
3339
|
+
return { truncated: true, reason: "maxDepth" };
|
|
3340
|
+
}
|
|
3341
|
+
const out = {};
|
|
3342
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3343
|
+
out[key] = boundValue(value, key, maxMetadataValueLength, maxPreviewLength, seen, depth);
|
|
3344
|
+
}
|
|
3345
|
+
return out;
|
|
3346
|
+
}
|
|
3347
|
+
function boundValue(value, key, maxMetadataValueLength, maxPreviewLength, seen, depth) {
|
|
3348
|
+
if (value === null || typeof value !== "object") {
|
|
3349
|
+
if (typeof value === "string") {
|
|
3350
|
+
return truncateStringForProfile(
|
|
3351
|
+
value,
|
|
3352
|
+
key,
|
|
3353
|
+
maxMetadataValueLength,
|
|
3354
|
+
maxPreviewLength
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3357
|
+
return value;
|
|
3358
|
+
}
|
|
3359
|
+
if (seen.has(value)) return "[Circular]";
|
|
3360
|
+
seen.add(value);
|
|
3361
|
+
if (Array.isArray(value)) {
|
|
3362
|
+
return value.slice(0, 50).map(
|
|
3363
|
+
(item, index) => boundValue(
|
|
3364
|
+
item,
|
|
3365
|
+
String(index),
|
|
3366
|
+
maxMetadataValueLength,
|
|
3367
|
+
maxPreviewLength,
|
|
3368
|
+
seen,
|
|
3369
|
+
depth + 1
|
|
3370
|
+
)
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
return boundAttributeValues(
|
|
3374
|
+
value,
|
|
3375
|
+
maxMetadataValueLength,
|
|
3376
|
+
maxPreviewLength,
|
|
3377
|
+
seen,
|
|
3378
|
+
depth + 1
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
function redactEventAttributes(attrs, redactor, maxMetadataValueLength, maxPreviewLength) {
|
|
3382
|
+
if (!attrs || Object.keys(attrs).length === 0) {
|
|
3383
|
+
return attrs;
|
|
3384
|
+
}
|
|
3385
|
+
const redacted = redactor.redactRecord(attrs);
|
|
3386
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3387
|
+
const bounded = boundAttributeValues(
|
|
3388
|
+
redacted,
|
|
3389
|
+
maxMetadataValueLength,
|
|
3390
|
+
maxPreviewLength,
|
|
3391
|
+
seen,
|
|
3392
|
+
0
|
|
3393
|
+
);
|
|
3394
|
+
const err = bounded.error;
|
|
3395
|
+
if (isRecord9(err) && typeof err.message === "string") {
|
|
3396
|
+
bounded.error = {
|
|
3397
|
+
...err,
|
|
3398
|
+
message: truncateStringForProfile(
|
|
3399
|
+
err.message,
|
|
3400
|
+
"message",
|
|
3401
|
+
maxMetadataValueLength,
|
|
3402
|
+
maxPreviewLength
|
|
3403
|
+
),
|
|
3404
|
+
...typeof err.stack === "string" ? {
|
|
3405
|
+
stack: truncateStringForProfile(
|
|
3406
|
+
err.stack,
|
|
3407
|
+
"stack",
|
|
3408
|
+
maxMetadataValueLength,
|
|
3409
|
+
maxPreviewLength
|
|
3410
|
+
)
|
|
3411
|
+
} : {}
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
return bounded;
|
|
3415
|
+
}
|
|
3416
|
+
function redactRunTreeForExport(tree, options) {
|
|
3417
|
+
const profile = options?.redactionProfile ?? "local";
|
|
3418
|
+
if (profile === "local") {
|
|
3419
|
+
return deepClone(tree);
|
|
3420
|
+
}
|
|
3421
|
+
const resolved = resolveRedactionProfile(profile);
|
|
3422
|
+
const { maxMetadataValueLength, maxPreviewLength } = applyProfileMetadataCaps(
|
|
3423
|
+
2e3,
|
|
3424
|
+
500,
|
|
3425
|
+
resolved
|
|
3426
|
+
);
|
|
3427
|
+
const redactor = new Redactor({ extraKeys: resolved.extraKeys });
|
|
3428
|
+
const clone = deepClone(tree);
|
|
3429
|
+
function walk(nodes) {
|
|
3430
|
+
for (const node of nodes) {
|
|
3431
|
+
if (node.event.attributes !== void 0) {
|
|
3432
|
+
node.event.attributes = redactEventAttributes(
|
|
3433
|
+
node.event.attributes,
|
|
3434
|
+
redactor,
|
|
3435
|
+
maxMetadataValueLength,
|
|
3436
|
+
maxPreviewLength
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
if (node.children.length > 0) {
|
|
3440
|
+
walk(node.children);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
walk(clone.children);
|
|
3445
|
+
return clone;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3210
3448
|
// packages/core/src/exporters/html-exporter.ts
|
|
3211
3449
|
function renderTreeHtml(nodes, ulClass = "tree") {
|
|
3212
3450
|
if (nodes.length === 0) return "";
|
|
@@ -3941,20 +4179,22 @@ function mergeExportDefaults(options) {
|
|
|
3941
4179
|
includeErrors: options.includeErrors ?? true,
|
|
3942
4180
|
pretty: options.pretty,
|
|
3943
4181
|
redacted: options.redacted,
|
|
3944
|
-
maxAttributeLength: options.maxAttributeLength
|
|
4182
|
+
maxAttributeLength: options.maxAttributeLength,
|
|
4183
|
+
redactionProfile: options.redactionProfile ?? "local"
|
|
3945
4184
|
};
|
|
3946
4185
|
}
|
|
3947
4186
|
function exportRunTree(tree, options) {
|
|
3948
4187
|
const opts = mergeExportDefaults(options);
|
|
4188
|
+
const exportTree = opts.redactionProfile === "local" ? tree : redactRunTreeForExport(tree, { redactionProfile: opts.redactionProfile });
|
|
3949
4189
|
switch (opts.format) {
|
|
3950
4190
|
case "markdown":
|
|
3951
|
-
return exportMarkdown(
|
|
4191
|
+
return exportMarkdown(exportTree, opts);
|
|
3952
4192
|
case "html":
|
|
3953
|
-
return exportHtml(
|
|
4193
|
+
return exportHtml(exportTree, opts);
|
|
3954
4194
|
case "openinference":
|
|
3955
|
-
return exportOpenInference(
|
|
4195
|
+
return exportOpenInference(exportTree, opts);
|
|
3956
4196
|
case "otlp-json":
|
|
3957
|
-
return exportOtlpJson(
|
|
4197
|
+
return exportOtlpJson(exportTree, opts);
|
|
3958
4198
|
default: {
|
|
3959
4199
|
const _x = opts.format;
|
|
3960
4200
|
throw new Error(`Unsupported export format: ${String(_x)}`);
|
|
@@ -4790,6 +5030,15 @@ async function tail(options = {}) {
|
|
|
4790
5030
|
process.exitCode = 1;
|
|
4791
5031
|
}
|
|
4792
5032
|
}
|
|
5033
|
+
function parseRedactionProfile(s) {
|
|
5034
|
+
const v = (s ?? "local").trim().toLowerCase();
|
|
5035
|
+
if (v === "local" || v === "share" || v === "strict") {
|
|
5036
|
+
return v;
|
|
5037
|
+
}
|
|
5038
|
+
throw new Error(
|
|
5039
|
+
`Unsupported --redaction-profile "${s ?? ""}". Use local, share, or strict.`
|
|
5040
|
+
);
|
|
5041
|
+
}
|
|
4793
5042
|
function parseExportFormat(s) {
|
|
4794
5043
|
const v = (s ?? "markdown").trim().toLowerCase();
|
|
4795
5044
|
if (v === "markdown" || v === "html" || v === "openinference" || v === "otlp-json") {
|
|
@@ -4807,8 +5056,10 @@ async function exportCommand(runId, options = {}) {
|
|
|
4807
5056
|
return;
|
|
4808
5057
|
}
|
|
4809
5058
|
let format;
|
|
5059
|
+
let redactionProfile;
|
|
4810
5060
|
try {
|
|
4811
5061
|
format = parseExportFormat(options.format);
|
|
5062
|
+
redactionProfile = parseRedactionProfile(options.redactionProfile);
|
|
4812
5063
|
} catch (e) {
|
|
4813
5064
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4814
5065
|
console.error(msg);
|
|
@@ -4847,7 +5098,8 @@ Trace directory: ${traceDir}`);
|
|
|
4847
5098
|
includeErrors: options.noErrors === true ? false : true,
|
|
4848
5099
|
pretty: true,
|
|
4849
5100
|
redacted: true,
|
|
4850
|
-
maxAttributeLength: 500
|
|
5101
|
+
maxAttributeLength: 500,
|
|
5102
|
+
redactionProfile
|
|
4851
5103
|
};
|
|
4852
5104
|
const result = exportRunTree(tree, exportOpts);
|
|
4853
5105
|
const validation = options.validate === true ? validateExport(result) : void 0;
|
|
@@ -5080,7 +5332,12 @@ function createCliProgram() {
|
|
|
5080
5332
|
"openinference",
|
|
5081
5333
|
"otlp-json"
|
|
5082
5334
|
])
|
|
5083
|
-
).option("-o, --output <path>", "write export to file (creates parent dirs)").option("--json", "emit JSON wrapper about the export (includes content when writing to stdout)").option("--validate", "validate exported payload shape after generation").option("--include-attributes", "include bounded attributes (review before sharing)").option("--no-metadata", "omit summary / metadata sections").option("--no-errors", "omit error sections").
|
|
5335
|
+
).option("-o, --output <path>", "write export to file (creates parent dirs)").option("--json", "emit JSON wrapper about the export (includes content when writing to stdout)").option("--validate", "validate exported payload shape after generation").option("--include-attributes", "include bounded attributes (review before sharing)").option("--no-metadata", "omit summary / metadata sections").option("--no-errors", "omit error sections").addOption(
|
|
5336
|
+
new Option(
|
|
5337
|
+
"--redaction-profile <profile>",
|
|
5338
|
+
"redaction profile for exported copies: local, share, strict (default: local)"
|
|
5339
|
+
).choices(["local", "share", "strict"])
|
|
5340
|
+
).action((runId, opts) => {
|
|
5084
5341
|
runCommand(() => exportCommand(runId, opts));
|
|
5085
5342
|
});
|
|
5086
5343
|
program.command("diff").description("Compare two local AgentInspect JSONL traces (read-only)").argument("<left-run-id>", "first run id").argument("<right-run-id>", "second run id").option("--dir <path>", "trace directory").option("--json", "print diff result as JSON").option("--ignore-duration", "omit duration comparisons").option(
|