agent-first-data 0.35.1 → 0.36.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 CHANGED
@@ -17,10 +17,12 @@ console.log(render(event.toJSON(), "json"));
17
17
  console.log(render(event.toJSON(), "plain"));
18
18
  ```
19
19
 
20
- Useful names use TypeScript casing: `render`, `outputOptionsForPolicy`, `redactedValue`, `redactUrlSecrets`, `redactUrlsInText`, `redactArgv`, `normalizeUtcOffset`, `isValidRfc3339Date`, `isValidRfc3339Time`, `isValidRfc3339`, `isValidBcp47`, `decodeProtocolEvent`, `cliParseOutput`, `cliParseLogFilters`, `buildCliError`, and `buildCliVersion`.
20
+ Useful names use TypeScript casing: `render`, `outputOptionsForPolicy`, `redactedValue`, `redactUrlSecrets`, `redactUrlsInText`, `redactArgv`, `normalizeUtcOffset`, `isValidRfc3339Date`, `isValidRfc3339Time`, `isValidRfc3339`, `isValidBcp47`, `decodeProtocolEvent`, `cliParseOutput`, `cliParseLogFilters`, `buildCliError`, `buildCliVersion`, and `CliEmitter`.
21
21
 
22
22
  ## Behavior Notes
23
23
 
24
+ - `OutputOptions.redaction.revealNames` reveals exact structured field names at the selected output boundary. Children still pass through redaction; URL parameters and argv receive no exemption.
25
+ - The protocol reader accepts base-profile events. An absent `error.retryable` remains nullable; strict validation is a separate opt-in.
24
26
  - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
25
27
  - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed, and internal whitespace or a `://` that is not the start of a clean URL redacts the whole field, where whitespace is the explicit Unicode `White_Space` set the spec enumerates, not the language's native predicate.
26
28
  - `OutputOptions.redaction.urlNames` applies URL treatment to exact legacy field names
@@ -29,7 +31,7 @@ Useful names use TypeScript casing: `render`, `outputOptionsForPolicy`, `redacte
29
31
  - YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON; it quotes a value only when it contains ASCII space, tab, newline, carriage return, form feed, vertical tab, NBSP, `=`, `"`, or `\`, and escapes `\`, `"`, newline, carriage return, tab, form feed, and vertical tab inside the quotes.
30
32
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
31
33
  - `buildCliError(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
32
- - Do not add a raw version/help pre-parser. Until the TypeScript `CliSpec` compiler lands, applications should keep lifecycle parsing in their own parser rather than claim AFDATA closed-world help compatibility.
34
+ - Do not add a raw version/help pre-parser. This SDK provides emitters and readers; argv and lifecycle parsing belong to the application. Claim help-v2 or closed-world compatibility only when using a registry compiler.
33
35
 
34
36
  ## Reference
35
37
 
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "exit_codes": {
46
46
  "type": "array",
47
- "description": "Exit codes this CLI returns beyond AFDATA's 0/1/2, rendered into the reference's exit-code table. 0/1/2 are AFDATA's own and may not be redeclared.",
47
+ "description": "Exit codes this CLI returns beyond AFDATA's 0/1/2/4, rendered into the reference's exit-code table. AFDATA's codes may not be redeclared.",
48
48
  "items": {
49
49
  "type": "object",
50
50
  "additionalProperties": false,
@@ -56,7 +56,10 @@
56
56
  "code": {
57
57
  "type": "integer",
58
58
  "minimum": 3,
59
- "maximum": 255
59
+ "maximum": 255,
60
+ "not": {
61
+ "const": 4
62
+ }
60
63
  },
61
64
  "meaning": {
62
65
  "$ref": "#/$defs/nonEmpty"
package/dist/cli.d.ts CHANGED
@@ -108,6 +108,9 @@ export declare class CliEmitter {
108
108
  private terminalEmitted;
109
109
  private diagnostic?;
110
110
  private strictProtocol;
111
+ private writeFailed;
112
+ /** Where an event-stream emitter reports a failed write; tests replace it. */
113
+ private processStderr;
111
114
  /**
112
115
  * Create an event-stream emitter: every event, including `error`, goes to the
113
116
  * single `writer`. Use CliEmitter.finite for a one-shot command that should
@@ -154,10 +157,17 @@ export declare class CliEmitter {
154
157
  * exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
155
158
  *
156
159
  * A successful write returns `successCode`; a broken pipe (the reader hung up)
157
- * returns `0`; any other write or validation failure returns `4`. A library
158
- * never calls process.exit itself — return this code and let the caller exit.
160
+ * returns `0` and says nothing. Any other write failure returns `4` after
161
+ * reporting `output_write_failed` as an `error` event: on the diagnostic
162
+ * writer in finite mode, on the process's stderr in event-stream mode, whose
163
+ * only writer is the one that just failed. That report is best effort. An
164
+ * event the emitter refuses (a protocol violation or a second terminal
165
+ * event) is the command's own failure, not an output one, and returns `1`.
166
+ * A library never calls process.exit itself — return this code and let the
167
+ * caller exit.
159
168
  */
160
169
  finish(event: Event, successCode: number): number;
170
+ private reportOutputWriteFailed;
161
171
  /**
162
172
  * Convenience over finish: emit a `result` payload and return `0` on success.
163
173
  *
package/dist/cli.js CHANGED
@@ -156,6 +156,9 @@ export class CliEmitter {
156
156
  terminalEmitted = false;
157
157
  diagnostic;
158
158
  strictProtocol = false;
159
+ writeFailed = false;
160
+ /** Where an event-stream emitter reports a failed write; tests replace it. */
161
+ processStderr = processStderrWriter;
159
162
  /**
160
163
  * Create an event-stream emitter: every event, including `error`, goes to the
161
164
  * single `writer`. Use CliEmitter.finite for a one-shot command that should
@@ -217,6 +220,7 @@ export class CliEmitter {
217
220
  return this;
218
221
  }
219
222
  emit(event) {
223
+ this.writeFailed = false;
220
224
  const jsonValue = event.toJSON();
221
225
  validateProtocolEvent(jsonValue, this.strictProtocol);
222
226
  const kind = jsonValue.kind;
@@ -233,7 +237,15 @@ export class CliEmitter {
233
237
  else {
234
238
  throw new Error(`unsupported event kind ${JSON.stringify(kind)}`);
235
239
  }
236
- this.sinkFor(kind)(`${render(jsonValue, this.format, this.outputOptions)}\n`);
240
+ const line = `${render(jsonValue, this.format, this.outputOptions)}\n`;
241
+ try {
242
+ this.sinkFor(kind)(line);
243
+ }
244
+ catch (err) {
245
+ // Marks the failure as output's, not the event's, for finish().
246
+ this.writeFailed = true;
247
+ throw err;
248
+ }
237
249
  if (kind === "result" || kind === "error")
238
250
  this.terminalEmitted = true;
239
251
  }
@@ -265,8 +277,14 @@ export class CliEmitter {
265
277
  * exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
266
278
  *
267
279
  * A successful write returns `successCode`; a broken pipe (the reader hung up)
268
- * returns `0`; any other write or validation failure returns `4`. A library
269
- * never calls process.exit itself — return this code and let the caller exit.
280
+ * returns `0` and says nothing. Any other write failure returns `4` after
281
+ * reporting `output_write_failed` as an `error` event: on the diagnostic
282
+ * writer in finite mode, on the process's stderr in event-stream mode, whose
283
+ * only writer is the one that just failed. That report is best effort. An
284
+ * event the emitter refuses (a protocol violation or a second terminal
285
+ * event) is the command's own failure, not an output one, and returns `1`.
286
+ * A library never calls process.exit itself — return this code and let the
287
+ * caller exit.
270
288
  */
271
289
  finish(event, successCode) {
272
290
  try {
@@ -274,7 +292,28 @@ export class CliEmitter {
274
292
  return successCode;
275
293
  }
276
294
  catch (err) {
277
- return CliEmitter.isBrokenPipe(err) ? 0 : 4;
295
+ if (CliEmitter.isBrokenPipe(err))
296
+ return 0;
297
+ if (!this.writeFailed)
298
+ return 1;
299
+ this.reportOutputWriteFailed();
300
+ return 4;
301
+ }
302
+ }
303
+ reportOutputWriteFailed() {
304
+ try {
305
+ const event = jsonError("output_write_failed", "could not write command output").build();
306
+ if (this.diagnostic) {
307
+ this.emit(event);
308
+ }
309
+ else {
310
+ // Event-stream mode has no diagnostic writer, so the report goes to
311
+ // the process's stderr through an emitter of its own.
312
+ CliEmitter.stream(this.processStderr, this.format).withStrictProtocol().emit(event);
313
+ }
314
+ }
315
+ catch {
316
+ // Best effort: an unwritable stderr leaves only the exit code.
278
317
  }
279
318
  }
280
319
  /**
package/dist/format.d.ts CHANGED
@@ -10,9 +10,10 @@
10
10
  * Number literal fidelity: decodeProtocolEvent parses with `lossless-json`
11
11
  * instead of JSON.parse, so a number's exact source literal (arbitrary
12
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. Native bigint values
13
+ * decode. Parsed numbers become LosslessNumber internally; every rendering
14
+ * path here treats it as an opaque scalar leaf (never destructured as a plain
15
+ * object). JSON re-emits its exact literal text; YAML/plain render it in the
16
+ * spec's canonical number form without losing a digit. Native bigint values
16
17
  * are also supported so callers can render integers beyond Number's safe
17
18
  * range without first corrupting them.
18
19
  */
@@ -22,7 +23,8 @@ export type JsonValue = string | number | bigint | boolean | null | JsonValue[]
22
23
  export type LogLevel = "debug" | "info" | "warn" | "error";
23
24
  /**
24
25
  * Event is an opaque type wrapping a protocol event envelope.
25
- * Guaranteed to be strict-valid by construction.
26
+ * Non-error builders retain caller-provided traces verbatim; validate the
27
+ * envelope before accepting externally supplied traces as protocol-valid.
26
28
  */
27
29
  export declare class Event {
28
30
  private readonly value;
@@ -100,7 +102,7 @@ export type DecodedError = {
100
102
  kind: "error";
101
103
  code: string;
102
104
  message: string;
103
- retryable: boolean;
105
+ retryable: boolean | null;
104
106
  hint?: string;
105
107
  fields: Record<string, JsonValue>;
106
108
  trace?: Record<string, JsonValue>;
@@ -117,12 +119,12 @@ export type DecodedLog = {
117
119
  };
118
120
  /** Discriminated union of decoded protocol events, narrow on `kind`. */
119
121
  export type DecodedEvent = DecodedResult | DecodedError | DecodedProgress | DecodedLog;
120
- /** Thrown by decodeProtocolEvent for malformed JSON or a strict-invalid event. */
122
+ /** Thrown by decodeProtocolEvent for malformed JSON or a base-invalid event. */
121
123
  export declare class EventDecodeError extends Error {
122
124
  constructor(message: string);
123
125
  }
124
126
  /**
125
- * Parse one protocol line (a single JSON text value), strict-validate it, and
127
+ * Parse one protocol line (a single JSON text value), base-validate it, and
126
128
  * return a typed decoded event.
127
129
  */
128
130
  export declare function decodeProtocolEvent(text: string): DecodedEvent;
@@ -158,6 +160,8 @@ export type OutputOptions = {
158
160
  * redaction in addition to _url/_URL fields.
159
161
  */
160
162
  urlNames?: readonly string[];
163
+ /** Exact structured field names to reveal; URL parameters and argv stay redacted. */
164
+ revealNames?: readonly string[];
161
165
  };
162
166
  /** Rendering style for plain (logfmt) output only. Omitted means readable.
163
167
  * JSON and YAML are structure-preserving and ignore this setting. */
@@ -191,6 +195,8 @@ export declare function redactedValue(value: unknown, options?: {
191
195
  policy?: RedactionPolicy;
192
196
  secretNames?: readonly string[];
193
197
  urlNames?: readonly string[];
198
+ /** Exact structured field names to reveal; URL parameters and argv stay redacted. */
199
+ revealNames?: readonly string[];
194
200
  }): JsonValue;
195
201
  /**
196
202
  * Redact secret components of a single URL string.
package/dist/format.js CHANGED
@@ -10,9 +10,10 @@
10
10
  * Number literal fidelity: decodeProtocolEvent parses with `lossless-json`
11
11
  * instead of JSON.parse, so a number's exact source literal (arbitrary
12
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. Native bigint values
13
+ * decode. Parsed numbers become LosslessNumber internally; every rendering
14
+ * path here treats it as an opaque scalar leaf (never destructured as a plain
15
+ * object). JSON re-emits its exact literal text; YAML/plain render it in the
16
+ * spec's canonical number form without losing a digit. Native bigint values
16
17
  * are also supported so callers can render integers beyond Number's safe
17
18
  * range without first corrupting them.
18
19
  */
@@ -22,7 +23,8 @@ import { parse as losslessParse, stringify as losslessStringify, isLosslessNumbe
22
23
  // ═══════════════════════════════════════════
23
24
  /**
24
25
  * Event is an opaque type wrapping a protocol event envelope.
25
- * Guaranteed to be strict-valid by construction.
26
+ * Non-error builders retain caller-provided traces verbatim; validate the
27
+ * envelope before accepting externally supplied traces as protocol-valid.
26
28
  */
27
29
  export class Event {
28
30
  value;
@@ -113,7 +115,7 @@ export class JsonErrorBuilder {
113
115
  this.issues.push(`cannot write reserved error field ${JSON.stringify(name)}`);
114
116
  }
115
117
  else {
116
- this.extensionFields[name] = value;
118
+ setJsonField(this.extensionFields, name, value);
117
119
  }
118
120
  return this;
119
121
  }
@@ -127,7 +129,7 @@ export class JsonErrorBuilder {
127
129
  this.issues.push(`cannot write reserved error field ${JSON.stringify(key)}`);
128
130
  }
129
131
  else {
130
- this.extensionFields[key] = val;
132
+ setJsonField(this.extensionFields, key, val);
131
133
  }
132
134
  }
133
135
  return this;
@@ -142,7 +144,7 @@ export class JsonErrorBuilder {
142
144
  this.issues.push(`cannot write reserved error field ${JSON.stringify(key)}`);
143
145
  }
144
146
  else {
145
- this.extensionFields[key] = val;
147
+ setJsonField(this.extensionFields, key, val);
146
148
  }
147
149
  }
148
150
  return this;
@@ -277,6 +279,9 @@ function validateErrorPayload(error) {
277
279
  if ("hint" in error && typeof error.hint !== "string") {
278
280
  throw new Error("event.error.hint must be a string when present");
279
281
  }
282
+ if ("retryable" in error && typeof error.retryable !== "boolean") {
283
+ throw new Error("event.error.retryable must be a boolean when present");
284
+ }
280
285
  }
281
286
  /** Validate finite CLI lifecycle: (log | progress)* -> exactly one terminal. `strict` also enforces the recommended strict protocol profile (default true). */
282
287
  export function validateProtocolStream(events, strict = true) {
@@ -319,7 +324,7 @@ function validateStrictError(error) {
319
324
  throw new Error("event.error.hint must be a string when present in strict mode");
320
325
  }
321
326
  }
322
- /** Thrown by decodeProtocolEvent for malformed JSON or a strict-invalid event. */
327
+ /** Thrown by decodeProtocolEvent for malformed JSON or a base-invalid event. */
323
328
  export class EventDecodeError extends Error {
324
329
  constructor(message) {
325
330
  super(message);
@@ -327,8 +332,70 @@ export class EventDecodeError extends Error {
327
332
  }
328
333
  }
329
334
  const ERROR_RESERVED_FIELDS = new Set(["code", "message", "hint", "retryable"]);
335
+ /** How many arrays and objects a protocol line may nest, counting the envelope object. Every SDK enforces the same bound. */
336
+ const MAX_PROTOCOL_NESTING = 127;
337
+ function hexEscape(text, at) {
338
+ if (text.charCodeAt(at) !== 0x5c || text.charCodeAt(at + 1) !== 0x75)
339
+ return null;
340
+ const digits = text.slice(at + 2, at + 6);
341
+ return /^[0-9a-fA-F]{4}$/.test(digits) ? parseInt(digits, 16) : null;
342
+ }
343
+ /**
344
+ * The decode rules lossless-json does not enforce: a nesting bound, and no
345
+ * lone UTF-16 surrogate, escaped or raw (lossless-json keeps one in the
346
+ * string). Only strings and escapes are tracked; the rest of the grammar is
347
+ * left to the parser.
348
+ */
349
+ function checkProtocolLine(text) {
350
+ let depth = 0;
351
+ let inString = false;
352
+ for (let i = 0; i < text.length; i++) {
353
+ const c = text.charCodeAt(i);
354
+ if (inString) {
355
+ if (c === 0x22) {
356
+ inString = false;
357
+ }
358
+ else if (c === 0x5c) {
359
+ const unit = hexEscape(text, i);
360
+ if (unit !== null && unit >= 0xd800 && unit <= 0xdbff) {
361
+ const low = hexEscape(text, i + 6);
362
+ if (low === null || low < 0xdc00 || low > 0xdfff) {
363
+ throw new SyntaxError(`lone surrogate escape at position ${i}`);
364
+ }
365
+ i += 11;
366
+ }
367
+ else if (unit !== null && unit >= 0xdc00 && unit <= 0xdfff) {
368
+ throw new SyntaxError(`lone surrogate escape at position ${i}`);
369
+ }
370
+ else {
371
+ i++;
372
+ }
373
+ }
374
+ else if (c >= 0xd800 && c <= 0xdbff) {
375
+ const next = text.charCodeAt(i + 1);
376
+ if (!(next >= 0xdc00 && next <= 0xdfff))
377
+ throw new SyntaxError(`lone surrogate at position ${i}`);
378
+ i++;
379
+ }
380
+ else if (c >= 0xdc00 && c <= 0xdfff) {
381
+ throw new SyntaxError(`lone surrogate at position ${i}`);
382
+ }
383
+ }
384
+ else if (c === 0x22) {
385
+ inString = true;
386
+ }
387
+ else if (c === 0x5b || c === 0x7b) {
388
+ depth++;
389
+ if (depth > MAX_PROTOCOL_NESTING)
390
+ throw new SyntaxError(`nesting deeper than ${MAX_PROTOCOL_NESTING}`);
391
+ }
392
+ else if (c === 0x5d || c === 0x7d) {
393
+ depth--;
394
+ }
395
+ }
396
+ }
330
397
  /**
331
- * Parse one protocol line (a single JSON text value), strict-validate it, and
398
+ * Parse one protocol line (a single JSON text value), base-validate it, and
332
399
  * return a typed decoded event.
333
400
  */
334
401
  export function decodeProtocolEvent(text) {
@@ -338,13 +405,16 @@ export function decodeProtocolEvent(text) {
338
405
  // source literal (see module-level fidelity note) instead of collapsing
339
406
  // it through float64. isSyntaxError-compatible: throws the same
340
407
  // SyntaxError shape JSON.parse would.
341
- parsed = losslessParse(text);
408
+ checkProtocolLine(text);
409
+ // A repeated key keeps its last value, as JSON.parse and every other
410
+ // SDK's decoder do; lossless-json would otherwise throw.
411
+ parsed = losslessParse(text, null, { onDuplicateKey: ({ newValue }) => newValue });
342
412
  }
343
413
  catch (e) {
344
414
  throw new EventDecodeError(`invalid JSON: ${e.message}`);
345
415
  }
346
416
  try {
347
- validateProtocolEvent(parsed, true);
417
+ validateProtocolEvent(parsed, false);
348
418
  }
349
419
  catch (e) {
350
420
  throw new EventDecodeError(e.message);
@@ -359,13 +429,13 @@ export function decodeProtocolEvent(text) {
359
429
  const fields = {};
360
430
  for (const [k, v] of Object.entries(error)) {
361
431
  if (!ERROR_RESERVED_FIELDS.has(k))
362
- fields[k] = v;
432
+ setJsonField(fields, k, v);
363
433
  }
364
434
  const decoded = {
365
435
  kind: "error",
366
436
  code: error.code,
367
437
  message: error.message,
368
- retryable: error.retryable,
438
+ retryable: typeof error.retryable === "boolean" ? error.retryable : null,
369
439
  fields,
370
440
  trace,
371
441
  };
@@ -443,8 +513,12 @@ export function formatJsonValue(value, options = {}) {
443
513
  * and formatting for everything else.
444
514
  */
445
515
  function canonicalJsonString(value) {
446
- return losslessStringify(value) ?? "null";
516
+ return losslessStringify(value, null, undefined, NUMBER_STRINGIFIERS) ?? "null";
447
517
  }
518
+ // JSON.stringify spells negative zero "0"; the other SDKs keep its sign.
519
+ const NUMBER_STRINGIFIERS = [
520
+ { test: (value) => Object.is(value, -0), stringify: () => "-0" },
521
+ ];
448
522
  /**
449
523
  * Format as multi-line YAML. Structure-preserving: like JSON, original keys,
450
524
  * scalar types, and numeric semantics are kept after redaction; secrets are
@@ -761,6 +835,7 @@ function isRedacted(value) {
761
835
  const DEFAULT_CONTEXT = {
762
836
  secretNames: new Set(),
763
837
  urlNames: new Set(),
838
+ revealNames: new Set(),
764
839
  };
765
840
  function secretNameSet(redactionOptions) {
766
841
  return new Set(redactionOptions.secretNames ?? []);
@@ -769,6 +844,7 @@ function contextFromOptions(redactionOptions) {
769
844
  return {
770
845
  secretNames: secretNameSet(redactionOptions),
771
846
  urlNames: new Set(redactionOptions.urlNames ?? []),
847
+ revealNames: new Set(redactionOptions.revealNames ?? []),
772
848
  };
773
849
  }
774
850
  function keyHasSecretSuffix(key) {
@@ -791,7 +867,7 @@ function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
791
867
  if (isObject(value)) {
792
868
  for (const k of Object.keys(value)) {
793
869
  const v = value[k];
794
- if (isSecretKey(k, context.secretNames)) {
870
+ if (isSecretKey(k, context.secretNames) && !context.revealNames.has(k)) {
795
871
  // A null secret is an absent secret. Masking it would manufacture the
796
872
  // appearance of a configured credential — readers cannot tell
797
873
  // "***"-because-set from "***"-because-null. Redaction hides a value
@@ -832,7 +908,7 @@ function redactUrlValue(value, context, depth) {
832
908
  if (isObject(value)) {
833
909
  for (const key of Object.keys(value)) {
834
910
  const item = value[key];
835
- if (isSecretKey(key, context.secretNames)) {
911
+ if (isSecretKey(key, context.secretNames) && !context.revealNames.has(key)) {
836
912
  if (item !== null)
837
913
  value[key] = REDACTED_MARKER;
838
914
  }
@@ -1216,21 +1292,34 @@ function tryStripGenericMicro(key) {
1216
1292
  return null;
1217
1293
  return [stripped, code];
1218
1294
  }
1219
- function isInt(value) {
1220
- return typeof value === "number" && Number.isInteger(value);
1221
- }
1295
+ /**
1296
+ * The exact digits of an integer-valued number or decimal integer string, of
1297
+ * any magnitude. Bitcoin amounts and `_epoch_ns` only append a unit or divide,
1298
+ * so unlike money suffixes they are not bounded by i64: an integer literal
1299
+ * keeps its digits, and a float counts when its value is an integer below 1e21
1300
+ * (the canonical number form's integer range). A numeric -0 is zero.
1301
+ */
1222
1302
  function decimalIntText(value) {
1223
- if (typeof value === "string" && /^-?\d+$/.test(value))
1224
- return value;
1303
+ if (typeof value === "string")
1304
+ return /^-?\d+$/.test(value) ? value : null;
1225
1305
  if (typeof value === "bigint")
1226
1306
  return value.toString();
1227
- if (isInt(value) && Number.isSafeInteger(value))
1228
- return String(value);
1229
- if (isLosslessNumber(value)) {
1230
- const text = value.toString();
1231
- return /^-?\d+$/.test(text) ? text : null;
1307
+ let literal;
1308
+ if (typeof value === "number") {
1309
+ if (!Number.isFinite(value))
1310
+ return null;
1311
+ literal = value.toString();
1232
1312
  }
1233
- return null;
1313
+ else if (isLosslessNumber(value)) {
1314
+ literal = value.toString();
1315
+ }
1316
+ else {
1317
+ return null;
1318
+ }
1319
+ const integer = /[.eE]/.test(literal) ? integralFloatText(literal) : literal;
1320
+ if (integer === null)
1321
+ return null;
1322
+ return integer === "-0" ? "0" : integer;
1234
1323
  }
1235
1324
  const MIN_I64 = -9223372036854775808n;
1236
1325
  const MAX_I64 = 9223372036854775807n;
@@ -1240,14 +1329,23 @@ const MAX_U64 = 18446744073709551615n;
1240
1329
  function integerValue(value) {
1241
1330
  if (typeof value === "bigint")
1242
1331
  return value;
1243
- if (typeof value === "number")
1244
- return Number.isInteger(value) ? BigInt(value) : null;
1332
+ if (typeof value === "number") {
1333
+ // Read through the shortest spelling, the text the canonical number form
1334
+ // prints, so 2**60 counts as 1152921504606847000 exactly as a decoded
1335
+ // literal of that spelling does in every SDK; BigInt(value) would give
1336
+ // ...846976 and disagree with Rust.
1337
+ if (!Number.isFinite(value))
1338
+ return null;
1339
+ const literal = value.toString();
1340
+ const integer = /[.eE]/.test(literal) ? integralFloatText(literal) : literal;
1341
+ return integer !== null ? BigInt(integer) : null;
1342
+ }
1245
1343
  if (isLosslessNumber(value)) {
1246
1344
  const literal = value.toString();
1247
1345
  if (/^-?\d+$/.test(literal))
1248
1346
  return BigInt(literal);
1249
- const parsed = Number(literal);
1250
- return Number.isInteger(parsed) ? BigInt(parsed) : null;
1347
+ const integer = integralFloatText(literal);
1348
+ return integer !== null ? BigInt(integer) : null;
1251
1349
  }
1252
1350
  return null;
1253
1351
  }
@@ -1288,24 +1386,14 @@ function isNum(value) {
1288
1386
  }
1289
1387
  function tryProcessField(key, value) {
1290
1388
  let stripped;
1291
- // Plain-format suffix processing below does real arithmetic (division,
1292
- // modulo, Math.floor/abs) that only works on a native JS number. A decoded
1293
- // LosslessNumber is normalized to one here via Number() \u2014 which never
1294
- // throws, unlike LosslessNumber's own valueOf() (documented to throw for
1295
- // values it can't safely represent as number/bigint) \u2014 so an ordinary
1296
- // decoded value (e.g. duration_ms: 42) keeps formatting exactly as before,
1297
- // and an exotic decoded value degrades gracefully (loses precision here,
1298
- // same as it always would for a >2^53 native number) rather than crashing.
1299
- // This is Plain's documented lossy/human path; JSON/YAML stay exact via
1300
- // yamlScalar/plainScalar/canonicalJsonString, and decimalIntText below
1301
- // (msats/sats/epoch_ns) keeps exactness by reading `value` directly instead
1302
- // of `numeric`.
1303
- const numeric = isLosslessNumber(value) ? Number(value.toString()) : value;
1389
+ // Appending a unit does no arithmetic and must retain decoded digits.
1390
+ const isNumber = isNum(value) || isLosslessNumber(value);
1304
1391
  // Group 1: compound timestamp suffixes
1305
1392
  stripped = stripSuffixCI(key, "_epoch_ms");
1306
1393
  if (stripped !== null) {
1307
- if (isInt(numeric)) {
1308
- const formatted = formatRfc3339Ms(numeric);
1394
+ const integer = signedI64(value);
1395
+ if (integer !== null) {
1396
+ const formatted = formatRfc3339Ms(Number(integer));
1309
1397
  if (formatted !== null)
1310
1398
  return [stripped, formatted];
1311
1399
  }
@@ -1313,8 +1401,9 @@ function tryProcessField(key, value) {
1313
1401
  }
1314
1402
  stripped = stripSuffixCI(key, "_epoch_s");
1315
1403
  if (stripped !== null) {
1316
- if (isInt(numeric)) {
1317
- const formatted = formatRfc3339Ms(numeric * 1000);
1404
+ const integer = signedI64(value);
1405
+ if (integer !== null) {
1406
+ const formatted = formatRfc3339Ms(Number(integer) * 1000);
1318
1407
  if (formatted !== null)
1319
1408
  return [stripped, formatted];
1320
1409
  }
@@ -1376,20 +1465,20 @@ function tryProcessField(key, value) {
1376
1465
  }
1377
1466
  stripped = stripSuffixCI(key, "_minutes");
1378
1467
  if (stripped !== null) {
1379
- if (isNum(numeric))
1380
- return [stripped, `${plainScalar(numeric)} minutes`];
1468
+ if (isNumber)
1469
+ return [stripped, `${plainScalar(value)} minutes`];
1381
1470
  return null;
1382
1471
  }
1383
1472
  stripped = stripSuffixCI(key, "_hours");
1384
1473
  if (stripped !== null) {
1385
- if (isNum(numeric))
1386
- return [stripped, `${plainScalar(numeric)} hours`];
1474
+ if (isNumber)
1475
+ return [stripped, `${plainScalar(value)} hours`];
1387
1476
  return null;
1388
1477
  }
1389
1478
  stripped = stripSuffixCI(key, "_days");
1390
1479
  if (stripped !== null) {
1391
- if (isNum(numeric))
1392
- return [stripped, `${plainScalar(numeric)} days`];
1480
+ if (isNumber)
1481
+ return [stripped, `${plainScalar(value)} days`];
1393
1482
  return null;
1394
1483
  }
1395
1484
  // Group 4: single-unit suffixes
@@ -1416,8 +1505,8 @@ function tryProcessField(key, value) {
1416
1505
  }
1417
1506
  stripped = stripSuffixCI(key, "_percent");
1418
1507
  if (stripped !== null) {
1419
- if (isNum(numeric))
1420
- return [stripped, `${plainScalar(numeric)}%`];
1508
+ if (isNumber)
1509
+ return [stripped, `${plainScalar(value)}%`];
1421
1510
  return null;
1422
1511
  }
1423
1512
  // Group 5: short suffixes (last to avoid false positives)
@@ -1433,27 +1522,27 @@ function tryProcessField(key, value) {
1433
1522
  }
1434
1523
  stripped = stripSuffixCI(key, "_ns");
1435
1524
  if (stripped !== null) {
1436
- if (isNum(numeric))
1437
- return [stripped, `${plainScalar(numeric)}ns`];
1525
+ if (isNumber)
1526
+ return [stripped, `${plainScalar(value)}ns`];
1438
1527
  return null;
1439
1528
  }
1440
1529
  stripped = stripSuffixCI(key, "_us");
1441
1530
  if (stripped !== null) {
1442
- if (isNum(numeric))
1443
- return [stripped, `${plainScalar(numeric)}\u03bcs`];
1531
+ if (isNumber)
1532
+ return [stripped, `${plainScalar(value)}\u03bcs`];
1444
1533
  return null;
1445
1534
  }
1446
1535
  stripped = stripSuffixCI(key, "_ms");
1447
1536
  if (stripped !== null) {
1448
- const fv = formatMsValue(numeric);
1537
+ const fv = formatMsValue(value);
1449
1538
  if (fv !== null)
1450
1539
  return [stripped, fv];
1451
1540
  return null;
1452
1541
  }
1453
1542
  stripped = stripSuffixCI(key, "_s");
1454
1543
  if (stripped !== null) {
1455
- if (isNum(numeric))
1456
- return [stripped, `${plainScalar(numeric)}s`];
1544
+ if (isNumber)
1545
+ return [stripped, `${plainScalar(value)}s`];
1457
1546
  return null;
1458
1547
  }
1459
1548
  return null;
@@ -1500,17 +1589,18 @@ function processObjectFields(obj) {
1500
1589
  // Formatting Helpers
1501
1590
  // ═══════════════════════════════════════════
1502
1591
  function formatMsAsSeconds(ms) {
1503
- const formatted = (ms / 1000).toFixed(3);
1592
+ const formatted = toFixedHalfEven(ms / 1000, 3);
1504
1593
  const trimmed = formatted.replace(/0+$/, "");
1505
1594
  if (trimmed.endsWith("."))
1506
1595
  return trimmed + "0s";
1507
1596
  return trimmed + "s";
1508
1597
  }
1509
1598
  function formatMsValue(value) {
1510
- if (!isNum(value))
1599
+ const numeric = isLosslessNumber(value) ? Number(value.toString()) : value;
1600
+ if (!isNum(numeric) || !Number.isFinite(numeric))
1511
1601
  return null;
1512
- if (Math.abs(value) >= 1000)
1513
- return formatMsAsSeconds(value);
1602
+ if (Math.abs(numeric) >= 1000)
1603
+ return formatMsAsSeconds(numeric);
1514
1604
  return `${plainScalar(value)}ms`;
1515
1605
  }
1516
1606
  const MIN_RFC3339_MS = -62135596800000;
@@ -1524,25 +1614,30 @@ function formatRfc3339Ms(ms) {
1524
1614
  return d.toISOString().replace(/(\.\d{3})\d*Z$/, "$1Z");
1525
1615
  }
1526
1616
  /**
1527
- * Round to `digits` decimals using round-half-to-even (banker's rounding) and
1528
- * format with a fixed decimal count. `Number.toFixed` rounds half away from
1529
- * zero, which diverges from the printf `%.1f` used by Rust/Go/Python on exact
1530
- * ties (e.g. 1280 bytes = 1.25 KiB → "1.2KiB", not "1.3KiB").
1617
+ * Format `x` with `digits` decimals the way printf `%.Nf` does in Rust, Go and
1618
+ * Python: round the double's exact binary value, half to even.
1619
+ *
1620
+ * `Number.toFixed` already rounds the exact value, but breaks a true tie
1621
+ * (1.0625 at 3 places) away from zero and switches to exponent form from
1622
+ * 1e21. So take the exact expansion — 100 places covers every double this
1623
+ * can tie on — and round the decimal string here. An epsilon test on
1624
+ * `x * 10 ** digits` is not a substitute: it calls near-ties ties.
1531
1625
  */
1532
1626
  function toFixedHalfEven(x, digits) {
1533
- const factor = 10 ** digits;
1534
- const scaled = x * factor;
1535
- const floor = Math.floor(scaled);
1536
- const diff = scaled - floor;
1537
- const eps = 1e-9;
1538
- let rounded;
1539
- if (diff > 0.5 + eps)
1540
- rounded = floor + 1;
1541
- else if (diff < 0.5 - eps)
1542
- rounded = floor;
1543
- else
1544
- rounded = floor % 2 === 0 ? floor : floor + 1;
1545
- return (rounded / factor).toFixed(digits);
1627
+ const negative = x < 0;
1628
+ const a = Math.abs(x);
1629
+ // Every double from 1e21 up is an integer, which BigInt spells exactly.
1630
+ const exact = a >= 1e21 ? `${BigInt(a)}.${"0".repeat(100)}` : a.toFixed(100);
1631
+ const [whole, fraction] = exact.split(".");
1632
+ const next = fraction.charCodeAt(digits) - 48;
1633
+ let units = BigInt(whole + fraction.slice(0, digits));
1634
+ const tieBroken = /[1-9]/.test(fraction.slice(digits + 1)) || units % 2n === 1n;
1635
+ if (next > 5 || (next === 5 && tieBroken))
1636
+ units += 1n;
1637
+ const text = units.toString().padStart(digits + 1, "0");
1638
+ const point = text.length - digits;
1639
+ const body = digits === 0 ? text : `${text.slice(0, point)}.${text.slice(point)}`;
1640
+ return negative ? `-${body}` : body;
1546
1641
  }
1547
1642
  function formatBytesHuman(bytes) {
1548
1643
  const KiB = 1024;
@@ -1696,6 +1791,100 @@ function isAmbiguousYamlKey(key) {
1696
1791
  return ["inf", "infinity", "nan"].includes(key.replace(/^[+-]/, "").toLowerCase())
1697
1792
  || /^[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?$/.test(key);
1698
1793
  }
1794
+ /**
1795
+ * Render a number's literal canonically for YAML/plain: a float literal whose
1796
+ * value is an integer below 1e21 in magnitude renders as that integer ("3.0"
1797
+ * -> "3", "-0.0" -> "-0"); anything else keeps its digits, placed by
1798
+ * placeDecimalPoint. Native numbers and decoded literals share it, and it
1799
+ * works on the text so a digit is never lost to a float64 round trip.
1800
+ */
1801
+ function formatNumberLiteral(literal) {
1802
+ return integralFloatText(literal) ?? placeDecimalPoint(literal) ?? normalizeExponent(literal);
1803
+ }
1804
+ const I64_MIN = -(2n ** 63n);
1805
+ const I64_MAX = 2n ** 63n - 1n;
1806
+ /**
1807
+ * Spell a non-integral float literal with the notation `Number#toString`
1808
+ * picks: positional when the value lies in [1e-6, 1e21), exponent form
1809
+ * otherwise. A decoded literal carries its own spelling and the other SDKs'
1810
+ * native formatters switch elsewhere, so the choice is made here, on the
1811
+ * digits as written. Null for integer literals and exponents beyond i64,
1812
+ * which keep their spelling.
1813
+ */
1814
+ function placeDecimalPoint(literal) {
1815
+ if (!/[.eE]/.test(literal))
1816
+ return null;
1817
+ const sign = literal.startsWith("-") ? "-" : "";
1818
+ const unsigned = sign ? literal.slice(1) : literal;
1819
+ const e = unsigned.search(/[eE]/);
1820
+ const mantissa = e >= 0 ? unsigned.slice(0, e) : unsigned;
1821
+ const [whole, fraction = ""] = mantissa.split(".");
1822
+ const digits = whole + fraction;
1823
+ const significant = digits.replace(/^0+/, "");
1824
+ if (!significant)
1825
+ return null;
1826
+ const shape = /^([-+]?)0*(\d{0,19})$/.exec(e >= 0 ? unsigned.slice(e + 1) : "0");
1827
+ if (!shape || (e >= 0 && unsigned.length === e + 1 + shape[1].length))
1828
+ return null;
1829
+ const exponent = BigInt(shape[1] + (shape[2] || "0"));
1830
+ // point: how many significant digits precede the decimal point.
1831
+ const point = BigInt(whole.length - (digits.length - significant.length)) + exponent;
1832
+ if (exponent < I64_MIN || exponent > I64_MAX || point - 1n < I64_MIN || point > I64_MAX) {
1833
+ return null;
1834
+ }
1835
+ let text;
1836
+ if (point >= 1n && point <= 21n) {
1837
+ const integer = significant.slice(0, Number(point));
1838
+ const rest = significant.slice(Number(point));
1839
+ if (!rest)
1840
+ return null;
1841
+ text = `${integer}.${rest}`;
1842
+ }
1843
+ else if (point >= -5n && point <= 0n) {
1844
+ text = `0.${"0".repeat(Number(-point))}${significant}`;
1845
+ }
1846
+ else {
1847
+ const scale = point - 1n;
1848
+ const rest = significant.length > 1 ? `.${significant.slice(1)}` : "";
1849
+ text = `${significant[0]}${rest}e${scale < 0n ? "-" : "+"}${scale < 0n ? -scale : scale}`;
1850
+ }
1851
+ return sign + text;
1852
+ }
1853
+ function integralFloatText(literal) {
1854
+ if (!/[.eE]/.test(literal))
1855
+ return null;
1856
+ const negative = literal.startsWith("-");
1857
+ const unsigned = negative ? literal.slice(1) : literal;
1858
+ const e = unsigned.search(/[eE]/);
1859
+ const mantissa = e >= 0 ? unsigned.slice(0, e) : unsigned;
1860
+ const [whole, fraction = ""] = mantissa.split(".");
1861
+ const digits = whole + fraction;
1862
+ if (!/[^0]/.test(digits))
1863
+ return negative ? "-0" : "0";
1864
+ const exponent = e >= 0 ? Number(unsigned.slice(e + 1)) : 0;
1865
+ if (!Number.isSafeInteger(exponent))
1866
+ return null;
1867
+ const point = whole.length + exponent;
1868
+ const split = Math.min(Math.max(point, 0), digits.length);
1869
+ if (/[^0]/.test(digits.slice(split)))
1870
+ return null;
1871
+ const padding = Math.min(Math.max(point - digits.length, 0), 22);
1872
+ const integer = (digits.slice(0, split) + "0".repeat(padding)).replace(/^0+/, "");
1873
+ if (integer.length > 21)
1874
+ return null;
1875
+ return (negative ? "-" : "") + (integer || "0");
1876
+ }
1877
+ function normalizeExponent(literal) {
1878
+ const match = /^(.*?)[eE]([-+]?)0*(\d*)$/.exec(literal);
1879
+ if (!match)
1880
+ return literal;
1881
+ // An unsigned exponent is positive; spell it like toString() does (e+23).
1882
+ return `${match[1]}e${match[2] || "+"}${match[3] || "0"}`;
1883
+ }
1884
+ function numberText(value) {
1885
+ // `toString` spells negative zero "0"; the canonical form keeps the sign.
1886
+ return Object.is(value, -0) ? "-0" : formatNumberLiteral(value.toString());
1887
+ }
1699
1888
  function yamlScalar(value) {
1700
1889
  if (typeof value === "string")
1701
1890
  return `"${escapeYamlStr(value)}"`;
@@ -1704,14 +1893,14 @@ function yamlScalar(value) {
1704
1893
  if (typeof value === "boolean")
1705
1894
  return value.toString();
1706
1895
  if (typeof value === "number")
1707
- return value.toString();
1896
+ return numberText(value);
1708
1897
  if (typeof value === "bigint")
1709
1898
  return value.toString();
1710
- // Decoded number: emit the exact source literal unquoted, matching JSON's
1711
- // structure-preserving fidelity contract for YAML too.
1899
+ // Decoded number: its exact digits, in the same canonical form a native
1900
+ // number gets.
1712
1901
  if (isLosslessNumber(value))
1713
- return value.toString();
1714
- return `"${escapeYamlStr(canonicalJsonString(sortJsonValue(value)))}"`;
1902
+ return formatNumberLiteral(value.toString());
1903
+ return `"${escapeYamlStr(embeddedJson(value))}"`;
1715
1904
  }
1716
1905
  // ═══════════════════════════════════════════
1717
1906
  // Plain Rendering (logfmt)
@@ -1772,19 +1961,19 @@ function plainScalar(value) {
1772
1961
  if (typeof value === "boolean")
1773
1962
  return value.toString();
1774
1963
  if (typeof value === "number")
1775
- return value.toString();
1964
+ return numberText(value);
1776
1965
  if (typeof value === "bigint")
1777
1966
  return value.toString();
1778
- // Decoded number: emit the exact source literal (Plain is documented lossy
1779
- // for arithmetic-derived formatting, but a bare pass-through scalar still
1780
- // gets its exact digits, not a mangled float64 round-trip).
1967
+ // Decoded number: its exact digits (Plain is documented lossy only for
1968
+ // arithmetic-derived formatting), in the same canonical form a native
1969
+ // number gets.
1781
1970
  if (isLosslessNumber(value))
1782
- return value.toString();
1783
- return canonicalJsonString(sortJsonValue(value));
1971
+ return formatNumberLiteral(value.toString());
1972
+ return embeddedJson(value);
1784
1973
  }
1785
1974
  function plainScalarRaw(value) {
1786
1975
  if (isObject(value) || Array.isArray(value)) {
1787
- return canonicalJsonString(sortJsonValue(value));
1976
+ return embeddedJson(value);
1788
1977
  }
1789
1978
  return plainScalar(value);
1790
1979
  }
@@ -1809,6 +1998,11 @@ function quoteLogfmtKey(key) {
1809
1998
  // ═══════════════════════════════════════════
1810
1999
  // Utilities
1811
2000
  // ═══════════════════════════════════════════
2001
+ // Arbitrary JSON names, including __proto__, are own data properties. A normal
2002
+ // bracket assignment would invoke Object.prototype's legacy prototype setter.
2003
+ function setJsonField(target, key, value) {
2004
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
2005
+ }
1812
2006
  function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
1813
2007
  if (depth >= MAX_DEPTH)
1814
2008
  return MAX_DEPTH_MARKER;
@@ -1823,8 +2017,9 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
1823
2017
  const n = value;
1824
2018
  if (!Number.isFinite(n))
1825
2019
  return "<unsupported:number>";
1826
- if (Number.isInteger(n) && !Number.isSafeInteger(n))
1827
- return "<unsupported:unsafe-integer>";
2020
+ // An integral number past 2^53 is still a float64, like every SDK's native
2021
+ // float, and renders by the same canonical rule. A caller that needs such
2022
+ // an integer exactly passes a bigint.
1828
2023
  return n;
1829
2024
  }
1830
2025
  if (t === "bigint")
@@ -1859,7 +2054,7 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
1859
2054
  stack.add(obj);
1860
2055
  const out = {};
1861
2056
  for (const [k, v] of Object.entries(obj)) {
1862
- out[k] = sanitizeForJson(v, stack, depth + 1);
2057
+ setJsonField(out, k, sanitizeForJson(v, stack, depth + 1));
1863
2058
  }
1864
2059
  stack.delete(obj);
1865
2060
  return out;
@@ -1878,17 +2073,23 @@ function isObject(value) {
1878
2073
  function sortedObjectKeys(obj) {
1879
2074
  return Object.keys(obj).sort(jcsCompare);
1880
2075
  }
1881
- function sortJsonValue(value) {
2076
+ /**
2077
+ * A container nested inside a Plain array (or in a YAML scalar position) is
2078
+ * written as compact JSON with sorted keys. Its number leaves take the
2079
+ * canonical number form, the same text the value prints as a scalar, so a
2080
+ * nested `1.0` reads `1` and a decoded `1e5` reads `100000` in every SDK
2081
+ * rather than in each runtime's own JSON spelling.
2082
+ */
2083
+ function embeddedJson(value) {
1882
2084
  if (Array.isArray(value))
1883
- return value.map(sortJsonValue);
2085
+ return `[${value.map(embeddedJson).join(",")}]`;
1884
2086
  if (isObject(value)) {
1885
- const out = {};
1886
- for (const key of sortedObjectKeys(value)) {
1887
- out[key] = sortJsonValue(value[key]);
1888
- }
1889
- return out;
2087
+ const members = sortedObjectKeys(value).map((key) => `${JSON.stringify(key)}:${embeddedJson(value[key])}`);
2088
+ return `{${members.join(",")}}`;
1890
2089
  }
1891
- return value;
2090
+ if (typeof value === "string")
2091
+ return JSON.stringify(value);
2092
+ return plainScalar(value);
1892
2093
  }
1893
2094
  function jcsCompare(a, b) {
1894
2095
  const ua = Array.from(a).flatMap((c) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-first-data",
3
- "version": "0.35.1",
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 Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.",
3
+ "version": "0.36.0",
4
+ "description": "A naming convention that lets AI agents understand your data without being told what it means, plus the afdata CLI and a Rust library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -24,10 +24,10 @@
24
24
  "scripts": {
25
25
  "build": "tsc",
26
26
  "prepack": "npm run build",
27
- "test": "tsx --test src/*.test.ts"
27
+ "test": "node --import tsx --test src/*.test.ts"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/node": "^26.6.5",
30
+ "@types/node": "^26.6.6",
31
31
  "tsx": "^4.23.15",
32
32
  "typescript": "^7.0.2"
33
33
  },