agent-first-data 0.12.1 → 0.13.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/cli.js CHANGED
@@ -71,10 +71,7 @@ export function cliOutputWithOptions(value, format, outputOptions) {
71
71
  export function buildCliError(message, hint) {
72
72
  const m = {
73
73
  code: "error",
74
- error_code: "invalid_request",
75
74
  error: message,
76
- retryable: false,
77
- trace: { duration_ms: 0 },
78
75
  };
79
76
  if (hint !== undefined)
80
77
  m.hint = hint;
package/dist/format.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
- * 18 public APIs and 4 types: protocol builders, value redactors (copy and
4
+ * 19 public APIs and 4 types: protocol builders, value redactors (copy and
5
5
  * in-place; cover _secret and _url fields), output formatters, URL-string
6
- * redactors (redactUrlSecrets / WithOptions), parseSize, RedactionPolicy,
7
- * RedactionOptions, OutputStyle, and OutputOptions.
6
+ * redactors (redactUrlSecrets / WithOptions), parseSize, normalizeUtcOffset,
7
+ * RedactionPolicy, RedactionOptions, OutputStyle, and OutputOptions.
8
8
  */
9
9
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
10
10
  [key: string]: JsonValue;
@@ -17,8 +17,7 @@ export declare function buildJsonError(message: string, hint?: string, trace?: J
17
17
  export declare function buildJson(code: string, fields: JsonValue, trace?: JsonValue): JsonValue;
18
18
  export declare enum RedactionPolicy {
19
19
  RedactionTraceOnly = "RedactionTraceOnly",
20
- RedactionNone = "RedactionNone",
21
- RedactionStrict = "RedactionStrict"
20
+ RedactionNone = "RedactionNone"
22
21
  }
23
22
  export type RedactionOptions = {
24
23
  /** Optional scoped policy. Omitted means default full redaction. */
@@ -56,9 +55,9 @@ export declare function outputPlain(value: JsonValue): string;
56
55
  /** Format as single-line logfmt with explicit output options. */
57
56
  export declare function outputPlainWithOptions(value: JsonValue, outputOptions: OutputOptions): string;
58
57
  /** Redact _secret fields in-place. */
59
- export declare function internalRedactSecrets(value: JsonValue): void;
58
+ export declare function redactSecretsInPlace(value: JsonValue): void;
60
59
  /** Redact secret fields in-place using explicit redaction options. */
61
- export declare function internalRedactSecretsWithOptions(value: JsonValue, redactionOptions: RedactionOptions): void;
60
+ export declare function redactSecretsInPlaceWithOptions(value: JsonValue, redactionOptions: RedactionOptions): void;
62
61
  /** Return a JSON-safe copy with default _secret redaction applied. */
63
62
  export declare function redactedValue(value: unknown): JsonValue;
64
63
  /** Return a JSON-safe copy with an explicit redaction policy applied. */
@@ -89,3 +88,9 @@ export declare function redactUrlSecretsWithOptions(url: string, redactionOption
89
88
  * Case-insensitive. Trims whitespace. Returns null for invalid input.
90
89
  */
91
90
  export declare function parseSize(s: string): number | null;
91
+ /**
92
+ * Normalize a fixed UTC offset string to "UTC" or ±HH:MM.
93
+ * This helper handles fixed offsets only; IANA timezone names and DST rules
94
+ * are intentionally out of scope.
95
+ */
96
+ export declare function normalizeUtcOffset(value: string): string | null;
package/dist/format.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
- * 18 public APIs and 4 types: protocol builders, value redactors (copy and
4
+ * 19 public APIs and 4 types: protocol builders, value redactors (copy and
5
5
  * in-place; cover _secret and _url fields), output formatters, URL-string
6
- * redactors (redactUrlSecrets / WithOptions), parseSize, RedactionPolicy,
7
- * RedactionOptions, OutputStyle, and OutputOptions.
6
+ * redactors (redactUrlSecrets / WithOptions), parseSize, normalizeUtcOffset,
7
+ * RedactionPolicy, RedactionOptions, OutputStyle, and OutputOptions.
8
8
  */
9
9
  // ═══════════════════════════════════════════
10
10
  // Public API: Protocol Builders
@@ -40,7 +40,6 @@ export var RedactionPolicy;
40
40
  (function (RedactionPolicy) {
41
41
  RedactionPolicy["RedactionTraceOnly"] = "RedactionTraceOnly";
42
42
  RedactionPolicy["RedactionNone"] = "RedactionNone";
43
- RedactionPolicy["RedactionStrict"] = "RedactionStrict";
44
43
  })(RedactionPolicy || (RedactionPolicy = {}));
45
44
  export var OutputStyle;
46
45
  (function (OutputStyle) {
@@ -91,18 +90,18 @@ export function outputPlainWithOptions(value, outputOptions) {
91
90
  }
92
91
  pairs.sort(([a], [b]) => jcsCompare(a, b));
93
92
  return pairs
94
- .map(([k, v]) => `${k}=${quoteLogfmtValue(v)}`)
93
+ .map(([k, v]) => `${quoteLogfmtKey(k)}=${quoteLogfmtValue(v)}`)
95
94
  .join(" ");
96
95
  }
97
96
  // ═══════════════════════════════════════════
98
97
  // Public API: Redaction & Utility
99
98
  // ═══════════════════════════════════════════
100
99
  /** Redact _secret fields in-place. */
101
- export function internalRedactSecrets(value) {
100
+ export function redactSecretsInPlace(value) {
102
101
  redactSecrets(value);
103
102
  }
104
103
  /** Redact secret fields in-place using explicit redaction options. */
105
- export function internalRedactSecretsWithOptions(value, redactionOptions) {
104
+ export function redactSecretsInPlaceWithOptions(value, redactionOptions) {
106
105
  applyRedactionOptions(value, redactionOptions);
107
106
  }
108
107
  /** Return a JSON-safe copy with default _secret redaction applied. */
@@ -172,7 +171,7 @@ export function parseSize(s) {
172
171
  else {
173
172
  return null;
174
173
  }
175
- if (!numStr)
174
+ if (!numStr || !/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(numStr))
176
175
  return null;
177
176
  const n = Number(numStr);
178
177
  if (isNaN(n) || n < 0 || !isFinite(n))
@@ -182,6 +181,49 @@ export function parseSize(s) {
182
181
  return null;
183
182
  return result;
184
183
  }
184
+ /**
185
+ * Normalize a fixed UTC offset string to "UTC" or ±HH:MM.
186
+ * This helper handles fixed offsets only; IANA timezone names and DST rules
187
+ * are intentionally out of scope.
188
+ */
189
+ export function normalizeUtcOffset(value) {
190
+ const s = value.trim();
191
+ if (s.toLowerCase() === "utc" || s.toLowerCase() === "z")
192
+ return "UTC";
193
+ if (!s || (s[0] !== "+" && s[0] !== "-"))
194
+ return null;
195
+ const parsed = parseUtcOffsetBody(s.slice(1));
196
+ if (parsed === null)
197
+ return null;
198
+ const [hours, minutes] = parsed;
199
+ if (hours > 23 || minutes > 59)
200
+ return null;
201
+ if (hours === 0 && minutes === 0)
202
+ return "UTC";
203
+ return `${s[0]}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
204
+ }
205
+ function parseUtcOffsetBody(body) {
206
+ if (!body)
207
+ return null;
208
+ if (body.includes(":")) {
209
+ const parts = body.split(":");
210
+ if (parts.length !== 2)
211
+ return null;
212
+ const [hours, minutes] = parts;
213
+ if (!hours || hours.length > 2 || minutes.length !== 2)
214
+ return null;
215
+ if (!/^[0-9]+$/.test(hours) || !/^[0-9]+$/.test(minutes))
216
+ return null;
217
+ return [Number(hours), Number(minutes)];
218
+ }
219
+ if (!/^[0-9]+$/.test(body))
220
+ return null;
221
+ if (body.length === 1 || body.length === 2)
222
+ return [Number(body), 0];
223
+ if (body.length === 4)
224
+ return [Number(body.slice(0, 2)), Number(body.slice(2))];
225
+ return null;
226
+ }
185
227
  const DEFAULT_CONTEXT = {
186
228
  secretNames: new Set(),
187
229
  };
@@ -200,54 +242,35 @@ function keyHasUrlSuffix(key) {
200
242
  function isSecretKey(key, secretNames) {
201
243
  return keyHasSecretSuffix(key) || secretNames.has(key);
202
244
  }
203
- function redactSecrets(value, context = DEFAULT_CONTEXT) {
245
+ const MAX_DEPTH = 256;
246
+ function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
247
+ if (depth >= MAX_DEPTH)
248
+ return;
204
249
  if (isObject(value)) {
205
250
  for (const k of Object.keys(value)) {
206
251
  const v = value[k];
207
252
  if (isSecretKey(k, context.secretNames)) {
208
- if (isObject(v) || Array.isArray(v)) {
209
- redactSecrets(v, context);
253
+ value[k] = "***";
254
+ }
255
+ else if (keyHasUrlSuffix(k)) {
256
+ if (typeof v === "string") {
257
+ value[k] = redactUrlFieldValue(v, context.secretNames);
210
258
  }
211
259
  else {
212
- value[k] = "***";
260
+ value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
213
261
  }
214
262
  }
215
- else if (keyHasUrlSuffix(k) && typeof v === "string") {
216
- const redacted = redactUrlInStr(v, context.secretNames);
217
- if (redacted !== null)
218
- value[k] = redacted;
219
- }
220
263
  else {
221
- redactSecrets(v, context);
264
+ value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
222
265
  }
223
266
  }
224
267
  }
225
268
  else if (Array.isArray(value)) {
226
269
  for (let i = 0; i < value.length; i++) {
227
- redactSecrets(value[i], context);
228
- }
229
- }
230
- }
231
- function redactSecretsStrict(value, context = DEFAULT_CONTEXT) {
232
- if (isObject(value)) {
233
- for (const k of Object.keys(value)) {
234
- const v = value[k];
235
- if (isSecretKey(k, context.secretNames)) {
236
- value[k] = "***";
237
- }
238
- else if (keyHasUrlSuffix(k) && typeof v === "string") {
239
- const redacted = redactUrlInStr(v, context.secretNames);
240
- if (redacted !== null)
241
- value[k] = redacted;
242
- }
243
- else {
244
- redactSecretsStrict(v, context);
245
- }
246
- }
247
- }
248
- else if (Array.isArray(value)) {
249
- for (let i = 0; i < value.length; i++) {
250
- redactSecretsStrict(value[i], context);
270
+ if (depth + 1 >= MAX_DEPTH)
271
+ value[i] = "***";
272
+ else
273
+ redactSecrets(value[i], context, depth + 1);
251
274
  }
252
275
  }
253
276
  }
@@ -266,9 +289,6 @@ function applyRedactionPolicyWithContext(value, redactionPolicy, context) {
266
289
  break;
267
290
  case RedactionPolicy.RedactionNone:
268
291
  break;
269
- case RedactionPolicy.RedactionStrict:
270
- redactSecretsStrict(value, context);
271
- break;
272
292
  default:
273
293
  // Empty/unknown policy falls back to default full redaction.
274
294
  redactSecrets(value, context);
@@ -284,8 +304,13 @@ function applyRedactionPolicyWithContext(value, redactionPolicy, context) {
284
304
  * original). Only secret spans change; all other bytes are preserved.
285
305
  */
286
306
  function redactUrlInStr(s, secretNames) {
287
- // Fast path + precondition: a single, whitespace-free, scheme-prefixed URL.
288
- if (!s.includes("://") || !isSingleUrl(s) || !urlParses(s)) {
307
+ // Precondition (spec): a single, whitespace-free, scheme-prefixed URL.
308
+ // The gate is scheme + no-whitespace only — NOT "parses via `new URL`". Span
309
+ // location below is purely byte-wise (we never re-serialize the URL), so a
310
+ // `new URL` gate would only diverge across languages — it rejects inputs
311
+ // (ports > 65535, empty host) that other languages' URL libraries accept, and
312
+ // rejecting here would silently leak secrets in the values it rejects.
313
+ if (!s.includes("://") || !isSingleUrl(s)) {
289
314
  return null;
290
315
  }
291
316
  const schemeSep = s.indexOf("://");
@@ -327,12 +352,31 @@ function redactUrlInStr(s, secretNames) {
327
352
  }
328
353
  return `${scheme}://${newAuthority}${newRemainder}`;
329
354
  }
355
+ function redactUrlFieldValue(s, secretNames) {
356
+ const redacted = redactUrlInStr(s, secretNames);
357
+ if (redacted !== null)
358
+ return redacted;
359
+ const trimmed = s.trim();
360
+ if (trimmed !== s) {
361
+ const trimmedRedacted = redactUrlInStr(trimmed, secretNames);
362
+ if (trimmedRedacted !== null)
363
+ return trimmedRedacted;
364
+ }
365
+ // Fail closed: a _url value we could not parse as a clean scheme-prefixed
366
+ // URL, yet which carries a credential sigil ('@' userinfo) or internal
367
+ // whitespace, is redacted wholesale rather than passed through. A schemeless
368
+ // connection string like user:pass@host/db has no scheme anchor for the
369
+ // surgical span logic above, so blanket redaction is the safe default.
370
+ if (/\s/.test(s) || s.includes("@"))
371
+ return "***";
372
+ return s;
373
+ }
330
374
  /**
331
375
  * Replace the userinfo password (user:pass@) with "***", preserving the
332
376
  * username. Authority without '@', or userinfo without ':', is unchanged.
333
377
  */
334
378
  function redactUserinfoPassword(authority) {
335
- const at = authority.indexOf("@");
379
+ const at = authority.lastIndexOf("@");
336
380
  if (at < 0)
337
381
  return authority;
338
382
  const userinfo = authority.slice(0, at);
@@ -371,16 +415,6 @@ function formDecode(rawKey) {
371
415
  return withSpaces;
372
416
  }
373
417
  }
374
- /** True when s parses as a URL (mirrors Rust's url::Url::parse check). */
375
- function urlParses(s) {
376
- try {
377
- new URL(s);
378
- return true;
379
- }
380
- catch {
381
- return false;
382
- }
383
- }
384
418
  /**
385
419
  * True when s begins with a URL scheme (ALPHA *(ALPHA / DIGIT / "+" / "-" /
386
420
  * ".") "://") and contains no ASCII whitespace — i.e. a single bare URL, not a
@@ -449,20 +483,29 @@ function tryProcessField(key, value) {
449
483
  // Group 1: compound timestamp suffixes
450
484
  stripped = stripSuffixCI(key, "_epoch_ms");
451
485
  if (stripped !== null) {
452
- if (isInt(value))
453
- return [stripped, formatRfc3339Ms(value)];
486
+ if (isInt(value)) {
487
+ const formatted = formatRfc3339Ms(value);
488
+ if (formatted !== null)
489
+ return [stripped, formatted];
490
+ }
454
491
  return null;
455
492
  }
456
493
  stripped = stripSuffixCI(key, "_epoch_s");
457
494
  if (stripped !== null) {
458
- if (isInt(value))
459
- return [stripped, formatRfc3339Ms(value * 1000)];
495
+ if (isInt(value)) {
496
+ const formatted = formatRfc3339Ms(value * 1000);
497
+ if (formatted !== null)
498
+ return [stripped, formatted];
499
+ }
460
500
  return null;
461
501
  }
462
502
  stripped = stripSuffixCI(key, "_epoch_ns");
463
503
  if (stripped !== null) {
464
- if (isInt(value))
465
- return [stripped, formatRfc3339Ms(Math.floor(value / 1_000_000))];
504
+ if (isInt(value)) {
505
+ const formatted = formatRfc3339Ms(Math.floor(value / 1_000_000));
506
+ if (formatted !== null)
507
+ return [stripped, formatted];
508
+ }
466
509
  return null;
467
510
  }
468
511
  // Group 2: compound currency suffixes
@@ -535,9 +578,6 @@ function tryProcessField(key, value) {
535
578
  return [stripped, `${plainScalar(value)}%`];
536
579
  return null;
537
580
  }
538
- stripped = stripSuffixCI(key, "_secret");
539
- if (stripped !== null)
540
- return [stripped, "***"];
541
581
  // Group 5: short suffixes (last to avoid false positives)
542
582
  stripped = stripSuffixCI(key, "_btc");
543
583
  if (stripped !== null) {
@@ -581,6 +621,11 @@ function tryProcessField(key, value) {
581
621
  function processObjectFields(obj) {
582
622
  const entries = [];
583
623
  for (const [k, v] of Object.entries(obj)) {
624
+ const secretStripped = stripSuffixCI(k, "_secret");
625
+ if (secretStripped !== null) {
626
+ entries.push({ stripped: secretStripped, original: k, value: v, formatted: null });
627
+ continue;
628
+ }
584
629
  const result = tryProcessField(k, v);
585
630
  if (result !== null) {
586
631
  entries.push({ stripped: result[0], original: k, value: v, formatted: result[1] });
@@ -622,14 +667,36 @@ function formatMsValue(value) {
622
667
  return formatMsAsSeconds(value);
623
668
  return `${plainScalar(value)}ms`;
624
669
  }
670
+ const MIN_RFC3339_MS = -62135596800000;
671
+ const MAX_RFC3339_MS = 253402300799999;
625
672
  function formatRfc3339Ms(ms) {
626
- try {
627
- const d = new Date(ms);
628
- return d.toISOString().replace(/(\.\d{3})\d*Z$/, "$1Z");
629
- }
630
- catch {
631
- return String(ms);
632
- }
673
+ if (ms < MIN_RFC3339_MS || ms > MAX_RFC3339_MS)
674
+ return null;
675
+ const d = new Date(ms);
676
+ if (Number.isNaN(d.getTime()))
677
+ return null;
678
+ return d.toISOString().replace(/(\.\d{3})\d*Z$/, "$1Z");
679
+ }
680
+ /**
681
+ * Round to `digits` decimals using round-half-to-even (banker's rounding) and
682
+ * format with a fixed decimal count. `Number.toFixed` rounds half away from
683
+ * zero, which diverges from the printf `%.1f` used by Rust/Go/Python on exact
684
+ * ties (e.g. 1280 bytes = 1.25 KB → "1.2KB", not "1.3KB").
685
+ */
686
+ function toFixedHalfEven(x, digits) {
687
+ const factor = 10 ** digits;
688
+ const scaled = x * factor;
689
+ const floor = Math.floor(scaled);
690
+ const diff = scaled - floor;
691
+ const eps = 1e-9;
692
+ let rounded;
693
+ if (diff > 0.5 + eps)
694
+ rounded = floor + 1;
695
+ else if (diff < 0.5 - eps)
696
+ rounded = floor;
697
+ else
698
+ rounded = floor % 2 === 0 ? floor : floor + 1;
699
+ return (rounded / factor).toFixed(digits);
633
700
  }
634
701
  function formatBytesHuman(bytes) {
635
702
  const KB = 1024;
@@ -639,13 +706,13 @@ function formatBytesHuman(bytes) {
639
706
  const sign = bytes < 0 ? "-" : "";
640
707
  const b = Math.abs(bytes);
641
708
  if (b >= TB)
642
- return `${sign}${(b / TB).toFixed(1)}TB`;
709
+ return `${sign}${toFixedHalfEven(b / TB, 1)}TB`;
643
710
  if (b >= GB)
644
- return `${sign}${(b / GB).toFixed(1)}GB`;
711
+ return `${sign}${toFixedHalfEven(b / GB, 1)}GB`;
645
712
  if (b >= MB)
646
- return `${sign}${(b / MB).toFixed(1)}MB`;
713
+ return `${sign}${toFixedHalfEven(b / MB, 1)}MB`;
647
714
  if (b >= KB)
648
- return `${sign}${(b / KB).toFixed(1)}KB`;
715
+ return `${sign}${toFixedHalfEven(b / KB, 1)}KB`;
649
716
  return `${bytes}B`;
650
717
  }
651
718
  function formatWithCommas(n) {
@@ -671,7 +738,7 @@ function extractCurrencyCode(key) {
671
738
  if (idx < 0)
672
739
  return null;
673
740
  const code = withoutCents.slice(idx + 1);
674
- if (!code)
741
+ if (!/^[A-Za-z]{3,4}$/.test(code))
675
742
  return null;
676
743
  return code;
677
744
  }
@@ -686,23 +753,23 @@ function renderYamlProcessed(value, indent, lines) {
686
753
  }
687
754
  for (const pf of processObjectFields(value)) {
688
755
  if (pf.formatted !== null) {
689
- lines.push(`${prefix}${pf.key}: "${escapeYamlStr(pf.formatted)}"`);
756
+ lines.push(`${prefix}${yamlKey(pf.key)}: "${escapeYamlStr(pf.formatted)}"`);
690
757
  }
691
758
  else if (isObject(pf.value)) {
692
759
  if (Object.keys(pf.value).length > 0) {
693
- lines.push(`${prefix}${pf.key}:`);
760
+ lines.push(`${prefix}${yamlKey(pf.key)}:`);
694
761
  renderYamlProcessed(pf.value, indent + 1, lines);
695
762
  }
696
763
  else {
697
- lines.push(`${prefix}${pf.key}: {}`);
764
+ lines.push(`${prefix}${yamlKey(pf.key)}: {}`);
698
765
  }
699
766
  }
700
767
  else if (Array.isArray(pf.value)) {
701
768
  if (pf.value.length === 0) {
702
- lines.push(`${prefix}${pf.key}: []`);
769
+ lines.push(`${prefix}${yamlKey(pf.key)}: []`);
703
770
  }
704
771
  else {
705
- lines.push(`${prefix}${pf.key}:`);
772
+ lines.push(`${prefix}${yamlKey(pf.key)}:`);
706
773
  for (const item of pf.value) {
707
774
  if (isObject(item)) {
708
775
  lines.push(`${prefix} -`);
@@ -715,7 +782,7 @@ function renderYamlProcessed(value, indent, lines) {
715
782
  }
716
783
  }
717
784
  else {
718
- lines.push(`${prefix}${pf.key}: ${yamlScalar(pf.value)}`);
785
+ lines.push(`${prefix}${yamlKey(pf.key)}: ${yamlScalar(pf.value)}`);
719
786
  }
720
787
  }
721
788
  }
@@ -736,24 +803,24 @@ function renderYamlRaw(value, indent, lines) {
736
803
  function renderYamlFieldRaw(prefix, key, value, indent, lines) {
737
804
  if (isObject(value)) {
738
805
  if (Object.keys(value).length > 0) {
739
- lines.push(`${prefix}${key}:`);
806
+ lines.push(`${prefix}${yamlKey(key)}:`);
740
807
  renderYamlRaw(value, indent + 1, lines);
741
808
  }
742
809
  else {
743
- lines.push(`${prefix}${key}: {}`);
810
+ lines.push(`${prefix}${yamlKey(key)}: {}`);
744
811
  }
745
812
  }
746
813
  else if (Array.isArray(value)) {
747
814
  if (value.length > 0) {
748
- lines.push(`${prefix}${key}:`);
815
+ lines.push(`${prefix}${yamlKey(key)}:`);
749
816
  renderYamlArrayRaw(value, indent + 1, lines);
750
817
  }
751
818
  else {
752
- lines.push(`${prefix}${key}: []`);
819
+ lines.push(`${prefix}${yamlKey(key)}: []`);
753
820
  }
754
821
  }
755
822
  else {
756
- lines.push(`${prefix}${key}: ${yamlScalar(value)}`);
823
+ lines.push(`${prefix}${yamlKey(key)}: ${yamlScalar(value)}`);
757
824
  }
758
825
  }
759
826
  function renderYamlArrayRaw(arr, indent, lines) {
@@ -783,7 +850,17 @@ function renderYamlArrayRaw(arr, indent, lines) {
783
850
  }
784
851
  }
785
852
  function escapeYamlStr(s) {
786
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
853
+ return s
854
+ .replace(/\\/g, "\\\\")
855
+ .replace(/"/g, '\\"')
856
+ .replace(/\n/g, "\\n")
857
+ .replace(/\r/g, "\\r")
858
+ .replace(/\t/g, "\\t")
859
+ .replace(/\f/g, "\\f")
860
+ .replace(/\v/g, "\\v");
861
+ }
862
+ function yamlKey(key) {
863
+ return /^[A-Za-z0-9_.-]+$/.test(key) ? key : `"${escapeYamlStr(key)}"`;
787
864
  }
788
865
  function yamlScalar(value) {
789
866
  if (typeof value === "string")
@@ -794,7 +871,7 @@ function yamlScalar(value) {
794
871
  return value.toString();
795
872
  if (typeof value === "number")
796
873
  return value.toString();
797
- return `"${String(value).replace(/"/g, '\\"')}"`;
874
+ return `"${escapeYamlStr(JSON.stringify(sortJsonValue(value)))}"`;
798
875
  }
799
876
  // ═══════════════════════════════════════════
800
877
  // Plain Rendering (logfmt)
@@ -850,7 +927,7 @@ function plainScalar(value) {
850
927
  return value.toString();
851
928
  if (typeof value === "number")
852
929
  return value.toString();
853
- return String(value);
930
+ return JSON.stringify(sortJsonValue(value));
854
931
  }
855
932
  function plainScalarRaw(value) {
856
933
  if (isObject(value) || Array.isArray(value)) {
@@ -868,13 +945,20 @@ function quoteLogfmtValue(value) {
868
945
  .replace(/"/g, '\\"')
869
946
  .replace(/\n/g, "\\n")
870
947
  .replace(/\r/g, "\\r")
871
- .replace(/\t/g, "\\t");
948
+ .replace(/\t/g, "\\t")
949
+ .replace(/\f/g, "\\f")
950
+ .replace(/\v/g, "\\v");
872
951
  return `"${escaped}"`;
873
952
  }
953
+ function quoteLogfmtKey(key) {
954
+ return /^[A-Za-z0-9_.-]+$/.test(key) ? key : quoteLogfmtValue(key);
955
+ }
874
956
  // ═══════════════════════════════════════════
875
957
  // Utilities
876
958
  // ═══════════════════════════════════════════
877
- function sanitizeForJson(value, stack = new WeakSet()) {
959
+ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
960
+ if (depth >= MAX_DEPTH)
961
+ return "***";
878
962
  if (value === null)
879
963
  return null;
880
964
  const t = typeof value;
@@ -902,7 +986,7 @@ function sanitizeForJson(value, stack = new WeakSet()) {
902
986
  if (stack.has(value))
903
987
  return "<unsupported:circular>";
904
988
  stack.add(value);
905
- const out = value.map((item) => sanitizeForJson(item, stack));
989
+ const out = value.map((item) => sanitizeForJson(item, stack, depth + 1));
906
990
  stack.delete(value);
907
991
  return out;
908
992
  }
@@ -913,7 +997,7 @@ function sanitizeForJson(value, stack = new WeakSet()) {
913
997
  stack.add(obj);
914
998
  const out = {};
915
999
  for (const [k, v] of Object.entries(obj)) {
916
- out[k] = sanitizeForJson(v, stack);
1000
+ out[k] = sanitizeForJson(v, stack, depth + 1);
917
1001
  }
918
1002
  stack.delete(obj);
919
1003
  return out;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { buildJsonOk, buildJsonError, buildJson, type JsonValue, RedactionPolicy, type RedactionOptions, OutputStyle, type OutputOptions, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, internalRedactSecrets, internalRedactSecretsWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, redactUrlSecrets, redactUrlSecretsWithOptions, parseSize, } from "./format.js";
4
+ export { buildJsonOk, buildJsonError, buildJson, type JsonValue, RedactionPolicy, type RedactionOptions, OutputStyle, type OutputOptions, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, redactSecretsInPlace, redactSecretsInPlaceWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, redactUrlSecrets, redactUrlSecretsWithOptions, parseSize, normalizeUtcOffset, } from "./format.js";
5
5
  export { log, span, initJson, initPlain, initYaml } from "./afdata_logging.js";
6
6
  export { type OutputFormat, cliParseOutput, cliParseLogFilters, cliOutput, cliOutputWithOptions, buildCliError, } from "./cli.js";
7
7
  export { type SkillSpec, type SkillAction, type SkillScope, type SkillAgentSelection, type SkillAgent, type SkillOptions, type SkillTargetStatus, type SkillUninstallStatus, type SkillReport, SkillError, runSkillAdmin, } from "./skill.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { buildJsonOk, buildJsonError, buildJson, RedactionPolicy, OutputStyle, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, internalRedactSecrets, internalRedactSecretsWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, redactUrlSecrets, redactUrlSecretsWithOptions, parseSize, } from "./format.js";
4
+ export { buildJsonOk, buildJsonError, buildJson, RedactionPolicy, OutputStyle, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, redactSecretsInPlace, redactSecretsInPlaceWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, redactUrlSecrets, redactUrlSecretsWithOptions, parseSize, normalizeUtcOffset, } from "./format.js";
5
5
  export { log, span, initJson, initPlain, initYaml } from "./afdata_logging.js";
6
6
  export { cliParseOutput, cliParseLogFilters, cliOutput, cliOutputWithOptions, buildCliError, } from "./cli.js";
7
7
  export { SkillError, runSkillAdmin, } from "./skill.js";