Skip to content

Commit 1d306cd

Browse files
feat(cli): chfx — decode / query / capture ClickHouse wire formats to JSON (#43)
* feat(cli): add chfx CLI with decode command (items 1 + 4) A publish-ready npm bin (`chfx`) that decodes ClickHouse wire-format dumps to structured JSON for humans and agents. Reuses the src/core decoders (DOM-free) and bundles to a single ESM file via esbuild. - `chfx decode [file]`: decode a .chproto capture, raw Native body, or raw RowBinaryWithNamesAndTypes body. Autodetects .chproto by magic and raw bodies by trial decode; `--format` forces it. Reads stdin when no path (or `-`) is given. `--protocol-version` sets the Native client version. - Output: the web ParsedData/AstNode tree as JSON, a top-level `bytesHex` (whole decoded buffer once), and per-node inline `bytes` by default so a consumer can read a value's bytes without slicing by range (`--no-node-bytes` to omit). bigints → decimal strings, byte blobs → hex. - Agent-friendly: deterministic JSON on stdout, JSON error envelope on stderr, exit codes (0 ok / 2 usage / 1 io|decode), `--help`/`--version`, non-interactive. Packaging: `bin`/`files`/`prepublishOnly` wired; `npm run cli` (tsx, dev) and `npm run cli:build` (esbuild → dist/cli/index.js). Adds esbuild, tsx, @types/node devDeps. Tests: src/cli/cli.test.ts — arg parsing, decode of every protocol fixture (+ bigint-safe serialization), hand-built Native/RowBinary bodies, autodetect + override, per-node bytes match their range, and tsx end-to-end (stdin, exit codes). README + AGENTS.md + docs/cli-spec.md updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): add query + capture commands; UX one-shot and simpler surface Collapse the capture→file→decode dance into single commands under chfx, and make the dump file optional. - `chfx query --query "<sql>"`: run AND decode in one step (no temp file). - `--protocol tcp` (default): drive clickhouse-client through the capturing proxy and decode the native packet stream; `--save <f>` keeps the .chproto. - `--protocol http`: POST to ClickHouse HTTP requesting `--format` (native | RowBinaryWithNamesAndTypes, default native) and decode the body; `--protocol-version` sets the Native client version. Port defaults 8123; auth via X-ClickHouse-User/-Key headers. - `chfx capture --query "<sql>"`: capture to a .chproto dump only; `--out <f>` writes a file (+ JSON summary), otherwise streams raw bytes to stdout so `chfx capture … | chfx decode` works. `npm run capture` is now an alias to it; the standalone scripts/capture-native.mjs is folded in and removed. - Shared connection flags with env fallbacks (CH_NATIVE_HOST, CH_NATIVE_PORT / CH_HTTP_PORT, CH_USER, CH_PASSWORD, CH_DATABASE, CLICKHOUSE_CLIENT) and experimental type settings on by default (--no-experimental-settings, repeatable --setting k=v). Refactor: extract decodeCaptureStreams + buildDecodeEnvelope (shared by decode and query); commands return a JSON|raw CommandOutput union the entry point renders. Arg parser gains repeatable multiFlags (--setting). The TS CLI imports the JS proxy via a new scripts/native-proxy.d.mts declaration; query/capture reuse the same captureQuery the web/Electron paths use. Docs: README quick start now leads with `chfx query` and `npm link`; full transport/connection option tables. AGENTS.md + docs/cli-spec.md updated. Tests: query (tcp via injected capture; http via injected fetch for Native + RowBinary + error + flag-validation), capture (raw stdout + file summary), repeatable-flag parsing, and connection/env resolution. 44 tests pass; verified end-to-end against a live server on both transports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): address review findings (OOM on tiny input, EPIPE, stale schema help) - Blocker: a 0-column RowBinary header made decodeRows loop forever (offset never advances while remaining > 0), exhausting memory — trivially reachable since `decode` autodetect trials RowBinary first. `printf '\x00\x02' | chfx decode -` OOM'd the process. Guard the row loop to break on a non-advancing iteration (rowbinary-decoder.ts). Now terminates with a clean usage error. - High: piping decode output into a consumer that closes early (`… | head`) threw an unhandled EPIPE and dumped a Node stack trace to stderr, violating the clean-exit contract. Handle EPIPE on stdout/stderr and exit 0. - Stale `schema` references: general --help advertised a `chfx schema` command that was dropped; removed it and the registry comment. - Classify --save / --out write failures as io errors (not decode); wrap both writeFile calls. - README: build before `npm link` (link points at the not-yet-built binary). - Tidy an orphaned doc comment in connection.ts. Tests: regression for degenerate/tiny inputs terminating (no OOM). 45 CLI tests + 86 core unit tests pass; lint + tsc clean; both blockers verified fixed end-to-end (2-byte input → exit 2 usage error; `| head` → no stack trace). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cli): cover option combos, failure modes, env fallbacks, http request shape Fill the gaps from the coverage review (45 → 67 tests): - decode edge/error: empty input, missing file, invalid --protocol-version, ambiguous raw-body autodetect. - query http request construction (spied fetch): default_format, --setting and --database query params, X-ClickHouse-User/-Key headers, body, port 8123, and --protocol-version → client_protocol_version; RowBinaryWithNamesAndTypes format. - query/capture failure modes: tcp capture throw → io, --save write failure → io, empty http body → decode, http transport throw → io, unknown --protocol, unknown http --format, capture-command throw → io. - capture: -o alias, --out - raw stdout. - connection: CH_NATIVE_HOST/PORT + CH_HTTP_PORT env fallbacks and flag precedence, resolveHttpConnection defaults, --setting overriding an experimental default (added a withEnv save/restore helper and a shared fakeCaptureOf). - e2e via tsx: --version, unknown-command exit 2, and a clean-exit-on-EPIPE check (decode | head closes early → no stack trace). eslint + tsc clean; 67 CLI tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(capture): don't hang when clickhouse-client exits before connecting Fuzz testing found that a TCP `chfx query`/`capture` hangs forever on any flag the client rejects at startup (e.g. --setting totally_fake_setting_xyz=1 or a bad setting value): clickhouse-client exits before opening the proxied TCP connection, so the proxy's `done` promise (which only resolves once both ends of a connection close) never settles and `await done` blocks indefinitely. Fix in scripts/native-proxy.mjs (shared by the web/Electron/CLI capture paths): - Settle `done` exactly once via finishOk/finishErr, and make the proxy's close() resolve `done` with whatever was captured so far. - In captureQuery, on a non-zero client exit, race `done` against a 100ms grace window then force-close — so a pre-connect failure yields a clean io error ("clickhouse-client exited 40: …") instead of a hang. The happy path and the connected-but-failed path (server Exception captured) are unchanged. Regression test uses `false` as the client (exits before connecting) so it needs no server. 68 CLI tests pass; lint + tsc clean; verified against the live server that the original repros now return a clean error and normal queries still work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): reject unknown options and unexpected positionals (review) Address Copilot review on PR #43: the permissive parser meant commands silently ignored unknown flags (a typo like `--protcol http` ran the default tcp path) and extra positionals. Add rejectUnknownArgs(allowed, maxPositionals) and call it in decode/query/capture so unrecognized options or surplus arguments fail fast as `usage` errors (exit 2), matching the documented contract. Also correct the stringOption doc comment (it errors on a repeated *multi* flag, not any repeat). Tests: unknown-flag + extra-positional rejection for decode/query/capture. 73 CLI tests pass; lint + tsc clean; verified `--protcol` now errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): reject --save '-' and out-of-range ports (review round 2) Address the second Copilot review on PR #43: - query --save '-' previously wrote a file literally named "-"; since stdout carries the decoded JSON, reject "-" as a usage error with a clear message. - --port accepted values > 65535 (only >0 was checked); require 1..65535 so invalid ports fail fast with a clear message instead of at connect time. Tests added for both. 75 CLI tests pass; lint + tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a31a6d9 commit 1d306cd

21 files changed

Lines changed: 2518 additions & 217 deletions

AGENTS.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A tool for visualizing ClickHouse RowBinary and Native wire format data. Feature
1010

1111
The `NativeProtocol` format decodes a whole connection's packet stream rather than a single HTTP format body. A small TCP proxy (`scripts/native-proxy.mjs`, mirrored for the app in `electron/native-capture.ts`) sits between `clickhouse-client` and the server, forwarding bytes and teeing both directions into a capture. On localhost clickhouse-client disables compression, so the capture is plaintext, uncompressed packets (TLS/compression are out of scope).
1212

13-
- Capture a dump for tests/inspection: `npm run capture -- --query "SELECT 1" --out cap.chproto` (see `scripts/capture-native.mjs`).
13+
- Capture a dump for tests/inspection: `npm run capture -- --query "SELECT 1" --out cap.chproto` (alias for `chfx capture`, in `src/cli/commands/capture.ts`). Or `chfx query --query "SELECT 1"` to capture **and** decode in one step.
1414
- `.chproto` dump format and parsing: `scripts/native-proxy.mjs` (writer) and `src/core/decoder/protocol-dump.ts` (reader).
1515
- Decoder: `src/core/decoder/protocol-decoder.ts` (`ProtocolDecoder`) reuses `NativeDecoder.decodeProtocolBlock()` for the Block inside Data-family packets. It derives the negotiated version from `min(client, server)` Hello and gates every field per `docs/full_native_protocol_spec.md`.
1616
- Capture from the **web UI**: the browser POSTs SQL to a `/capture` endpoint that runs the proxy server-side and returns the `.chproto` dump (the browser can't open raw TCP itself). Served by: `npm run dev` / `vite preview` (Vite plugin in `vite.config.ts``scripts/capture-middleware.mjs`), and the **Docker** image (standalone `scripts/capture-server.mjs` under supervisord, proxied by nginx). Native-connection defaults come from env: `CH_NATIVE_HOST`/`CH_NATIVE_PORT`/`CH_USER`/`CH_PASSWORD`/`CLICKHOUSE_CLIENT`; `CAPTURE_EXPERIMENTAL_SETTINGS=0` stops sending experimental type settings per-query (for read-only users that reject them — rely on the profile instead).
@@ -36,6 +36,12 @@ npm run test # Run integration tests (uses testcontainers)
3636
npm run lint # ESLint check
3737
npm run test:e2e # Build Electron + run Playwright e2e tests
3838

39+
# CLI (chfx) — run/decode wire-format data to structured JSON
40+
npm run cli -- query --query "SELECT 1" # capture over native protocol + decode (one step)
41+
npm run cli -- decode capture.chproto # decode an existing dump (run from source via tsx)
42+
npm run capture -- --query "SELECT 1" -o cap.chproto # capture only (alias for `chfx capture`)
43+
npm run cli:build # bundle the publishable binary → dist/cli/index.js
44+
3945
# Electron desktop app
4046
npm run electron:dev # Dev mode with hot reload
4147
npm run electron:build # Package desktop installer for current platform
@@ -78,6 +84,18 @@ src/
7884
│ └── request-params.ts # Shared request parameter builder
7985
├── store/
8086
│ └── store.ts # Zustand store (query, parsed data, UI state)
87+
├── cli/ # chfx CLI (Node, bundled via esbuild; reuses src/core decoders)
88+
│ ├── index.ts # Entry: command dispatch, --help/--version, JSON|raw stdout, error envelope
89+
│ ├── commands/decode.ts# `decode` — decodeBuffer/decodeCaptureStreams + shared buildDecodeEnvelope
90+
│ ├── commands/query.ts # `query` — capture (proxy) + decode in one step; --save keeps the dump
91+
│ ├── commands/capture.ts# `capture` — capture to .chproto (file or raw stdout); `npm run capture` alias
92+
│ ├── connection.ts # Shared --host/port/user/... resolution + env fallbacks + experimental settings
93+
│ ├── args.ts # Dependency-free arg parser (value + repeatable flags)
94+
│ ├── output.ts # CliError, JSON-safe serializer (bigint→string, bytes→hex), CommandOutput
95+
│ ├── registry.ts # Command metadata for --help
96+
│ ├── version.ts # Build-injected version (esbuild define)
97+
│ └── cli.test.ts # Vitest unit + tsx e2e tests (uses fixtures/protocol/*.chproto)
98+
│ # query/capture reuse scripts/native-proxy.mjs (+ native-proxy.d.mts for types)
8199
└── styles/ # CSS files
82100
electron/
83101
├── main.ts # Electron main process (window, IPC handlers)

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A tool for visualizing ClickHouse RowBinary and Native format data. Features an
1313
- **Interactive Highlighting**: Selecting a node in the tree highlights corresponding bytes in the hex view (and vice versa)
1414
- **Full Type Support**: All ClickHouse types including Variant, Dynamic, JSON, Geo types, Nested, etc.
1515
- **Desktop App**: Electron app that connects to your existing ClickHouse server (no bundled DB)
16+
- **CLI (`chfx`)**: Decode `.chproto` / Native / RowBinary dumps to structured JSON from the terminal — agent-friendly
1617

1718
## Quick Start (Docker)
1819

@@ -41,6 +42,100 @@ CH_VERSION=24.3 docker compose build
4142

4243
The version is baked into the image — rebuild to change it.
4344

45+
## CLI (`chfx`)
46+
47+
A command-line tool that runs or decodes ClickHouse wire-format data and prints
48+
structured JSON — the same AST the web UI renders, plus the raw bytes — so it
49+
can be scripted or driven by an agent.
50+
51+
### Quick start
52+
53+
```bash
54+
npm install
55+
npm run cli:build # build the binary → dist/cli/index.js
56+
npm link # makes `chfx` available on your PATH (points at the built binary)
57+
58+
# Run a query and see it decoded — one step, no intermediate file:
59+
chfx query --query "SELECT number AS n, [number] AS arr FROM numbers(3)"
60+
61+
# Decode a dump you already have (or pipe one in):
62+
chfx decode capture.chproto
63+
clickhouse-client -q "SELECT 1 FORMAT Native" | chfx decode -f native -
64+
```
65+
66+
> Prefer not to `npm link`? Use `npm run cli -- <args>` (runs from source via
67+
> tsx, no build) or `node dist/cli/index.js <args>` after `cli:build`.
68+
69+
Output is a single JSON document on **stdout**; diagnostics and a JSON error
70+
envelope go to **stderr**. Exit codes: `0` success, `2` usage error, `1` I/O or
71+
decode error.
72+
73+
### Commands
74+
75+
| Command | Description |
76+
|---------|-------------|
77+
| `chfx query --query "<sql>"` | Run a query **and decode it** in one step (no file). `--protocol tcp` (default) captures the native packet stream via `clickhouse-client`; `--protocol http` POSTs to ClickHouse HTTP and decodes the `--format` body. `--save <f>` keeps the `.chproto` dump (tcp). |
78+
| `chfx capture --query "<sql>"` | Capture a query to a `.chproto` dump only (native protocol). Writes `--out <f>`, or streams raw bytes to stdout (so `chfx capture … \| chfx decode` works). `npm run capture` is an alias. |
79+
| `chfx decode [file]` | Decode a `.chproto`, Native, or RowBinary dump to JSON. Reads stdin when no file (or `-`) is given. |
80+
| `chfx --help` / `chfx <cmd> --help` | Human-readable help. |
81+
| `chfx --version` | Print the version. |
82+
83+
### `query` transport options
84+
85+
| Option | Description |
86+
|--------|-------------|
87+
| `--protocol tcp\|http` | Transport. `tcp` (default) = native capture via `clickhouse-client`. `http` = HTTP request. |
88+
| `--format native\|RowBinaryWithNamesAndTypes` | **http only** — the body format to request and decode (default `native`). |
89+
| `--protocol-version <N>` | `client_protocol_version` for an http Native query (default `0`). |
90+
| `--save <file>` | **tcp only** — also write the raw `.chproto` capture. |
91+
92+
### Connection options (`query` / `capture`)
93+
94+
| Option | Description |
95+
|--------|-------------|
96+
| `--query <sql>` | SQL to run (required). |
97+
| `--host` / `--port` | Server host / port. Env: `CH_NATIVE_HOST`, `CH_NATIVE_PORT` (tcp) / `CH_HTTP_PORT` (http). Default `127.0.0.1`, port `9000` (tcp) / `8123` (http). |
98+
| `--user` / `--password` | Credentials. Env: `CH_USER` / `CH_PASSWORD`. |
99+
| `--database <db>` | Default database. Env: `CH_DATABASE`. |
100+
| `--setting k=v` | Per-query setting; repeatable. |
101+
| `--no-experimental-settings` | Don't send the Variant/Dynamic/JSON/QBit enabling settings (sent by default). |
102+
| `--client <path>` | Path to `clickhouse-client` (tcp only). Env: `CLICKHOUSE_CLIENT`. |
103+
| `--out <file>` (`capture`) | Where to write the `.chproto` dump. |
104+
105+
### `decode` options
106+
107+
| Option | Description |
108+
|--------|-------------|
109+
| `--format`, `-f` `<chproto\|native\|rowbinary>` | Force the decoder. Omitted → autodetect: `.chproto` by magic header, raw bodies by trial decode (ambiguous input errors and asks for `--format`). |
110+
| `--protocol-version <N>` | Native `client_protocol_version` used to interpret a raw Native body (default `0`). |
111+
| `--no-node-bytes` | Omit each node's inline raw bytes (consumers slice `bytesHex` by range instead). Smaller output. |
112+
| `--compact` | Emit single-line JSON instead of pretty-printed. |
113+
114+
### Output shape
115+
116+
```jsonc
117+
{
118+
"chfx": { "tool": "chfx", "version": "...", "schemaVersion": 1, "command": "decode" }, // or "query"
119+
"source": { "kind": "file", "path": "...", "byteLength": 2417 }, // kind "stdin" | "query" too
120+
"format": "NativeProtocol", // | Native | RowBinaryWithNamesAndTypes
121+
"formatDetected": true, // false when forced via --format
122+
"protocolVersion": 54482, // negotiated (chproto) / requested (native) / null (rowbinary)
123+
"nodeBytes": true, // false when --no-node-bytes was passed
124+
"protocol": { "negotiatedVersion": 54482, "c2sLength": 191, "dumpMeta": { ... } },
125+
"bytesHex": "0011436c...", // the whole decoded buffer, encoded once
126+
"data": { /* ParsedData: header, rows|blocks, trailingNodes, metadata */ }
127+
}
128+
```
129+
130+
Every node has a `byteRange` of `{start, end}` byte offsets into `bytesHex` (two
131+
hex chars per byte; `start` inclusive, `end` exclusive). By default each node
132+
**also carries its own raw bytes inline** as a `bytes` hex string, so a consumer
133+
can read the bytes behind any value without slicing `bytesHex` itself — pass
134+
`--no-node-bytes` to drop them for smaller output.
135+
136+
> Decoded values are JSON-safe: 64-bit and larger integers become decimal
137+
> strings, and raw byte blobs become hex.
138+
44139
## Desktop App
45140

46141
For developers who already run ClickHouse locally. Download the latest release for your platform from the [Releases](../../releases) page:

docs/cli-spec.md

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,31 @@ Import a binary dump from a file **or stdin** and emit structured JSON.
3838
- Accepts binary on **stdin** (e.g. piped from clickhouse-client) as well as a
3939
file path argument.
4040

41-
#### `chfx query`
42-
Run SQL against a server and decode the result in one step.
43-
- Transport: **both, `--transport http|native`.**
44-
- `http`: POST to ClickHouse HTTP, request the chosen format, decode the body.
45-
- `native`: drive clickhouse-client through the capture proxy and decode the
46-
full `.chproto` packet stream.
47-
- Default `--format native` (richest). **Experimental type settings**
48-
(Variant/Dynamic/JSON enablement) are **sent by default**, with
49-
`--no-experimental-settings` to disable for read-only/strict servers that
50-
reject them.
51-
- Remote connection flags: `--host/--port/--user/--password`, env-var fallbacks,
52-
and HTTPS/TLS where applicable.
41+
#### `chfx query` (implemented)
42+
Run a query **and decode it in one step** — no intermediate file — over either
43+
transport, emitting the same envelope as `decode`:
44+
- **`--protocol tcp`** (default): drives `clickhouse-client` through the
45+
capturing proxy (`scripts/native-proxy.mjs`) and decodes the native packet
46+
stream. `--save <file>` also writes the raw `.chproto` dump.
47+
- **`--protocol http`**: POSTs to ClickHouse HTTP requesting `--format`
48+
(`native` | `RowBinaryWithNamesAndTypes`, default native) and decodes the
49+
body. `--protocol-version <N>` sets the Native `client_protocol_version`.
50+
Port defaults to 8123 (env `CH_HTTP_PORT`); user/password go via
51+
`X-ClickHouse-User`/`-Key` headers.
52+
- SQL via the **`--query` flag**. **Experimental type settings** sent by default
53+
(`--no-experimental-settings` to disable); `--setting k=v` repeatable.
54+
- Connection flags `--host/--port/--user/--password/--database/--client` with
55+
env fallbacks (`CH_NATIVE_HOST`, `CH_NATIVE_PORT`/`CH_HTTP_PORT`, `CH_USER`,
56+
`CH_PASSWORD`, `CH_DATABASE`, `CLICKHOUSE_CLIENT`).
57+
- **Deferred:** TLS. (The shelved own-TCP-client would remove the
58+
`clickhouse-client` dependency for tcp and could revive item 3.)
59+
60+
#### `chfx capture` (implemented)
61+
Capture a query to a `.chproto` dump **without decoding**. `--out <file>` (`-o`)
62+
writes the dump; omitted, it streams the raw dump bytes to stdout so
63+
`chfx capture … | chfx decode` works. Shares all `query` connection flags.
64+
**`npm run capture` is a thin alias** to `chfx capture` (the standalone
65+
`scripts/capture-native.mjs` was folded in and removed).
5366

5467
#### `chfx proxy` (item 5 — standalone capture proxy)
5568
A listener that forwards to a target server and captures the native TCP stream.
@@ -66,17 +79,26 @@ through it — the proxy does not spawn the client itself.
6679
- **Plaintext/uncompressed only** (same constraint as today). TLS/compressed
6780
streams are unsupported — error clearly and document it.
6881

69-
#### `chfx schema` / `--help`
70-
Machine-readable description of commands, flags, and the output JSON shape for
71-
agent discovery.
82+
#### `--help`
83+
Human-readable help (`chfx --help`, `chfx <command> --help`). A standalone
84+
machine-readable `schema` command was considered but **dropped** while the CLI
85+
has a single real command: `--help` covers human discovery and the `decode`
86+
output is already self-describing (carries `schemaVersion` and the byteRange/bytes
87+
conventions inline). Revisit a structured-discovery surface (a `schema` command
88+
or `--help --json`) once `query`/`proxy` add a multi-command contract.
7289

7390
### Output (item 4)
7491
- **Reuse the web `ParsedData`/`AstNode` shape verbatim**, serialized to JSON,
7592
wrapped with top-level metadata: tool/schema version, format, negotiated
76-
protocol version (for captures), and the raw bytes.
77-
- **Raw bytes inline, once**, as a top-level **hex** string. Agents read a node's
78-
`byteRange {start, end}` (exclusive end) and slice the hex to inspect bytes —
79-
no second command or sidecar file.
93+
protocol version (for captures), `nodeBytes`, and the raw bytes.
94+
- **Top-level `bytesHex`**: the whole decoded buffer encoded once as hex
95+
(for NativeProtocol this is the combined c2s+s2c stream the ranges index into).
96+
- **Per-node inline bytes (default on)**: every node with a `byteRange {start,
97+
end}` (exclusive end) also carries its own raw bytes as a `bytes` hex string,
98+
so a consumer reads a value's bytes directly without slicing `bytesHex`. This
99+
trades output size (parents duplicate children's bytes) for convenience;
100+
`--no-node-bytes` omits them and falls back to range lookups against `bytesHex`.
101+
- JSON-safe values: bigints → decimal strings, byte blobs → hex.
80102

81103
### Tests & docs (cross-cutting)
82104
- **Thorough CLI tests/fixtures**: decode against the existing

0 commit comments

Comments
 (0)