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
|
@@ -478,7 +478,7 @@ function stableHash(value) {
|
|
|
478
478
|
const h = crypto__default.default.createHash("sha256").update(value, "utf8").digest("hex");
|
|
479
479
|
return h.slice(0, 8);
|
|
480
480
|
}
|
|
481
|
-
function compileRules(rules) {
|
|
481
|
+
function compileRules(rules, extraKeys) {
|
|
482
482
|
const out = /* @__PURE__ */ new Map();
|
|
483
483
|
const set = (r) => {
|
|
484
484
|
const k = toKey(r.key);
|
|
@@ -487,6 +487,11 @@ function compileRules(rules) {
|
|
|
487
487
|
for (const k of DEFAULT_REDACT_KEYS) {
|
|
488
488
|
set({ key: k, strategy: "full" });
|
|
489
489
|
}
|
|
490
|
+
for (const k of extraKeys ?? []) {
|
|
491
|
+
if (typeof k === "string" && k.length > 0) {
|
|
492
|
+
set({ key: k, strategy: "full" });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
490
495
|
for (const r of rules ?? []) {
|
|
491
496
|
if (typeof r === "string") {
|
|
492
497
|
set({ key: r, strategy: "full" });
|
|
@@ -504,7 +509,7 @@ function compileRules(rules) {
|
|
|
504
509
|
var Redactor = class {
|
|
505
510
|
#rules;
|
|
506
511
|
constructor(options) {
|
|
507
|
-
this.#rules = compileRules(options?.rules);
|
|
512
|
+
this.#rules = compileRules(options?.rules, options?.extraKeys);
|
|
508
513
|
}
|
|
509
514
|
redactValue(key, value) {
|
|
510
515
|
const k = toKey(key);
|
|
@@ -1206,6 +1211,91 @@ function warn(message, error) {
|
|
|
1206
1211
|
}
|
|
1207
1212
|
console.warn(`${base}: ${formatError(error).message}`);
|
|
1208
1213
|
}
|
|
1214
|
+
|
|
1215
|
+
// packages/core/src/redaction-profiles.ts
|
|
1216
|
+
var SHARE_PROFILE_EXTRA_KEYS = [
|
|
1217
|
+
"userEmail",
|
|
1218
|
+
"customerEmail",
|
|
1219
|
+
"phone",
|
|
1220
|
+
"phoneNumber",
|
|
1221
|
+
"address",
|
|
1222
|
+
"ip",
|
|
1223
|
+
"ipAddress",
|
|
1224
|
+
"sessionId",
|
|
1225
|
+
"requestId",
|
|
1226
|
+
"correlationId",
|
|
1227
|
+
"decisionId",
|
|
1228
|
+
"groupId",
|
|
1229
|
+
"customerId",
|
|
1230
|
+
"userId",
|
|
1231
|
+
"accountId",
|
|
1232
|
+
"tenantId",
|
|
1233
|
+
"orgId",
|
|
1234
|
+
"organizationId",
|
|
1235
|
+
"traceId",
|
|
1236
|
+
"spanId",
|
|
1237
|
+
"parentSpanId"
|
|
1238
|
+
];
|
|
1239
|
+
var STRICT_PROFILE_EXTRA_KEYS = [
|
|
1240
|
+
"prompt",
|
|
1241
|
+
"completion",
|
|
1242
|
+
"input",
|
|
1243
|
+
"output",
|
|
1244
|
+
"inputPreview",
|
|
1245
|
+
"outputPreview",
|
|
1246
|
+
"message",
|
|
1247
|
+
"messages",
|
|
1248
|
+
"transcript",
|
|
1249
|
+
"context",
|
|
1250
|
+
"document",
|
|
1251
|
+
"documents",
|
|
1252
|
+
"chunk",
|
|
1253
|
+
"chunks",
|
|
1254
|
+
"retrieval",
|
|
1255
|
+
"query"
|
|
1256
|
+
];
|
|
1257
|
+
function resolveRedactionProfile(profile = "local") {
|
|
1258
|
+
switch (profile) {
|
|
1259
|
+
case "local":
|
|
1260
|
+
return { profile: "local", extraKeys: [] };
|
|
1261
|
+
case "share":
|
|
1262
|
+
return {
|
|
1263
|
+
profile: "share",
|
|
1264
|
+
extraKeys: SHARE_PROFILE_EXTRA_KEYS,
|
|
1265
|
+
maxMetadataValueLengthCap: 500,
|
|
1266
|
+
maxPreviewLengthCap: 200
|
|
1267
|
+
};
|
|
1268
|
+
case "strict":
|
|
1269
|
+
return {
|
|
1270
|
+
profile: "strict",
|
|
1271
|
+
extraKeys: [...SHARE_PROFILE_EXTRA_KEYS, ...STRICT_PROFILE_EXTRA_KEYS],
|
|
1272
|
+
maxMetadataValueLengthCap: 200,
|
|
1273
|
+
maxPreviewLengthCap: 80
|
|
1274
|
+
};
|
|
1275
|
+
default:
|
|
1276
|
+
return { profile: "local", extraKeys: [] };
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function isPreviewKey(key) {
|
|
1280
|
+
return key.toLowerCase().includes("preview");
|
|
1281
|
+
}
|
|
1282
|
+
function applyProfileMetadataCaps(maxMetadataValueLength, maxPreviewLength, resolved) {
|
|
1283
|
+
let meta = maxMetadataValueLength;
|
|
1284
|
+
let preview = maxPreviewLength;
|
|
1285
|
+
if (resolved.maxMetadataValueLengthCap !== void 0) {
|
|
1286
|
+
meta = Math.min(meta, resolved.maxMetadataValueLengthCap);
|
|
1287
|
+
}
|
|
1288
|
+
if (resolved.maxPreviewLengthCap !== void 0) {
|
|
1289
|
+
preview = Math.min(preview, resolved.maxPreviewLengthCap);
|
|
1290
|
+
}
|
|
1291
|
+
return { maxMetadataValueLength: meta, maxPreviewLength: preview };
|
|
1292
|
+
}
|
|
1293
|
+
function truncateStringForProfile(value, key, maxMetadataValueLength, maxPreviewLength) {
|
|
1294
|
+
const max = isPreviewKey(key) ? maxPreviewLength : maxMetadataValueLength;
|
|
1295
|
+
if (max <= 0) return "\u2026";
|
|
1296
|
+
if (value.length <= max) return value;
|
|
1297
|
+
return `${value.slice(0, max)}\u2026`;
|
|
1298
|
+
}
|
|
1209
1299
|
function isRecord6(value) {
|
|
1210
1300
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1211
1301
|
}
|
|
@@ -1356,7 +1446,7 @@ var DEFAULT_MAX_EVENT_BYTES = 65536;
|
|
|
1356
1446
|
function isRecord7(value) {
|
|
1357
1447
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1358
1448
|
}
|
|
1359
|
-
function
|
|
1449
|
+
function isPreviewKey2(key) {
|
|
1360
1450
|
return key.toLowerCase().includes("preview");
|
|
1361
1451
|
}
|
|
1362
1452
|
function truncateString(value, maxLen) {
|
|
@@ -1373,11 +1463,28 @@ function resolveTraceSafetyOptions(options) {
|
|
|
1373
1463
|
{
|
|
1374
1464
|
redactEnabled = true;
|
|
1375
1465
|
}
|
|
1466
|
+
const profile = options?.redactionProfile ?? "local";
|
|
1467
|
+
const resolvedProfile = resolveRedactionProfile(profile);
|
|
1468
|
+
const userMaxMetadata = "undefined" === "number" && Number.isFinite(options.maxMetadataValueLength) && options.maxMetadataValueLength >= 0 ? Math.floor(options.maxMetadataValueLength) : void 0;
|
|
1469
|
+
const userMaxPreview = "undefined" === "number" && Number.isFinite(options.maxPreviewLength) && options.maxPreviewLength >= 0 ? Math.floor(options.maxPreviewLength) : void 0;
|
|
1470
|
+
let maxMetadataValueLength = userMaxMetadata ?? DEFAULT_MAX_METADATA_VALUE_LENGTH;
|
|
1471
|
+
let maxPreviewLength = userMaxPreview ?? DEFAULT_MAX_PREVIEW_LENGTH;
|
|
1472
|
+
if (redactEnabled && profile !== "local") {
|
|
1473
|
+
const capped = applyProfileMetadataCaps(
|
|
1474
|
+
maxMetadataValueLength,
|
|
1475
|
+
maxPreviewLength,
|
|
1476
|
+
resolvedProfile
|
|
1477
|
+
);
|
|
1478
|
+
maxMetadataValueLength = capped.maxMetadataValueLength;
|
|
1479
|
+
maxPreviewLength = capped.maxPreviewLength;
|
|
1480
|
+
}
|
|
1376
1481
|
return {
|
|
1377
1482
|
redactEnabled,
|
|
1378
1483
|
redactionRules,
|
|
1379
|
-
|
|
1380
|
-
|
|
1484
|
+
redactionProfile: profile,
|
|
1485
|
+
profileExtraKeys: redactEnabled ? resolvedProfile.extraKeys : [],
|
|
1486
|
+
maxMetadataValueLength,
|
|
1487
|
+
maxPreviewLength,
|
|
1381
1488
|
maxEventBytes: "undefined" === "number" && Number.isFinite(options.maxEventBytes) && options.maxEventBytes > 0 ? Math.floor(options.maxEventBytes) : DEFAULT_MAX_EVENT_BYTES
|
|
1382
1489
|
};
|
|
1383
1490
|
}
|
|
@@ -1385,7 +1492,7 @@ function boundMetadataValue(key, value, opts, seen, depth) {
|
|
|
1385
1492
|
if (depth > 32) return "[MaxDepth]";
|
|
1386
1493
|
if (value === null || typeof value !== "object") {
|
|
1387
1494
|
if (typeof value === "string") {
|
|
1388
|
-
const max =
|
|
1495
|
+
const max = isPreviewKey2(key) ? opts.maxPreviewLength : opts.maxMetadataValueLength;
|
|
1389
1496
|
return truncateString(value, max);
|
|
1390
1497
|
}
|
|
1391
1498
|
return value;
|
|
@@ -1411,7 +1518,10 @@ function boundMetadataValue(key, value, opts, seen, depth) {
|
|
|
1411
1518
|
}
|
|
1412
1519
|
function redactMetadata(metadata, opts) {
|
|
1413
1520
|
if (!opts.redactEnabled) return { ...metadata };
|
|
1414
|
-
const redactor = new Redactor({
|
|
1521
|
+
const redactor = new Redactor({
|
|
1522
|
+
rules: opts.redactionRules,
|
|
1523
|
+
extraKeys: opts.profileExtraKeys
|
|
1524
|
+
});
|
|
1415
1525
|
return redactor.redactRecord(metadata);
|
|
1416
1526
|
}
|
|
1417
1527
|
function prepareMetadataForDisk(metadata, opts) {
|
|
@@ -3218,6 +3328,134 @@ Object.assign(stepImpl, {
|
|
|
3218
3328
|
// packages/core/src/exporters/types.ts
|
|
3219
3329
|
var EXPORT_PAYLOAD_VERSION = "0.1.2";
|
|
3220
3330
|
|
|
3331
|
+
// packages/core/src/exporters/redact-export.ts
|
|
3332
|
+
function isRecord9(value) {
|
|
3333
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3334
|
+
}
|
|
3335
|
+
function deepClone(value) {
|
|
3336
|
+
if (value === null || typeof value !== "object") {
|
|
3337
|
+
return value;
|
|
3338
|
+
}
|
|
3339
|
+
if (Array.isArray(value)) {
|
|
3340
|
+
return value.map((item) => deepClone(item));
|
|
3341
|
+
}
|
|
3342
|
+
const out = {};
|
|
3343
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3344
|
+
out[k] = deepClone(v);
|
|
3345
|
+
}
|
|
3346
|
+
return out;
|
|
3347
|
+
}
|
|
3348
|
+
function boundAttributeValues(record, maxMetadataValueLength, maxPreviewLength, seen, depth) {
|
|
3349
|
+
if (depth > 32) {
|
|
3350
|
+
return { truncated: true, reason: "maxDepth" };
|
|
3351
|
+
}
|
|
3352
|
+
const out = {};
|
|
3353
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3354
|
+
out[key] = boundValue(value, key, maxMetadataValueLength, maxPreviewLength, seen, depth);
|
|
3355
|
+
}
|
|
3356
|
+
return out;
|
|
3357
|
+
}
|
|
3358
|
+
function boundValue(value, key, maxMetadataValueLength, maxPreviewLength, seen, depth) {
|
|
3359
|
+
if (value === null || typeof value !== "object") {
|
|
3360
|
+
if (typeof value === "string") {
|
|
3361
|
+
return truncateStringForProfile(
|
|
3362
|
+
value,
|
|
3363
|
+
key,
|
|
3364
|
+
maxMetadataValueLength,
|
|
3365
|
+
maxPreviewLength
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
return value;
|
|
3369
|
+
}
|
|
3370
|
+
if (seen.has(value)) return "[Circular]";
|
|
3371
|
+
seen.add(value);
|
|
3372
|
+
if (Array.isArray(value)) {
|
|
3373
|
+
return value.slice(0, 50).map(
|
|
3374
|
+
(item, index) => boundValue(
|
|
3375
|
+
item,
|
|
3376
|
+
String(index),
|
|
3377
|
+
maxMetadataValueLength,
|
|
3378
|
+
maxPreviewLength,
|
|
3379
|
+
seen,
|
|
3380
|
+
depth + 1
|
|
3381
|
+
)
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
return boundAttributeValues(
|
|
3385
|
+
value,
|
|
3386
|
+
maxMetadataValueLength,
|
|
3387
|
+
maxPreviewLength,
|
|
3388
|
+
seen,
|
|
3389
|
+
depth + 1
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
function redactEventAttributes(attrs, redactor, maxMetadataValueLength, maxPreviewLength) {
|
|
3393
|
+
if (!attrs || Object.keys(attrs).length === 0) {
|
|
3394
|
+
return attrs;
|
|
3395
|
+
}
|
|
3396
|
+
const redacted = redactor.redactRecord(attrs);
|
|
3397
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3398
|
+
const bounded = boundAttributeValues(
|
|
3399
|
+
redacted,
|
|
3400
|
+
maxMetadataValueLength,
|
|
3401
|
+
maxPreviewLength,
|
|
3402
|
+
seen,
|
|
3403
|
+
0
|
|
3404
|
+
);
|
|
3405
|
+
const err = bounded.error;
|
|
3406
|
+
if (isRecord9(err) && typeof err.message === "string") {
|
|
3407
|
+
bounded.error = {
|
|
3408
|
+
...err,
|
|
3409
|
+
message: truncateStringForProfile(
|
|
3410
|
+
err.message,
|
|
3411
|
+
"message",
|
|
3412
|
+
maxMetadataValueLength,
|
|
3413
|
+
maxPreviewLength
|
|
3414
|
+
),
|
|
3415
|
+
...typeof err.stack === "string" ? {
|
|
3416
|
+
stack: truncateStringForProfile(
|
|
3417
|
+
err.stack,
|
|
3418
|
+
"stack",
|
|
3419
|
+
maxMetadataValueLength,
|
|
3420
|
+
maxPreviewLength
|
|
3421
|
+
)
|
|
3422
|
+
} : {}
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
return bounded;
|
|
3426
|
+
}
|
|
3427
|
+
function redactRunTreeForExport(tree, options) {
|
|
3428
|
+
const profile = options?.redactionProfile ?? "local";
|
|
3429
|
+
if (profile === "local") {
|
|
3430
|
+
return deepClone(tree);
|
|
3431
|
+
}
|
|
3432
|
+
const resolved = resolveRedactionProfile(profile);
|
|
3433
|
+
const { maxMetadataValueLength, maxPreviewLength } = applyProfileMetadataCaps(
|
|
3434
|
+
2e3,
|
|
3435
|
+
500,
|
|
3436
|
+
resolved
|
|
3437
|
+
);
|
|
3438
|
+
const redactor = new Redactor({ extraKeys: resolved.extraKeys });
|
|
3439
|
+
const clone = deepClone(tree);
|
|
3440
|
+
function walk(nodes) {
|
|
3441
|
+
for (const node of nodes) {
|
|
3442
|
+
if (node.event.attributes !== void 0) {
|
|
3443
|
+
node.event.attributes = redactEventAttributes(
|
|
3444
|
+
node.event.attributes,
|
|
3445
|
+
redactor,
|
|
3446
|
+
maxMetadataValueLength,
|
|
3447
|
+
maxPreviewLength
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
if (node.children.length > 0) {
|
|
3451
|
+
walk(node.children);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
walk(clone.children);
|
|
3456
|
+
return clone;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3221
3459
|
// packages/core/src/exporters/html-exporter.ts
|
|
3222
3460
|
function renderTreeHtml(nodes, ulClass = "tree") {
|
|
3223
3461
|
if (nodes.length === 0) return "";
|
|
@@ -3952,20 +4190,22 @@ function mergeExportDefaults(options) {
|
|
|
3952
4190
|
includeErrors: options.includeErrors ?? true,
|
|
3953
4191
|
pretty: options.pretty,
|
|
3954
4192
|
redacted: options.redacted,
|
|
3955
|
-
maxAttributeLength: options.maxAttributeLength
|
|
4193
|
+
maxAttributeLength: options.maxAttributeLength,
|
|
4194
|
+
redactionProfile: options.redactionProfile ?? "local"
|
|
3956
4195
|
};
|
|
3957
4196
|
}
|
|
3958
4197
|
function exportRunTree(tree, options) {
|
|
3959
4198
|
const opts = mergeExportDefaults(options);
|
|
4199
|
+
const exportTree = opts.redactionProfile === "local" ? tree : redactRunTreeForExport(tree, { redactionProfile: opts.redactionProfile });
|
|
3960
4200
|
switch (opts.format) {
|
|
3961
4201
|
case "markdown":
|
|
3962
|
-
return exportMarkdown(
|
|
4202
|
+
return exportMarkdown(exportTree, opts);
|
|
3963
4203
|
case "html":
|
|
3964
|
-
return exportHtml(
|
|
4204
|
+
return exportHtml(exportTree, opts);
|
|
3965
4205
|
case "openinference":
|
|
3966
|
-
return exportOpenInference(
|
|
4206
|
+
return exportOpenInference(exportTree, opts);
|
|
3967
4207
|
case "otlp-json":
|
|
3968
|
-
return exportOtlpJson(
|
|
4208
|
+
return exportOtlpJson(exportTree, opts);
|
|
3969
4209
|
default: {
|
|
3970
4210
|
const _x = opts.format;
|
|
3971
4211
|
throw new Error(`Unsupported export format: ${String(_x)}`);
|
|
@@ -4801,6 +5041,15 @@ async function tail(options = {}) {
|
|
|
4801
5041
|
process.exitCode = 1;
|
|
4802
5042
|
}
|
|
4803
5043
|
}
|
|
5044
|
+
function parseRedactionProfile(s) {
|
|
5045
|
+
const v = (s ?? "local").trim().toLowerCase();
|
|
5046
|
+
if (v === "local" || v === "share" || v === "strict") {
|
|
5047
|
+
return v;
|
|
5048
|
+
}
|
|
5049
|
+
throw new Error(
|
|
5050
|
+
`Unsupported --redaction-profile "${s ?? ""}". Use local, share, or strict.`
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
4804
5053
|
function parseExportFormat(s) {
|
|
4805
5054
|
const v = (s ?? "markdown").trim().toLowerCase();
|
|
4806
5055
|
if (v === "markdown" || v === "html" || v === "openinference" || v === "otlp-json") {
|
|
@@ -4818,8 +5067,10 @@ async function exportCommand(runId, options = {}) {
|
|
|
4818
5067
|
return;
|
|
4819
5068
|
}
|
|
4820
5069
|
let format;
|
|
5070
|
+
let redactionProfile;
|
|
4821
5071
|
try {
|
|
4822
5072
|
format = parseExportFormat(options.format);
|
|
5073
|
+
redactionProfile = parseRedactionProfile(options.redactionProfile);
|
|
4823
5074
|
} catch (e) {
|
|
4824
5075
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4825
5076
|
console.error(msg);
|
|
@@ -4858,7 +5109,8 @@ Trace directory: ${traceDir}`);
|
|
|
4858
5109
|
includeErrors: options.noErrors === true ? false : true,
|
|
4859
5110
|
pretty: true,
|
|
4860
5111
|
redacted: true,
|
|
4861
|
-
maxAttributeLength: 500
|
|
5112
|
+
maxAttributeLength: 500,
|
|
5113
|
+
redactionProfile
|
|
4862
5114
|
};
|
|
4863
5115
|
const result = exportRunTree(tree, exportOpts);
|
|
4864
5116
|
const validation = options.validate === true ? validateExport(result) : void 0;
|
|
@@ -5091,7 +5343,12 @@ function createCliProgram() {
|
|
|
5091
5343
|
"openinference",
|
|
5092
5344
|
"otlp-json"
|
|
5093
5345
|
])
|
|
5094
|
-
).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").
|
|
5346
|
+
).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(
|
|
5347
|
+
new commander.Option(
|
|
5348
|
+
"--redaction-profile <profile>",
|
|
5349
|
+
"redaction profile for exported copies: local, share, strict (default: local)"
|
|
5350
|
+
).choices(["local", "share", "strict"])
|
|
5351
|
+
).action((runId, opts) => {
|
|
5095
5352
|
runCommand(() => exportCommand(runId, opts));
|
|
5096
5353
|
});
|
|
5097
5354
|
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(
|