@tikoci/centrs 0.1.7 → 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 (79) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +2 -2
  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 +123 -41
  8. package/commands/api/examples.md +205 -17
  9. package/commands/devices/README.md +11 -3
  10. package/commands/execute/README.md +31 -3
  11. package/commands/execute/examples.md +136 -1
  12. package/commands/explain/README.md +30 -4
  13. package/commands/retrieve/README.md +348 -4
  14. package/commands/retrieve/examples.md +422 -5
  15. package/commands/terminal/README.md +14 -4
  16. package/commands/terminal/examples.md +22 -3
  17. package/commands/transfer/README.md +55 -9
  18. package/commands/transfer/examples.md +106 -7
  19. package/docs/CLI.md +28 -20
  20. package/docs/CONSTITUTION.md +30 -10
  21. package/docs/MATRIX.md +38 -1
  22. package/docs/errors/README.md +3 -1
  23. package/docs/errors/input/invalid-query.md +36 -0
  24. package/docs/errors/input/local-destination.md +20 -0
  25. package/docs/errors/input/unsupported-query.md +19 -0
  26. package/docs/errors/internal/byte-string.md +13 -0
  27. package/docs/errors/settings/invalid-timeout.md +6 -0
  28. package/docs/errors/transport/stream-overflow.md +21 -0
  29. package/docs/errors/usage/removed-flag.md +20 -0
  30. package/docs/errors/validation/menu-unavailable.md +22 -0
  31. package/docs/errors/validation/not-followable.md +23 -0
  32. package/docs/errors/validation/package-missing.md +29 -0
  33. package/docs/errors/validation/unknown-path.md +16 -4
  34. package/docs/errors/wait/deadline-exceeded.md +14 -0
  35. package/docs/errors/wait/interrupted.md +10 -0
  36. package/package.json +14 -11
  37. package/src/absent-menu.ts +280 -0
  38. package/src/api-fanout.ts +22 -12
  39. package/src/api.ts +511 -186
  40. package/src/btest.ts +19 -17
  41. package/src/cli/api.ts +126 -34
  42. package/src/cli/execute.ts +18 -5
  43. package/src/cli/retrieve.ts +216 -22
  44. package/src/cli/terminal.ts +10 -5
  45. package/src/cli/transfer.ts +23 -8
  46. package/src/core/envelope.ts +14 -0
  47. package/src/core/error-catalog.ts +56 -1
  48. package/src/core/inspect.ts +43 -5
  49. package/src/core/query.ts +508 -0
  50. package/src/core/routeros-errors.ts +1 -1
  51. package/src/devices.ts +6 -9
  52. package/src/discover.ts +7 -8
  53. package/src/errors.ts +116 -7
  54. package/src/execute-fanout.ts +13 -8
  55. package/src/execute.ts +106 -65
  56. package/src/explain/catalog.ts +58 -10
  57. package/src/explain/menus.ts +23 -2
  58. package/src/explain.ts +6 -6
  59. package/src/index.ts +29 -1
  60. package/src/mcp/tools.ts +9 -14
  61. package/src/protocols/adapter.ts +224 -118
  62. package/src/protocols/btest-session.ts +3 -2
  63. package/src/protocols/endpoint.ts +10 -0
  64. package/src/protocols/mac-telnet-console.ts +6 -3
  65. package/src/protocols/native-api.ts +223 -70
  66. package/src/protocols/sftp.ts +44 -14
  67. package/src/protocols/ssh.ts +42 -14
  68. package/src/resolver/cdb.ts +28 -7
  69. package/src/resolver/index.ts +1 -0
  70. package/src/resolver/target.ts +53 -7
  71. package/src/retrieve-fanout.ts +51 -12
  72. package/src/retrieve-follow.ts +862 -0
  73. package/src/retrieve-sample.ts +244 -0
  74. package/src/retrieve-stream.ts +115 -0
  75. package/src/retrieve-wait.ts +286 -0
  76. package/src/retrieve.ts +727 -108
  77. package/src/settings.ts +15 -15
  78. package/src/transfer-fanout.ts +40 -15
  79. package/src/transfer.ts +234 -32
package/CHANGELOG.md CHANGED
@@ -6,6 +6,241 @@ The product evolves as a stream of changes; there are no alpha gates.
6
6
  Per-feature status is tracked in `docs/MATRIX.md`. Use this file for
7
7
  documenting cross-cutting shifts that affect contributors and consumers.
8
8
 
9
+ ## Unreleased
10
+
11
+ ## 0.1.8-next.0 — 2026-10-12
12
+
13
+ Pre-release (npm tag `next`; `latest` stays on 0.1.7). Install with
14
+ `bunx @tikoci/centrs@next`.
15
+
16
+ ### Maintenance
17
+
18
+ - Refresh vulnerable dependencies within existing ranges and use Bun's
19
+ Dependabot ecosystem for `bun.lock` (#390). Preserve the dependency group
20
+ and the `has-flag` major-version ignore. Remaining dependency advisories
21
+ and updater verification are tracked in #390.
22
+ - Pre-release dependency alignment (#390): runtime ranges now floor at the
23
+ tested versions (`@modelcontextprotocol/sdk ^1.32.1`, `zod ^4.6.7`),
24
+ `smol-toml` is overridden past its advisories, and `@types/bun` is pinned
25
+ exactly to the 1.3.14 runtime (re-pin tracked in #438). The two residual
26
+ advisories (`braces`, `katex`) are dev-tool-only and do not ship in the
27
+ package.
28
+
29
+ ### Added
30
+
31
+ - **`retrieve --wait` establishes readiness or a condition by a deadline (#364).**
32
+ One final envelope over REST or native API, with elapsed time, attempts and
33
+ completed observations. `--until` uses the shared query grammar and succeeds
34
+ on any matching selected row; `--until-empty` requires a completed zero-row
35
+ read. Already-true conditions succeed immediately. Only transient transport
36
+ errors retry; authentication and validation fail immediately. `--timeout`
37
+ bounds each request, and optional `--sample` sets observation cadence.
38
+ - **`retrieve --follow` keeps a menu's rows current over native API (#396).**
39
+ Snapshot rows, one `synced` line, then a line per change, as NDJSON; an
40
+ `.id` sweep (`--sweep`, default 10s) reports removals RouterOS never sends
41
+ on view menus. Single target; `--query` / `--filter` (#397) use device
42
+ membership reads and filtered sweeps, with membership removals distinguished
43
+ from wire deletions. `--sweep`
44
+ needs a unit (`10s`); only `0` (off) may be bare.
45
+ - **`retrieve --sample <interval>` re-reads a menu on a timer.** One line per
46
+ complete read (`data` is the whole read), then a summary; `--count` and
47
+ `--duration` bound it. Works over REST and native API, on list menus and
48
+ singletons, and sees counters `--follow` cannot. The interval needs a unit
49
+ (`5s`): a bare number is refused because RouterOS and centrs read it
50
+ differently.
51
+ - **`retrieve --query` filters rows on the router (#397).** Write what you
52
+ would after `print where` (`type=ether and !disabled`, `mtu>=1500`,
53
+ `(a=1 or b=2)`); centrs compiles it to the API query, so the router filters.
54
+ `--filter` is the same flag; repeats are AND-ed. Property names are
55
+ validated, and bare names are asked of the router (`disabled` → `=yes`,
56
+ `comment` → "is set"). `~`, `in` and script values fail
57
+ `input/unsupported-query`. Works with `--sample`, fan-out and `--follow`.
58
+ Checked on CHR against RouterOS's own `find where`.
59
+ - `--format ndjson` for `retrieve`. Library: `retrieveFollow()`,
60
+ `retrieveSample()`, and `renderRetrieveStreamLine()` for both (it replaces
61
+ the unreleased `renderRetrieveFollowLine()`).
62
+
63
+ ### Fixed
64
+
65
+ - `execute` honors an explicit target URL, and native-api honors a typed port
66
+ (#426, from #384). With no `via` pinned, an `http(s)://host[:port]` target
67
+ selects rest-api on that scheme and port instead of silently dialing native
68
+ API on 8728; `meta.settings.via` reports `via:target-url`. A port typed into
69
+ the target (`host:9`, `--host`, `CENTRS_HOST`) now reaches native-api on
70
+ every command, below the port settings (`--port`, `CENTRS_PORT`, `port=`,
71
+ config), as it already did for ssh and REST. An explicit `:80`/`:443`, which
72
+ URL parsing normalizes away, is kept for native-api and ssh. A matched CDB
73
+ record's own port is unchanged. A `--quickchr` target keeps native-api
74
+ whatever `CENTRS_HOST` holds.
75
+
76
+ - A pinned CDB no longer refuses an address it has no record for (#427, from
77
+ #384). With `--cdb-file`, `CENTRS_CDB_FILE`, `CENTRS_CDB_PASSWORD` or a
78
+ `centrs.env` CDB path, an IP, `[IPv6]:port`, `http(s)://` URL or MAC that
79
+ matches no record is dialed as a literal host, as it is with no CDB. This
80
+ holds on every command and in fan-out, where each such member also failed. A
81
+ DNS name or identity that matches nothing still fails closed with
82
+ `cdb/not-found-target`, and MCP still accepts only registered targets. An
83
+ IPv6 literal now dials over native-api, ssh, terminal and btest. Before, the
84
+ address was passed to them still in brackets (`[2001:db8::1]`) and failed to
85
+ resolve. SFTP, which needs the brackets, keeps them. A bare IPv6 target
86
+ (`2001:db8::1`, no port) is now accepted everywhere instead of
87
+ `input/invalid-target`. Error messages bracket an IPv6 endpoint
88
+ (`[2001:db8::1]:8728`).
89
+
90
+ - The `--host` flag can stand in for `<router>` (#428, from #384).
91
+ `execute --host http://192.0.2.10:8080 '<cmd>'`, `execute --host <host> --
92
+ <cmd>`, `retrieve --host <host> <path>`, `api --host <host> <endpoint>`,
93
+ `transfer --host <host> <verb>` and `terminal --host <host>` (ssh) now reach
94
+ the router instead of failing `input/invalid-command` for a missing target.
95
+ With no `<router>` there is no CDB lookup, so auth comes from flags, env or
96
+ config. `CENTRS_HOST` doesn't change how positionals are read. Two or more
97
+ `execute` positionals without `--` still split as `<target> <command>`, so
98
+ `execute <mac> <cmd> --via mac-telnet --host <udp-host>` is unchanged. The
99
+ `upload`/`download` aliases still read their first positional as the target.
100
+
101
+ - A one-shot `retrieve` that fails after resolution keeps the resolved meta
102
+ (#433). A refused connection, auth failure or validation error now reports
103
+ `meta.target` (host, port, base URL, record, sources), `meta.via`,
104
+ `meta.settings` and `meta.operation.auth`, as `execute`, `api` and `transfer`
105
+ already did. Before, it reported `meta.via: null` and only the typed input.
106
+ The MCP `centrs_retrieve` tool had the same gap and is fixed too. New
107
+ library export: `retrieveEnvelope()`, which never throws.
108
+
109
+ - Error envelopes no longer report validation `failed` when the validator
110
+ never answered or had passed (#435). A transport or auth failure (and, for
111
+ `retrieve`, a `routeros/*` error) that stops the gate now omits
112
+ `meta.validation.result` and the `syntax`/`semantic` summaries. Each stage
113
+ the gate did not reach reports `skipped` with
114
+ `reason: "not reached: <error.code>"`. `retrieve` now records the gate's
115
+ result, so a read that fails after inspect passed reports `passed`. Success
116
+ envelopes now carry `result: "passed"` too. `--validate=false` reports
117
+ `{enabled: false, source: "disabled"}` instead of `result: "failed"`. Only a
118
+ gate refusal reports `failed`. On an `execute` rejection, `syntax: true` now
119
+ means only a name-level refusal; a `:parse` refusal (syntax, unknown path,
120
+ missing package, unavailable menu) is `syntax: false` with no `semantic`.
121
+ Before, every device rejection said `syntax: true`. A failure in `api`'s
122
+ `--query` boolean probe after the gate passed now reports `passed`. Applies
123
+ to `execute`, `api` (including `--stream`) and every `retrieve` mode. `ValidationTrace` is now a public
124
+ type, and `retrieve()` accepts `{ trace }`. The shape is interim until the
125
+ live-explain review (#236).
126
+
127
+ - SSH execute, SFTP and terminal add `IdentitiesOnly=yes` when a private key is
128
+ selected, preventing fallback to unrelated SSH agent identities (#177).
129
+ Explicit OpenSSH `IdentityFile` configuration continues to apply.
130
+
131
+ - Query QA declares the RouterOS version split for unquoted firewall `where`
132
+ literals. Testing/development builds select rows that 7.24.5 does not;
133
+ both API transports retain exact result checks on either side of the split.
134
+
135
+ - Fan-out FAIL rows and btest client/server CSV error comments replace terminal
136
+ controls with spaces (#395, #415). Compact error text stays on one line,
137
+ including target labels and verbose remediation; JSON/YAML keep the raw values.
138
+
139
+ - **`api --stream` is now a literal stream of the wire (#402).** Following a
140
+ menu is the `<menu>/listen` endpoint (GET; implies `--stream` and
141
+ native-api); a GET with `--stream` streams one literal print. RouterOS's
142
+ `!empty` is no longer dropped: every reply is a frame, `reply: "re"` (a row)
143
+ or `"empty"` (`data: null`), and one received after a local stop carries
144
+ `afterStop: true`. `--count` counts rows only. Property lists and queries go out
145
+ as typed; `tip/filtered-follow` and `tip/follow-proplist` say what RouterOS
146
+ will not report, and request-shape tips lead the stream as a `notice`
147
+ envelope. `--format ndjson` prints every envelope as one compact line,
148
+ one-shot or streamed. **Breaking:** `--listen` and `ApiRequest.listen` are
149
+ removed (`usage/removed-flag`, which prints the replacement command with
150
+ values redacted); `apiListen()` is renamed `apiStream()`, and
151
+ `ProtocolAdapter.listen()` is `stream()`.
152
+
153
+ - **`transfer` moves `/file` contents as bytes on REST and native API
154
+ (#404).** centrs decoded and encoded file contents as UTF-8, so every
155
+ non-UTF-8 byte became U+FFFD (a 2,842-byte file downloaded as 3,422 bytes).
156
+ Library: `ProtocolExecuteRequest` and `NativeApiCommand` gain an opt-in
157
+ `binary` mode; a code unit above U+00FF is refused
158
+ (`internal/byte-string`). Every other command stays UTF-8.
159
+
160
+ - `transfer download` checks the local destination before reading the device
161
+ (#403). A missing parent directory, a destination that is a directory, or a
162
+ write that still fails reports the new `input/local-destination` (the OS
163
+ error rides as `cause`) instead of `internal/unhandled` after a full read.
164
+ Download replaces an existing local file, like `curl -o` and `scp` (#406);
165
+ `--force` guards device destinations only, and the docs now say so.
166
+
167
+ - A `transfer` failure after resolution keeps the resolved meta (#391):
168
+ `meta.target`, `meta.via`, `meta.settings` and `meta.operation.auth`, with
169
+ no secret values. Batch SSH (sftp `transfer`, `execute --via ssh`) never
170
+ sends a password, and the `auth/failed` remediation now says so and names
171
+ the key source; `error.context.sshKeySelected` is new. When a password was
172
+ resolved, `tip/sftp-password-not-used` and
173
+ `tip/transfer-password-transport` explain it and suggest `--via rest` or
174
+ `native-api`. New library export: `transferEnvelope()`, which never throws.
175
+
176
+ - `explain` reads the CLI Reference layout `manual.mikrotik.com` has served
177
+ since 2026-10-06, and its path catalog now includes RouterOS 7.26beta1
178
+ (#417). 47 paths are added (`/cmr/*`, `/system/dashboard`,
179
+ `/interface/wifi/dashboard`, `/tool/apptraffic`, and others), so commands
180
+ such as `/cmr/device/reboot` resolve instead of warning.
181
+
182
+ - **`api --query` uses the `print where` grammar too (#397).** A bare name is
183
+ now `=yes` for a boolean (was "has the property"; that is
184
+ `--raw-query name`), `<`/`<=` include rows without the property as `where`
185
+ does, and a value containing an operator stays one value (`comment=a>b` was
186
+ sent as `>comment=a=b`). With `--validate=false` or `--raw`, a bare name
187
+ fails `input/invalid-query`.
188
+
189
+ - Native API one-shot and incremental commands share their sentence builder;
190
+ structured POST queries/projections reach both transports. Mutation and script
191
+ requests reject unused query/projection flags, and PUT/POST reject a
192
+ trailing row id instead of dropping it. Stream outcomes are read from
193
+ `meta.operation.stream`; failed summaries retain cancellation warnings, and
194
+ received terminal attributes survive a local stop (#400).
195
+
196
+ - **Native `api --stream` runs commands instead of always appending `/listen`
197
+ (#399).** POST ping/monitor/print commands forward their attributes and emit
198
+ replies incrementally; terminal attributes are retained. (#402, above,
199
+ then made GET-menu streams literal.) Natural completion is `completed`; RouterOS/transport
200
+ failures produce one `ok:false` summary and a nonzero exit, even after rows.
201
+ **Breaking stream contract:** structured error envelopes now appear on stdout;
202
+ failed summary counts/reason live in metadata, with no `data` key. Raw errors
203
+ retain stderr behavior. Fatal sessions close locally even if the peer does not.
204
+
205
+ - **A menu the device does not have is no longer reported as a syntax error
206
+ (#361).** RouterOS's `:parse` answers `/zerotier/print` on a router without
207
+ the package with `syntax error (line 1 column 10)`, and `/zerotier print`
208
+ with `bad command name zerotier`. Both point at the segment it could not
209
+ resolve. centrs now locates that segment and looks the path up in its path
210
+ catalog (every RouterOS 7.10–7.25 build, all extra packages):
211
+ `validation/package-missing` when MikroTik publishes the menu behind a
212
+ package (`error.context.packages`, with `/system/package/print` as the fix),
213
+ `validation/menu-unavailable` when the gate is hardware, build or version
214
+ (`/interface/w60g` needs `60ghz`; `error.context.gates`), and
215
+ `validation/unknown-path` when no published build has it. The path can end
216
+ in a command (`/system/license/output` is `nochr`), and the summary says so.
217
+ A real syntax fault, and any rejection centrs cannot locate exactly (a
218
+ menu-scope block, a menu named on an earlier line, a segment written twice),
219
+ stays `validation/syntax`. Applies to every
220
+ transport, since all of them classify the same `:parse` return. **Breaking
221
+ for code that branches on `validation/syntax`** for these rejections: match
222
+ `validation/package-missing`, `validation/menu-unavailable` and
223
+ `validation/unknown-path` as well.
224
+ - **Every command now renders errors the same way, and keeps the router's own
225
+ words (#362).** `execute`, `retrieve`, `api`, `transfer`, `settings`, the
226
+ fan-outs and five others each had their own text renderer; most dropped the
227
+ `Details:` link and RouterOS's position, and the device's own verdict was
228
+ shown only with `--verbose`. All of them now use `formatCentrsErrorText`,
229
+ which prints, in order: `[code] summary`, `At:` (RouterOS's line/column, or
230
+ the offline gate's byte span with the bytes it covers), `Device said:`
231
+ (RouterOS's message, one line, omitted when the summary already quotes it),
232
+ `Fix:` and `Details:`. A test pins all fourteen renderers to the same text.
233
+ `api --stream --format text` prints the same fields on one line, joined by
234
+ ` — ` (`formatCentrsErrorLine`).
235
+ Two remediation messages stopped claiming a cause they can't know: a device-stage
236
+ `:parse` rejection no longer says "fix the quotes" (an absent menu or missing
237
+ package is rejected in the same words; #361 separates them) and points to
238
+ `centrs explain`, and the `routeros/api-trap` catch-all no longer says the
239
+ command is likely invalid. The `:parse` preflight wrapper in `execute` also
240
+ stopped dropping RouterOS's message from `context.detail`. `--json` output is
241
+ unchanged apart from those remediation strings and the restored
242
+ `context.detail`.
243
+
9
244
  ## 0.1.7 — 2026-09-27
10
245
 
11
246
  ### Added
package/GLOSSARY.txt CHANGED
@@ -69,6 +69,7 @@ Addrs
69
69
  ajv
70
70
  amazonq
71
71
  amm
72
+ ~askpass # OpenSSH SSH_ASKPASS helper; the CHR password-login control in ssh-auth-policy.test.ts
72
73
  AT-SPI
73
74
  awk
74
75
  axuielement
@@ -148,6 +149,8 @@ nullglob # bash shopt that makes an unmatched glob expand to zero words instead
148
149
  npk # MikroTik package format
149
150
  nvme-over-tcp # supported by RouterOS **with** rose-storage.npk extra-package
150
151
  OID
152
+ # %OSPF: Open Shortest Path First; RouterOS dynamic routing adjacency protocol.
153
+ OSPF
151
154
  PII # Personally Identifiable Information
152
155
  PON # passive optical network; RouterOS /interface/ethernet/pon menu
153
156
  POV
@@ -309,6 +312,7 @@ ENOTSUP
309
312
  EAFNOSUPPORT
310
313
  ECONNREFUSED
311
314
  ECONNRESET
315
+ EHOSTDOWN
312
316
  REUSEPORT
313
317
  REUSEADDR
314
318
  # L2 bridge frame-parsing terms (test/integration/mndp-l2-bridge.ts):
@@ -339,6 +343,15 @@ creds
339
343
  Streamable
340
344
  # modelcontextprotocol: npm scope of the MCP SDK (@modelcontextprotocol/sdk).
341
345
  modelcontextprotocol
346
+ # nextid: RouterOS `.nextid` on a listen frame after a move (the next row's id; *FFFFFFFF = last).
347
+ nextid
348
+ # WHATWG: the web standards body whose Encoding spec makes TextDecoder("latin1") windows-1252, not a byte decoder (#404).
349
+ WHATWG
350
+ # followable: a RouterOS menu `retrieve --follow` can follow — a list menu whose `print` takes `follow-only` in `/console/inspect` (`validation/not-followable` otherwise; #396).
351
+ followable
352
+ # BFD: Bidirectional Forwarding Detection (`/routing/bfd`); its sessions accept `listen` but never send changes (#396 round 2).
353
+ BFD
354
+ jsc # Bun's `bun:jsc` module (JavaScriptCore heap stats), used by the `--sample` memory regression test
342
355
  # WIP: work in progress (used for the upstream MikroTik Docusaurus docs site).
343
356
  WIP
344
357
  # adhoc: future inline host+credential targets; Phase 1 MCP uses CDB-only targets.
@@ -468,6 +481,12 @@ toarray
468
481
  # markdown dictionary knows it but the TypeScript one does not.
469
482
  iff
470
483
 
484
+ # Dependency vocabulary (package.json overrides)
485
+ # smol: smol-toml, a markdownlint-cli2/cspell transitive pinned past an advisory.
486
+ # katex: a markdownlint-cli2 transitive with a residual dev-only advisory.
487
+ smol
488
+ katex
489
+
471
490
  # GitHub Pages build vocabulary (scripts/build-pages-site.ts, pages.yaml)
472
491
  # typedoc/pandoc: the API-reference generator and the markdown->HTML converter
473
492
  # the Pages build chains together. gfm: GitHub-Flavored Markdown, pandoc's
@@ -757,3 +776,16 @@ dgram # Node's `node:dgram` UDP socket module — reached by `mac-telnet`, and o
757
776
  elsy # the misspelled `else` in `:if (1=1) do={…} elsy {…}` — rejected as `expected end of command` on CHR 7.21.5/7.23.5/7.24.2/7.25beta3, which is what proves the bare-brace `else {` acceptance is a real device fact and not a probe artifact (#347)
758
777
  prnt # a misspelt `print` verb — `/ip/address/prnt` is the offline analyzer's ABSTENTION fixture (`verdict: warn`, not `fail`), which is what proves the two-stage validation gate passes an abstention through to the device rather than refusing it (#354)
759
778
  zzznotamenu # a menu name no RouterOS build has — `/ip { zzznotamenu { print } }` is a `syntax error` on device, so menu-scope resolution is schema-dependent and a catalog MISS must abstain rather than accept (#348)
779
+
780
+ # #361 absent-menu controls. Deliberate NON-words: each names a path no RouterOS
781
+ # build has, or quotes a published gate verbatim, so the spelling is load-bearing.
782
+ nosuch # a top-level menu no RouterOS build has — `/nosuch` is `bad command name nosuch` on CHR 7.23.7, the `validation/unknown-path` control (#361)
783
+ nosuchmenu # a submenu no RouterOS build has — `/ip/nosuchmenu/print` is `syntax error (line 1 column 15)` on CHR 7.23.7, pointing at the `/` after it (#361)
784
+ nosuchverb # a command no RouterOS build has under a real menu — `/ip/address/nosuchverb` is `bad command name nosuchverb` on CHR 7.23.7 (#361)
785
+ qqq # the second segment of the parse-oracle row `/zzz qqq print`, captured on CHR 7.23.3 (GH#230)
786
+ userman # MikroTik's published package name `userman-5` for `/user-manager`; the installable package is `user-manager` (#361)
787
+ nochr # a CLI-Reference syscap that excludes CHR — `/system/license/output` is published `nochr` (#361)
788
+
789
+ # #417 CLI Reference refresh (7.26beta1).
790
+ cmr # MikroTik's optional `cmr` controller package and the `/cmr` menu tree it adds (7.26beta1); the catalog carries `package="cmr"` as its published gate, e.g. `/cmr/device/reboot` (#417)
791
+ apptraffic # two paths new in 7.26beta1 with different published gates: the `/tool/apptraffic` settings menu (conditions `!mmips, !powerpc, !smips`) and the `/cmr/device/apptraffic` command (package `cmr`, no conditions) (#417)
package/README.md CHANGED
@@ -58,9 +58,9 @@ product; without them this would just be a worse `curl`.
58
58
 
59
59
  | Command | Purpose |
60
60
  | ---------- | ------- |
61
- | `retrieve` | Read RouterOS state over REST/native API and SNMP OID/MIB values. |
61
+ | `retrieve` | Read RouterOS state over REST/native API and SNMP OID/MIB values. `--follow` keeps a menu's rows current over native API (snapshot, then a line per change; NDJSON); `--sample <interval>` re-reads it on a timer for counters; `--query` filters on the router with the `print where` grammar; `--wait` retries until a read succeeds (or an optional `--until` condition holds), up to a deadline. |
62
62
  | `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API/REST/L2 surfaces. |
63
- | `api` | Structured RouterOS API passthrough (gh-api style): one command per operation, structured in/out, can write, over REST or native API. Open-ended follow is `api … --stream` (native API; NDJSON). |
63
+ | `api` | Structured RouterOS API passthrough (gh-api style): one command per operation, structured in/out, can write, over REST or native API. Follow changes with `api … <menu>/listen`; `--stream` streams any command's replies (native API; NDJSON). |
64
64
  | `terminal` | Open an interactive console, primarily SSH or MAC-Telnet. |
65
65
  | `check` | Probe reachability and management protocol availability. **Designed, not implemented** — see `docs/MATRIX.md`. |
66
66
  | `explain` | Analyze a RouterOS command before running it: canonical form, syntax diagnostics, and what is valid at a path. Offline analysis runs today; the live-device probes are designed. |
package/SECURITY.md CHANGED
@@ -17,7 +17,7 @@ The repository's [Security tab](https://github.com/tikoci/centrs/security) is th
17
17
  - **CodeQL** — repo-managed workflow at [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml) with config [`.github/codeql-config.yml`](.github/codeql-config.yml). Query suite: `security-and-quality` (security-extended + code-quality) plus a small custom pack, [`.github/codeql-house-rules/`](.github/codeql-house-rules/), that structurally checks a handful of repo-specific invariants (currently: `apiFanout` must call its named protocol/validation gates) — see `.github/instructions/github-security-quality.instructions.md` for scope and the verify-before-promote discipline for adding more rules there. Languages: `javascript-typescript`, `actions`. Schedule: push to `main`, pull requests to `main`, weekly cron, and manual dispatch.
18
18
  - **Code Quality (AI findings, preview)** — confirmed active in GitHub as of 2026-07-03 (not just "intended"; see [issue #166](https://github.com/tikoci/centrs/issues/166)). [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml) includes a non-failing forward-compat probe for a future AI-findings API. AI findings are noisy and self-contradicting; we accept the noise because the second-opinion catches real issues that the static suite misses. Steady-state goal is 0 open findings. False positives are dismissed via the GitHub UI with a written justification — that text is the audit-log contract. Side effect: GitHub's own Code Quality scan runs in parallel with our Advanced Setup and posts a same-named `Analyze (javascript-typescript)` check, so PR checks lists show it twice — cosmetic, tracked in #166, not a required check either way (see issue #118).
19
19
  - **Dependency review** — not enabled. GitHub dependency review follows dependency-graph ecosystem support, and this Bun-only repository currently does not have a supported lockfile for that gate.
20
- - **Dependabot security updates** — enabled and configured in [`.github/dependabot.yaml`](.github/dependabot.yaml).
20
+ - **Dependabot security updates** — unsupported for Bun in GitHub's [ecosystem support table](https://docs.github.com/en/code-security/reference/supply-chain-security/supported-ecosystems-and-repositories). [`.github/dependabot.yaml`](.github/dependabot.yaml) configures weekly Bun **version updates** and GitHub Actions updates. Dependency security cleanup also requires `bun audit` and reviewed lockfile upgrades.
21
21
  - **Secret scanning** — enabled in GitHub, with push protection.
22
22
  - **Private vulnerability reporting** — enabled via GitHub Security Advisories.
23
23
 
@@ -29,7 +29,8 @@ dependencies within the selected capability; they do not replace its status.
29
29
  Commands are centrs **verbs** (`retrieve`, `execute`, `api`, `devices`,
30
30
  `discover`, `check`, `explain`, `terminal`, `settings`), never one tool per
31
31
  RouterOS command. Open-ended follow
32
- is folded into `api --stream` (no separate `stream` verb). Sub-verbs and their
32
+ is a flag, not a verb: `retrieve --follow` keeps state, `api --stream` is the
33
+ literal wire (no separate `stream` verb). Sub-verbs and their
33
34
  aliases are canonical across CLI / API / MCP:
34
35
 
35
36
  | Canonical | Aliases | Meaning |
@@ -29,18 +29,13 @@ inspect-client TS patterns — `tikoci/lsp-routeros-ts`
29
29
  - **HTTP method map (REST):** `GET`→print, `PUT`→add (**create**), `PATCH`→set,
30
30
  `DELETE`→remove, `POST`→universal (run any console command). PUT — not POST — is
31
31
  RouterOS's create.
32
- - **Multi-frame results: bounded → array; open-ended → native `/listen` only.**
33
- *Nuance (do not over-read the REST docs).* RouterOS REST **does** run
34
- `duration=` / `monitor` commands — it regularizes the native `!re` frames into a
35
- JSON **array of `.section` records** and returns the whole array in one response,
36
- bounded by the **60 s** REST cap (a command asked to run longer terminates early
37
- with an error). Native has **no** such cap. So a `duration=`/`monitor` command is
38
- an **ordinary bounded `api` call** that returns a `.section` array on both
39
- transports (native's `talk()` accumulates the `!re` until `!done`) — **not**
40
- NDJSON. The only thing that needs the NDJSON streaming path is **open-ended
41
- follow** (native `/listen`, which never sends `!done`); REST genuinely cannot do
42
- open-ended follow. ⇒ `--listen` (open-ended) is native-only; `duration=` commands
43
- are normal one-shot calls. (CONFIRMED below.)
32
+ - **Multi-frame results: one-shot → array; incremental replies → native API.**
33
+ REST regularizes bounded native `!re` replies into a JSON array and retains
34
+ its 60 s cap. A one-shot native call also accumulates rows until `!done`.
35
+ GH#399 adds incremental native command replies with `--stream`, including
36
+ bounded commands and open-ended ping/monitor/print interval. Method semantics,
37
+ terminal outcomes and subscription limitations live in the README;
38
+ examples L6–L16 ground the extended path.
44
39
  - **`/listen` semantics.** Emits `!re` on change; a deleted/disappeared item's
45
40
  `!re` carries the dead flag (docs say `=.dead=yes`; **CHR 7.23.1 sends
46
41
  `.dead=true`** — see CONFIRMED below); it never self-terminates. Cancel with
@@ -68,15 +63,18 @@ inspect-client TS patterns — `tikoci/lsp-routeros-ts`
68
63
 
69
64
  Two layers, both grounded above:
70
65
 
71
- - **`--query` / `--filter`** — structured convenience, **AND-combined** (RouterOS
72
- default), repeatable. Each maps to one query word:
73
- - `name=value` → `name=value` (eq)
74
- - `name!=value` → two words `name=value`, `#!` (eq then NOT-top)
75
- - `name>value` / `name<value` → `>name=value` / `<name=value`
76
- - `name` (no `=`) → `name` (has-property)
77
- Multiple `--query` emit their words in order; RouterOS's implicit AND applies (no
78
- `#&` injected). `--query` cannot express OR or arbitrary stacks — that is
79
- `--raw-query`.
66
+ - **`--query` / `--filter`** — the `print where` grammar, shared with
67
+ `retrieve --query` (`src/core/query.ts`, #397), repeatable and AND-ed.
68
+ `and`/`or`/`!`/parentheses compile to `#&`/`#|`/`#!`; `a!=v` → `a=v` `#!`;
69
+ `a<v` → `<a=v` `-a` `#|` and `a<=v` → `>a=v` `#!` (`where` counts a row
70
+ without `a` as smaller than any value; API `<`/`>` skip it); a bare name → `a=yes` for a boolean, otherwise
71
+ the has-property word `a`, decided by asking the device (completion after
72
+ `where a=` offers exactly `yes`/`no` for a boolean). The table, refusals and
73
+ CHR differential (`find where` vs `.query`, 58 expressions, 7.23.7 + 7.24.5)
74
+ are in `commands/retrieve/README.md` → Query. Until #397 the structured form
75
+ was a per-word splitter where a bare name meant has-property, `<` skipped
76
+ rows without the property, and an operator anywhere in the string split it
77
+ (`comment=a>b` → `>comment=a=b`).
80
78
  - **`--raw-query <word>`** — power-user escape hatch, repeatable. Each value is a
81
79
  **verbatim RouterOS query word** (absence `-name`, OR `#|`, AND `#&`, NOT `#!`,
82
80
  or any `>`/`<`/`=` form). Emitted as-is: a `.query` element over REST, a
@@ -203,8 +201,9 @@ green on CHR 7.23.1.
203
201
  ## CONFIRMED ON CHR (Phase 4 `--stream` follow — CHR 7.23.1, 2026-06-30)
204
202
 
205
203
  Validated by `test/integration/api-listen.test.ts` (L1–L4) on CHR 7.23.1.
206
- `--stream` is the primary flag; `--listen` is an alias; a `/listen` endpoint
207
- infers both. Native-api only (REST's 60s cap → `transport/capability-unsupported`).
204
+ Since #402 the `/listen` endpoint is the only way to listen (it implies
205
+ `--stream`); `--listen` is removed. Native-api only (REST's 60s cap →
206
+ `transport/capability-unsupported`).
208
207
 
209
208
  - **CONFIRMED — `NativeApiSession.listen()` + `/cancel` end-to-end.** Opening
210
209
  `/ip/address/listen` (own `.tag`) and adding an address over REST yields an
@@ -212,13 +211,39 @@ infers both. Native-api only (REST's 60s cap → `transport/capability-unsupport
212
211
  .tag=<n>` and the listen closes via the `interrupted` trap (category 2) then
213
212
  `!done` — the generator ends **without** throwing (the interrupted trap is the
214
213
  normal cancel, not an error). A non-interrupted trap or transport closure does
215
- throw. Re-confirms the Phase-0 spike through the real `apiListen()` path.
214
+ throw. Re-confirms the Phase-0 spike through the real `apiListen()` path (now
215
+ `apiStream()`).
216
216
  - **CONFIRMED — deletion frames carry `.dead=true`.** Removing a pre-seeded
217
217
  address over REST while listening emits a minimal `{ ".id", ".dead":"true" }`
218
218
  `!re` (re-confirms `.dead=true`, not the docs' `=.dead=yes`).
219
- - **Stream shape:** each `!re` → one NDJSON envelope frame
220
- (`meta.operation.stream.kind="frame"`, string values, `.dead` preserved); the
221
- stream ends with a summary envelope (`stream.kind="summary"`,
222
- `data.stopReason` ∈ `count-reached`/`duration-elapsed`/`interrupted`/
223
- `transport-error`, plus `frames`/`durationMs`). The CLI exit code keys on
224
- whether the stream *started* cleanly, not on per-frame `ok`.
219
+ - **Stream shape:** each `!re` → one envelope frame
220
+ (`meta.operation.stream.kind="frame"`, string values, `.dead` preserved).
221
+ GH#399 supersedes the original terminal/exit behavior: natural completion is
222
+ distinct from cancellation; a failure summary has `ok:false`, and any failure
223
+ exits nonzero. The current contract lives in the README.
224
+
225
+ - **CONFIRMED — addressed-listen deletes (L15), CHR 7.23.7:** `test/integration/api-listen.test.ts`
226
+ covers `/ip/address/listen ?.id=<id>` both without projection and with
227
+ `.proplist=address,.id,.dead`. Removing the seeded address must produce
228
+ `{ ".id": <id>, ".dead": "true" }`. The ID query matches a deletion frame
229
+ because it carries `.id`; a predicate on another column may hide that frame.
230
+
231
+ ## CONFIRMED ON CHR (#402 literal `--stream` — 7.23.7 + 7.24.5, 2026-10-07)
232
+
233
+ `api` stopped adding `.id,.dead` to a listen's `.proplist` and stopped
234
+ rejecting filtered listens, so these are now the device's own answers, pinned
235
+ by `test/integration/api-listen.test.ts` (L4, L9, L10, L15, L18):
236
+
237
+ - **`.proplist=address` strips `.id` and `.dead` from delete frames, even
238
+ under `?.id=`.** An addressed listen's delete arrives as an `!re` with no
239
+ attributes (`{}`); an unaddressed one has no `.dead` frame for the row.
240
+ The `?.id=` exception only holds when the projection names `.id,.dead`
241
+ (or there is none). This corrects the 2026-10-06 note that it held "with
242
+ and without `.proplist`": that run had centrs's forced `.id,.dead`.
243
+ - **A filtered listen (`?interface=ether1`) sends nothing for the delete of
244
+ a matching row.** The only reply in the window is the `!empty` that answers
245
+ the cancel.
246
+ - **Cancelling a listen that sent nothing** → `!trap` interrupted, `!empty`,
247
+ `!done`. centrs emits that `!empty` as a frame with `afterStop: true`.
248
+ - **`print interval=1` on an empty table** (`/ip/firewall/raw`) → one
249
+ `!empty` per tick.