@tikoci/centrs 0.1.7 → 0.1.8

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 (82) hide show
  1. package/CHANGELOG.md +256 -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/mcp/README.md +5 -1
  14. package/commands/mcp/examples.md +18 -1
  15. package/commands/retrieve/README.md +348 -4
  16. package/commands/retrieve/examples.md +422 -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 +28 -20
  22. package/docs/CONSTITUTION.md +30 -10
  23. package/docs/MATRIX.md +38 -1
  24. package/docs/errors/README.md +3 -1
  25. package/docs/errors/input/invalid-query.md +36 -0
  26. package/docs/errors/input/local-destination.md +20 -0
  27. package/docs/errors/input/unsupported-query.md +19 -0
  28. package/docs/errors/internal/byte-string.md +13 -0
  29. package/docs/errors/settings/invalid-timeout.md +6 -0
  30. package/docs/errors/transport/stream-overflow.md +21 -0
  31. package/docs/errors/usage/removed-flag.md +20 -0
  32. package/docs/errors/validation/menu-unavailable.md +22 -0
  33. package/docs/errors/validation/not-followable.md +23 -0
  34. package/docs/errors/validation/package-missing.md +29 -0
  35. package/docs/errors/validation/unknown-path.md +16 -4
  36. package/docs/errors/wait/deadline-exceeded.md +14 -0
  37. package/docs/errors/wait/interrupted.md +10 -0
  38. package/package.json +14 -11
  39. package/src/absent-menu.ts +280 -0
  40. package/src/api-fanout.ts +22 -12
  41. package/src/api.ts +511 -186
  42. package/src/btest.ts +19 -17
  43. package/src/cli/api.ts +126 -34
  44. package/src/cli/execute.ts +18 -5
  45. package/src/cli/retrieve.ts +223 -22
  46. package/src/cli/terminal.ts +10 -5
  47. package/src/cli/transfer.ts +23 -8
  48. package/src/core/envelope.ts +14 -0
  49. package/src/core/error-catalog.ts +56 -1
  50. package/src/core/inspect.ts +43 -5
  51. package/src/core/query.ts +508 -0
  52. package/src/core/routeros-errors.ts +1 -1
  53. package/src/devices.ts +6 -9
  54. package/src/discover.ts +7 -8
  55. package/src/errors.ts +116 -7
  56. package/src/execute-fanout.ts +13 -8
  57. package/src/execute.ts +106 -65
  58. package/src/explain/catalog.ts +58 -10
  59. package/src/explain/menus.ts +23 -2
  60. package/src/explain.ts +6 -6
  61. package/src/index.ts +29 -1
  62. package/src/mcp/safety.ts +43 -18
  63. package/src/mcp/tools.ts +14 -17
  64. package/src/protocols/adapter.ts +224 -118
  65. package/src/protocols/btest-session.ts +3 -2
  66. package/src/protocols/endpoint.ts +10 -0
  67. package/src/protocols/mac-telnet-console.ts +6 -3
  68. package/src/protocols/native-api.ts +223 -70
  69. package/src/protocols/sftp.ts +44 -14
  70. package/src/protocols/ssh.ts +42 -14
  71. package/src/resolver/cdb.ts +28 -7
  72. package/src/resolver/index.ts +1 -0
  73. package/src/resolver/target.ts +53 -7
  74. package/src/retrieve-fanout.ts +51 -12
  75. package/src/retrieve-follow.ts +862 -0
  76. package/src/retrieve-sample.ts +244 -0
  77. package/src/retrieve-stream.ts +115 -0
  78. package/src/retrieve-wait.ts +286 -0
  79. package/src/retrieve.ts +732 -108
  80. package/src/settings.ts +15 -15
  81. package/src/transfer-fanout.ts +40 -15
  82. package/src/transfer.ts +234 -32
package/CHANGELOG.md CHANGED
@@ -6,6 +6,262 @@ 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 — 2026-10-12
12
+
13
+ Release (npm tag `latest`; `next` moves forward to 0.1.8). Everything in
14
+ 0.1.8-next.0 below, plus two fixes from field-testing it.
15
+
16
+ ### Fixed
17
+
18
+ - `retrieve --wait` with no deadline value names `--wait` (#445). Written as a
19
+ bare switch, `--wait --until 'expr'` read `--until` as the deadline and
20
+ shifted the predicate into a second target, so the CLI reported
21
+ `usage/fanout-not-supported`. It now fails `settings/invalid-timeout`
22
+ ("Received the flag: --until"), and every deadline without a unit gets the
23
+ `--wait`-specific message. A real multi-target `--wait` still gets the
24
+ fanout refusal.
25
+ - MCP accepts a registered device by its `identity=`, `mac=` or `ip=` comment
26
+ key, as the CLI does (#444). The allowlist gate now matches with the same
27
+ resolver the call dials with, so the `mcp=` write policy checked is the
28
+ policy of the record the call runs against. A name on more than one record
29
+ fails `identity/ambiguous` with the candidates in `context.matches` and an
30
+ MCP remediation, instead of resolving to the first.
31
+
32
+ ## 0.1.8-next.0 — 2026-10-12
33
+
34
+ Pre-release (npm tag `next`; `latest` stays on 0.1.7). Install with
35
+ `bunx @tikoci/centrs@next`.
36
+
37
+ ### Maintenance
38
+
39
+ - Refresh vulnerable dependencies within existing ranges and use Bun's
40
+ Dependabot ecosystem for `bun.lock` (#390). Preserve the dependency group
41
+ and the `has-flag` major-version ignore. Remaining dependency advisories
42
+ and updater verification are tracked in #390.
43
+ - Pre-release dependency alignment (#390): runtime ranges now floor at the
44
+ tested versions (`@modelcontextprotocol/sdk ^1.32.1`, `zod ^4.6.7`),
45
+ `smol-toml` is overridden past its advisories, and `@types/bun` is pinned
46
+ exactly to the 1.3.14 runtime (re-pin tracked in #438). The two residual
47
+ advisories (`braces`, `katex`) are dev-tool-only and do not ship in the
48
+ package.
49
+
50
+ ### Added
51
+
52
+ - **`retrieve --wait` establishes readiness or a condition by a deadline (#364).**
53
+ One final envelope over REST or native API, with elapsed time, attempts and
54
+ completed observations. `--until` uses the shared query grammar and succeeds
55
+ on any matching selected row; `--until-empty` requires a completed zero-row
56
+ read. Already-true conditions succeed immediately. Only transient transport
57
+ errors retry; authentication and validation fail immediately. `--timeout`
58
+ bounds each request, and optional `--sample` sets observation cadence.
59
+ - **`retrieve --follow` keeps a menu's rows current over native API (#396).**
60
+ Snapshot rows, one `synced` line, then a line per change, as NDJSON; an
61
+ `.id` sweep (`--sweep`, default 10s) reports removals RouterOS never sends
62
+ on view menus. Single target; `--query` / `--filter` (#397) use device
63
+ membership reads and filtered sweeps, with membership removals distinguished
64
+ from wire deletions. `--sweep`
65
+ needs a unit (`10s`); only `0` (off) may be bare.
66
+ - **`retrieve --sample <interval>` re-reads a menu on a timer.** One line per
67
+ complete read (`data` is the whole read), then a summary; `--count` and
68
+ `--duration` bound it. Works over REST and native API, on list menus and
69
+ singletons, and sees counters `--follow` cannot. The interval needs a unit
70
+ (`5s`): a bare number is refused because RouterOS and centrs read it
71
+ differently.
72
+ - **`retrieve --query` filters rows on the router (#397).** Write what you
73
+ would after `print where` (`type=ether and !disabled`, `mtu>=1500`,
74
+ `(a=1 or b=2)`); centrs compiles it to the API query, so the router filters.
75
+ `--filter` is the same flag; repeats are AND-ed. Property names are
76
+ validated, and bare names are asked of the router (`disabled` → `=yes`,
77
+ `comment` → "is set"). `~`, `in` and script values fail
78
+ `input/unsupported-query`. Works with `--sample`, fan-out and `--follow`.
79
+ Checked on CHR against RouterOS's own `find where`.
80
+ - `--format ndjson` for `retrieve`. Library: `retrieveFollow()`,
81
+ `retrieveSample()`, and `renderRetrieveStreamLine()` for both (it replaces
82
+ the unreleased `renderRetrieveFollowLine()`).
83
+
84
+ ### Fixed
85
+
86
+ - `execute` honors an explicit target URL, and native-api honors a typed port
87
+ (#426, from #384). With no `via` pinned, an `http(s)://host[:port]` target
88
+ selects rest-api on that scheme and port instead of silently dialing native
89
+ API on 8728; `meta.settings.via` reports `via:target-url`. A port typed into
90
+ the target (`host:9`, `--host`, `CENTRS_HOST`) now reaches native-api on
91
+ every command, below the port settings (`--port`, `CENTRS_PORT`, `port=`,
92
+ config), as it already did for ssh and REST. An explicit `:80`/`:443`, which
93
+ URL parsing normalizes away, is kept for native-api and ssh. A matched CDB
94
+ record's own port is unchanged. A `--quickchr` target keeps native-api
95
+ whatever `CENTRS_HOST` holds.
96
+
97
+ - A pinned CDB no longer refuses an address it has no record for (#427, from
98
+ #384). With `--cdb-file`, `CENTRS_CDB_FILE`, `CENTRS_CDB_PASSWORD` or a
99
+ `centrs.env` CDB path, an IP, `[IPv6]:port`, `http(s)://` URL or MAC that
100
+ matches no record is dialed as a literal host, as it is with no CDB. This
101
+ holds on every command and in fan-out, where each such member also failed. A
102
+ DNS name or identity that matches nothing still fails closed with
103
+ `cdb/not-found-target`, and MCP still accepts only registered targets. An
104
+ IPv6 literal now dials over native-api, ssh, terminal and btest. Before, the
105
+ address was passed to them still in brackets (`[2001:db8::1]`) and failed to
106
+ resolve. SFTP, which needs the brackets, keeps them. A bare IPv6 target
107
+ (`2001:db8::1`, no port) is now accepted everywhere instead of
108
+ `input/invalid-target`. Error messages bracket an IPv6 endpoint
109
+ (`[2001:db8::1]:8728`).
110
+
111
+ - The `--host` flag can stand in for `<router>` (#428, from #384).
112
+ `execute --host http://192.0.2.10:8080 '<cmd>'`, `execute --host <host> --
113
+ <cmd>`, `retrieve --host <host> <path>`, `api --host <host> <endpoint>`,
114
+ `transfer --host <host> <verb>` and `terminal --host <host>` (ssh) now reach
115
+ the router instead of failing `input/invalid-command` for a missing target.
116
+ With no `<router>` there is no CDB lookup, so auth comes from flags, env or
117
+ config. `CENTRS_HOST` doesn't change how positionals are read. Two or more
118
+ `execute` positionals without `--` still split as `<target> <command>`, so
119
+ `execute <mac> <cmd> --via mac-telnet --host <udp-host>` is unchanged. The
120
+ `upload`/`download` aliases still read their first positional as the target.
121
+
122
+ - A one-shot `retrieve` that fails after resolution keeps the resolved meta
123
+ (#433). A refused connection, auth failure or validation error now reports
124
+ `meta.target` (host, port, base URL, record, sources), `meta.via`,
125
+ `meta.settings` and `meta.operation.auth`, as `execute`, `api` and `transfer`
126
+ already did. Before, it reported `meta.via: null` and only the typed input.
127
+ The MCP `centrs_retrieve` tool had the same gap and is fixed too. New
128
+ library export: `retrieveEnvelope()`, which never throws.
129
+
130
+ - Error envelopes no longer report validation `failed` when the validator
131
+ never answered or had passed (#435). A transport or auth failure (and, for
132
+ `retrieve`, a `routeros/*` error) that stops the gate now omits
133
+ `meta.validation.result` and the `syntax`/`semantic` summaries. Each stage
134
+ the gate did not reach reports `skipped` with
135
+ `reason: "not reached: <error.code>"`. `retrieve` now records the gate's
136
+ result, so a read that fails after inspect passed reports `passed`. Success
137
+ envelopes now carry `result: "passed"` too. `--validate=false` reports
138
+ `{enabled: false, source: "disabled"}` instead of `result: "failed"`. Only a
139
+ gate refusal reports `failed`. On an `execute` rejection, `syntax: true` now
140
+ means only a name-level refusal; a `:parse` refusal (syntax, unknown path,
141
+ missing package, unavailable menu) is `syntax: false` with no `semantic`.
142
+ Before, every device rejection said `syntax: true`. A failure in `api`'s
143
+ `--query` boolean probe after the gate passed now reports `passed`. Applies
144
+ to `execute`, `api` (including `--stream`) and every `retrieve` mode. `ValidationTrace` is now a public
145
+ type, and `retrieve()` accepts `{ trace }`. The shape is interim until the
146
+ live-explain review (#236).
147
+
148
+ - SSH execute, SFTP and terminal add `IdentitiesOnly=yes` when a private key is
149
+ selected, preventing fallback to unrelated SSH agent identities (#177).
150
+ Explicit OpenSSH `IdentityFile` configuration continues to apply.
151
+
152
+ - Query QA declares the RouterOS version split for unquoted firewall `where`
153
+ literals. Testing/development builds select rows that 7.24.5 does not;
154
+ both API transports retain exact result checks on either side of the split.
155
+
156
+ - Fan-out FAIL rows and btest client/server CSV error comments replace terminal
157
+ controls with spaces (#395, #415). Compact error text stays on one line,
158
+ including target labels and verbose remediation; JSON/YAML keep the raw values.
159
+
160
+ - **`api --stream` is now a literal stream of the wire (#402).** Following a
161
+ menu is the `<menu>/listen` endpoint (GET; implies `--stream` and
162
+ native-api); a GET with `--stream` streams one literal print. RouterOS's
163
+ `!empty` is no longer dropped: every reply is a frame, `reply: "re"` (a row)
164
+ or `"empty"` (`data: null`), and one received after a local stop carries
165
+ `afterStop: true`. `--count` counts rows only. Property lists and queries go out
166
+ as typed; `tip/filtered-follow` and `tip/follow-proplist` say what RouterOS
167
+ will not report, and request-shape tips lead the stream as a `notice`
168
+ envelope. `--format ndjson` prints every envelope as one compact line,
169
+ one-shot or streamed. **Breaking:** `--listen` and `ApiRequest.listen` are
170
+ removed (`usage/removed-flag`, which prints the replacement command with
171
+ values redacted); `apiListen()` is renamed `apiStream()`, and
172
+ `ProtocolAdapter.listen()` is `stream()`.
173
+
174
+ - **`transfer` moves `/file` contents as bytes on REST and native API
175
+ (#404).** centrs decoded and encoded file contents as UTF-8, so every
176
+ non-UTF-8 byte became U+FFFD (a 2,842-byte file downloaded as 3,422 bytes).
177
+ Library: `ProtocolExecuteRequest` and `NativeApiCommand` gain an opt-in
178
+ `binary` mode; a code unit above U+00FF is refused
179
+ (`internal/byte-string`). Every other command stays UTF-8.
180
+
181
+ - `transfer download` checks the local destination before reading the device
182
+ (#403). A missing parent directory, a destination that is a directory, or a
183
+ write that still fails reports the new `input/local-destination` (the OS
184
+ error rides as `cause`) instead of `internal/unhandled` after a full read.
185
+ Download replaces an existing local file, like `curl -o` and `scp` (#406);
186
+ `--force` guards device destinations only, and the docs now say so.
187
+
188
+ - A `transfer` failure after resolution keeps the resolved meta (#391):
189
+ `meta.target`, `meta.via`, `meta.settings` and `meta.operation.auth`, with
190
+ no secret values. Batch SSH (sftp `transfer`, `execute --via ssh`) never
191
+ sends a password, and the `auth/failed` remediation now says so and names
192
+ the key source; `error.context.sshKeySelected` is new. When a password was
193
+ resolved, `tip/sftp-password-not-used` and
194
+ `tip/transfer-password-transport` explain it and suggest `--via rest` or
195
+ `native-api`. New library export: `transferEnvelope()`, which never throws.
196
+
197
+ - `explain` reads the CLI Reference layout `manual.mikrotik.com` has served
198
+ since 2026-10-06, and its path catalog now includes RouterOS 7.26beta1
199
+ (#417). 47 paths are added (`/cmr/*`, `/system/dashboard`,
200
+ `/interface/wifi/dashboard`, `/tool/apptraffic`, and others), so commands
201
+ such as `/cmr/device/reboot` resolve instead of warning.
202
+
203
+ - **`api --query` uses the `print where` grammar too (#397).** A bare name is
204
+ now `=yes` for a boolean (was "has the property"; that is
205
+ `--raw-query name`), `<`/`<=` include rows without the property as `where`
206
+ does, and a value containing an operator stays one value (`comment=a>b` was
207
+ sent as `>comment=a=b`). With `--validate=false` or `--raw`, a bare name
208
+ fails `input/invalid-query`.
209
+
210
+ - Native API one-shot and incremental commands share their sentence builder;
211
+ structured POST queries/projections reach both transports. Mutation and script
212
+ requests reject unused query/projection flags, and PUT/POST reject a
213
+ trailing row id instead of dropping it. Stream outcomes are read from
214
+ `meta.operation.stream`; failed summaries retain cancellation warnings, and
215
+ received terminal attributes survive a local stop (#400).
216
+
217
+ - **Native `api --stream` runs commands instead of always appending `/listen`
218
+ (#399).** POST ping/monitor/print commands forward their attributes and emit
219
+ replies incrementally; terminal attributes are retained. (#402, above,
220
+ then made GET-menu streams literal.) Natural completion is `completed`; RouterOS/transport
221
+ failures produce one `ok:false` summary and a nonzero exit, even after rows.
222
+ **Breaking stream contract:** structured error envelopes now appear on stdout;
223
+ failed summary counts/reason live in metadata, with no `data` key. Raw errors
224
+ retain stderr behavior. Fatal sessions close locally even if the peer does not.
225
+
226
+ - **A menu the device does not have is no longer reported as a syntax error
227
+ (#361).** RouterOS's `:parse` answers `/zerotier/print` on a router without
228
+ the package with `syntax error (line 1 column 10)`, and `/zerotier print`
229
+ with `bad command name zerotier`. Both point at the segment it could not
230
+ resolve. centrs now locates that segment and looks the path up in its path
231
+ catalog (every RouterOS 7.10–7.25 build, all extra packages):
232
+ `validation/package-missing` when MikroTik publishes the menu behind a
233
+ package (`error.context.packages`, with `/system/package/print` as the fix),
234
+ `validation/menu-unavailable` when the gate is hardware, build or version
235
+ (`/interface/w60g` needs `60ghz`; `error.context.gates`), and
236
+ `validation/unknown-path` when no published build has it. The path can end
237
+ in a command (`/system/license/output` is `nochr`), and the summary says so.
238
+ A real syntax fault, and any rejection centrs cannot locate exactly (a
239
+ menu-scope block, a menu named on an earlier line, a segment written twice),
240
+ stays `validation/syntax`. Applies to every
241
+ transport, since all of them classify the same `:parse` return. **Breaking
242
+ for code that branches on `validation/syntax`** for these rejections: match
243
+ `validation/package-missing`, `validation/menu-unavailable` and
244
+ `validation/unknown-path` as well.
245
+ - **Every command now renders errors the same way, and keeps the router's own
246
+ words (#362).** `execute`, `retrieve`, `api`, `transfer`, `settings`, the
247
+ fan-outs and five others each had their own text renderer; most dropped the
248
+ `Details:` link and RouterOS's position, and the device's own verdict was
249
+ shown only with `--verbose`. All of them now use `formatCentrsErrorText`,
250
+ which prints, in order: `[code] summary`, `At:` (RouterOS's line/column, or
251
+ the offline gate's byte span with the bytes it covers), `Device said:`
252
+ (RouterOS's message, one line, omitted when the summary already quotes it),
253
+ `Fix:` and `Details:`. A test pins all fourteen renderers to the same text.
254
+ `api --stream --format text` prints the same fields on one line, joined by
255
+ ` — ` (`formatCentrsErrorLine`).
256
+ Two remediation messages stopped claiming a cause they can't know: a device-stage
257
+ `:parse` rejection no longer says "fix the quotes" (an absent menu or missing
258
+ package is rejected in the same words; #361 separates them) and points to
259
+ `centrs explain`, and the `routeros/api-trap` catch-all no longer says the
260
+ command is likely invalid. The `:parse` preflight wrapper in `execute` also
261
+ stopped dropping RouterOS's message from `context.detail`. `--json` output is
262
+ unchanged apart from those remediation strings and the restored
263
+ `context.detail`.
264
+
9
265
  ## 0.1.7 — 2026-09-27
10
266
 
11
267
  ### 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.