@tikoci/centrs 0.1.6 → 0.1.8-next.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +342 -0
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +10 -6
  4. package/SECURITY.md +1 -1
  5. package/commands/AGENTS.md +2 -1
  6. package/commands/api/AGENTS.md +55 -30
  7. package/commands/api/README.md +135 -42
  8. package/commands/api/examples.md +275 -19
  9. package/commands/devices/README.md +21 -4
  10. package/commands/devices/examples.md +33 -2
  11. package/commands/execute/README.md +60 -18
  12. package/commands/execute/examples.md +224 -0
  13. package/commands/explain/README.md +42 -5
  14. package/commands/mcp/README.md +2 -1
  15. package/commands/retrieve/README.md +348 -4
  16. package/commands/retrieve/examples.md +442 -5
  17. package/commands/terminal/README.md +14 -4
  18. package/commands/terminal/examples.md +22 -3
  19. package/commands/transfer/README.md +55 -9
  20. package/commands/transfer/examples.md +106 -7
  21. package/docs/CLI.md +29 -21
  22. package/docs/CONSTITUTION.md +41 -16
  23. package/docs/MATRIX.md +38 -1
  24. package/docs/errors/README.md +3 -1
  25. package/docs/errors/cdb/create-failed.md +20 -0
  26. package/docs/errors/cdb/file-exists.md +23 -0
  27. package/docs/errors/input/invalid-query.md +36 -0
  28. package/docs/errors/input/invalid-target.md +25 -3
  29. package/docs/errors/input/local-destination.md +20 -0
  30. package/docs/errors/input/unsupported-query.md +19 -0
  31. package/docs/errors/internal/byte-string.md +13 -0
  32. package/docs/errors/routeros/invalid-value.md +3 -2
  33. package/docs/errors/settings/invalid-timeout.md +6 -0
  34. package/docs/errors/transport/cancel-unacknowledged.md +24 -0
  35. package/docs/errors/transport/stream-overflow.md +21 -0
  36. package/docs/errors/usage/removed-flag.md +20 -0
  37. package/docs/errors/validation/menu-unavailable.md +22 -0
  38. package/docs/errors/validation/not-followable.md +23 -0
  39. package/docs/errors/validation/package-missing.md +29 -0
  40. package/docs/errors/validation/unknown-path.md +16 -4
  41. package/docs/errors/wait/deadline-exceeded.md +14 -0
  42. package/docs/errors/wait/interrupted.md +10 -0
  43. package/package.json +14 -11
  44. package/src/absent-menu.ts +280 -0
  45. package/src/api-fanout.ts +22 -12
  46. package/src/api.ts +603 -194
  47. package/src/btest.ts +19 -17
  48. package/src/cli/api.ts +126 -34
  49. package/src/cli/devices.ts +16 -3
  50. package/src/cli/execute.ts +18 -5
  51. package/src/cli/retrieve.ts +216 -22
  52. package/src/cli/terminal.ts +10 -5
  53. package/src/cli/transfer.ts +23 -8
  54. package/src/core/envelope.ts +14 -0
  55. package/src/core/error-catalog.ts +70 -1
  56. package/src/core/execute-command.ts +313 -6
  57. package/src/core/inspect.ts +65 -27
  58. package/src/core/query.ts +508 -0
  59. package/src/core/routeros-errors.ts +67 -1
  60. package/src/data/winbox-cdb-write.ts +22 -8
  61. package/src/devices.ts +148 -37
  62. package/src/discover.ts +7 -8
  63. package/src/errors.ts +116 -7
  64. package/src/execute-fanout.ts +14 -9
  65. package/src/execute.ts +128 -116
  66. package/src/explain/catalog.ts +64 -16
  67. package/src/explain/menus.ts +27 -6
  68. package/src/explain.ts +6 -6
  69. package/src/index.ts +31 -1
  70. package/src/mcp/tools.ts +10 -15
  71. package/src/protocols/adapter.ts +235 -118
  72. package/src/protocols/btest-session.ts +3 -2
  73. package/src/protocols/endpoint.ts +10 -0
  74. package/src/protocols/mac-telnet-console.ts +12 -7
  75. package/src/protocols/native-api.ts +252 -73
  76. package/src/protocols/sftp.ts +44 -14
  77. package/src/protocols/ssh.ts +42 -14
  78. package/src/resolver/cdb.ts +28 -7
  79. package/src/resolver/index.ts +1 -0
  80. package/src/resolver/target.ts +84 -18
  81. package/src/retrieve-fanout.ts +51 -12
  82. package/src/retrieve-follow.ts +862 -0
  83. package/src/retrieve-sample.ts +244 -0
  84. package/src/retrieve-stream.ts +115 -0
  85. package/src/retrieve-wait.ts +286 -0
  86. package/src/retrieve.ts +741 -109
  87. package/src/settings.ts +15 -15
  88. package/src/transfer-fanout.ts +40 -15
  89. package/src/transfer.ts +234 -32
@@ -1,8 +1,10 @@
1
1
  # retrieve
2
2
 
3
3
  Status: `CHR-passed` over `rest-api` and `native-api`, including multi-target
4
- fan-out (see **Target selection**). `snmp` is `not-started`. Matches
5
- `docs/MATRIX.md`.
4
+ fan-out (see **Target selection**), `--query` row filters (see **Query**),
5
+ `--follow` over `native-api` (see **Follow**) and `--sample` over both (see
6
+ **Sample**) and bounded readiness/conditions (see **Wait**). `snmp` is
7
+ `not-started`. Matches `docs/MATRIX.md`.
6
8
 
7
9
  Read RouterOS state. RouterOS menu reads model `<path>/<verb>` where the verb
8
10
  is `print`-style (`print`, `get`, and async POST-shaped reads as they're
@@ -50,6 +52,8 @@ duplicate that table. Behavior notes the generated reference cannot carry:
50
52
  - `--format json` (shortcut `--json`): set `CENTRS_FORMAT=json` to make the
51
53
  structured envelope the default. `--format yaml` renders the same envelope.
52
54
  - `--port`: native-api defaults to 8728; `--port 8729` selects TLS `api-ssl`.
55
+ A port typed into the target (`host:port`) is honored too, below the port
56
+ settings ladder (`--port`, `CENTRS_PORT`, a CDB `port=` comment, config).
53
57
  - `--query`/`--filter` are RouterOS **row** filters and always return an
54
58
  array (see Synopsis); `--where` filters *which devices* fan out, not rows.
55
59
  - `--all-attributes` conflicts with `--attribute(s)` (see Validation).
@@ -63,7 +67,7 @@ Spec-tier flags with no implementation yet — today they fail as unknown flags:
63
67
 
64
68
  | Flag | Designed behavior |
65
69
  | ----------------------------------- | ----------------------------------------------------------------------------------------- |
66
- | `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is `api … --stream`. See constitution: protocol selection. |
70
+ | `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is `--follow` (below), or `api … <menu>/listen` for the literal wire. See constitution: protocol selection. |
67
71
  | `--max-bytes <n>` | Byte budget for the rendered payload. If the response would exceed it, centrs truncates to fit, keeps `ok: true`, and adds a warning + `meta.truncated`. Not an error. (Will replace the implemented `--max-results`, which fails instead of truncating.) |
68
72
  | `--max-rows <n>` | Maximum row count for list reads. Excess rows are clipped; `ok: true` with a warning + `meta.truncated`. |
69
73
  | `--yaml` | Shortcut for `--format yaml`. |
@@ -77,6 +81,15 @@ Default validator: `/console/inspect request=syntax path=<comma-joined>,print`
77
81
  unknown attributes with `validation/*` codes that include the inspect-suggested
78
82
  alternatives in the error envelope.
79
83
 
84
+ `meta.validation.result` follows
85
+ [the constitution](../../docs/CONSTITUTION.md#validation-is-the-product). It is
86
+ `passed` once inspect accepts the read, even if the read fails afterward. It is
87
+ `failed` when the gate refuses: `validation/*`, or `--query` on a singleton.
88
+ It is absent when a `transport/*`, `auth/*`, `routeros/*` or `internal/*` error
89
+ stopped inspect from answering; the device stage then says `not reached`
90
+ (examples 12 and 16b; 16c is the `passed` case). With `--validate=false` it
91
+ is `{enabled: false, source: "disabled"}`.
92
+
80
93
  If `--all-attributes` is combined with `--attribute(s)`, fail with
81
94
  `usage/conflicting-flags` *before* hitting the network.
82
95
 
@@ -140,6 +153,334 @@ warning entry — it is not an error. (Earlier text said a byte overflow returns
140
153
  *error* with the needed size; superseded — truncation is an `ok: true` footnote so
141
154
  partial data stays usable.)
142
155
 
156
+ A failure **after** target resolution keeps what resolution found. The error
157
+ envelope carries the resolved `meta.target` (host, port, base URL, record,
158
+ sources), `meta.via`, the setting sources in `meta.settings`, and
159
+ `meta.operation.auth` (`username`, `passwordProvided`; provenance only, never
160
+ the secret). This holds for one-shot reads, `--follow`, `--sample`, `--wait`,
161
+ each fan-out member and the MCP `centrs_retrieve` tool. A failure before
162
+ resolution (bad shape, unknown target) keeps `meta.target.input` and a valid
163
+ pinned `via`, with empty `meta.settings`. The
164
+ library exposes this as `retrieveEnvelope()`, which never throws (examples 13,
165
+ 16b).
166
+
167
+ ## Query (`--query`)
168
+
169
+ `retrieve <router> <menu> --query '<expr>'` returns only the rows that match,
170
+ filtered on the router. `<expr>` is what you would write after `print where`:
171
+
172
+ ```bash
173
+ centrs retrieve $R /interface --query 'type=ether and !disabled'
174
+ centrs retrieve $R /ip/route --query '(dst-address="0.0.0.0/0" or distance>=10) and active'
175
+ ```
176
+
177
+ `--filter` is the same flag. Repeat either, and the expressions are AND-ed. A
178
+ list read with `--query` is always an array, `[]` when nothing matches. The
179
+ result works with `--attribute(s)`, `--sample`, `--follow` and fan-out.
180
+ A singleton (`/system/identity`) has no rows to filter and
181
+ fails `usage/conflicting-flags`.
182
+
183
+ **What it compiles to.** centrs parses the expression before connecting and
184
+ sends it as the API query (native `?` words, REST `.query`; one language,
185
+ `commands/api/AGENTS.md`). The router evaluates it, so comparisons use
186
+ RouterOS's own per-property typing:
187
+
188
+ | You write | RouterOS query words |
189
+ | --------- | -------------------- |
190
+ | `a=v`, `a!=v` | `a=v`; `a=v` `#!` |
191
+ | `a<v`, `a>v` | `<a=v` `-a` `#\|`; `>a=v` |
192
+ | `a<=v`, `a>=v` | `>a=v` `#!`; `<a=v` `-a` `#\|` `#!` |
193
+ | bare `a` | `a=yes` if `a` is a boolean, otherwise `a` (has a value) |
194
+ | `!x`, `x and y`, `x or y` | x `#!`; x y `#&`; x y `#\|` |
195
+
196
+ As in `print where`, a row without the property counts as smaller than any
197
+ value: `timeout<1d` includes static address-list entries (no `timeout`), and
198
+ `timeout>1h` does not. API `<`/`>` skip such rows on their own, hence the `-a`
199
+ ("has no `a`") word. `and` (`&&`) binds tighter than `or` (`||`), as in RouterOS. Values run to the
200
+ next space or parenthesis, and start right after the operator (RouterOS
201
+ rejects `comment= x`). Quote a value with spaces or an `=` (`comment="uplink a"`,
202
+ `comment="x=yes"`), using RouterOS escapes: `\"`, `\\`, `\$`, `\?`, `\_`, `\a`
203
+ `\b` `\f` `\n` `\r` `\t` `\v`, and two uppercase hex digits (`\41`; `\ff` is `\f`
204
+ then `f`). A backslash before whitespace drops it and the whole whitespace run
205
+ after it, so `"x\<newline> y"` is `xy`. A hex
206
+ byte above `\7F` is refused: centrs sends values as UTF-8, so it could not match
207
+ the raw byte RouterOS stores.
208
+
209
+ **A bare name.** `where disabled` means `disabled=yes`, but `where comment`
210
+ means "comment is set" (RouterOS stores an empty comment as absent). Only the
211
+ router knows which kind a property is, so centrs asks it: `/console/inspect`
212
+ completion after `where <name>=` offers exactly `yes`/`no` for a boolean. With
213
+ `--validate=false` there is no one to ask, and a bare name fails
214
+ `input/invalid-query`; write `disabled=yes`.
215
+
216
+ **Validation.** Every property in the expression is checked against the
217
+ menu's attributes (as for `--attributes`, plus `.id`). RouterOS answers a query
218
+ on a misspelled property with no rows and no error, so a typo fails
219
+ `validation/unknown-attribute` instead.
220
+
221
+ **Same rows as `print where`, with these differences.** On CHR 7.23.7 and
222
+ 7.24.5, 58 expressions over three menus return exactly the rows
223
+ `find where` selects (examples QY1, QY2). The exceptions come from the CLI
224
+ typing an unquoted literal, which the query does not do:
225
+
226
+ - `where dst-port=80` reads `80` as a number and matches nothing, and
227
+ `where protocol=tcp` likewise; the query matches the `"80"` and `"tcp"` that
228
+ `retrieve` shows. Quoted (`dst-port="80"`), the CLI agrees. This difference
229
+ is version-dependent: on CHR 7.25beta3, 7.25beta5, 7.25rc1 and 7.26beta1,
230
+ both unquoted CLI expressions also match. QY2 keeps exact expectations for
231
+ both behaviors; API query results stay the same over both transports.
232
+ - Ordering (`<`, `>`, `<=`, `>=`) on a property RouterOS returns as text
233
+ (address-list `address`, `dst-port`) is text order: `192.0.2.10` sorts
234
+ before `192.0.2.9`. Numbers (`mtu`, `vlan-id`, `distance`) compare as
235
+ numbers.
236
+ - The query accepts `disabled=true` as `yes`; the CLI rejects it.
237
+
238
+ **Refused, never approximated.** `~` (regex) and `in` (prefix membership) have
239
+ no query word, and a `$variable`, `[command]` or `{array}` value needs the
240
+ script evaluator: these fail `input/unsupported-query` with a tip. Four
241
+ spellings RouterOS reads differently from how they look fail
242
+ `input/invalid-query`: `!a=v` (RouterOS reads `(!a)=v`; write `!(a=v)` or
243
+ `a!=v`), `not` (not a RouterOS operator; use `!`), conditions separated
244
+ only by a space (join them with `and` or `or`), and an unquoted `=` inside a
245
+ value (RouterOS reads `comment=x=yes` as `(comment=x)=yes`, and `comment=x!=y`
246
+ matches nothing; quote it: `comment="x=yes"`).
247
+
248
+ ## Follow (`--follow`)
249
+
250
+ `retrieve <router> <menu> --follow` keeps a menu's rows current: first the
251
+ rows that exist now, then a line for every change, as NDJSON. It is the
252
+ friendly wrapper over native-API `listen`; `api <router> <menu>/listen` is the
253
+ literal wire (`commands/api/README.md`). Decisions and CHR evidence: #396.
254
+
255
+ **What starts it.** `--follow` on a list menu whose `print` takes
256
+ `follow-only` in `/console/inspect` (checked live). A menu without it, a
257
+ singleton included, fails `validation/not-followable` before anything is
258
+ followed. Native-api only: an unset `via` infers native-api, and a `via` of
259
+ `rest-api` from any source (flag, env, CDB, settings) fails
260
+ `transport/capability-unsupported`; there is no polling fallback. Single
261
+ target only: a fan-out selector fails `usage/fanout-not-supported`. Follow is
262
+ filtered with the same `--query`/`--filter` grammar as a one-shot read (#397).
263
+
264
+ **What each line means.** Every line is one envelope; read
265
+ `meta.operation.stream`:
266
+
267
+ | `kind` | When | `data` | Fields |
268
+ | ------ | ---- | ------ | ------ |
269
+ | `frame` | One row change | the row (`upsert`), `null` (`removed`) | `index`, `phase` (`snapshot`/`live`), `change` (`upsert`/`removed`), `id`, `source` (`print`/`listen`/`sweep`/`membership`) |
270
+ | `synced` | Once, after the snapshot | `null` | `rows`: rows held at that point |
271
+ | `notice` | First, only when there is advice | `null` | — (`tips` carries it) |
272
+ | `summary` | Last, exactly once | the counts | `stopReason`, `frames`, `snapshot`, `changes`, `sweeps`, `synced`, `durationMs` |
273
+
274
+ - **Bootstrap (A1).** centrs starts the `listen`, then the `print`, on one
275
+ connection. It emits the `print` rows, then every `listen` change received
276
+ since the `listen` started, all as `phase: "snapshot"`, then `synced`.
277
+ Applying the lines in order gives the menu's state at `synced`. That order
278
+ had 0 errors over about 14.7k writes on 7.23.7 and 7.24.5; merging by arrival
279
+ order resurrects deleted rows (#396 round 3). `synced` means the bootstrap
280
+ was processed. It is not an atomic snapshot, a full event history, or
281
+ routing convergence.
282
+ - **`upsert`** carries the latest full row; replace what you hold for `id`.
283
+ **`removed`** carries `data: null`; drop `id`. The row identity is always
284
+ `meta.operation.stream.id`, even when `--attributes` leaves `.id` out of
285
+ `data`.
286
+ - **Changes are coalesced state, not an event log.** RouterOS sends a change
287
+ about 200 ms after a write and folds writes in between. A row created and
288
+ deleted inside that window may never appear, and a flap may arrive as one
289
+ line or none. A `.dead` for an `id` centrs never reported is dropped, so a
290
+ removal the sweep saw first is reported once, as `source: "sweep"`.
291
+ - **Removals RouterOS does not send.** View menus (`/interface/<type>`,
292
+ `/ip/route`, `/ipv6/route`) never send `.dead`. An `.id`-only sweep
293
+ (`print .proplist=.id`) runs every `--sweep` (default `10s`) on every followed
294
+ menu and reports an `id` that is gone as `removed` with `source: "sweep"`.
295
+ An `id` that changed since that sweep was sent is never removed by it. A
296
+ failed sweep ends the follow with an error; it never infers a removal. A
297
+ sweep only checks membership: it does not repair a missed field update or
298
+ report rows `listen` never announced. `--sweep` needs a unit (`10s`,
299
+ `500ms`); a bare number fails `settings/invalid-timeout`, as for `--sample`.
300
+ `--sweep 0` turns it off, with a notice tip saying view-menu removals will
301
+ not be reported.
302
+ - **What `listen` never reports.** Hot counters (`rx-byte`, firewall
303
+ `bytes`/`packets`) and some protocol state (BFD sessions accept `listen` but
304
+ stay silent). The follow is correct but quiet there; `--sample` (below)
305
+ is the answer for those.
306
+ - **Filtered membership.** The snapshot and sweep carry the predicate; the
307
+ listen stays unfiltered, retaining `.id,.dead`. For changed ids centrs reads
308
+ the current matching rows from RouterOS, in batches of at most 256 ids with
309
+ one membership read in flight. No client type coercion is involved, and
310
+ predicate fields need not appear in the caller's projection. These frames
311
+ have `source: "membership"`: a matching row is upserted, and a held row that
312
+ no longer matches (or no longer exists) is removed. This removal is a change
313
+ in the selected set, not a claim that RouterOS sent `.dead`. A real `.dead`
314
+ still has `source: "listen"` and only removes a held id. Delayed membership
315
+ results are discarded for ids with newer wire changes; sweeps and membership
316
+ reads are serialized. Bootstrap fixes its changed-id/version cutoff at the
317
+ snapshot's wire completion and reconciles those ids before `synced`. A
318
+ reply superseded by a newer change is discarded; that newer version stays
319
+ queued for live reconciliation. Continuous writes cannot extend bootstrap
320
+ indefinitely or postpone the first sweep and live `--count`. This may
321
+ coalesce more changes than unfiltered A1 replay.
322
+ The filtered sweep also detects silent exits from a predicate (e.g. a
323
+ decreasing address-list `timeout`). It does not discover silent entries or
324
+ repair silent field updates: use `--sample` for such state. With `--sweep 0`,
325
+ silent exits remain held until a listen notification arrives.
326
+ - **Projection.** `--attribute(s)` projects `data`; centrs still asks RouterOS
327
+ for `.id,.dead` and removes them from `data` unless you named them.
328
+ `--all-attributes` sends `detail` to both `listen` and `print`.
329
+ `--list-attributes` and `--max-results` conflict with `--follow`
330
+ (`usage/conflicting-flags`).
331
+
332
+ **What ends it.** `--count N` stops after N `live` frames; snapshot frames and
333
+ `synced` never count, so `--count 1` is "the next observed change" (coalesced,
334
+ so not a guaranteed transition). `--duration <d>` is a wall-clock bound that
335
+ includes the bootstrap. Ctrl-C (`SIGINT`) stops it. Each ends with a
336
+ successful summary whose `stopReason` is `count-reached`, `duration-elapsed`,
337
+ or `interrupted`. A RouterOS error, a lost connection, a failed sweep or a full
338
+ buffer ends it with a failed summary (`ok: false`, `stopReason`
339
+ `routeros-error` or `transport-error`), whose counts are partial.
340
+ `routeros-error` means RouterOS answered with an error; `transport-error`
341
+ covers every other failure, and `error.code` names the cause. Buffers are
342
+ bounded: if more than 50,000 changes are waiting (a consumer that stopped
343
+ reading, or a burst during a long snapshot), the follow ends with
344
+ `transport/stream-overflow`. The state you hold is stale; follow again to get a
345
+ new snapshot. `--sweep` without `--follow`, and `--count` or `--duration`
346
+ without `--follow` or `--sample`, fail `usage/conflicting-flags`.
347
+
348
+ **Format.** Under `--follow`, `json`, `yaml` and `ndjson` all print one compact
349
+ envelope per line (as `api --stream` does); `text` prints one short row per
350
+ line. Every envelope, errors included, goes to stdout, and the exit code is
351
+ nonzero for any failure, even after rows. Without `--follow`,
352
+ `--format ndjson` prints the ordinary envelope as one compact line; it never
353
+ starts a follow.
354
+
355
+ Recipes (bounded, so an agent can run them unattended):
356
+
357
+ ```bash
358
+ # Watch: hold /ip/address for 30s, then stop.
359
+ centrs retrieve $R /ip/address --follow --duration 30s
360
+ # Wait for the next change, at most 60s.
361
+ centrs retrieve $R /ip/address --follow --count 1 --duration 60s
362
+ ```
363
+
364
+ ## Sample (`--sample`)
365
+
366
+ `retrieve <router> <menu> --sample <interval>` reads the menu again every
367
+ interval: one line per complete read, as NDJSON. It sees what `--follow`
368
+ cannot (hot counters such as `rx-byte`, and state `listen` never announces),
369
+ at the cost of missing whatever changes and changes back between two reads.
370
+
371
+ **What starts it.** `--sample` with an interval that has a unit (`5s`,
372
+ `500ms`, `1m`). A bare number fails `settings/invalid-timeout`: RouterOS reads
373
+ `interval=5` as seconds and centrs reads `5` as milliseconds, so centrs does
374
+ not guess. Any readable menu works, list or singleton. Each sample is the
375
+ ordinary one-shot read (`print` or `get`) with the same validation (once, before
376
+ the first read), projection and transport as `retrieve` without `--sample`:
377
+ `rest-api` by default, `native-api` when pinned. Single target only; a fan-out
378
+ selector fails `usage/fanout-not-supported`. `--follow` with `--sample` fails
379
+ `usage/conflicting-flags`.
380
+
381
+ Why reads and not RouterOS's own `print interval=`: on CHR 7.24.5 that feed has
382
+ no end-of-tick marker, so a tick is only known complete when the next tick (or
383
+ `!empty`) arrives, one whole interval later. A tick heavier than its interval
384
+ also stalls the connection. A one-shot read ends with its own `!done` or HTTP
385
+ response, so every sample is known complete when it arrives. The raw tick feed
386
+ stays available as `api <router> <menu>/print -X POST -f interval=1s --stream`
387
+ (`commands/api/README.md`).
388
+
389
+ **What each line means.** Every line is one envelope; read
390
+ `meta.operation.stream`:
391
+
392
+ | `kind` | When | `data` | Fields |
393
+ | ------ | ---- | ------ | ------ |
394
+ | `sample` | One complete read | the whole read: a row array (list), the record (singleton), or the bare value (one `--attribute` on a singleton) | `index` (1-based), `at` (when the read was sent, host clock, ISO 8601), `readMs` |
395
+ | `summary` | Last, exactly once | the counts | `stopReason`, `samples`, `durationMs` |
396
+
397
+ - **A sample is the whole state at that read.** A row missing from a sample
398
+ was gone when it was read; there is no separate removal line. A list read
399
+ with no rows is `data: []`, still a complete sample.
400
+ - **Cadence.** Reads start one interval apart, measured start to start. A
401
+ read slower than the interval delays the next read rather than overlapping
402
+ it, and missed slots are not made up in a burst. `at` gives the real spacing,
403
+ which is what a counter rate needs.
404
+ - **Not an event log.** A change shorter than the interval can fall between
405
+ two reads. Use `--follow` for object changes RouterOS announces.
406
+
407
+ **What ends it.** `--count N` stops after N samples. `--duration <d>` is a
408
+ wall-clock bound counted from the end of validation. Ctrl-C (`SIGINT`) stops
409
+ it. Each ends with a successful summary whose `stopReason` is `count-reached`,
410
+ `duration-elapsed` or `interrupted`. A read still in flight when the run stops
411
+ is not reported, because it is not complete. A failed read (a RouterOS error,
412
+ a lost connection, a timeout) ends the run with a failed summary (`ok: false`,
413
+ `stopReason` `routeros-error` or `transport-error`, as for `--follow`)
414
+ carrying the partial `samples` count; there is no retry. `--list-attributes`, `--max-results` and
415
+ `--sweep` conflict with `--sample` (`usage/conflicting-flags`).
416
+
417
+ **Format.** As for `--follow`: `json`, `yaml` and `ndjson` print one compact
418
+ envelope per line, and `text` prints `index`, `at` and the data on one line.
419
+ Every envelope, errors included, goes to stdout; the exit code is nonzero for
420
+ any failure, even after samples.
421
+
422
+ Recipes (bounded, so an agent can run them unattended):
423
+
424
+ ```bash
425
+ # Interface counters every 5s, three samples.
426
+ centrs retrieve $R /interface --attributes name,rx-byte,tx-byte --sample 5s --count 3
427
+ # CPU load once a second for a minute.
428
+ centrs retrieve $R /system/resource --attribute cpu-load --sample 1s --duration 1m
429
+ ```
430
+
431
+ ## Wait (`--wait`, `--until`, `--until-empty`)
432
+
433
+ `retrieve <router> <menu> --wait <deadline>` waits for a successful read.
434
+ It retries refused, closed, reset and timed-out connections, and a host that is
435
+ down or unreachable, so it can start while a router or CHR is rebooting or its
436
+ services are coming up. Authentication,
437
+ TLS, DNS, RouterOS and validation errors fail immediately. Identity/CDB/quickchr
438
+ resolution errors also fail immediately; `--quickchr` must name a running VM.
439
+ The caller's pinned protocol is preserved.
440
+
441
+ ```bash
442
+ centrs retrieve --quickchr sun /system/resource --wait 5m --json
443
+ centrs retrieve $R /routing/ospf/neighbor --wait 2m --until state=Full --json
444
+ centrs retrieve $R /routing/ospf/neighbor --wait 2m --until-empty --json
445
+ ```
446
+
447
+ - **Deadline and cadence.** `--wait` is the overall deadline, including
448
+ validation and retries, and needs a unit (`30s`, `5m`): a bare number is
449
+ refused, as for `--sample`. `--timeout` bounds each request. Reads begin one
450
+ second apart by default; `--sample 500ms` selects another cadence, measured
451
+ start to start without overlapping reads. With `--wait`, `--sample` yields
452
+ one final result rather than a stream. Slow reads delay the next one and
453
+ unfinished reads at the deadline are abandoned.
454
+ - **Any matching row.** `--until '<expr>'` uses the same grammar, property
455
+ validation and device typing as `--query`. Repeat it to AND conditions.
456
+ `--query` selects the rows to consider; `--until` terminates the wait when
457
+ any of those rows match. The final `data` contains those matching rows,
458
+ projected as requested; predicate properties need not appear in `data`.
459
+ Already true succeeds on the initial read. Zero rows never satisfy it.
460
+ Predicates apply to list menus; singleton readiness uses `--wait` alone.
461
+ - **Zero rows.** `--until-empty` succeeds only on a completed read with zero
462
+ rows selected by `--query`. Silence, transport failure, unfinished reads and
463
+ cancellation do not prove emptiness. It needs a list menu and conflicts
464
+ with `--until`.
465
+ - **One result.** After request resolution, every terminal envelope carries `meta.operation.wait` with
466
+ `stopReason`, `elapsedMs`, `attempts` and `observations`. Attempts include
467
+ failed validation/connection attempts; observations count completed data
468
+ reads. `ready` and `condition-met` are successful. On deadline expiry,
469
+ `deadline-elapsed` is failed: it preserves the last transient transport error,
470
+ or reports `wait/deadline-exceeded` when the last completed observation did
471
+ not satisfy the condition. Ctrl-C or an aborted library signal fails with
472
+ `wait/interrupted`, `stopReason: "interrupted"`. Other errors have
473
+ `stopReason: "failed"`. CLI exit is 0 on success and 1 on any failure;
474
+ the one final envelope, including failures, is written to stdout.
475
+
476
+ Single target only. `--until` and `--until-empty` require `--wait`.
477
+ `--follow`, `--count`, `--duration` and `--list-attributes` conflict with it.
478
+ This waiter uses completed reads over REST or native-api, including counters
479
+ and protocol state that listen never reports. For open-ended change feeds use
480
+ `--follow`; `--duration` finishing a watch does not establish a wait condition.
481
+ The library entry is `retrieveWait(request, env?, { signal? })`, returning a
482
+ success or failure envelope; `retrieve()` remains the one-shot entry.
483
+
143
484
  ## Target selection
144
485
 
145
486
  retrieve fans out over the **shared target-selection grammar** — multiple
@@ -155,7 +496,10 @@ stays the single-target envelope; a selector flag or a second positional target
155
496
  switches to fan-out. `--quickchr <name>` (the named-live-provider) behaves like a
156
497
  lone positional: one flag stays single-target, repeating it fans out, and mixing
157
498
  it with positionals or CDB selectors is `usage/conflicting-flags` (constitution:
158
- resolution providers).
499
+ resolution providers). The `--host` **flag** with no `<router>` positional
500
+ (`retrieve --host http://192.0.2.10:8080 /system/resource`) is a single target
501
+ too: no CDB lookup, so auth comes from flags, env or config. `CENTRS_HOST` never
502
+ stands in for `<router>`.
159
503
 
160
504
  retrieve-specific behavior on top of the shared engine:
161
505