agent-first-data 0.18.0 → 0.19.1
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/assets/skills/agent-first-data/SKILL.md +139 -38
- package/assets/skills/agent-first-data/references/rules.md +16 -12
- package/dist/cli.d.ts +92 -1
- package/dist/cli.js +125 -3
- package/dist/format.d.ts +8 -0
- package/dist/format.js +100 -37
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-first-data
|
|
3
|
-
description: Apply and review the formal Agent-First Data specification using the Rust/Go/Python/TypeScript libraries or afdata CLI for naming, redaction, formatting, protocol envelopes, logging, linting, validation, and reading or safely editing structured JSON/TOML/YAML/dotenv/INI documents by dot-path. Use proactively when writing or reviewing structured data, configs, logs, transport payloads, database/wire fields, CLI output, compatibility-sensitive public and persistent field names, or when reading or editing a config/document file from the shell.
|
|
3
|
+
description: Apply and review the formal Agent-First Data specification using the Rust/Go/Python/TypeScript libraries or afdata CLI for naming, redaction, formatting, protocol envelopes, logging, linting, validation, and reading or safely editing structured JSON/TOML/YAML/dotenv/INI documents (and the +++/--- frontmatter block of a Markdown page) by dot-path. Use proactively when writing or reviewing structured data, configs, logs, transport payloads, database/wire fields, CLI output, compatibility-sensitive public and persistent field names, or when reading or editing a config/document/Markdown-frontmatter file from the shell.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
<!-- Canonical source: skills/agent-first-data/SKILL.md.
|
|
@@ -97,7 +97,7 @@ Fields named `_secret` or `_url` and values passing through AFDATA output render
|
|
|
97
97
|
|
|
98
98
|
## Logging behavior contract
|
|
99
99
|
|
|
100
|
-
One-time CLI output uses `json_log()` or `CliEmitter` for a single event; serialization via `render()` applies output formatting. Long-running services or processes depending on structured logging (tonic, sqlx, hyper, etc. via tracing) initialize with `afdata_tracing::try_init()` (Rust only) to wire process-wide logging; other languages emit log events explicitly via builders or integrate their own structured logging.
|
|
100
|
+
One-time CLI output uses `json_log()` or `CliEmitter` for a single event; serialization via `render()` applies output formatting. Construct the emitter in finite mode for a one-shot command (`result`→stdout, `error`/`progress`/`log`→stderr) or stream mode for an interleaved event stream (all events on one destination) — see the Output stream contract. Long-running services or processes depending on structured logging (tonic, sqlx, hyper, etc. via tracing) initialize with `afdata_tracing::try_init()` (Rust only) to wire process-wide logging; other languages emit log events explicitly via builders or integrate their own structured logging.
|
|
101
101
|
|
|
102
102
|
## CLI workflow
|
|
103
103
|
|
|
@@ -117,63 +117,164 @@ afdata render payload.json --output yaml
|
|
|
117
117
|
afdata skill validate skills/example-skill
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
-
|
|
120
|
+
- Every command's first positional is its input; `-` reads stdin explicitly.
|
|
121
|
+
There is no implicit stdin fallback, and omitting the input is always a
|
|
122
|
+
usage error that names the fix — never a hang.
|
|
123
|
+
- Run `afdata lint` for ordinary JSON/JSONL, JSON Schema, MCP input/output
|
|
124
|
+
schemas, or a structured document (`--input-format`/extension detection
|
|
125
|
+
picks TOML/YAML/dotenv/INI; JSON/JSONL stays the default dual-mode input).
|
|
121
126
|
- Run `afdata validate` for AFDATA protocol events or finite event streams.
|
|
122
|
-
- Run `afdata render` to apply AFDATA redaction/formatting to arbitrary JSON
|
|
123
|
-
`plain`; JSON and YAML keep
|
|
124
|
-
|
|
127
|
+
- Run `afdata render` to apply AFDATA redaction/formatting to arbitrary JSON
|
|
128
|
+
or JSONL. `--output` accepts `json`, `yaml`, or `plain`; JSON and YAML keep
|
|
129
|
+
original keys/values (structure-preserving), Plain is the lossy human
|
|
130
|
+
renderer.
|
|
131
|
+
- Treat findings as contract issues unless the owning tool intentionally
|
|
132
|
+
documents a non-AFDATA field.
|
|
133
|
+
|
|
134
|
+
## Output stream contract
|
|
135
|
+
|
|
136
|
+
AFDATA CLIs route protocol events by consumption mode, not by event shape.
|
|
137
|
+
|
|
138
|
+
- A **finite one-shot command** (the default) puts `kind:"result"` on `stdout`
|
|
139
|
+
and `kind:"error"`/`progress`/`log` on `stderr`, so `x=$(tool …)` never
|
|
140
|
+
captures a failure as data, `tool … >/dev/null` never swallows a diagnostic,
|
|
141
|
+
and `tool … | next` never pipes an error envelope in as input. The exit
|
|
142
|
+
code — not the stream — is set by the terminal kind; routing follows `kind`,
|
|
143
|
+
not the exit code.
|
|
144
|
+
- An **event stream** (interleaved events consumed in order) keeps every event,
|
|
145
|
+
including `error`, on one stream so ordering survives.
|
|
146
|
+
- `afdata` and any CLI built on the library expose `--output-to
|
|
147
|
+
<split|stdout|stderr>` (default `split`). `stdout`/`stderr` collapse the whole
|
|
148
|
+
stream — errors included — onto one destination, for a consumer that would
|
|
149
|
+
rather read one stream and branch on `kind`. `--output` picks the format,
|
|
150
|
+
`--output-to` picks the destination. Send an event stream to a file by
|
|
151
|
+
collapsing it (`--output-to stdout`) and redirecting (`--stdout-file PATH`).
|
|
152
|
+
- The raw-scalar readers (`value`/`paths`/`keys`) are intrinsically split (raw
|
|
153
|
+
data on `stdout`, error envelope on `stderr`) and reject a non-default
|
|
154
|
+
`--output-to`.
|
|
155
|
+
- When building a CLI on the library, choose the mode by how the output is
|
|
156
|
+
consumed — do not reflexively pick finite. A plain one-shot shell command uses
|
|
157
|
+
**finite** mode (errors → stderr, so shell capture and pipelines stay safe). A
|
|
158
|
+
tool whose output is itself an event stream (interleaved `progress`/`log` then
|
|
159
|
+
a terminal `result`/`error` — e.g. streaming query rows), or that is
|
|
160
|
+
agent-facing and wants the caller to read a single structured channel and
|
|
161
|
+
branch on `kind`, uses **stream/unified** mode (every event, errors included,
|
|
162
|
+
on `stdout`). A stream's terminal error MUST stay on the stream, so such a tool
|
|
163
|
+
keeps even its usage errors on `stdout` for one consistent channel. Never write
|
|
164
|
+
to `stderr` ad hoc — route every diagnostic through the emitter so redaction
|
|
165
|
+
and framing still apply.
|
|
166
|
+
- Emit a terminal event with the emitter's `finish` / `finish_result` helpers,
|
|
167
|
+
which map the write outcome to a broken-pipe-safe process exit code (success →
|
|
168
|
+
the given code, a hung-up reader → 0, any other write failure → a nonzero
|
|
169
|
+
output-failure code) instead of hand-rolling that dance — they work in either
|
|
170
|
+
mode (the emitter's mode decides the stream; `finish` only maps the outcome).
|
|
171
|
+
Construct an error with the error builder (`json_error(code, message)` with
|
|
172
|
+
`hint`/`retryable`/extra fields as needed) — that builder *is* the error type —
|
|
173
|
+
and pass the built event to `finish`; there is no separate error-emitting
|
|
174
|
+
convenience.
|
|
125
175
|
|
|
126
176
|
## Document workflow
|
|
127
177
|
|
|
128
|
-
Use `
|
|
129
|
-
and `set`/`add`/`remove`/`unset` to edit one in place, instead of
|
|
130
|
-
regular expressions, or a generic reserializer — those can reorder
|
|
131
|
-
comments, or change quoting/anchors in the rest of the file.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
178
|
+
Use `get`/`value`/`paths`/`keys` to read a JSON, TOML, YAML, dotenv, or INI
|
|
179
|
+
document and `set`/`add`/`remove`/`unset` to edit one in place, instead of
|
|
180
|
+
`sed`, regular expressions, or a generic reserializer — those can reorder
|
|
181
|
+
keys, drop comments, or change quoting/anchors in the rest of the file. Every
|
|
182
|
+
document command's first positional is `FILE` (or `-` for stdin on a read
|
|
183
|
+
command); mutation commands never read stdin and reject `-` as a usage error.
|
|
184
|
+
To edit only the metadata block of a Markdown page, pass `--input-format
|
|
185
|
+
toml-frontmatter` (a `+++` block) or `yaml-frontmatter` (a `---` block): the
|
|
186
|
+
same read/mutate commands then address dot-paths inside the frontmatter and
|
|
187
|
+
leave the Markdown body byte-for-byte untouched. Frontmatter is never
|
|
188
|
+
auto-detected — the format must be named explicitly, since extension detection
|
|
189
|
+
only ever resolves to a whole-file format — so reach for it deliberately
|
|
190
|
+
instead of `sed`-ing a page's `+++`/`---` header.
|
|
191
|
+
|
|
192
|
+
- `get FILE` returns the whole document as one AFDATA record
|
|
193
|
+
(`code:"document"`); `get FILE KEY` narrows it to one dot-path value in the
|
|
194
|
+
same shape. Pass every known sensitive legacy field name with
|
|
195
|
+
`--secret-name FIELD` (an exact field name, not a dot-path). A secret-named
|
|
137
196
|
leaf (`_secret`/`_SECRET`, or an exact `--secret-name` match) is still
|
|
138
|
-
redacted to `"***"` even
|
|
139
|
-
|
|
140
|
-
- `value
|
|
141
|
-
and no forced trailing newline, for direct use in shell
|
|
142
|
-
`value=$(afdata value
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`--reveal-secret` is passed
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
197
|
+
redacted to `"***"` even when directly targeted — use `value
|
|
198
|
+
--reveal-secret` when the real value is genuinely needed.
|
|
199
|
+
- `value FILE KEY` writes only the raw scalar to stdout, with no AFDATA
|
|
200
|
+
envelope and no forced trailing newline, for direct use in shell
|
|
201
|
+
substitution: `value=$(afdata value config.toml server.host)`. Only scalars
|
|
202
|
+
are supported; a container errors (`document_not_scalar`) — use `get` for a
|
|
203
|
+
subtree. A secret-named leaf errors (`document_secret_redacted`) unless
|
|
204
|
+
`--reveal-secret` is passed. On any failure `value`'s stdout is always
|
|
205
|
+
empty — the error envelope goes to stderr instead — so
|
|
206
|
+
`port=$(afdata value config.toml server.port)` never captures a JSON error
|
|
207
|
+
as data. `--default VAL` prints `VAL` instead of erroring when the path is
|
|
208
|
+
absent or the value is `null` (an empty string is a real value and does not
|
|
209
|
+
trigger it), collapsing the common "read or fall back" shell idiom into one
|
|
210
|
+
call with no `2>/dev/null` guard needed.
|
|
211
|
+
- `paths FILE [KEY]` and `keys FILE [KEY]` enumerate a container's immediate
|
|
212
|
+
children, one per line, with the same empty-stdout-on-failure contract as
|
|
213
|
+
`value`. **`paths` output feeds back into afdata** (full grammar-escaped
|
|
214
|
+
dot-paths from the root — pipe straight into `value`/`get`/`unset` or
|
|
215
|
+
extend with `"$p.field"`); **`keys` output goes to external tools** (raw,
|
|
216
|
+
unescaped key names/array indices — what a package manager or another CLI
|
|
217
|
+
expects). Never use `paths`'s output as a literal name, or `keys`'s output
|
|
218
|
+
as a dot-path — they only diverge on a key containing a dot or space, so
|
|
219
|
+
the wrong one works until it silently doesn't. A scalar target errors
|
|
220
|
+
(`document_not_container`); `--missing-ok` turns a missing KEY specifically
|
|
221
|
+
into empty output + exit 0 (other errors still fail). Path grammar does not
|
|
222
|
+
escape spaces or other IFS characters, so the only correct consumption
|
|
223
|
+
shape is a `while IFS= read -r` loop, never `for x in $(...)`:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
while IFS= read -r p; do
|
|
227
|
+
afdata value config.toml "$p.slug"
|
|
228
|
+
done < <(afdata paths config.toml extra.tools)
|
|
229
|
+
```
|
|
150
230
|
- Escape a literal dot as `\.` and a literal backslash as `\\` in a dot-path;
|
|
151
|
-
every command shares this grammar
|
|
152
|
-
|
|
231
|
+
every command shares this grammar (`paths`'s escaped output re-parses
|
|
232
|
+
through the exact same rules it was emitted from), so an unrecognized
|
|
233
|
+
escape or empty segment is always an error, never a guess.
|
|
153
234
|
- `add`/`remove` operate on a keyed list — an array of objects addressed by a
|
|
154
235
|
slug — and always require an explicit `--slug-field FIELD`; the CLI never
|
|
155
236
|
infers an identity field. Keyed editing is implemented for JSON and YAML
|
|
156
237
|
only; TOML, dotenv, and INI return a structured "not implemented" error, so
|
|
157
|
-
edit their list entries by writing the whole value another way.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
238
|
+
edit their list entries by writing the whole value another way. Adding a
|
|
239
|
+
slug that already exists, or removing/unsetting one that does not, is
|
|
240
|
+
always an error (`document_slug_exists`/`document_slug_not_found`/
|
|
241
|
+
`document_path_not_found`) — never a silent no-op; wrap the call yourself
|
|
242
|
+
when idempotence is wanted.
|
|
243
|
+
- `set`'s bare VALUE (and `add`'s `FIELD=VALUE`) is always a plain string —
|
|
244
|
+
no shape-guessing, so a leading-zero ID or SHA never gets reinterpreted as
|
|
245
|
+
a number. Writing an exact type goes through `--value-type
|
|
246
|
+
string|number|bool|null|json` instead; `json` is the only entry point for
|
|
247
|
+
an array or object. Overwriting an *existing* scalar of a different kind
|
|
248
|
+
with a bare VALUE is a usage error that names both escape hatches (keep the
|
|
249
|
+
type with `--value-type <kind>`, or convert explicitly with `--value-type
|
|
250
|
+
string`) — a brand-new key never needs `--value-type`.
|
|
251
|
+
- For `set`, choose `--secret-from stdin|prompt|fd:<N>|env:<VAR>` instead of a
|
|
252
|
+
positional value when the value must not land in shell history or `ps`:
|
|
253
|
+
`stdin` (pipe), `prompt` (human, terminal echo off), `fd:N` (Unix
|
|
254
|
+
automation that needs stdin for something else), or `env:VAR` (already in
|
|
255
|
+
the process environment). There is no inline `--secret-from=VALUE`-style
|
|
256
|
+
argv source — that would defeat the point.
|
|
164
257
|
- Mutations preserve the rest of the source document — comments, key order,
|
|
165
258
|
unrelated formatting — and refuse to write through a symlink or (on Unix) a
|
|
166
259
|
multi-link hardlink target. Treat a failed mutation as leaving the original
|
|
167
260
|
file untouched rather than assuming a partial write, and reread before
|
|
168
261
|
retrying. A format backend that isn't compiled in, or input outside the
|
|
169
262
|
documented dotenv/INI dialect, is a structured error to report, not a cue to
|
|
170
|
-
hand-edit the file.
|
|
263
|
+
hand-edit the file. A successful mutation's result carries the `path`
|
|
264
|
+
actually written, for confirming which file changed when scripting several.
|
|
265
|
+
- Branch on `error.code`, not the message text: document errors use stable
|
|
266
|
+
codes (`document_path_not_found`, `document_type_mismatch`,
|
|
267
|
+
`document_slug_not_found`, `document_slug_exists`, `document_parse_failed`,
|
|
268
|
+
`document_io_failed`, and a few narrower ones), while a malformed
|
|
269
|
+
invocation (`--input-format`, `--value-type`, `FIELD=VALUE`, a mutation
|
|
270
|
+
given `-`, …) is always `document_usage_error` at exit 2 — distinct from
|
|
271
|
+
every runtime document error, which exits 1.
|
|
171
272
|
|
|
172
273
|
## Review checklist
|
|
173
274
|
|
|
174
275
|
1. Numeric fields with units carry the right suffix and safe integer/string representation.
|
|
175
276
|
2. Timestamps use `_epoch_s`, `_epoch_ms`, decimal-string `_epoch_ns`, or `_rfc3339`.
|
|
176
277
|
3. Secrets use `_secret` or configured exact secret names; no fallback path leaks them.
|
|
177
|
-
4. CLI output uses AFDATA helpers for JSON/YAML/plain formatting and structured errors.
|
|
278
|
+
4. CLI output uses AFDATA helpers for JSON/YAML/plain formatting and structured errors, and routes by consumption mode (finite: `result`→stdout, `error`→stderr; stream: one destination) rather than writing to stderr ad hoc.
|
|
178
279
|
5. Logs use AFDATA structured logging or equivalent field-name redaction.
|
|
179
280
|
6. Transport payloads preserve the protocol envelope when AFDATA compliance is claimed.
|
|
@@ -41,6 +41,8 @@ Integer vs string is not uniform across siblings: `_epoch_s` and `_epoch_ms` are
|
|
|
41
41
|
|
|
42
42
|
`_rfc3339` needs a **mandatory** offset: `YYYY-MM-DDThh:mm:ss[.fff]` followed by `Z` or `±HH:MM`. A bare `2026-02-14T10:30:00` (no offset), a space instead of `T`, or a trailing IANA name (`...Asia/Shanghai`) is rejected by `afdata lint`. `afdata lint` also type-checks the numeric suffixes (durations `_s`/`_ms`/…, currency `_cents`/`_micro`/`_jpy`) and rejects a `_url` with internal whitespace or bare `user:pass@host` credentials.
|
|
43
43
|
|
|
44
|
+
A `null` value is exempt from every suffix type constraint above: `null` means the field is absent/unset, not present-with-the-wrong-type, so `law_repeal_at_epoch_s: null` is not a `suffix_type_mismatch`. Absence may be written as an omitted key or as an explicit `null` — both are valid. The constraint applies only once a value is present and non-null.
|
|
45
|
+
|
|
44
46
|
### Strict string formats
|
|
45
47
|
|
|
46
48
|
| Suffix | Format | Example |
|
|
@@ -225,22 +227,24 @@ Every protocol event uses `kind` plus one same-named payload:
|
|
|
225
227
|
| `"result"` | Terminal success |
|
|
226
228
|
| `"error"` | Terminal error with non-empty `error.code` and `error.message` |
|
|
227
229
|
|
|
228
|
-
Channel policy:
|
|
229
|
-
- `stdout`
|
|
230
|
-
-
|
|
231
|
-
- `stderr`
|
|
230
|
+
Channel policy — the stream follows the consumption mode, not the event shape:
|
|
231
|
+
- **Finite one-shot command (default):** `result` → `stdout`; `error`/`progress`/`log` → `stderr`. Routing follows `kind`, not the exit code, so `stdout` carries only successful payloads (`x=$(tool …)`, `tool | next`, and `tool >/dev/null` all stay safe).
|
|
232
|
+
- **Event stream (interleaved events consumed in order):** every event, including `error`, stays on one stream so ordering survives; do not split it across `stdout`/`stderr`.
|
|
233
|
+
- `--output-to <split|stdout|stderr>` (default `split`) selects the mode: `stdout`/`stderr` collapse the whole stream onto one destination. `--output` selects format, `--output-to` selects destination.
|
|
234
|
+
- Raw-scalar readers (`value`/`paths`/`keys`) are intrinsically split and reject a non-default `--output-to`.
|
|
235
|
+
- Choose the mode by how the output is consumed, not reflexively: a plain one-shot shell command uses **finite**; a tool whose output is itself an event stream (streaming rows, interleaved progress/log) or that is agent-facing and wants one structured channel uses **stream/unified** (all on `stdout`, errors included — a stream's terminal error must stay on the stream). When building a CLI on the library, emit terminal events with the emitter's `finish`/`finish_result` helpers (success → the given code, broken pipe → 0, other write failure → a nonzero output-failure code); build errors with the `json_error` builder (the error *type*, carrying hint/retryable/fields) and pass the event to `finish` — the helpers work in either mode.
|
|
232
236
|
|
|
233
237
|
Optional stream redirection:
|
|
234
|
-
- use `--stdout-file <PATH>` to redirect
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
- do not convert stderr diagnostics to JSON; Rust panics, Python tracebacks, and runtime errors stay native stderr bytes
|
|
238
|
+
- use `--stdout-file <PATH>` / `--stderr-file <PATH>` to redirect a stream's bytes to a file (process-level, beneath the channel routing above)
|
|
239
|
+
- send an event stream to a file by collapsing it (`--output-to stdout`) and redirecting (`--stdout-file <PATH>`); there is no separate events-file flag
|
|
240
|
+
- native panics/tracebacks stay raw `stderr` bytes; in finite mode `stderr` also carries the formatted AFDATA `error`/`progress`/`log` envelopes, so a reader parses JSON lines and tolerates interleaved native bytes
|
|
238
241
|
- treat this as stream destination control, not a second protocol stream and not stream copying
|
|
239
242
|
- do not implement application-level rotation
|
|
240
243
|
|
|
241
244
|
Recommended enforcement:
|
|
242
|
-
-
|
|
243
|
-
-
|
|
245
|
+
- route every `stderr` write through the emitter's diagnostic/error path; no ad-hoc stderr that bypasses the formatter in runtime code
|
|
246
|
+
- Rust: keep native panic output as the only unstructured `stderr` (no stray `eprintln!` / `std::io::stderr`)
|
|
247
|
+
- Go/Python/TypeScript: source-policy tests that fail on ad-hoc stderr APIs in runtime code, with the emitter's own sink as the sanctioned exception
|
|
244
248
|
|
|
245
249
|
### Templates
|
|
246
250
|
|
|
@@ -263,7 +267,7 @@ Startup payload fields are tool-defined; `config` is recommended, while `version
|
|
|
263
267
|
| MCP tool | JSON |
|
|
264
268
|
| SSE stream | JSONL |
|
|
265
269
|
|
|
266
|
-
All use `kind` plus the same-named payload and optional `trace`.
|
|
270
|
+
All use `kind` plus the same-named payload and optional `trace`. An event stream keeps its events on one stream (never split); a finite one-shot command splits by `kind` (`result` → stdout, `error` → stderr) — see Channel policy.
|
|
267
271
|
|
|
268
272
|
Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event and `retryable` on every error. Log and progress payloads remain tool-defined in both profiles. Use strict validation before claiming compliance with all recommendations.
|
|
269
273
|
|
|
@@ -351,4 +355,4 @@ Use this checklist when the skill is evaluating a change, not just documenting c
|
|
|
351
355
|
7. Environment variables follow `UPPER_SNAKE_CASE` with the same suffixes
|
|
352
356
|
8. One-time CLI output uses `json_log()`/`CliEmitter` + output helpers; long-running services use `afdata_tracing::try_init()` (Rust) to capture via tracing; other languages emit log events explicitly via builders
|
|
353
357
|
9. Database columns use AFDATA suffixes on generic types (`duration_ms INTEGER`, not `duration INTEGER`); native types like `TIMESTAMPTZ` don't need suffixes
|
|
354
|
-
10. CLI flag parsing uses `cli_parse_output()`/`cli_parse_log_filters()`/`build_cli_error()`/version helpers — not custom reimplementations; uses `try_parse()` not `parse()` in Rust so clap
|
|
358
|
+
10. CLI flag parsing uses `cli_parse_output()`/`cli_parse_log_filters()`/`build_cli_error()`/version helpers — not custom reimplementations; uses `try_parse()` not `parse()` in Rust so a clap/usage error becomes a structured `kind:"error"` envelope (routed to stderr under the default split, exit 2), never raw clap text
|
package/dist/cli.d.ts
CHANGED
|
@@ -62,17 +62,108 @@ export declare function cliParseLogFilters(entries: string[]): LogFilters;
|
|
|
62
62
|
*/
|
|
63
63
|
export declare function render(value: JsonValue | Event, format: OutputFormat, options?: OutputOptions): string;
|
|
64
64
|
export type CliEventWriter = (line: string) => void;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Where a CliEmitter sends its events, selected by `--output-to`.
|
|
67
|
+
*
|
|
68
|
+
* The stream an event lands on follows the program's consumption mode, not the
|
|
69
|
+
* event's shape (see the spec's CLI Event Framing):
|
|
70
|
+
*
|
|
71
|
+
* - "split" (the default) is finite one-shot mode: `result` → stdout, while
|
|
72
|
+
* `error`/`progress`/`log` → stderr. stdout therefore carries only successful
|
|
73
|
+
* payloads, so a shell capture or pipe never mistakes a failure for data.
|
|
74
|
+
* - "stdout" / "stderr" are event-stream mode: every event, including `error`,
|
|
75
|
+
* is collapsed onto that one stream so a consumer reading it in order
|
|
76
|
+
* (branching on `kind`) sees preserved ordering.
|
|
77
|
+
*/
|
|
78
|
+
export type OutputTo = "split" | "stdout" | "stderr";
|
|
79
|
+
/**
|
|
80
|
+
* Parse an `--output-to` value into an OutputTo: "split" (default), "stdout", or
|
|
81
|
+
* "stderr". Throws on unknown values; catch and pass the message to
|
|
82
|
+
* buildCliError.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* parseOutputTo("split") // → "split"
|
|
86
|
+
* parseOutputTo("both") // throws Error
|
|
87
|
+
*/
|
|
88
|
+
export declare function parseOutputTo(value: string): OutputTo;
|
|
89
|
+
/**
|
|
90
|
+
* Stateful emitter for structured CLI executions.
|
|
91
|
+
*
|
|
92
|
+
* Routing follows the consumption mode (OutputTo):
|
|
93
|
+
*
|
|
94
|
+
* - Finite one-shot (`CliEmitter.finite` / `finiteWith` /
|
|
95
|
+
* `fromOutputTo("split")`): `result` → the primary writer (stdout);
|
|
96
|
+
* `error`/`progress`/`log` → the diagnostic writer (stderr). The recommended
|
|
97
|
+
* default for a one-shot CLI, so shell capture and pipelines never treat a
|
|
98
|
+
* failure as data.
|
|
99
|
+
* - Event stream (`new CliEmitter(...)` / `CliEmitter.stream` /
|
|
100
|
+
* `fromOutputTo("stdout"|"stderr")`): every event, including `error`, goes to
|
|
101
|
+
* the single writer, preserving interleaved ordering.
|
|
102
|
+
*/
|
|
66
103
|
export declare class CliEmitter {
|
|
67
104
|
private readonly writer;
|
|
68
105
|
private readonly format;
|
|
69
106
|
private readonly outputOptions;
|
|
70
107
|
private terminalEmitted;
|
|
71
108
|
private logFieldsProvider?;
|
|
109
|
+
private diagnostic?;
|
|
110
|
+
/**
|
|
111
|
+
* Create an event-stream emitter: every event, including `error`, goes to the
|
|
112
|
+
* single `writer`. Use CliEmitter.finite for a one-shot command that should
|
|
113
|
+
* split `result`/`error` across stdout/stderr.
|
|
114
|
+
*/
|
|
72
115
|
constructor(writer: CliEventWriter, format: OutputFormat, outputOptions?: OutputOptions);
|
|
116
|
+
/**
|
|
117
|
+
* Create an event-stream emitter: every event goes to `writer`, preserving
|
|
118
|
+
* interleaved ordering. Pick this when the consumer reads one ordered stream
|
|
119
|
+
* and branches on `kind`. Alias for the constructor's unified form.
|
|
120
|
+
*/
|
|
121
|
+
static stream(writer: CliEventWriter, format: OutputFormat, outputOptions?: OutputOptions): CliEmitter;
|
|
122
|
+
/**
|
|
123
|
+
* Create a finite one-shot emitter with explicit sinks: `result` goes to
|
|
124
|
+
* `resultWriter`, while `error`/`progress`/`log` go to `diagnostic`.
|
|
125
|
+
*/
|
|
126
|
+
static finiteWith(resultWriter: CliEventWriter, diagnostic: CliEventWriter, format: OutputFormat, outputOptions?: OutputOptions): CliEmitter;
|
|
127
|
+
/**
|
|
128
|
+
* Create a finite one-shot emitter wired to the process streams: `result` →
|
|
129
|
+
* stdout, `error`/`progress`/`log` → stderr. The recommended default for a
|
|
130
|
+
* one-shot CLI.
|
|
131
|
+
*/
|
|
132
|
+
static finite(format: OutputFormat, outputOptions?: OutputOptions): CliEmitter;
|
|
133
|
+
/**
|
|
134
|
+
* Build an emitter from a parsed OutputTo selector, wired to the process
|
|
135
|
+
* streams: "split" is finite mode (`result` → stdout, everything else →
|
|
136
|
+
* stderr); "stdout"/"stderr" are event-stream mode onto that one stream.
|
|
137
|
+
*/
|
|
138
|
+
static fromOutputTo(selector: OutputTo, format: OutputFormat, outputOptions?: OutputOptions): CliEmitter;
|
|
73
139
|
withLogFields(provider: () => Record<string, JsonValue>): this;
|
|
140
|
+
/**
|
|
141
|
+
* Select the sink for an event by `kind`. Finite mode (a diagnostic sink is
|
|
142
|
+
* present) keeps `result` on the primary writer (stdout) and routes
|
|
143
|
+
* `error`/`progress`/`log` to the diagnostic writer (stderr). Event-stream
|
|
144
|
+
* mode (no diagnostic sink) keeps every event on the single writer.
|
|
145
|
+
*/
|
|
146
|
+
private sinkFor;
|
|
74
147
|
emit(event: Event): void;
|
|
75
148
|
emitValidatedValue(value: JsonValue): void;
|
|
149
|
+
private static isBrokenPipe;
|
|
150
|
+
/**
|
|
151
|
+
* Emit `event` as the terminal event and resolve the outcome to a process
|
|
152
|
+
* exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
|
|
153
|
+
*
|
|
154
|
+
* A successful write returns `successCode`; a broken pipe (the reader hung up)
|
|
155
|
+
* returns `0`; any other write or validation failure returns `4`. A library
|
|
156
|
+
* never calls process.exit itself — return this code and let the caller exit.
|
|
157
|
+
*/
|
|
158
|
+
finish(event: Event, successCode: number): number;
|
|
159
|
+
/**
|
|
160
|
+
* Convenience over finish: emit a `result` payload and return `0` on success.
|
|
161
|
+
*
|
|
162
|
+
* Errors — simple or rich — go through the builder instead: construct the event
|
|
163
|
+
* with jsonError(code, message).hint(...)…build() (the builder is the error
|
|
164
|
+
* "type", so it scales to `retryable`/extra fields) and call finish(event, code).
|
|
165
|
+
*/
|
|
166
|
+
finishResult(payload: JsonValue): number;
|
|
76
167
|
}
|
|
77
168
|
/** Build a standard CLI version value. */
|
|
78
169
|
export declare function buildCliVersion(version: string): Event;
|
package/dist/cli.js
CHANGED
|
@@ -94,22 +94,114 @@ export function render(value, format, options = {}) {
|
|
|
94
94
|
return formatPlainValue(jsonValue, options);
|
|
95
95
|
return formatJsonValue(jsonValue, options);
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Parse an `--output-to` value into an OutputTo: "split" (default), "stdout", or
|
|
99
|
+
* "stderr". Throws on unknown values; catch and pass the message to
|
|
100
|
+
* buildCliError.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* parseOutputTo("split") // → "split"
|
|
104
|
+
* parseOutputTo("both") // throws Error
|
|
105
|
+
*/
|
|
106
|
+
export function parseOutputTo(value) {
|
|
107
|
+
if (value === "split" || value === "stdout" || value === "stderr") {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`invalid --output-to '${value}': expected split, stdout, or stderr`);
|
|
111
|
+
}
|
|
112
|
+
/** Event writer bound to the process stdout stream. */
|
|
113
|
+
const processStdoutWriter = (line) => {
|
|
114
|
+
process.stdout.write(line);
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Event writer bound to the process stderr stream. This is the CliEmitter's
|
|
118
|
+
* blessed diagnostic sink: stderr is sanctioned here, and only here, precisely
|
|
119
|
+
* because output flows through the emitter rather than an ad-hoc write. The
|
|
120
|
+
* `stderr-sink` marker is what no_stderr_policy.test.ts allows through.
|
|
121
|
+
*/
|
|
122
|
+
const processStderrWriter = (line) => {
|
|
123
|
+
process.stderr.write(line); // stderr-sink: CliEmitter diagnostic channel
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Stateful emitter for structured CLI executions.
|
|
127
|
+
*
|
|
128
|
+
* Routing follows the consumption mode (OutputTo):
|
|
129
|
+
*
|
|
130
|
+
* - Finite one-shot (`CliEmitter.finite` / `finiteWith` /
|
|
131
|
+
* `fromOutputTo("split")`): `result` → the primary writer (stdout);
|
|
132
|
+
* `error`/`progress`/`log` → the diagnostic writer (stderr). The recommended
|
|
133
|
+
* default for a one-shot CLI, so shell capture and pipelines never treat a
|
|
134
|
+
* failure as data.
|
|
135
|
+
* - Event stream (`new CliEmitter(...)` / `CliEmitter.stream` /
|
|
136
|
+
* `fromOutputTo("stdout"|"stderr")`): every event, including `error`, goes to
|
|
137
|
+
* the single writer, preserving interleaved ordering.
|
|
138
|
+
*/
|
|
98
139
|
export class CliEmitter {
|
|
99
140
|
writer;
|
|
100
141
|
format;
|
|
101
142
|
outputOptions;
|
|
102
143
|
terminalEmitted = false;
|
|
103
144
|
logFieldsProvider;
|
|
145
|
+
diagnostic;
|
|
146
|
+
/**
|
|
147
|
+
* Create an event-stream emitter: every event, including `error`, goes to the
|
|
148
|
+
* single `writer`. Use CliEmitter.finite for a one-shot command that should
|
|
149
|
+
* split `result`/`error` across stdout/stderr.
|
|
150
|
+
*/
|
|
104
151
|
constructor(writer, format, outputOptions = {}) {
|
|
105
152
|
this.writer = writer;
|
|
106
153
|
this.format = format;
|
|
107
154
|
this.outputOptions = outputOptions;
|
|
108
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Create an event-stream emitter: every event goes to `writer`, preserving
|
|
158
|
+
* interleaved ordering. Pick this when the consumer reads one ordered stream
|
|
159
|
+
* and branches on `kind`. Alias for the constructor's unified form.
|
|
160
|
+
*/
|
|
161
|
+
static stream(writer, format, outputOptions = {}) {
|
|
162
|
+
return new CliEmitter(writer, format, outputOptions);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Create a finite one-shot emitter with explicit sinks: `result` goes to
|
|
166
|
+
* `resultWriter`, while `error`/`progress`/`log` go to `diagnostic`.
|
|
167
|
+
*/
|
|
168
|
+
static finiteWith(resultWriter, diagnostic, format, outputOptions = {}) {
|
|
169
|
+
const emitter = new CliEmitter(resultWriter, format, outputOptions);
|
|
170
|
+
emitter.diagnostic = diagnostic;
|
|
171
|
+
return emitter;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Create a finite one-shot emitter wired to the process streams: `result` →
|
|
175
|
+
* stdout, `error`/`progress`/`log` → stderr. The recommended default for a
|
|
176
|
+
* one-shot CLI.
|
|
177
|
+
*/
|
|
178
|
+
static finite(format, outputOptions = {}) {
|
|
179
|
+
return CliEmitter.finiteWith(processStdoutWriter, processStderrWriter, format, outputOptions);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Build an emitter from a parsed OutputTo selector, wired to the process
|
|
183
|
+
* streams: "split" is finite mode (`result` → stdout, everything else →
|
|
184
|
+
* stderr); "stdout"/"stderr" are event-stream mode onto that one stream.
|
|
185
|
+
*/
|
|
186
|
+
static fromOutputTo(selector, format, outputOptions = {}) {
|
|
187
|
+
if (selector === "split")
|
|
188
|
+
return CliEmitter.finite(format, outputOptions);
|
|
189
|
+
const writer = selector === "stdout" ? processStdoutWriter : processStderrWriter;
|
|
190
|
+
return CliEmitter.stream(writer, format, outputOptions);
|
|
191
|
+
}
|
|
109
192
|
withLogFields(provider) {
|
|
110
193
|
this.logFieldsProvider = provider;
|
|
111
194
|
return this;
|
|
112
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Select the sink for an event by `kind`. Finite mode (a diagnostic sink is
|
|
198
|
+
* present) keeps `result` on the primary writer (stdout) and routes
|
|
199
|
+
* `error`/`progress`/`log` to the diagnostic writer (stderr). Event-stream
|
|
200
|
+
* mode (no diagnostic sink) keeps every event on the single writer.
|
|
201
|
+
*/
|
|
202
|
+
sinkFor(kind) {
|
|
203
|
+
return kind !== "result" && this.diagnostic ? this.diagnostic : this.writer;
|
|
204
|
+
}
|
|
113
205
|
emit(event) {
|
|
114
206
|
const jsonValue = event.toJSON();
|
|
115
207
|
validateProtocolEvent(jsonValue);
|
|
@@ -127,7 +219,7 @@ export class CliEmitter {
|
|
|
127
219
|
else {
|
|
128
220
|
throw new Error(`unsupported event kind ${JSON.stringify(kind)}`);
|
|
129
221
|
}
|
|
130
|
-
this.
|
|
222
|
+
this.sinkFor(kind)(`${render(jsonValue, this.format, this.outputOptions)}\n`);
|
|
131
223
|
if (kind === "result" || kind === "error")
|
|
132
224
|
this.terminalEmitted = true;
|
|
133
225
|
}
|
|
@@ -147,10 +239,40 @@ export class CliEmitter {
|
|
|
147
239
|
else {
|
|
148
240
|
throw new Error(`unsupported event kind ${JSON.stringify(kind)}`);
|
|
149
241
|
}
|
|
150
|
-
this.
|
|
242
|
+
this.sinkFor(kind)(`${render(value, this.format, this.outputOptions)}\n`);
|
|
151
243
|
if (kind === "result" || kind === "error")
|
|
152
244
|
this.terminalEmitted = true;
|
|
153
245
|
}
|
|
246
|
+
static isBrokenPipe(err) {
|
|
247
|
+
return (typeof err === "object" && err !== null && err.code === "EPIPE");
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Emit `event` as the terminal event and resolve the outcome to a process
|
|
251
|
+
* exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
|
|
252
|
+
*
|
|
253
|
+
* A successful write returns `successCode`; a broken pipe (the reader hung up)
|
|
254
|
+
* returns `0`; any other write or validation failure returns `4`. A library
|
|
255
|
+
* never calls process.exit itself — return this code and let the caller exit.
|
|
256
|
+
*/
|
|
257
|
+
finish(event, successCode) {
|
|
258
|
+
try {
|
|
259
|
+
this.emit(event);
|
|
260
|
+
return successCode;
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
return CliEmitter.isBrokenPipe(err) ? 0 : 4;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Convenience over finish: emit a `result` payload and return `0` on success.
|
|
268
|
+
*
|
|
269
|
+
* Errors — simple or rich — go through the builder instead: construct the event
|
|
270
|
+
* with jsonError(code, message).hint(...)…build() (the builder is the error
|
|
271
|
+
* "type", so it scales to `retryable`/extra fields) and call finish(event, code).
|
|
272
|
+
*/
|
|
273
|
+
finishResult(payload) {
|
|
274
|
+
return this.finish(jsonResult(payload).build(), 0);
|
|
275
|
+
}
|
|
154
276
|
}
|
|
155
277
|
/** Build a standard CLI version value. */
|
|
156
278
|
export function buildCliVersion(version) {
|
package/dist/format.d.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* isValidRfc3339Time, isValidRfc3339, isValidBcp47, RedactionPolicy,
|
|
7
7
|
* PlainStyle, and OutputOptions. The single render entry point (`render`)
|
|
8
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.
|
|
9
17
|
*/
|
|
10
18
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
11
19
|
[key: string]: JsonValue;
|
package/dist/format.js
CHANGED
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
* isValidRfc3339Time, isValidRfc3339, isValidBcp47, RedactionPolicy,
|
|
7
7
|
* PlainStyle, and OutputOptions. The single render entry point (`render`)
|
|
8
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.
|
|
9
17
|
*/
|
|
18
|
+
import { parse as losslessParse, stringify as losslessStringify, isLosslessNumber } from "lossless-json";
|
|
10
19
|
// ═══════════════════════════════════════════
|
|
11
20
|
// Event type: opaque branded wrapper
|
|
12
21
|
// ═══════════════════════════════════════════
|
|
@@ -320,7 +329,11 @@ const ERROR_RESERVED_FIELDS = new Set(["code", "message", "hint", "retryable"]);
|
|
|
320
329
|
export function decodeProtocolEvent(text) {
|
|
321
330
|
let parsed;
|
|
322
331
|
try {
|
|
323
|
-
|
|
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);
|
|
324
337
|
}
|
|
325
338
|
catch (e) {
|
|
326
339
|
throw new EventDecodeError(`invalid JSON: ${e.message}`);
|
|
@@ -390,7 +403,20 @@ export function outputOptionsForPolicy(policy) {
|
|
|
390
403
|
*/
|
|
391
404
|
export function formatJsonValue(value, options = {}) {
|
|
392
405
|
const unwrapped = value instanceof Event ? value.toJSON() : value;
|
|
393
|
-
return
|
|
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";
|
|
394
420
|
}
|
|
395
421
|
/**
|
|
396
422
|
* Format as multi-line YAML. Structure-preserving: like JSON, original keys,
|
|
@@ -865,6 +891,10 @@ function decimalIntText(value) {
|
|
|
865
891
|
return value;
|
|
866
892
|
if (isInt(value))
|
|
867
893
|
return String(value);
|
|
894
|
+
if (isLosslessNumber(value)) {
|
|
895
|
+
const text = value.toString();
|
|
896
|
+
return /^-?\d+$/.test(text) ? text : null;
|
|
897
|
+
}
|
|
868
898
|
return null;
|
|
869
899
|
}
|
|
870
900
|
function epochNsToMs(value) {
|
|
@@ -891,11 +921,24 @@ function isNum(value) {
|
|
|
891
921
|
}
|
|
892
922
|
function tryProcessField(key, value) {
|
|
893
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;
|
|
894
937
|
// Group 1: compound timestamp suffixes
|
|
895
938
|
stripped = stripSuffixCI(key, "_epoch_ms");
|
|
896
939
|
if (stripped !== null) {
|
|
897
|
-
if (isInt(
|
|
898
|
-
const formatted = formatRfc3339Ms(
|
|
940
|
+
if (isInt(numeric)) {
|
|
941
|
+
const formatted = formatRfc3339Ms(numeric);
|
|
899
942
|
if (formatted !== null)
|
|
900
943
|
return [stripped, formatted];
|
|
901
944
|
}
|
|
@@ -903,8 +946,8 @@ function tryProcessField(key, value) {
|
|
|
903
946
|
}
|
|
904
947
|
stripped = stripSuffixCI(key, "_epoch_s");
|
|
905
948
|
if (stripped !== null) {
|
|
906
|
-
if (isInt(
|
|
907
|
-
const formatted = formatRfc3339Ms(
|
|
949
|
+
if (isInt(numeric)) {
|
|
950
|
+
const formatted = formatRfc3339Ms(numeric * 1000);
|
|
908
951
|
if (formatted !== null)
|
|
909
952
|
return [stripped, formatted];
|
|
910
953
|
}
|
|
@@ -923,28 +966,28 @@ function tryProcessField(key, value) {
|
|
|
923
966
|
// Group 2: compound currency suffixes
|
|
924
967
|
stripped = stripSuffixCI(key, "_usd_cents");
|
|
925
968
|
if (stripped !== null) {
|
|
926
|
-
if (isInt(
|
|
927
|
-
return [stripped, `$${Math.floor(
|
|
969
|
+
if (isInt(numeric) && numeric >= 0)
|
|
970
|
+
return [stripped, `$${Math.floor(numeric / 100)}.${String(numeric % 100).padStart(2, "0")}`];
|
|
928
971
|
return null;
|
|
929
972
|
}
|
|
930
973
|
stripped = stripSuffixCI(key, "_eur_cents");
|
|
931
974
|
if (stripped !== null) {
|
|
932
|
-
if (isInt(
|
|
933
|
-
return [stripped, `\u20ac${Math.floor(
|
|
975
|
+
if (isInt(numeric) && numeric >= 0)
|
|
976
|
+
return [stripped, `\u20ac${Math.floor(numeric / 100)}.${String(numeric % 100).padStart(2, "0")}`];
|
|
934
977
|
return null;
|
|
935
978
|
}
|
|
936
979
|
const gc = tryStripGenericCents(key);
|
|
937
980
|
if (gc !== null) {
|
|
938
981
|
const [gcStripped, code] = gc;
|
|
939
|
-
if (isInt(
|
|
940
|
-
return [gcStripped, `${Math.floor(
|
|
982
|
+
if (isInt(numeric) && numeric >= 0)
|
|
983
|
+
return [gcStripped, `${Math.floor(numeric / 100)}.${String(numeric % 100).padStart(2, "0")} ${code.toUpperCase()}`];
|
|
941
984
|
return null;
|
|
942
985
|
}
|
|
943
986
|
const gm = tryStripGenericMicro(key);
|
|
944
987
|
if (gm !== null) {
|
|
945
988
|
const [gmStripped, code] = gm;
|
|
946
|
-
if (isInt(
|
|
947
|
-
return [gmStripped, `${Math.floor(
|
|
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()}`];
|
|
948
991
|
return null;
|
|
949
992
|
}
|
|
950
993
|
// Group 3: multi-char suffixes
|
|
@@ -956,20 +999,20 @@ function tryProcessField(key, value) {
|
|
|
956
999
|
}
|
|
957
1000
|
stripped = stripSuffixCI(key, "_minutes");
|
|
958
1001
|
if (stripped !== null) {
|
|
959
|
-
if (isNum(
|
|
960
|
-
return [stripped, `${plainScalar(
|
|
1002
|
+
if (isNum(numeric))
|
|
1003
|
+
return [stripped, `${plainScalar(numeric)} minutes`];
|
|
961
1004
|
return null;
|
|
962
1005
|
}
|
|
963
1006
|
stripped = stripSuffixCI(key, "_hours");
|
|
964
1007
|
if (stripped !== null) {
|
|
965
|
-
if (isNum(
|
|
966
|
-
return [stripped, `${plainScalar(
|
|
1008
|
+
if (isNum(numeric))
|
|
1009
|
+
return [stripped, `${plainScalar(numeric)} hours`];
|
|
967
1010
|
return null;
|
|
968
1011
|
}
|
|
969
1012
|
stripped = stripSuffixCI(key, "_days");
|
|
970
1013
|
if (stripped !== null) {
|
|
971
|
-
if (isNum(
|
|
972
|
-
return [stripped, `${plainScalar(
|
|
1014
|
+
if (isNum(numeric))
|
|
1015
|
+
return [stripped, `${plainScalar(numeric)} days`];
|
|
973
1016
|
return null;
|
|
974
1017
|
}
|
|
975
1018
|
// Group 4: single-unit suffixes
|
|
@@ -989,46 +1032,46 @@ function tryProcessField(key, value) {
|
|
|
989
1032
|
}
|
|
990
1033
|
stripped = stripSuffixCI(key, "_bytes");
|
|
991
1034
|
if (stripped !== null) {
|
|
992
|
-
if (isInt(
|
|
993
|
-
return [stripped, formatBytesHuman(
|
|
1035
|
+
if (isInt(numeric) && numeric >= 0)
|
|
1036
|
+
return [stripped, formatBytesHuman(numeric)];
|
|
994
1037
|
return null;
|
|
995
1038
|
}
|
|
996
1039
|
stripped = stripSuffixCI(key, "_percent");
|
|
997
1040
|
if (stripped !== null) {
|
|
998
|
-
if (isNum(
|
|
999
|
-
return [stripped, `${plainScalar(
|
|
1041
|
+
if (isNum(numeric))
|
|
1042
|
+
return [stripped, `${plainScalar(numeric)}%`];
|
|
1000
1043
|
return null;
|
|
1001
1044
|
}
|
|
1002
1045
|
// Group 5: short suffixes (last to avoid false positives)
|
|
1003
1046
|
stripped = stripSuffixCI(key, "_jpy");
|
|
1004
1047
|
if (stripped !== null) {
|
|
1005
|
-
if (isInt(
|
|
1006
|
-
return [stripped, `\u00a5${formatWithCommas(
|
|
1048
|
+
if (isInt(numeric) && numeric >= 0)
|
|
1049
|
+
return [stripped, `\u00a5${formatWithCommas(numeric)}`];
|
|
1007
1050
|
return null;
|
|
1008
1051
|
}
|
|
1009
1052
|
stripped = stripSuffixCI(key, "_ns");
|
|
1010
1053
|
if (stripped !== null) {
|
|
1011
|
-
if (isNum(
|
|
1012
|
-
return [stripped, `${plainScalar(
|
|
1054
|
+
if (isNum(numeric))
|
|
1055
|
+
return [stripped, `${plainScalar(numeric)}ns`];
|
|
1013
1056
|
return null;
|
|
1014
1057
|
}
|
|
1015
1058
|
stripped = stripSuffixCI(key, "_us");
|
|
1016
1059
|
if (stripped !== null) {
|
|
1017
|
-
if (isNum(
|
|
1018
|
-
return [stripped, `${plainScalar(
|
|
1060
|
+
if (isNum(numeric))
|
|
1061
|
+
return [stripped, `${plainScalar(numeric)}\u03bcs`];
|
|
1019
1062
|
return null;
|
|
1020
1063
|
}
|
|
1021
1064
|
stripped = stripSuffixCI(key, "_ms");
|
|
1022
1065
|
if (stripped !== null) {
|
|
1023
|
-
const fv = formatMsValue(
|
|
1066
|
+
const fv = formatMsValue(numeric);
|
|
1024
1067
|
if (fv !== null)
|
|
1025
1068
|
return [stripped, fv];
|
|
1026
1069
|
return null;
|
|
1027
1070
|
}
|
|
1028
1071
|
stripped = stripSuffixCI(key, "_s");
|
|
1029
1072
|
if (stripped !== null) {
|
|
1030
|
-
if (isNum(
|
|
1031
|
-
return [stripped, `${plainScalar(
|
|
1073
|
+
if (isNum(numeric))
|
|
1074
|
+
return [stripped, `${plainScalar(numeric)}s`];
|
|
1032
1075
|
return null;
|
|
1033
1076
|
}
|
|
1034
1077
|
return null;
|
|
@@ -1258,7 +1301,11 @@ function yamlScalar(value) {
|
|
|
1258
1301
|
return value.toString();
|
|
1259
1302
|
if (typeof value === "number")
|
|
1260
1303
|
return value.toString();
|
|
1261
|
-
|
|
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)))}"`;
|
|
1262
1309
|
}
|
|
1263
1310
|
// ═══════════════════════════════════════════
|
|
1264
1311
|
// Plain Rendering (logfmt)
|
|
@@ -1314,11 +1361,16 @@ function plainScalar(value) {
|
|
|
1314
1361
|
return value.toString();
|
|
1315
1362
|
if (typeof value === "number")
|
|
1316
1363
|
return value.toString();
|
|
1317
|
-
|
|
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));
|
|
1318
1370
|
}
|
|
1319
1371
|
function plainScalarRaw(value) {
|
|
1320
1372
|
if (isObject(value) || Array.isArray(value)) {
|
|
1321
|
-
return
|
|
1373
|
+
return canonicalJsonString(sortJsonValue(value));
|
|
1322
1374
|
}
|
|
1323
1375
|
return plainScalar(value);
|
|
1324
1376
|
}
|
|
@@ -1365,6 +1417,11 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
|
|
|
1365
1417
|
return "<unsupported:function>";
|
|
1366
1418
|
if (t === "symbol")
|
|
1367
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;
|
|
1368
1425
|
if (value instanceof Error)
|
|
1369
1426
|
return value.message;
|
|
1370
1427
|
if (value instanceof Date)
|
|
@@ -1392,7 +1449,13 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
|
|
|
1392
1449
|
return "<unsupported:unknown>";
|
|
1393
1450
|
}
|
|
1394
1451
|
function isObject(value) {
|
|
1395
|
-
|
|
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);
|
|
1396
1459
|
}
|
|
1397
1460
|
function sortedObjectKeys(obj) {
|
|
1398
1461
|
return Object.keys(obj).sort(jcsCompare);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
|
|
3
3
|
*/
|
|
4
4
|
export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, type LogLevel, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, type DecodedEvent, type DecodedResult, type DecodedError, type DecodedProgress, type DecodedLog, EventDecodeError, type JsonValue, RedactionPolicy, PlainStyle, type OutputOptions, outputOptionsForPolicy, redactedValue, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
|
|
5
|
-
export { type OutputFormat, type CliEventWriter, CliEmitter, LogFilters, cliParseOutput, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.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
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
|
|
3
3
|
*/
|
|
4
4
|
export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, EventDecodeError, RedactionPolicy, PlainStyle, outputOptionsForPolicy, redactedValue, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
|
|
5
|
-
export { CliEmitter, LogFilters, cliParseOutput, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.js";
|
|
5
|
+
export { CliEmitter, LogFilters, cliParseOutput, parseOutputTo, cliParseLogFilters, render, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-first-data",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -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
|
}
|