agent-first-data 0.17.3 → 0.19.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/dist/format.js CHANGED
@@ -2,10 +2,20 @@
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
4
  * Public APIs include protocol event builders, redactedValue (covers _secret
5
- * and _url fields), output formatters, redactUrlSecrets,
6
- * normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47,
7
- * RedactionPolicy, OutputStyle, and OutputOptions.
5
+ * and _url fields), redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date,
6
+ * isValidRfc3339Time, isValidRfc3339, isValidBcp47, RedactionPolicy,
7
+ * PlainStyle, and OutputOptions. The single render entry point (`render`)
8
+ * lives in cli.ts and calls this module's internal JSON/YAML/plain formatters.
9
+ *
10
+ * Number literal fidelity: decodeProtocolEvent parses with `lossless-json`
11
+ * instead of JSON.parse, so a number's exact source literal (arbitrary
12
+ * precision integers, high-precision floats, exponent forms, "-0") survives
13
+ * decode. Parsed numbers become LosslessNumber internally; every JSON/YAML
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. This is internal —
16
+ * LosslessNumber is never part of the public JsonValue type or exports.
8
17
  */
18
+ import { parse as losslessParse, stringify as losslessStringify, isLosslessNumber } from "lossless-json";
9
19
  // ═══════════════════════════════════════════
10
20
  // Event type: opaque branded wrapper
11
21
  // ═══════════════════════════════════════════
@@ -46,25 +56,17 @@ export class EventBuildError extends Error {
46
56
  export class JsonResultBuilder {
47
57
  payload;
48
58
  traceValue = {};
49
- issues = [];
50
59
  constructor(payload) {
51
60
  this.payload = payload;
52
61
  }
53
62
  trace(value) {
54
63
  if (value !== undefined) {
55
- if (!isObject(value)) {
56
- this.issues.push("trace must be a JSON object");
57
- }
58
- else {
59
- this.traceValue = value;
60
- }
64
+ this.traceValue = value;
61
65
  }
62
66
  return this;
63
67
  }
68
+ /** Build the event. This builder cannot fail. */
64
69
  build() {
65
- if (this.issues.length > 0) {
66
- throw new EventBuildError(`Failed to build result event: ${this.issues.join("; ")}`, this.issues);
67
- }
68
70
  const m = { kind: "result", result: this.payload, trace: this.traceValue };
69
71
  return new Event(m);
70
72
  }
@@ -178,25 +180,17 @@ export class JsonErrorBuilder {
178
180
  export class JsonProgressBuilder {
179
181
  payload;
180
182
  traceValue = {};
181
- issues = [];
182
183
  constructor(payload) {
183
184
  this.payload = payload;
184
185
  }
185
186
  trace(value) {
186
187
  if (value !== undefined) {
187
- if (!isObject(value)) {
188
- this.issues.push("trace must be a JSON object");
189
- }
190
- else {
191
- this.traceValue = value;
192
- }
188
+ this.traceValue = value;
193
189
  }
194
190
  return this;
195
191
  }
192
+ /** Build the event. This builder cannot fail. */
196
193
  build() {
197
- if (this.issues.length > 0) {
198
- throw new EventBuildError(`Failed to build progress event: ${this.issues.join("; ")}`, this.issues);
199
- }
200
194
  const m = { kind: "progress", progress: this.payload, trace: this.traceValue };
201
195
  return new Event(m);
202
196
  }
@@ -204,25 +198,17 @@ export class JsonProgressBuilder {
204
198
  export class JsonLogBuilder {
205
199
  payload;
206
200
  traceValue = {};
207
- issues = [];
208
201
  constructor(payload) {
209
202
  this.payload = payload;
210
203
  }
211
204
  trace(value) {
212
205
  if (value !== undefined) {
213
- if (!isObject(value)) {
214
- this.issues.push("trace must be a JSON object");
215
- }
216
- else {
217
- this.traceValue = value;
218
- }
206
+ this.traceValue = value;
219
207
  }
220
208
  return this;
221
209
  }
210
+ /** Build the event. This builder cannot fail. */
222
211
  build() {
223
- if (this.issues.length > 0) {
224
- throw new EventBuildError(`Failed to build log event: ${this.issues.join("; ")}`, this.issues);
225
- }
226
212
  const m = { kind: "log", log: this.payload, trace: this.traceValue };
227
213
  return new Event(m);
228
214
  }
@@ -343,7 +329,11 @@ const ERROR_RESERVED_FIELDS = new Set(["code", "message", "hint", "retryable"]);
343
329
  export function decodeProtocolEvent(text) {
344
330
  let parsed;
345
331
  try {
346
- parsed = JSON.parse(text);
332
+ // lossless-json instead of JSON.parse: preserves each number's exact
333
+ // source literal (see module-level fidelity note) instead of collapsing
334
+ // it through float64. isSyntaxError-compatible: throws the same
335
+ // SyntaxError shape JSON.parse would.
336
+ parsed = losslessParse(text);
347
337
  }
348
338
  catch (e) {
349
339
  throw new EventDecodeError(`invalid JSON: ${e.message}`);
@@ -388,46 +378,71 @@ export function decodeProtocolEvent(text) {
388
378
  }
389
379
  }
390
380
  // ═══════════════════════════════════════════
391
- // Public API: Output Formatters
381
+ // Internal: Output Formatters (called by render() in cli.ts)
392
382
  // ═══════════════════════════════════════════
383
+ /** Which fields a redaction pass scrubs. The default is `All`. */
393
384
  export var RedactionPolicy;
394
385
  (function (RedactionPolicy) {
395
- RedactionPolicy["RedactionTraceOnly"] = "RedactionTraceOnly";
396
- RedactionPolicy["RedactionNone"] = "RedactionNone";
386
+ RedactionPolicy["All"] = "All";
387
+ RedactionPolicy["TraceOnly"] = "TraceOnly";
388
+ RedactionPolicy["Off"] = "Off";
397
389
  })(RedactionPolicy || (RedactionPolicy = {}));
398
- export var OutputStyle;
399
- (function (OutputStyle) {
400
- OutputStyle["Readable"] = "Readable";
401
- OutputStyle["Raw"] = "Raw";
402
- })(OutputStyle || (OutputStyle = {}));
390
+ export var PlainStyle;
391
+ (function (PlainStyle) {
392
+ PlainStyle["Readable"] = "Readable";
393
+ PlainStyle["Raw"] = "Raw";
394
+ })(PlainStyle || (PlainStyle = {}));
403
395
  /** Convenience: build OutputOptions scoped to a RedactionPolicy. */
404
396
  export function outputOptionsForPolicy(policy) {
405
397
  return { redaction: { policy } };
406
398
  }
407
- /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
408
- export function outputJson(value, options = {}) {
399
+ /**
400
+ * Format as single-line JSON. Secrets redacted, original keys, raw values.
401
+ * Internal: called by `render` in cli.ts, which is the single public render
402
+ * entry point. Not re-exported from index.ts.
403
+ */
404
+ export function formatJsonValue(value, options = {}) {
409
405
  const unwrapped = value instanceof Event ? value.toJSON() : value;
410
- return JSON.stringify(redactedValue(unwrapped, options.redaction ?? {}));
406
+ return canonicalJsonString(redactedValue(unwrapped, options.redaction ?? {}));
407
+ }
408
+ /**
409
+ * Compact JSON serialization, byte-identical to `JSON.stringify(value)` for
410
+ * every value that contains no LosslessNumber, and additionally correct for
411
+ * values that do: `JSON.stringify` cannot serialize a LosslessNumber (it has
412
+ * no `toJSON` and isn't a boxed Number, so it would fall back to serializing
413
+ * its internal `{value, isLosslessNumber}` fields as if they were user data).
414
+ * lossless-json's `stringify` recognizes LosslessNumber and re-emits its
415
+ * exact literal text unquoted, and matches native JSON.stringify's escaping
416
+ * and formatting for everything else.
417
+ */
418
+ function canonicalJsonString(value) {
419
+ return losslessStringify(value) ?? "null";
411
420
  }
412
- /** Format as multi-line YAML. Keys stripped, values formatted, secrets redacted. */
413
- export function outputYaml(value, options = {}) {
421
+ /**
422
+ * Format as multi-line YAML. Structure-preserving: like JSON, original keys,
423
+ * scalar types, and numeric semantics are kept after redaction; secrets are
424
+ * still redacted. `options.style` is ignored — YAML output does not vary by
425
+ * PlainStyle.
426
+ * Internal: called by `render` in cli.ts, which is the single public render
427
+ * entry point. Not re-exported from index.ts.
428
+ */
429
+ export function formatYamlValue(value, options = {}) {
414
430
  const unwrapped = value instanceof Event ? value.toJSON() : value;
415
431
  const redacted = redactedValue(unwrapped, options.redaction ?? {});
416
432
  const lines = ["---"];
417
- if (options.style === OutputStyle.Raw) {
418
- renderYamlRaw(redacted, 0, lines);
419
- }
420
- else {
421
- renderYamlProcessed(redacted, 0, lines);
422
- }
433
+ renderYamlRaw(redacted, 0, lines);
423
434
  return lines.join("\n");
424
435
  }
425
- /** Format as single-line logfmt. Keys stripped, values formatted, secrets redacted. */
426
- export function outputPlain(value, options = {}) {
436
+ /**
437
+ * Format as single-line logfmt. Keys stripped, values formatted, secrets redacted.
438
+ * Internal: called by `render` in cli.ts, which is the single public render
439
+ * entry point. Not re-exported from index.ts.
440
+ */
441
+ export function formatPlainValue(value, options = {}) {
427
442
  const unwrapped = value instanceof Event ? value.toJSON() : value;
428
443
  const redacted = redactedValue(unwrapped, options.redaction ?? {});
429
444
  const pairs = [];
430
- if (options.style === OutputStyle.Raw) {
445
+ if (options.style === PlainStyle.Raw) {
431
446
  collectPlainPairsRaw(redacted, "", pairs);
432
447
  }
433
448
  else {
@@ -668,15 +683,15 @@ function applyRedactionOptions(value, redactionOptions) {
668
683
  }
669
684
  function applyRedactionPolicyWithContext(value, redactionPolicy, context) {
670
685
  switch (redactionPolicy) {
671
- case RedactionPolicy.RedactionTraceOnly:
686
+ case RedactionPolicy.TraceOnly:
672
687
  if (isObject(value) && value.trace !== undefined) {
673
688
  redactSecrets(value.trace, context);
674
689
  }
675
690
  break;
676
- case RedactionPolicy.RedactionNone:
691
+ case RedactionPolicy.Off:
677
692
  break;
678
693
  default:
679
- // Empty/unknown policy falls back to default full redaction.
694
+ // undefined or RedactionPolicy.All = full redaction (default).
680
695
  redactSecrets(value, context);
681
696
  break;
682
697
  }
@@ -876,6 +891,10 @@ function decimalIntText(value) {
876
891
  return value;
877
892
  if (isInt(value))
878
893
  return String(value);
894
+ if (isLosslessNumber(value)) {
895
+ const text = value.toString();
896
+ return /^-?\d+$/.test(text) ? text : null;
897
+ }
879
898
  return null;
880
899
  }
881
900
  function epochNsToMs(value) {
@@ -902,11 +921,24 @@ function isNum(value) {
902
921
  }
903
922
  function tryProcessField(key, value) {
904
923
  let stripped;
924
+ // Plain-format suffix processing below does real arithmetic (division,
925
+ // modulo, Math.floor/abs) that only works on a native JS number. A decoded
926
+ // LosslessNumber is normalized to one here via Number() \u2014 which never
927
+ // throws, unlike LosslessNumber's own valueOf() (documented to throw for
928
+ // values it can't safely represent as number/bigint) \u2014 so an ordinary
929
+ // decoded value (e.g. duration_ms: 42) keeps formatting exactly as before,
930
+ // and an exotic decoded value degrades gracefully (loses precision here,
931
+ // same as it always would for a >2^53 native number) rather than crashing.
932
+ // This is Plain's documented lossy/human path; JSON/YAML stay exact via
933
+ // yamlScalar/plainScalar/canonicalJsonString, and decimalIntText below
934
+ // (msats/sats/epoch_ns) keeps exactness by reading `value` directly instead
935
+ // of `numeric`.
936
+ const numeric = isLosslessNumber(value) ? Number(value.toString()) : value;
905
937
  // Group 1: compound timestamp suffixes
906
938
  stripped = stripSuffixCI(key, "_epoch_ms");
907
939
  if (stripped !== null) {
908
- if (isInt(value)) {
909
- const formatted = formatRfc3339Ms(value);
940
+ if (isInt(numeric)) {
941
+ const formatted = formatRfc3339Ms(numeric);
910
942
  if (formatted !== null)
911
943
  return [stripped, formatted];
912
944
  }
@@ -914,8 +946,8 @@ function tryProcessField(key, value) {
914
946
  }
915
947
  stripped = stripSuffixCI(key, "_epoch_s");
916
948
  if (stripped !== null) {
917
- if (isInt(value)) {
918
- const formatted = formatRfc3339Ms(value * 1000);
949
+ if (isInt(numeric)) {
950
+ const formatted = formatRfc3339Ms(numeric * 1000);
919
951
  if (formatted !== null)
920
952
  return [stripped, formatted];
921
953
  }
@@ -934,28 +966,28 @@ function tryProcessField(key, value) {
934
966
  // Group 2: compound currency suffixes
935
967
  stripped = stripSuffixCI(key, "_usd_cents");
936
968
  if (stripped !== null) {
937
- if (isInt(value) && value >= 0)
938
- return [stripped, `$${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`];
969
+ if (isInt(numeric) && numeric >= 0)
970
+ return [stripped, `$${Math.floor(numeric / 100)}.${String(numeric % 100).padStart(2, "0")}`];
939
971
  return null;
940
972
  }
941
973
  stripped = stripSuffixCI(key, "_eur_cents");
942
974
  if (stripped !== null) {
943
- if (isInt(value) && value >= 0)
944
- return [stripped, `\u20ac${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`];
975
+ if (isInt(numeric) && numeric >= 0)
976
+ return [stripped, `\u20ac${Math.floor(numeric / 100)}.${String(numeric % 100).padStart(2, "0")}`];
945
977
  return null;
946
978
  }
947
979
  const gc = tryStripGenericCents(key);
948
980
  if (gc !== null) {
949
981
  const [gcStripped, code] = gc;
950
- if (isInt(value) && value >= 0)
951
- return [gcStripped, `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")} ${code.toUpperCase()}`];
982
+ if (isInt(numeric) && numeric >= 0)
983
+ return [gcStripped, `${Math.floor(numeric / 100)}.${String(numeric % 100).padStart(2, "0")} ${code.toUpperCase()}`];
952
984
  return null;
953
985
  }
954
986
  const gm = tryStripGenericMicro(key);
955
987
  if (gm !== null) {
956
988
  const [gmStripped, code] = gm;
957
- if (isInt(value) && value >= 0)
958
- return [gmStripped, `${Math.floor(value / 1_000_000)}.${String(value % 1_000_000).padStart(6, "0")} ${code.toUpperCase()}`];
989
+ if (isInt(numeric) && numeric >= 0)
990
+ return [gmStripped, `${Math.floor(numeric / 1_000_000)}.${String(numeric % 1_000_000).padStart(6, "0")} ${code.toUpperCase()}`];
959
991
  return null;
960
992
  }
961
993
  // Group 3: multi-char suffixes
@@ -967,20 +999,20 @@ function tryProcessField(key, value) {
967
999
  }
968
1000
  stripped = stripSuffixCI(key, "_minutes");
969
1001
  if (stripped !== null) {
970
- if (isNum(value))
971
- return [stripped, `${plainScalar(value)} minutes`];
1002
+ if (isNum(numeric))
1003
+ return [stripped, `${plainScalar(numeric)} minutes`];
972
1004
  return null;
973
1005
  }
974
1006
  stripped = stripSuffixCI(key, "_hours");
975
1007
  if (stripped !== null) {
976
- if (isNum(value))
977
- return [stripped, `${plainScalar(value)} hours`];
1008
+ if (isNum(numeric))
1009
+ return [stripped, `${plainScalar(numeric)} hours`];
978
1010
  return null;
979
1011
  }
980
1012
  stripped = stripSuffixCI(key, "_days");
981
1013
  if (stripped !== null) {
982
- if (isNum(value))
983
- return [stripped, `${plainScalar(value)} days`];
1014
+ if (isNum(numeric))
1015
+ return [stripped, `${plainScalar(numeric)} days`];
984
1016
  return null;
985
1017
  }
986
1018
  // Group 4: single-unit suffixes
@@ -1000,46 +1032,46 @@ function tryProcessField(key, value) {
1000
1032
  }
1001
1033
  stripped = stripSuffixCI(key, "_bytes");
1002
1034
  if (stripped !== null) {
1003
- if (isInt(value) && value >= 0)
1004
- return [stripped, formatBytesHuman(value)];
1035
+ if (isInt(numeric) && numeric >= 0)
1036
+ return [stripped, formatBytesHuman(numeric)];
1005
1037
  return null;
1006
1038
  }
1007
1039
  stripped = stripSuffixCI(key, "_percent");
1008
1040
  if (stripped !== null) {
1009
- if (isNum(value))
1010
- return [stripped, `${plainScalar(value)}%`];
1041
+ if (isNum(numeric))
1042
+ return [stripped, `${plainScalar(numeric)}%`];
1011
1043
  return null;
1012
1044
  }
1013
1045
  // Group 5: short suffixes (last to avoid false positives)
1014
1046
  stripped = stripSuffixCI(key, "_jpy");
1015
1047
  if (stripped !== null) {
1016
- if (isInt(value) && value >= 0)
1017
- return [stripped, `\u00a5${formatWithCommas(value)}`];
1048
+ if (isInt(numeric) && numeric >= 0)
1049
+ return [stripped, `\u00a5${formatWithCommas(numeric)}`];
1018
1050
  return null;
1019
1051
  }
1020
1052
  stripped = stripSuffixCI(key, "_ns");
1021
1053
  if (stripped !== null) {
1022
- if (isNum(value))
1023
- return [stripped, `${plainScalar(value)}ns`];
1054
+ if (isNum(numeric))
1055
+ return [stripped, `${plainScalar(numeric)}ns`];
1024
1056
  return null;
1025
1057
  }
1026
1058
  stripped = stripSuffixCI(key, "_us");
1027
1059
  if (stripped !== null) {
1028
- if (isNum(value))
1029
- return [stripped, `${plainScalar(value)}\u03bcs`];
1060
+ if (isNum(numeric))
1061
+ return [stripped, `${plainScalar(numeric)}\u03bcs`];
1030
1062
  return null;
1031
1063
  }
1032
1064
  stripped = stripSuffixCI(key, "_ms");
1033
1065
  if (stripped !== null) {
1034
- const fv = formatMsValue(value);
1066
+ const fv = formatMsValue(numeric);
1035
1067
  if (fv !== null)
1036
1068
  return [stripped, fv];
1037
1069
  return null;
1038
1070
  }
1039
1071
  stripped = stripSuffixCI(key, "_s");
1040
1072
  if (stripped !== null) {
1041
- if (isNum(value))
1042
- return [stripped, `${plainScalar(value)}s`];
1073
+ if (isNum(numeric))
1074
+ return [stripped, `${plainScalar(numeric)}s`];
1043
1075
  return null;
1044
1076
  }
1045
1077
  return null;
@@ -1184,47 +1216,6 @@ function extractCurrencyCodeFromStem(stem) {
1184
1216
  // ═══════════════════════════════════════════
1185
1217
  // YAML Rendering
1186
1218
  // ═══════════════════════════════════════════
1187
- function renderYamlProcessed(value, indent, lines) {
1188
- const prefix = " ".repeat(indent);
1189
- if (!isObject(value)) {
1190
- lines.push(`${prefix}${yamlScalar(value)}`);
1191
- return;
1192
- }
1193
- for (const pf of processObjectFields(value)) {
1194
- if (pf.formatted !== null) {
1195
- lines.push(`${prefix}${yamlKey(pf.key)}: "${escapeYamlStr(pf.formatted)}"`);
1196
- }
1197
- else if (isObject(pf.value)) {
1198
- if (Object.keys(pf.value).length > 0) {
1199
- lines.push(`${prefix}${yamlKey(pf.key)}:`);
1200
- renderYamlProcessed(pf.value, indent + 1, lines);
1201
- }
1202
- else {
1203
- lines.push(`${prefix}${yamlKey(pf.key)}: {}`);
1204
- }
1205
- }
1206
- else if (Array.isArray(pf.value)) {
1207
- if (pf.value.length === 0) {
1208
- lines.push(`${prefix}${yamlKey(pf.key)}: []`);
1209
- }
1210
- else {
1211
- lines.push(`${prefix}${yamlKey(pf.key)}:`);
1212
- for (const item of pf.value) {
1213
- if (isObject(item)) {
1214
- lines.push(`${prefix} -`);
1215
- renderYamlProcessed(item, indent + 2, lines);
1216
- }
1217
- else {
1218
- lines.push(`${prefix} - ${yamlScalar(item)}`);
1219
- }
1220
- }
1221
- }
1222
- }
1223
- else {
1224
- lines.push(`${prefix}${yamlKey(pf.key)}: ${yamlScalar(pf.value)}`);
1225
- }
1226
- }
1227
- }
1228
1219
  function renderYamlRaw(value, indent, lines) {
1229
1220
  const prefix = " ".repeat(indent);
1230
1221
  if (isObject(value)) {
@@ -1310,7 +1301,11 @@ function yamlScalar(value) {
1310
1301
  return value.toString();
1311
1302
  if (typeof value === "number")
1312
1303
  return value.toString();
1313
- return `"${escapeYamlStr(JSON.stringify(sortJsonValue(value)))}"`;
1304
+ // Decoded number: emit the exact source literal unquoted, matching JSON's
1305
+ // structure-preserving fidelity contract for YAML too.
1306
+ if (isLosslessNumber(value))
1307
+ return value.toString();
1308
+ return `"${escapeYamlStr(canonicalJsonString(sortJsonValue(value)))}"`;
1314
1309
  }
1315
1310
  // ═══════════════════════════════════════════
1316
1311
  // Plain Rendering (logfmt)
@@ -1366,11 +1361,16 @@ function plainScalar(value) {
1366
1361
  return value.toString();
1367
1362
  if (typeof value === "number")
1368
1363
  return value.toString();
1369
- return JSON.stringify(sortJsonValue(value));
1364
+ // Decoded number: emit the exact source literal (Plain is documented lossy
1365
+ // for arithmetic-derived formatting, but a bare pass-through scalar still
1366
+ // gets its exact digits, not a mangled float64 round-trip).
1367
+ if (isLosslessNumber(value))
1368
+ return value.toString();
1369
+ return canonicalJsonString(sortJsonValue(value));
1370
1370
  }
1371
1371
  function plainScalarRaw(value) {
1372
1372
  if (isObject(value) || Array.isArray(value)) {
1373
- return JSON.stringify(sortJsonValue(value));
1373
+ return canonicalJsonString(sortJsonValue(value));
1374
1374
  }
1375
1375
  return plainScalar(value);
1376
1376
  }
@@ -1417,6 +1417,11 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
1417
1417
  return "<unsupported:function>";
1418
1418
  if (t === "symbol")
1419
1419
  return "<unsupported:symbol>";
1420
+ // A LosslessNumber (from decodeProtocolEvent) is typeof "object" but must
1421
+ // stay opaque: the generic object branch below would otherwise flatten it
1422
+ // into its internal {value, isLosslessNumber} fields via Object.entries.
1423
+ if (isLosslessNumber(value))
1424
+ return value;
1420
1425
  if (value instanceof Error)
1421
1426
  return value.message;
1422
1427
  if (value instanceof Date)
@@ -1444,7 +1449,13 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
1444
1449
  return "<unsupported:unknown>";
1445
1450
  }
1446
1451
  function isObject(value) {
1447
- return typeof value === "object" && value !== null && !Array.isArray(value);
1452
+ // isLosslessNumber excludes decoded numbers: a LosslessNumber is
1453
+ // typeof "object" but is an opaque scalar leaf, never a plain JSON object —
1454
+ // every object-walking function below (redaction, YAML/plain rendering,
1455
+ // sortJsonValue, validation) funnels through this check, so excluding it
1456
+ // here is the single choke point that keeps LosslessNumber from being
1457
+ // destructured into its internal {value, isLosslessNumber} fields.
1458
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !isLosslessNumber(value);
1448
1459
  }
1449
1460
  function sortedObjectKeys(obj) {
1450
1461
  return Object.keys(obj).sort(jcsCompare);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
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, OutputStyle, type OutputOptions, outputOptionsForPolicy, outputJson, outputYaml, outputPlain, redactedValue, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
5
- export { type OutputFormat, type CliEventWriter, CliEmitter, LogFilters, cliParseOutput, cliParseLogFilters, cliOutput, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.js";
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, 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, cliHandleVersionOrContinue, } from "./cli.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, EventDecodeError, RedactionPolicy, OutputStyle, outputOptionsForPolicy, outputJson, outputYaml, outputPlain, redactedValue, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
5
- export { CliEmitter, LogFilters, cliParseOutput, cliParseLogFilters, cliOutput, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.js";
4
+ export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, EventDecodeError, RedactionPolicy, PlainStyle, outputOptionsForPolicy, redactedValue, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
5
+ export { CliEmitter, LogFilters, cliParseOutput, parseOutputTo, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.js";
package/dist/skill.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * supported coding agents (Codex, Claude Code, opencode, Hermes).
7
7
  *
8
8
  * The function performs the filesystem work and returns the protocol value (rendered by
9
- * the caller with `cliOutput`) or throws a {@link SkillError}. It never writes to
9
+ * the caller with `render`) or throws a {@link SkillError}. It never writes to
10
10
  * stdout/stderr itself.
11
11
  */
12
12
  /**
@@ -67,7 +67,7 @@ export interface SkillUninstallStatus {
67
67
  }
68
68
  /**
69
69
  * The result of a skill action. Read fields off the discriminated union (narrow on
70
- * `code`), or pass it to `cliOutput` to serialize — every field is already JSON-shaped.
70
+ * `code`), or pass it to `render` to serialize — every field is already JSON-shaped.
71
71
  */
72
72
  export type SkillReport = {
73
73
  code: "skill_status";
@@ -90,7 +90,7 @@ export type SkillReport = {
90
90
  };
91
91
  /**
92
92
  * Install, uninstall, or report status of `spec`'s skill across the selected agent
93
- * target(s). Returns the protocol value (caller renders with `cliOutput`) or throws a
93
+ * target(s). Returns the protocol value (caller renders with `render`) or throws a
94
94
  * {@link SkillError}. Does not touch stdout/stderr.
95
95
  */
96
96
  export declare function runSkillAdmin(spec: SkillSpec, action: SkillAction, options: SkillOptions): SkillReport;
package/dist/skill.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * supported coding agents (Codex, Claude Code, opencode, Hermes).
7
7
  *
8
8
  * The function performs the filesystem work and returns the protocol value (rendered by
9
- * the caller with `cliOutput`) or throws a {@link SkillError}. It never writes to
9
+ * the caller with `render`) or throws a {@link SkillError}. It never writes to
10
10
  * stdout/stderr itself.
11
11
  */
12
12
  import * as fs from "node:fs";
@@ -26,7 +26,7 @@ export class SkillError extends Error {
26
26
  }
27
27
  /**
28
28
  * Install, uninstall, or report status of `spec`'s skill across the selected agent
29
- * target(s). Returns the protocol value (caller renders with `cliOutput`) or throws a
29
+ * target(s). Returns the protocol value (caller renders with `render`) or throws a
30
30
  * {@link SkillError}. Does not touch stdout/stderr.
31
31
  */
32
32
  export function runSkillAdmin(spec, action, options) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-first-data",
3
- "version": "0.17.3",
4
- "description": "A naming convention that lets AI agents understand your data without being told what it means.",
3
+ "version": "0.19.0",
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": {
7
7
  "type": "git",
@@ -38,5 +38,8 @@
38
38
  "@types/node": "^26.1.1",
39
39
  "tsx": "^4.23.1",
40
40
  "typescript": "^7.0.2"
41
+ },
42
+ "dependencies": {
43
+ "lossless-json": "^4.3.0"
41
44
  }
42
45
  }