agent-first-data 0.26.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/assets/cli-help-v2.schema.json +115 -0
- package/assets/cli-spec-v1.schema.json +334 -0
- package/assets/protocol-v1.schema.json +140 -29
- package/dist/cli.d.ts +3 -22
- package/dist/cli.js +7 -109
- package/dist/format.d.ts +27 -9
- package/dist/format.js +189 -66
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/assets/cli-help-v1.schema.json +0 -227
package/dist/format.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* precision integers, high-precision floats, exponent forms, "-0") survives
|
|
13
13
|
* decode. Parsed numbers become LosslessNumber internally; every JSON/YAML
|
|
14
14
|
* rendering path here treats it as an opaque scalar leaf (never destructured
|
|
15
|
-
* as a plain object) and re-emits its exact literal text.
|
|
16
|
-
*
|
|
15
|
+
* as a plain object) and re-emits its exact literal text. Native bigint values
|
|
16
|
+
* are also supported so callers can render integers beyond Number's safe
|
|
17
|
+
* range without first corrupting them.
|
|
17
18
|
*/
|
|
18
19
|
import { parse as losslessParse, stringify as losslessStringify, isLosslessNumber } from "lossless-json";
|
|
19
20
|
// ═══════════════════════════════════════════
|
|
@@ -96,10 +97,7 @@ export class JsonErrorBuilder {
|
|
|
96
97
|
return this;
|
|
97
98
|
}
|
|
98
99
|
hint(value) {
|
|
99
|
-
if (
|
|
100
|
-
this.issues.push("hint must be a non-empty string");
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
100
|
+
if (value && value !== "") {
|
|
103
101
|
this.hintValue = value;
|
|
104
102
|
}
|
|
105
103
|
return this;
|
|
@@ -174,6 +172,13 @@ export class JsonErrorBuilder {
|
|
|
174
172
|
if (this.hintValue !== undefined)
|
|
175
173
|
error.hint = this.hintValue;
|
|
176
174
|
const m = { kind: "error", error, trace: this.traceValue };
|
|
175
|
+
try {
|
|
176
|
+
losslessStringify(m);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const issue = `event is not JSON serializable: ${error instanceof Error ? error.message : String(error)}`;
|
|
180
|
+
throw new EventBuildError(`Failed to build error event: ${issue}`, [issue]);
|
|
181
|
+
}
|
|
177
182
|
return new Event(m);
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -380,13 +385,35 @@ export function decodeProtocolEvent(text) {
|
|
|
380
385
|
// ═══════════════════════════════════════════
|
|
381
386
|
// Internal: Output Formatters (called by render() in cli.ts)
|
|
382
387
|
// ═══════════════════════════════════════════
|
|
383
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Which fields a redaction pass scrubs. The default is `All`.
|
|
390
|
+
*
|
|
391
|
+
* The policy selects a *scope* inside a structured value. A command line
|
|
392
|
+
* (`redactArgv`) and a bare URL string (`redactUrlSecrets`) have no
|
|
393
|
+
* result/trace split to scope to, so on those two paths `TraceOnly` redacts in
|
|
394
|
+
* full like `All`, and only `Off` turns redaction off.
|
|
395
|
+
*/
|
|
384
396
|
export var RedactionPolicy;
|
|
385
397
|
(function (RedactionPolicy) {
|
|
386
398
|
RedactionPolicy["All"] = "All";
|
|
387
399
|
RedactionPolicy["TraceOnly"] = "TraceOnly";
|
|
388
400
|
RedactionPolicy["Off"] = "Off";
|
|
389
401
|
})(RedactionPolicy || (RedactionPolicy = {}));
|
|
402
|
+
/**
|
|
403
|
+
* Whether a policy redacts an input that carries no scope of its own — a
|
|
404
|
+
* command line or a single URL string, as opposed to a JSON value with a
|
|
405
|
+
* `trace` object to narrow to.
|
|
406
|
+
*
|
|
407
|
+
* `TraceOnly` narrows *where* redaction applies within a value; it does not
|
|
408
|
+
* weaken redaction. A standalone argv or URL has no non-`trace` half to leave
|
|
409
|
+
* alone — and both are diagnostic material by construction, the very thing
|
|
410
|
+
* `TraceOnly` scrubs — so `TraceOnly` redacts them in full, exactly like `All`.
|
|
411
|
+
* Only `Off`, the caller explicitly asking for raw output, disables redaction,
|
|
412
|
+
* and it does so on all three paths alike.
|
|
413
|
+
*/
|
|
414
|
+
function redactsUnscopedInput(policy) {
|
|
415
|
+
return policy !== RedactionPolicy.Off;
|
|
416
|
+
}
|
|
390
417
|
export var PlainStyle;
|
|
391
418
|
(function (PlainStyle) {
|
|
392
419
|
PlainStyle["Readable"] = "Readable";
|
|
@@ -441,6 +468,9 @@ export function formatYamlValue(value, options = {}) {
|
|
|
441
468
|
export function formatPlainValue(value, options = {}) {
|
|
442
469
|
const unwrapped = value instanceof Event ? value.toJSON() : value;
|
|
443
470
|
const redacted = redactedValue(unwrapped, options.redaction ?? {});
|
|
471
|
+
if (!isObject(redacted)) {
|
|
472
|
+
return plainScalar(redacted);
|
|
473
|
+
}
|
|
444
474
|
const pairs = [];
|
|
445
475
|
if (options.style === PlainStyle.Raw) {
|
|
446
476
|
collectPlainPairsRaw(redacted, "", pairs);
|
|
@@ -448,6 +478,9 @@ export function formatPlainValue(value, options = {}) {
|
|
|
448
478
|
else {
|
|
449
479
|
collectPlainPairs(redacted, "", pairs);
|
|
450
480
|
}
|
|
481
|
+
if (pairs.length === 0) {
|
|
482
|
+
return "{}";
|
|
483
|
+
}
|
|
451
484
|
pairs.sort(([a], [b]) => jcsCompare(a, b));
|
|
452
485
|
return pairs
|
|
453
486
|
.map(([k, v]) => `${quoteLogfmtKey(k)}=${quoteLogfmtValue(v)}`)
|
|
@@ -466,13 +499,21 @@ export function redactedValue(value, options) {
|
|
|
466
499
|
* Redact secret components of a single URL string.
|
|
467
500
|
*
|
|
468
501
|
* A query parameter is redacted iff its (form-decoded) name ends in
|
|
469
|
-
* _secret/_SECRET or matches an exact entry in secretNames.
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
502
|
+
* _secret/_SECRET or matches an exact entry in secretNames. A fragment written
|
|
503
|
+
* in the same `k=v&k=v` shape — how an OAuth implicit-flow response carries its
|
|
504
|
+
* token — is redacted by that same rule; any other fragment passes through
|
|
505
|
+
* byte-for-byte. The userinfo password (scheme://user:pass@host) is always
|
|
506
|
+
* redacted as a structural rule. Only the secret spans are replaced with "***";
|
|
507
|
+
* every other byte is preserved. A string that is not a single,
|
|
508
|
+
* whitespace-free, scheme-prefixed URL (including a URL embedded in surrounding
|
|
509
|
+
* prose) is returned unchanged.
|
|
510
|
+
*
|
|
511
|
+
* A URL is unscoped input: `RedactionPolicy.Off` returns it unchanged, every
|
|
512
|
+
* other policy redacts it in full.
|
|
474
513
|
*/
|
|
475
514
|
export function redactUrlSecrets(url, options) {
|
|
515
|
+
if (!redactsUnscopedInput(options?.policy))
|
|
516
|
+
return url;
|
|
476
517
|
const redacted = redactUrlInStr(url, secretNameSet(options ?? {}));
|
|
477
518
|
return redacted ?? url;
|
|
478
519
|
}
|
|
@@ -496,9 +537,12 @@ export function redactUrlSecrets(url, options) {
|
|
|
496
537
|
* Intended for CLIs that record their own invocation — startup diagnostics,
|
|
497
538
|
* audit trails, crash reports — where writing argv verbatim would put a
|
|
498
539
|
* credential in the log.
|
|
540
|
+
*
|
|
541
|
+
* A command line is unscoped input: `RedactionPolicy.Off` returns `args`
|
|
542
|
+
* unchanged, every other policy redacts in full.
|
|
499
543
|
*/
|
|
500
544
|
export function redactArgv(args, options) {
|
|
501
|
-
if (options?.policy
|
|
545
|
+
if (!redactsUnscopedInput(options?.policy))
|
|
502
546
|
return [...args];
|
|
503
547
|
const secretNames = secretNameSet(options ?? {});
|
|
504
548
|
const out = [];
|
|
@@ -507,7 +551,7 @@ export function redactArgv(args, options) {
|
|
|
507
551
|
if (redactNext) {
|
|
508
552
|
redactNext = false;
|
|
509
553
|
if (!arg.startsWith("-")) {
|
|
510
|
-
out.push(
|
|
554
|
+
out.push(REDACTED_MARKER);
|
|
511
555
|
continue;
|
|
512
556
|
}
|
|
513
557
|
}
|
|
@@ -517,7 +561,7 @@ export function redactArgv(args, options) {
|
|
|
517
561
|
if (equals >= 0) {
|
|
518
562
|
const name = rest.slice(0, equals);
|
|
519
563
|
if (isSecretFlagName(name, secretNames)) {
|
|
520
|
-
out.push(`--${name}
|
|
564
|
+
out.push(`--${name}=${REDACTED_MARKER}`);
|
|
521
565
|
continue;
|
|
522
566
|
}
|
|
523
567
|
}
|
|
@@ -688,6 +732,21 @@ function daysInMonth(year, month) {
|
|
|
688
732
|
function isLeapYear(year) {
|
|
689
733
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
690
734
|
}
|
|
735
|
+
// ═══════════════════════════════════════════
|
|
736
|
+
// Secret Redaction
|
|
737
|
+
// ═══════════════════════════════════════════
|
|
738
|
+
/**
|
|
739
|
+
* The scalar every redacted span, value, and subtree is replaced with. Also the
|
|
740
|
+
* signal plain rendering reads to tell a hidden field from a live one.
|
|
741
|
+
*/
|
|
742
|
+
const REDACTED_MARKER = "***";
|
|
743
|
+
/**
|
|
744
|
+
* True when a field already carries the redaction marker, i.e. redaction ran
|
|
745
|
+
* over it and hid the value.
|
|
746
|
+
*/
|
|
747
|
+
function isRedacted(value) {
|
|
748
|
+
return value === REDACTED_MARKER;
|
|
749
|
+
}
|
|
691
750
|
const DEFAULT_CONTEXT = {
|
|
692
751
|
secretNames: new Set(),
|
|
693
752
|
};
|
|
@@ -715,7 +774,7 @@ function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
|
|
|
715
774
|
for (const k of Object.keys(value)) {
|
|
716
775
|
const v = value[k];
|
|
717
776
|
if (isSecretKey(k, context.secretNames)) {
|
|
718
|
-
value[k] =
|
|
777
|
+
value[k] = REDACTED_MARKER;
|
|
719
778
|
}
|
|
720
779
|
else if (keyHasUrlSuffix(k)) {
|
|
721
780
|
if (typeof v === "string") {
|
|
@@ -790,29 +849,22 @@ function redactUrlInStr(s, secretNames) {
|
|
|
790
849
|
const authority = rest.slice(0, authEnd);
|
|
791
850
|
const remainder = rest.slice(authEnd);
|
|
792
851
|
const newAuthority = redactUserinfoPassword(authority);
|
|
793
|
-
//
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
810
|
-
newRemainder = `${path}?${redactQuery(query, secretNames)}${fragment}`;
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
newRemainder = remainder;
|
|
814
|
-
}
|
|
815
|
-
return `${scheme}://${newAuthority}${newRemainder}`;
|
|
852
|
+
// `remainder` is `path[?query][#fragment]`; '#' ends the query, so split the
|
|
853
|
+
// fragment off first and the query out of what is left.
|
|
854
|
+
const hash = remainder.indexOf("#");
|
|
855
|
+
const beforeFragment = hash >= 0 ? remainder.slice(0, hash) : remainder;
|
|
856
|
+
const fragment = hash >= 0 ? remainder.slice(hash + 1) : null;
|
|
857
|
+
const q = beforeFragment.indexOf("?");
|
|
858
|
+
const newBeforeFragment = q >= 0
|
|
859
|
+
? `${beforeFragment.slice(0, q)}?${redactQuery(beforeFragment.slice(q + 1), secretNames)}`
|
|
860
|
+
: beforeFragment;
|
|
861
|
+
// A fragment gets the same treatment as the query: `k=v&k=v` after the '#' is
|
|
862
|
+
// exactly how an OAuth implicit-flow response hands back a token, so a
|
|
863
|
+
// secret-named fragment parameter must not survive where the identically
|
|
864
|
+
// named query parameter would not. A fragment that is not in that shape has
|
|
865
|
+
// no '=' in its segments and passes through byte-for-byte.
|
|
866
|
+
const newFragment = fragment === null ? "" : `#${redactQuery(fragment, secretNames)}`;
|
|
867
|
+
return `${scheme}://${newAuthority}${newBeforeFragment}${newFragment}`;
|
|
816
868
|
}
|
|
817
869
|
function redactUrlFieldValue(s, secretNames) {
|
|
818
870
|
const redacted = redactUrlInStr(s, secretNames);
|
|
@@ -830,7 +882,7 @@ function redactUrlFieldValue(s, secretNames) {
|
|
|
830
882
|
// connection string like user:pass@host/db has no scheme anchor for the
|
|
831
883
|
// surgical span logic above, so blanket redaction is the safe default.
|
|
832
884
|
if (/\s/.test(s) || s.includes("@"))
|
|
833
|
-
return
|
|
885
|
+
return REDACTED_MARKER;
|
|
834
886
|
return s;
|
|
835
887
|
}
|
|
836
888
|
/**
|
|
@@ -845,7 +897,7 @@ function redactUserinfoPassword(authority) {
|
|
|
845
897
|
const colon = userinfo.indexOf(":");
|
|
846
898
|
if (colon < 0)
|
|
847
899
|
return authority;
|
|
848
|
-
return `${authority.slice(0, colon)}
|
|
900
|
+
return `${authority.slice(0, colon)}:${REDACTED_MARKER}${authority.slice(at)}`;
|
|
849
901
|
}
|
|
850
902
|
/**
|
|
851
903
|
* Redact the values of secret-named query parameters, preserving raw bytes of
|
|
@@ -861,7 +913,7 @@ function redactQuery(query, secretNames) {
|
|
|
861
913
|
const rawKey = segment.slice(0, eq);
|
|
862
914
|
const name = formDecode(rawKey);
|
|
863
915
|
if (isSecretKey(name, secretNames)) {
|
|
864
|
-
return `${rawKey}
|
|
916
|
+
return `${rawKey}=${REDACTED_MARKER}`;
|
|
865
917
|
}
|
|
866
918
|
return segment;
|
|
867
919
|
})
|
|
@@ -950,7 +1002,9 @@ function isInt(value) {
|
|
|
950
1002
|
function decimalIntText(value) {
|
|
951
1003
|
if (typeof value === "string" && /^-?\d+$/.test(value))
|
|
952
1004
|
return value;
|
|
953
|
-
if (
|
|
1005
|
+
if (typeof value === "bigint")
|
|
1006
|
+
return value.toString();
|
|
1007
|
+
if (isInt(value) && Number.isSafeInteger(value))
|
|
954
1008
|
return String(value);
|
|
955
1009
|
if (isLosslessNumber(value)) {
|
|
956
1010
|
const text = value.toString();
|
|
@@ -958,6 +1012,33 @@ function decimalIntText(value) {
|
|
|
958
1012
|
}
|
|
959
1013
|
return null;
|
|
960
1014
|
}
|
|
1015
|
+
const MAX_I64 = 9223372036854775807n;
|
|
1016
|
+
function nonNegativeI64(value) {
|
|
1017
|
+
let text = null;
|
|
1018
|
+
if (typeof value === "bigint") {
|
|
1019
|
+
text = value.toString();
|
|
1020
|
+
}
|
|
1021
|
+
else if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
1022
|
+
text = String(value);
|
|
1023
|
+
}
|
|
1024
|
+
else if (isLosslessNumber(value)) {
|
|
1025
|
+
const literal = value.toString();
|
|
1026
|
+
if (/^-?\d+$/.test(literal))
|
|
1027
|
+
text = literal;
|
|
1028
|
+
}
|
|
1029
|
+
if (text === null)
|
|
1030
|
+
return null;
|
|
1031
|
+
try {
|
|
1032
|
+
const integer = BigInt(text);
|
|
1033
|
+
return integer >= 0n && integer <= MAX_I64 ? integer : null;
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function formatCents(value, symbol) {
|
|
1040
|
+
return `${symbol}${value / 100n}.${String(value % 100n).padStart(2, "0")}`;
|
|
1041
|
+
}
|
|
961
1042
|
function epochNsToMs(value) {
|
|
962
1043
|
const text = decimalIntText(value);
|
|
963
1044
|
if (text === null)
|
|
@@ -1027,28 +1108,34 @@ function tryProcessField(key, value) {
|
|
|
1027
1108
|
// Group 2: compound currency suffixes
|
|
1028
1109
|
stripped = stripSuffixCI(key, "_usd_cents");
|
|
1029
1110
|
if (stripped !== null) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1111
|
+
const integer = nonNegativeI64(value);
|
|
1112
|
+
if (integer !== null)
|
|
1113
|
+
return [stripped, formatCents(integer, "$")];
|
|
1032
1114
|
return null;
|
|
1033
1115
|
}
|
|
1034
1116
|
stripped = stripSuffixCI(key, "_eur_cents");
|
|
1035
1117
|
if (stripped !== null) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1118
|
+
const integer = nonNegativeI64(value);
|
|
1119
|
+
if (integer !== null)
|
|
1120
|
+
return [stripped, formatCents(integer, "\u20ac")];
|
|
1038
1121
|
return null;
|
|
1039
1122
|
}
|
|
1040
1123
|
const gc = tryStripGenericCents(key);
|
|
1041
1124
|
if (gc !== null) {
|
|
1042
1125
|
const [gcStripped, code] = gc;
|
|
1043
|
-
|
|
1044
|
-
|
|
1126
|
+
const integer = nonNegativeI64(value);
|
|
1127
|
+
if (integer !== null) {
|
|
1128
|
+
return [gcStripped, `${integer / 100n}.${String(integer % 100n).padStart(2, "0")} ${code.toUpperCase()}`];
|
|
1129
|
+
}
|
|
1045
1130
|
return null;
|
|
1046
1131
|
}
|
|
1047
1132
|
const gm = tryStripGenericMicro(key);
|
|
1048
1133
|
if (gm !== null) {
|
|
1049
1134
|
const [gmStripped, code] = gm;
|
|
1050
|
-
|
|
1051
|
-
|
|
1135
|
+
const integer = nonNegativeI64(value);
|
|
1136
|
+
if (integer !== null) {
|
|
1137
|
+
return [gmStripped, `${integer / 1000000n}.${String(integer % 1000000n).padStart(6, "0")} ${code.toUpperCase()}`];
|
|
1138
|
+
}
|
|
1052
1139
|
return null;
|
|
1053
1140
|
}
|
|
1054
1141
|
// Group 3: multi-char suffixes
|
|
@@ -1106,8 +1193,9 @@ function tryProcessField(key, value) {
|
|
|
1106
1193
|
// Group 5: short suffixes (last to avoid false positives)
|
|
1107
1194
|
stripped = stripSuffixCI(key, "_jpy");
|
|
1108
1195
|
if (stripped !== null) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1196
|
+
const integer = nonNegativeI64(value);
|
|
1197
|
+
if (integer !== null)
|
|
1198
|
+
return [stripped, `\u00a5${formatWithCommas(integer)}`];
|
|
1111
1199
|
return null;
|
|
1112
1200
|
}
|
|
1113
1201
|
stripped = stripSuffixCI(key, "_ns");
|
|
@@ -1142,7 +1230,13 @@ function processObjectFields(obj) {
|
|
|
1142
1230
|
for (const [k, v] of Object.entries(obj)) {
|
|
1143
1231
|
const secretStripped = stripSuffixCI(k, "_secret");
|
|
1144
1232
|
if (secretStripped !== null) {
|
|
1145
|
-
|
|
1233
|
+
// Dropping `_secret` is the readable half of an actual redaction: the
|
|
1234
|
+
// marker has done its job once the value reads `***`. When the value was
|
|
1235
|
+
// *not* redacted — policy `Off`, or a field outside `trace` under
|
|
1236
|
+
// `TraceOnly` — the suffix is the only thing telling a downstream reader
|
|
1237
|
+
// that `api_key_secret=sk-live-xxx` is a credential, so it stays.
|
|
1238
|
+
const displayKey = isRedacted(v) ? secretStripped : k;
|
|
1239
|
+
entries.push({ stripped: displayKey, original: k, value: v, formatted: null });
|
|
1146
1240
|
continue;
|
|
1147
1241
|
}
|
|
1148
1242
|
const result = tryProcessField(k, v);
|
|
@@ -1344,14 +1438,29 @@ function escapeYamlStr(s) {
|
|
|
1344
1438
|
return s
|
|
1345
1439
|
.replace(/\\/g, "\\\\")
|
|
1346
1440
|
.replace(/"/g, '\\"')
|
|
1347
|
-
.replace(
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1441
|
+
.replace(/[\x00-\x1f]/g, (character) => {
|
|
1442
|
+
switch (character) {
|
|
1443
|
+
case "\0": return "\\0";
|
|
1444
|
+
case "\b": return "\\b";
|
|
1445
|
+
case "\t": return "\\t";
|
|
1446
|
+
case "\n": return "\\n";
|
|
1447
|
+
case "\v": return "\\v";
|
|
1448
|
+
case "\f": return "\\f";
|
|
1449
|
+
case "\r": return "\\r";
|
|
1450
|
+
default: return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1352
1453
|
}
|
|
1353
1454
|
function yamlKey(key) {
|
|
1354
|
-
return /^[A-Za-z0-9_.-]+$/.test(key)
|
|
1455
|
+
return /^[A-Za-z0-9_.-]+$/.test(key) && !isAmbiguousYamlKey(key)
|
|
1456
|
+
? key
|
|
1457
|
+
: `"${escapeYamlStr(key)}"`;
|
|
1458
|
+
}
|
|
1459
|
+
function isAmbiguousYamlKey(key) {
|
|
1460
|
+
if (["true", "false", "null", "~", ".nan", ".inf", "+.inf", "-.inf"].includes(key.toLowerCase())) {
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
return key.trim() !== "" && Number.isFinite(Number(key));
|
|
1355
1464
|
}
|
|
1356
1465
|
function yamlScalar(value) {
|
|
1357
1466
|
if (typeof value === "string")
|
|
@@ -1362,6 +1471,8 @@ function yamlScalar(value) {
|
|
|
1362
1471
|
return value.toString();
|
|
1363
1472
|
if (typeof value === "number")
|
|
1364
1473
|
return value.toString();
|
|
1474
|
+
if (typeof value === "bigint")
|
|
1475
|
+
return value.toString();
|
|
1365
1476
|
// Decoded number: emit the exact source literal unquoted, matching JSON's
|
|
1366
1477
|
// structure-preserving fidelity contract for YAML too.
|
|
1367
1478
|
if (isLosslessNumber(value))
|
|
@@ -1380,10 +1491,13 @@ function collectPlainPairs(value, prefix, pairs) {
|
|
|
1380
1491
|
pairs.push([fullKey, pf.formatted]);
|
|
1381
1492
|
}
|
|
1382
1493
|
else if (isObject(pf.value)) {
|
|
1383
|
-
|
|
1494
|
+
if (Object.keys(pf.value).length === 0)
|
|
1495
|
+
pairs.push([fullKey, "{}"]);
|
|
1496
|
+
else
|
|
1497
|
+
collectPlainPairs(pf.value, fullKey, pairs);
|
|
1384
1498
|
}
|
|
1385
1499
|
else if (Array.isArray(pf.value)) {
|
|
1386
|
-
pairs.push([fullKey, pf.value.map((i) => plainScalar(i)).join(",")]);
|
|
1500
|
+
pairs.push([fullKey, pf.value.length === 0 ? "[]" : pf.value.map((i) => plainScalar(i)).join(",")]);
|
|
1387
1501
|
}
|
|
1388
1502
|
else if (pf.value === null) {
|
|
1389
1503
|
pairs.push([fullKey, ""]);
|
|
@@ -1400,10 +1514,13 @@ function collectPlainPairsRaw(value, prefix, pairs) {
|
|
|
1400
1514
|
const v = value[key];
|
|
1401
1515
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1402
1516
|
if (isObject(v)) {
|
|
1403
|
-
|
|
1517
|
+
if (Object.keys(v).length === 0)
|
|
1518
|
+
pairs.push([fullKey, "{}"]);
|
|
1519
|
+
else
|
|
1520
|
+
collectPlainPairsRaw(v, fullKey, pairs);
|
|
1404
1521
|
}
|
|
1405
1522
|
else if (Array.isArray(v)) {
|
|
1406
|
-
pairs.push([fullKey, v.map((i) => plainScalarRaw(i)).join(",")]);
|
|
1523
|
+
pairs.push([fullKey, v.length === 0 ? "[]" : v.map((i) => plainScalarRaw(i)).join(",")]);
|
|
1407
1524
|
}
|
|
1408
1525
|
else if (v === null) {
|
|
1409
1526
|
pairs.push([fullKey, ""]);
|
|
@@ -1422,6 +1539,8 @@ function plainScalar(value) {
|
|
|
1422
1539
|
return value.toString();
|
|
1423
1540
|
if (typeof value === "number")
|
|
1424
1541
|
return value.toString();
|
|
1542
|
+
if (typeof value === "bigint")
|
|
1543
|
+
return value.toString();
|
|
1425
1544
|
// Decoded number: emit the exact source literal (Plain is documented lossy
|
|
1426
1545
|
// for arithmetic-derived formatting, but a bare pass-through scalar still
|
|
1427
1546
|
// gets its exact digits, not a mangled float64 round-trip).
|
|
@@ -1468,10 +1587,14 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
|
|
|
1468
1587
|
return value;
|
|
1469
1588
|
if (t === "number") {
|
|
1470
1589
|
const n = value;
|
|
1471
|
-
|
|
1590
|
+
if (!Number.isFinite(n))
|
|
1591
|
+
return "<unsupported:number>";
|
|
1592
|
+
if (Number.isInteger(n) && !Number.isSafeInteger(n))
|
|
1593
|
+
return "<unsupported:unsafe-integer>";
|
|
1594
|
+
return n;
|
|
1472
1595
|
}
|
|
1473
1596
|
if (t === "bigint")
|
|
1474
|
-
return
|
|
1597
|
+
return value;
|
|
1475
1598
|
if (t === "undefined")
|
|
1476
1599
|
return "<unsupported:undefined>";
|
|
1477
1600
|
if (t === "function")
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
|
|
3
3
|
*/
|
|
4
4
|
export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, type LogLevel, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, type DecodedEvent, type DecodedResult, type DecodedError, type DecodedProgress, type DecodedLog, EventDecodeError, type JsonValue, RedactionPolicy, PlainStyle, type OutputOptions, outputOptionsForPolicy, redactedValue, redactArgv, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
|
|
5
|
-
export { type OutputFormat, type OutputTo, type CliEventWriter, CliEmitter, LogFilters, cliParseOutput, parseOutputTo, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion,
|
|
5
|
+
export { type OutputFormat, type OutputTo, type CliEventWriter, CliEmitter, LogFilters, cliParseOutput, parseOutputTo, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion, } from "./cli.js";
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
|
|
3
3
|
*/
|
|
4
4
|
export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, EventDecodeError, RedactionPolicy, PlainStyle, outputOptionsForPolicy, redactedValue, redactArgv, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
|
|
5
|
-
export { CliEmitter, LogFilters, cliParseOutput, parseOutputTo, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion,
|
|
5
|
+
export { CliEmitter, LogFilters, cliParseOutput, parseOutputTo, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion, } from "./cli.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-first-data",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"prepack": "npm run build",
|
|
27
|
-
"test": "tsx --test src/*.test.ts
|
|
27
|
+
"test": "tsx --test src/*.test.ts"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^26.1.2",
|
|
@@ -32,6 +32,6 @@
|
|
|
32
32
|
"typescript": "^7.0.2"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"lossless-json": "^4.3.
|
|
35
|
+
"lossless-json": "^4.3.1"
|
|
36
36
|
}
|
|
37
37
|
}
|