@tikoci/rosetta 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -147
- package/package.json +1 -1
- package/src/browse.ts +116 -40
- package/src/db.ts +16 -0
- package/src/extract-devices.ts +5 -1
- package/src/extract-html.test.ts +67 -0
- package/src/extract-html.ts +239 -112
- package/src/mcp.ts +8 -4
- package/src/query.test.ts +36 -5
- package/src/query.ts +32 -20
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server that gives AI assistants searchable access to the complete [MikroTik RouterOS documentation](https://help.mikrotik.com/docs/spaces/ROS/overview) — 317 pages, 4,860 properties, 40,000-entry command tree, hardware specs for 144 products, 518 YouTube video transcripts, and direct links to help.mikrotik.com.
|
|
4
4
|
|
|
5
|
-
If you need MikroTik docs, you likely have a MikroTik. Install rosetta once as a container on your router using [RouterOS /app](#install-on-mikrotik-app), and any AI assistant on the network can use it. Or [run it locally](#install-locally-with-bun) on your workstation.
|
|
5
|
+
If you need MikroTik docs, you likely have a MikroTik. Install rosetta once as a container on your router using [RouterOS /app](#install-on-mikrotik-app), and any AI assistant on the network can use it. Or [run it locally](#install-locally-with-bun) on your workstation. **No AI required** — rosetta includes a [terminal browser](#browse-without-ai) for searching the database directly.
|
|
6
6
|
|
|
7
7
|
### SQL-as-RAG
|
|
8
8
|
|
|
@@ -10,16 +10,18 @@ Instead of vector embeddings, rosetta uses **SQLite [FTS5](https://www.sqlite.or
|
|
|
10
10
|
|
|
11
11
|
### What's Inside
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
| Data Source | Coverage |
|
|
14
|
+
|-------------|----------|
|
|
15
|
+
| Documentation pages | 317 pages (~515K words) from help.mikrotik.com |
|
|
16
|
+
| Property definitions | 4,860 with types, defaults, descriptions |
|
|
17
|
+
| Command tree | 5,114 commands, 551 dirs, 34K arguments |
|
|
18
|
+
| Version history | 46 RouterOS versions tracked (7.9–7.23beta2) |
|
|
19
|
+
| Hardware products | 144 devices — specs, pricing, block diagrams |
|
|
20
|
+
| Performance benchmarks | 2,874 tests across 125 devices (ethernet + IPSec) |
|
|
21
|
+
| YouTube transcripts | 518 videos, ~1,890 chapter-level segments |
|
|
22
|
+
| Callout blocks | 1,034 warnings, notes, and tips |
|
|
23
|
+
|
|
24
|
+
Documentation covers RouterOS **v7 only**, aligned with the long-term release (~7.22) at export time.
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
|
@@ -91,7 +93,13 @@ Point any HTTP-capable MCP client at the URL from the previous step:
|
|
|
91
93
|
|
|
92
94
|
> **CHR note:** Cloud Hosted Router in free or trial mode does not include the `/ip/cloud` service needed for HTTPS certificates. Set `use-https=no` on the /app — the URL will use HTTP instead. The UI URL always reflects the correct protocol.
|
|
93
95
|
|
|
94
|
-
> **HTTP option:** On any platform, you may choose `use-https=no` if you prefer HTTP or are on an isolated network.
|
|
96
|
+
> **HTTP option:** On any platform, you may choose `use-https=no` if you prefer HTTP or are on an isolated network.
|
|
97
|
+
|
|
98
|
+
> **Browse the database from the router:** If rosetta is running as a `/app`, you can use `/container/shell` to access the TUI browser directly:
|
|
99
|
+
> ```routeros
|
|
100
|
+
> /container/shell app-rosetta
|
|
101
|
+
> # /app/rosetta browse
|
|
102
|
+
> ```
|
|
95
103
|
|
|
96
104
|
---
|
|
97
105
|
|
|
@@ -107,7 +115,7 @@ bunx @tikoci/rosetta --setup
|
|
|
107
115
|
|
|
108
116
|
This downloads the database and prints config snippets for all supported MCP clients. Copy-paste the config for your client and you're done.
|
|
109
117
|
|
|
110
|
-
###
|
|
118
|
+
### Configure your MCP client
|
|
111
119
|
|
|
112
120
|
<details>
|
|
113
121
|
<summary><b>VS Code Copilot</b></summary>
|
|
@@ -214,34 +222,36 @@ powershell -c "irm bun.sh/install.ps1 | iex"
|
|
|
214
222
|
|
|
215
223
|
> **Auto-update:** `bunx` checks the npm registry each session and uses the latest published version automatically. The database in `~/.rosetta/ros-help.db` persists across updates.
|
|
216
224
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
If your MCP client supports resources, rosetta also exposes two read-only CSV datasets for bulk analysis and reporting:
|
|
225
|
+
---
|
|
220
226
|
|
|
221
|
-
|
|
222
|
-
- `rosetta://datasets/devices.csv`
|
|
227
|
+
## Browse Without AI
|
|
223
228
|
|
|
224
|
-
|
|
229
|
+
Rosetta includes a terminal-based "card catalog" browser — **no AI assistant or MCP client required**. It searches the same database the MCP tools use, with a keyboard-driven REPL modeled after a 1980s library terminal.
|
|
225
230
|
|
|
226
|
-
|
|
231
|
+
```sh
|
|
232
|
+
bunx @tikoci/rosetta browse
|
|
233
|
+
```
|
|
227
234
|
|
|
228
|
-
|
|
235
|
+
Type a search query to find documentation pages, then select a numbered result to drill in. Beyond page search, the browser covers every data source in the database:
|
|
229
236
|
|
|
230
|
-
|
|
237
|
+
| Command | What it searches |
|
|
238
|
+
|---------|-----------------|
|
|
239
|
+
| *(bare text)* | Documentation pages (default) |
|
|
240
|
+
| `dev <query>` | Device hardware specs, block diagrams, benchmarks |
|
|
241
|
+
| `cmd [path]` | Command tree hierarchy |
|
|
242
|
+
| `prop <name>` | Property definitions (scoped to current page when viewing one) |
|
|
243
|
+
| `cal [query]` | Warnings, notes, and tips |
|
|
244
|
+
| `cl [version]` | Changelogs — `cl breaking` for breaking changes only |
|
|
245
|
+
| `vid <query>` | YouTube video transcripts with timestamped chapter links |
|
|
246
|
+
| `diff <from> <to>` | Command tree diff between RouterOS versions |
|
|
247
|
+
| `tests [type]` | Cross-device performance benchmarks |
|
|
248
|
+
| `ver` | Live-fetch current RouterOS versions |
|
|
231
249
|
|
|
232
|
-
|
|
233
|
-
|----------|------|
|
|
234
|
-
| macOS (Apple Silicon) | `rosetta-macos-arm64.zip` |
|
|
235
|
-
| macOS (Intel) | `rosetta-macos-x64.zip` |
|
|
236
|
-
| Windows | `rosetta-windows-x64.zip` |
|
|
237
|
-
| Linux | `rosetta-linux-x64.zip` |
|
|
250
|
+
Type `help` for the full command list. URLs are clickable in terminals that support OSC 8 hyperlinks (iTerm2, Windows Terminal, GNOME Terminal, etc.).
|
|
238
251
|
|
|
239
|
-
|
|
240
|
-
./rosetta --setup # downloads DB + prints MCP client config
|
|
241
|
-
```
|
|
252
|
+
The browser is also useful as a test harness — it interacts with the data the same way an AI agent would through MCP, so gaps or rough edges visible here often point to MCP tool improvements too.
|
|
242
253
|
|
|
243
|
-
> **
|
|
244
|
-
> **Windows SmartScreen:** Click **More info → Run anyway**.
|
|
254
|
+
> **From a router:** If rosetta is installed as a `/app`, access the browser via `/container/shell app-rosetta` then `/app/rosetta browse`.
|
|
245
255
|
|
|
246
256
|
---
|
|
247
257
|
|
|
@@ -252,7 +262,6 @@ Ask your AI assistant questions like:
|
|
|
252
262
|
- *"What are the DHCP server properties in RouterOS?"*
|
|
253
263
|
- *"How do I set up a bridge VLAN?"*
|
|
254
264
|
- *"Is the /container command available in RouterOS 7.12?"*
|
|
255
|
-
- *"What are the firewall filter default chains?"*
|
|
256
265
|
- *"Show me warnings about hardware offloading"*
|
|
257
266
|
- *"Which MikroTik routers have L3HW offload, and more than 8 ports of 48V PoE? Include cost."*
|
|
258
267
|
- *"Compare the RB5009 and CCR2004 IPSec throughput at 1518-byte packets."*
|
|
@@ -260,122 +269,30 @@ Ask your AI assistant questions like:
|
|
|
260
269
|
|
|
261
270
|
## MCP Tools
|
|
262
271
|
|
|
263
|
-
The server
|
|
272
|
+
The server exposes 14 tools designed to work together — agents start with `routeros_search` and drill into specific data as needed:
|
|
264
273
|
|
|
265
274
|
| Tool | What it does |
|
|
266
275
|
|------|-------------|
|
|
267
|
-
| `routeros_search` | **Start here.** Full-text search across all pages
|
|
268
|
-
| `routeros_get_page` |
|
|
269
|
-
| `routeros_lookup_property` |
|
|
270
|
-
| `routeros_search_properties` |
|
|
271
|
-
| `routeros_command_tree` | Browse the `/ip/firewall/filter` style
|
|
272
|
-
| `routeros_search_callouts` | Search warnings, notes, and tips
|
|
273
|
-
| `routeros_search_changelogs` |
|
|
274
|
-
| `routeros_command_version_check` |
|
|
275
|
-
| `routeros_command_diff` |
|
|
276
|
-
| `routeros_device_lookup` | Hardware specs
|
|
277
|
-
| `routeros_search_tests` | Cross-device
|
|
278
|
-
| `routeros_search_videos` |
|
|
279
|
-
| `routeros_stats` | Database health
|
|
280
|
-
| `routeros_current_versions` |
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|-------|----------|
|
|
288
|
-
| **First launch is slow** | One-time database download (~50 MB). Subsequent starts are instant. |
|
|
289
|
-
| **`npx @tikoci/rosetta` fails** | This package requires Bun, not Node.js. Use `bunx` instead of `npx`. |
|
|
290
|
-
| **`npm install -g` then `rosetta` fails** | Global npm install works if Bun is on PATH — it delegates to `bun` at runtime. But prefer `bunx` — it's simpler and auto-updates. |
|
|
291
|
-
| **ChatGPT Apps can't connect** | ChatGPT Apps require a remote HTTPS MCP endpoint. Use the [MikroTik /app install](#install-on-mikrotik-app) for a hosted endpoint, or Codex CLI for local stdio. |
|
|
292
|
-
| **Claude Desktop can't find `bunx`** | Claude Desktop on macOS may not inherit shell PATH. Use the full path to bunx (run `which bunx` to find it, typically `~/.bun/bin/bunx`). `bunx @tikoci/rosetta --setup` prints the full-path config. |
|
|
293
|
-
| **macOS Gatekeeper blocks binary** | Use `bunx` install (no Gatekeeper issues), or: `xattr -d com.apple.quarantine ./rosetta` |
|
|
294
|
-
| **Windows SmartScreen warning** | Use `bunx` install (no SmartScreen issues), or click **More info → Run anyway** |
|
|
295
|
-
| **How to update** | `bunx` always uses the latest published version. For binaries, re-download from [Releases](https://github.com/tikoci/rosetta/releases/latest). MikroTik /app with `auto-update: true` pulls the latest image on each boot. |
|
|
296
|
-
|
|
297
|
-
## HTTP Transport
|
|
298
|
-
|
|
299
|
-
The [MikroTik /app install](#install-on-mikrotik-app) is the easiest way to get an HTTP endpoint. For other setups, rosetta supports the [MCP Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) via `--http`:
|
|
300
|
-
|
|
301
|
-
```sh
|
|
302
|
-
rosetta --http # http://localhost:8080/mcp
|
|
303
|
-
rosetta --http --port 9090 # custom port
|
|
304
|
-
rosetta --http --host 0.0.0.0 # accessible from LAN
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Then point your MCP client at the URL:
|
|
308
|
-
|
|
309
|
-
```json
|
|
310
|
-
{ "url": "http://localhost:8080/mcp" }
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
- **Read-only** — queries a local SQLite database, stores nothing.
|
|
314
|
-
- **No authentication** — designed for local/trusted-network use. Use a reverse proxy for public exposure.
|
|
315
|
-
- **TLS built-in** — `--tls-cert cert.pem --tls-key key.pem` for direct HTTPS without a proxy.
|
|
316
|
-
- **Defaults to localhost** — LAN binding (`--host 0.0.0.0`) requires an explicit flag.
|
|
317
|
-
|
|
318
|
-
## Container Images
|
|
319
|
-
|
|
320
|
-
Multi-arch OCI images (linux/amd64 + linux/arm64) are published with each release:
|
|
321
|
-
|
|
322
|
-
- `ghcr.io/tikoci/rosetta` (GitHub Container Registry)
|
|
323
|
-
- `ammo74/rosetta` (Docker Hub)
|
|
324
|
-
|
|
325
|
-
```sh
|
|
326
|
-
docker run --rm -p 8080:8080 ghcr.io/tikoci/rosetta:latest
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
These are the same images used by the [MikroTik /app install](#install-on-mikrotik-app). Tags: `latest`, version (e.g., `v0.2.1`), and `sha-<commit>`.
|
|
330
|
-
|
|
331
|
-
## Data Sources
|
|
332
|
-
|
|
333
|
-
The database combines multiple MikroTik data sources into a single SQLite file with [FTS5](https://www.sqlite.org/fts5.html) full-text search, [porter stemming](https://www.sqlite.org/fts5.html#porter_tokenizer), and [BM25 ranking](https://www.sqlite.org/fts5.html#the_bm25_function):
|
|
334
|
-
|
|
335
|
-
- **HTML Documentation** — Confluence space export from help.mikrotik.com (March 2026). 317 pages broken into sections, callouts, and property tables (~515K words) with links back to help.mikrotik.com.
|
|
336
|
-
|
|
337
|
-
- **Command Tree** — `inspect.json` from [tikoci/restraml](https://github.com/tikoci/restraml), generated by running `/console/inspect` against RouterOS CHR under QEMU for every version since 7.9 (46 versions tracked: 7.9–7.23beta2).
|
|
338
|
-
|
|
339
|
-
- **Product Matrix** — CSV export from mikrotik.com/products/matrix (144 products, 34 columns). Hardware specs, license levels, and pricing.
|
|
340
|
-
|
|
341
|
-
- **Device Benchmarks** — Ethernet bridging/routing and IPSec throughput test results scraped from individual product pages on mikrotik.com (2,874 measurements across 125 devices; 64/512/1518-byte packets, multiple configurations). Also captures block diagram image URLs for 110 devices.
|
|
342
|
-
|
|
343
|
-
- **YouTube Transcripts** — Auto-generated English transcripts from the official [MikroTik YouTube channel](https://www.youtube.com/@MikroTik/videos) (518 videos, ~1,800 transcript segments). Split by chapter when available, with timestamps for deep linking. MUM conference talks excluded. Extracted via yt-dlp, cached as NDJSON in the repo for reproducible CI builds.
|
|
344
|
-
|
|
345
|
-
Documentation covers RouterOS **v7 only** and aligns with the long-term release (~7.22) at export time. v6 had different syntax and major subsystems — answers for v6 are unreliable.
|
|
346
|
-
|
|
347
|
-
## Database (Standalone)
|
|
348
|
-
|
|
349
|
-
The SQLite database is downloadable on its own from [GitHub Releases](https://github.com/tikoci/rosetta/releases):
|
|
350
|
-
|
|
351
|
-
```text
|
|
352
|
-
https://github.com/tikoci/rosetta/releases/latest/download/ros-help.db.gz
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
Use it with any SQLite client:
|
|
356
|
-
|
|
357
|
-
```sh
|
|
358
|
-
sqlite3 ros-help.db "SELECT title, url FROM pages_fts WHERE pages_fts MATCH 'DHCP lease' ORDER BY rank LIMIT 5;"
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Tables
|
|
362
|
-
|
|
363
|
-
| Table | Rows | What's in it |
|
|
364
|
-
|-------|------|-------------|
|
|
365
|
-
| `pages` | 317 | Documentation pages — title, breadcrumb path, full text, code blocks, help.mikrotik.com URL |
|
|
366
|
-
| `sections` | 2,984 | Page chunks split by h1–h3 headings, with anchor IDs for deep linking |
|
|
367
|
-
| `callouts` | 1,034 | Warning/Note/Info/Tip boxes extracted from Confluence callout macros |
|
|
368
|
-
| `properties` | 4,860 | Command properties — name, type, default value, description (from doc tables) |
|
|
369
|
-
| `commands` | 40K+ | RouterOS command hierarchy — dirs, commands, arguments from `/console/inspect` |
|
|
370
|
-
| `command_versions` | 1.67M | Junction table: which command paths exist in which RouterOS versions (7.9–7.23beta2) |
|
|
371
|
-
| `ros_versions` | 46 | Tracked RouterOS versions with channel (stable/development) |
|
|
372
|
-
| `devices` | 144 | MikroTik hardware — CPU, RAM, storage, ports, PoE, wireless, license level, MSRP |
|
|
373
|
-
| `device_test_results` | 2,874 | Ethernet and IPSec throughput benchmarks for 125 devices — packet sizes, modes, Mbps/Kpps |
|
|
374
|
-
| `changelogs` | varies | Parsed changelog entries per RouterOS version — category, description, breaking flag |
|
|
375
|
-
| `videos` | 518 | MikroTik YouTube video metadata — title, description, duration, chapters |
|
|
376
|
-
| `video_segments` | ~1,890 | Chapter-level transcript segments with timestamps for deep linking |
|
|
377
|
-
|
|
378
|
-
Each content table has a corresponding FTS5 index (e.g., `pages_fts`, `properties_fts`, `devices_fts`, `video_segments_fts`).
|
|
276
|
+
| `routeros_search` | **Start here.** Full-text search across all documentation pages |
|
|
277
|
+
| `routeros_get_page` | Full page content by ID or title, section-aware for large pages |
|
|
278
|
+
| `routeros_lookup_property` | Property by exact name — type, default, description |
|
|
279
|
+
| `routeros_search_properties` | FTS across 4,860 property names and descriptions |
|
|
280
|
+
| `routeros_command_tree` | Browse the command hierarchy (`/ip/firewall/filter` style) |
|
|
281
|
+
| `routeros_search_callouts` | Search warnings, notes, and tips |
|
|
282
|
+
| `routeros_search_changelogs` | Changelogs filtered by version range, category, breaking flag |
|
|
283
|
+
| `routeros_command_version_check` | Which RouterOS versions include a command path |
|
|
284
|
+
| `routeros_command_diff` | Added/removed commands between two RouterOS versions |
|
|
285
|
+
| `routeros_device_lookup` | Hardware specs — filter by architecture, RAM, PoE, wireless, etc. |
|
|
286
|
+
| `routeros_search_tests` | Cross-device ethernet and IPSec benchmarks |
|
|
287
|
+
| `routeros_search_videos` | YouTube transcript search with chapter timestamps |
|
|
288
|
+
| `routeros_stats` | Database health and coverage stats |
|
|
289
|
+
| `routeros_current_versions` | Live-fetch current RouterOS versions from MikroTik |
|
|
290
|
+
|
|
291
|
+
Each tool description includes workflow arrows (`→ next_tool`) and empty-result hints so agents chain tools effectively.
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
## RTFM for Details
|
|
295
|
+
For additional install options, HTTP transport configuration, data source details, and the database schema, see [MANUAL.md](MANUAL.md).
|
|
379
296
|
|
|
380
297
|
## Contributing
|
|
381
298
|
|
package/package.json
CHANGED
package/src/browse.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as readline from "node:readline";
|
|
|
15
15
|
import { db, getDbStats, initDb } from "./db.ts";
|
|
16
16
|
import { resolveVersion } from "./paths.ts";
|
|
17
17
|
import type {
|
|
18
|
+
CalloutResult,
|
|
18
19
|
ChangelogResult,
|
|
19
20
|
DeviceResult,
|
|
20
21
|
DeviceTestRow,
|
|
@@ -151,11 +152,15 @@ async function paged(output: string): Promise<boolean> {
|
|
|
151
152
|
offset += pageSize;
|
|
152
153
|
if (offset < lines.length) {
|
|
153
154
|
const remaining = lines.length - offset;
|
|
154
|
-
process.stdout.write(dim(
|
|
155
|
+
process.stdout.write(dim(`-- ${remaining} more lines [Q quit | SPACE next page | ENTER next line]`));
|
|
155
156
|
const key = await waitForKey();
|
|
156
157
|
// Clear the "more" line
|
|
157
158
|
process.stdout.write(`\r${" ".repeat(termWidth())}\r`);
|
|
158
159
|
if (key === "q" || key === "Q") return true;
|
|
160
|
+
// ENTER or arrow-down → advance one line; SPACE → advance one page
|
|
161
|
+
if (key === "\r" || key === "\n" || key === "\x1b[B") {
|
|
162
|
+
offset -= pageSize - 1; // back up so only 1 line advances
|
|
163
|
+
}
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
return false;
|
|
@@ -184,14 +189,14 @@ type Context =
|
|
|
184
189
|
| { type: "search"; response: SearchResponse; results: SearchResult[] }
|
|
185
190
|
| { type: "page"; pageId: number; title: string; commandPath?: string }
|
|
186
191
|
| { type: "sections"; pageId: number; title: string; sections: SectionTocEntry[] }
|
|
187
|
-
| { type: "properties"; query: string; pageId?: number }
|
|
188
192
|
| { type: "commands"; path: string }
|
|
189
193
|
| { type: "devices"; query: string; results: DeviceResult[] }
|
|
190
194
|
| { type: "device"; device: DeviceResult }
|
|
191
195
|
| { type: "tests" }
|
|
192
|
-
| { type: "callouts"; query: string }
|
|
193
|
-
| { type: "changelogs" }
|
|
194
|
-
| { type: "videos"; query: string }
|
|
196
|
+
| { type: "callouts"; query: string; results: CalloutResult[] }
|
|
197
|
+
| { type: "changelogs"; results: ChangelogResult[] }
|
|
198
|
+
| { type: "videos"; query: string; results: VideoSearchResult[] }
|
|
199
|
+
| { type: "properties"; query: string; pageId?: number; results: Array<{ name: string; page_id: number; page_title: string }> }
|
|
195
200
|
| { type: "diff" }
|
|
196
201
|
| { type: "vcheck"; path: string };
|
|
197
202
|
|
|
@@ -209,6 +214,32 @@ function popCtx(): boolean {
|
|
|
209
214
|
return false;
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
/** Build a context-aware prompt string, mirroring RouterOS [admin@router]> style. */
|
|
218
|
+
function buildPrompt(): string {
|
|
219
|
+
const label = contextLabel(ctx);
|
|
220
|
+
if (label) return `${cyan("rosetta")}${dim("[")}${label}${dim("]>")} `;
|
|
221
|
+
return `${cyan("rosetta")}${dim(">")} `;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function contextLabel(c: Context): string {
|
|
225
|
+
switch (c.type) {
|
|
226
|
+
case "home": return "";
|
|
227
|
+
case "search": return truncate(`search: "${c.response.query}"`, 30);
|
|
228
|
+
case "page": return truncate(c.title, 30);
|
|
229
|
+
case "sections": return truncate(`${c.title} §`, 30);
|
|
230
|
+
case "properties": return truncate(`prop: "${c.query}"`, 30);
|
|
231
|
+
case "commands": return truncate(`cmd: ${c.path || "/"}`, 30);
|
|
232
|
+
case "devices": return truncate(`dev: "${c.query}"`, 30);
|
|
233
|
+
case "device": return truncate(c.device.product_name, 30);
|
|
234
|
+
case "tests": return "tests";
|
|
235
|
+
case "callouts": return c.query ? truncate(`cal: "${c.query}"`, 30) : "callouts";
|
|
236
|
+
case "changelogs": return "changelogs";
|
|
237
|
+
case "videos": return truncate(`vid: "${c.query}"`, 30);
|
|
238
|
+
case "diff": return "diff";
|
|
239
|
+
case "vcheck": return truncate(`vc: ${c.path}`, 30);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
212
243
|
// ── Renderers ──
|
|
213
244
|
|
|
214
245
|
function renderWelcome(): string {
|
|
@@ -318,7 +349,7 @@ function renderPage(page: NonNullable<ReturnType<typeof getPage>>): string {
|
|
|
318
349
|
if (page.code) {
|
|
319
350
|
out.push("");
|
|
320
351
|
out.push(dim("── code ──"));
|
|
321
|
-
out.push(page.code);
|
|
352
|
+
out.push(page.code.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
322
353
|
}
|
|
323
354
|
}
|
|
324
355
|
|
|
@@ -336,7 +367,7 @@ function renderPage(page: NonNullable<ReturnType<typeof getPage>>): string {
|
|
|
336
367
|
if (page.code) {
|
|
337
368
|
out.push("");
|
|
338
369
|
out.push(dim("── code ──"));
|
|
339
|
-
out.push(page.code);
|
|
370
|
+
out.push(page.code.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
340
371
|
}
|
|
341
372
|
}
|
|
342
373
|
|
|
@@ -582,18 +613,20 @@ function renderChangelogs(results: ChangelogResult[]): string {
|
|
|
582
613
|
}
|
|
583
614
|
|
|
584
615
|
let lastVersion = "";
|
|
585
|
-
for (
|
|
616
|
+
for (let i = 0; i < results.length; i++) {
|
|
617
|
+
const c = results[i];
|
|
586
618
|
if (c.version !== lastVersion) {
|
|
587
619
|
out.push("");
|
|
588
620
|
out.push(` ${bold(c.version)} ${dim(c.released ?? "")}`);
|
|
589
621
|
lastVersion = c.version;
|
|
590
622
|
}
|
|
591
|
-
const
|
|
623
|
+
const num = dim(`${String(i + 1).padStart(3)} `);
|
|
624
|
+
const breaking = c.is_breaking ? red("⚠ ") : "";
|
|
592
625
|
const cat = dim(pad(c.category, 14));
|
|
593
626
|
const desc = c.excerpt.includes("**")
|
|
594
627
|
? c.excerpt.replace(/\*\*/g, `${ESC}[1m`)
|
|
595
|
-
: truncate(c.description, termWidth() -
|
|
596
|
-
out.push(
|
|
628
|
+
: truncate(c.description, termWidth() - 26);
|
|
629
|
+
out.push(`${num}${breaking}${cat} ${desc}`);
|
|
597
630
|
}
|
|
598
631
|
|
|
599
632
|
out.push("");
|
|
@@ -688,6 +721,13 @@ function renderStats(): string {
|
|
|
688
721
|
const out: string[] = [];
|
|
689
722
|
out.push(` ${bold("Database Statistics")}`);
|
|
690
723
|
out.push(` ${dim("Path:")} ${stats.db_path}`);
|
|
724
|
+
if (stats.db_size_bytes != null) {
|
|
725
|
+
const mb = stats.db_size_bytes / (1024 * 1024);
|
|
726
|
+
out.push(` ${dim("Size:")} ${mb.toFixed(1)} MB`);
|
|
727
|
+
}
|
|
728
|
+
if (stats.schema_version != null) {
|
|
729
|
+
out.push(` ${dim("Schema:")} v${stats.schema_version}`);
|
|
730
|
+
}
|
|
691
731
|
out.push(` ${dim("Export:")} ${stats.doc_export}`);
|
|
692
732
|
out.push("");
|
|
693
733
|
|
|
@@ -719,25 +759,26 @@ function renderHelp(): string {
|
|
|
719
759
|
out.push(` ${bold("Commands")} ${dim("(bare text = search)")}`);
|
|
720
760
|
out.push("");
|
|
721
761
|
|
|
722
|
-
const cmd = (name: string, alias: string, desc: string) => {
|
|
723
|
-
|
|
762
|
+
const cmd = (name: string, alias: string, desc: string, mcp?: string) => {
|
|
763
|
+
const mcpHint = mcp ? ` ${dim(`(${mcp})`)}` : "";
|
|
764
|
+
out.push(` ${cyan(pad(name, 26))} ${dim(pad(alias, 6))} ${desc}${mcpHint}`);
|
|
724
765
|
};
|
|
725
766
|
|
|
726
|
-
cmd("<query>", "", "Search pages (default action)");
|
|
727
|
-
cmd("search <query>", "s", "Explicit page search");
|
|
728
|
-
cmd("page <id|title>", "", "View full page");
|
|
729
|
-
cmd("prop <name>", "p", "Look up property (scoped to current page)");
|
|
730
|
-
cmd("props <query>", "sp", "Search properties by FTS");
|
|
731
|
-
cmd("cmd [path]", "tree", "Browse command tree");
|
|
732
|
-
cmd("device <query>", "dev", "Look up device specs");
|
|
733
|
-
cmd("tests [
|
|
734
|
-
cmd("callouts [query]", "cal", "Search callouts (type filter: cal warning)");
|
|
735
|
-
cmd("changelog [query]", "cl", "Search changelogs (cl 7.22, cl breaking)");
|
|
736
|
-
cmd("videos <query>", "vid", "Search video transcripts");
|
|
737
|
-
cmd("diff <from> <to> [path]", "", "Command tree diff between versions");
|
|
738
|
-
cmd("vcheck <path>", "vc", "Version range for a command path");
|
|
739
|
-
cmd("versions", "ver", "Live-fetch current RouterOS versions");
|
|
740
|
-
cmd("stats", "", "Database health / counts");
|
|
767
|
+
cmd("<query>", "", "Search pages (default action)", "routeros_search");
|
|
768
|
+
cmd("search <query>", "s", "Explicit page search", "routeros_search");
|
|
769
|
+
cmd("page <id|title>", "", "View full page", "routeros_get_page");
|
|
770
|
+
cmd("prop <name>", "p", "Look up property (scoped to current page)", "routeros_lookup_property");
|
|
771
|
+
cmd("props <query>", "sp", "Search properties by FTS", "routeros_search_properties");
|
|
772
|
+
cmd("cmd [path]", "tree", "Browse command tree", "routeros_command_tree");
|
|
773
|
+
cmd("device <query>", "dev", "Look up device specs", "routeros_device_lookup");
|
|
774
|
+
cmd("tests [device] [type]", "", "Cross-device benchmarks", "routeros_search_tests");
|
|
775
|
+
cmd("callouts [query]", "cal", "Search callouts (type filter: cal warning)", "routeros_search_callouts");
|
|
776
|
+
cmd("changelog [query]", "cl", "Search changelogs (cl 7.22, cl breaking)", "routeros_search_changelogs");
|
|
777
|
+
cmd("videos <query>", "vid", "Search video transcripts", "routeros_search_videos");
|
|
778
|
+
cmd("diff <from> <to> [path]", "", "Command tree diff between versions", "routeros_command_diff");
|
|
779
|
+
cmd("vcheck <path>", "vc", "Version range for a command path", "routeros_command_version_check");
|
|
780
|
+
cmd("versions", "ver", "Live-fetch current RouterOS versions", "routeros_current_versions");
|
|
781
|
+
cmd("stats", "", "Database health / counts", "routeros_stats");
|
|
741
782
|
cmd("back", "b", "Go to previous view");
|
|
742
783
|
cmd("help", "?", "This help");
|
|
743
784
|
cmd("quit", "q", "Exit");
|
|
@@ -858,7 +899,7 @@ async function dispatch(input: string): Promise<void> {
|
|
|
858
899
|
const pageCallouts = results.filter((c) => c.page_id === (ctx as { pageId: number }).pageId);
|
|
859
900
|
if (pageCallouts.length > 0) {
|
|
860
901
|
await paged(renderCallouts(pageCallouts));
|
|
861
|
-
pushCtx({ type: "callouts", query: "" });
|
|
902
|
+
pushCtx({ type: "callouts", query: "", results: pageCallouts });
|
|
862
903
|
return;
|
|
863
904
|
}
|
|
864
905
|
}
|
|
@@ -935,6 +976,30 @@ async function handleNumberSelect(idx: number): Promise<void> {
|
|
|
935
976
|
}
|
|
936
977
|
return;
|
|
937
978
|
}
|
|
979
|
+
if (ctx.type === "callouts" && ctx.results[idx]) {
|
|
980
|
+
const c = ctx.results[idx];
|
|
981
|
+
await doPage(String(c.page_id));
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (ctx.type === "videos" && ctx.results[idx]) {
|
|
985
|
+
const v = ctx.results[idx];
|
|
986
|
+
const timeUrl = v.start_s > 0 ? `${v.url}&t=${v.start_s}` : v.url;
|
|
987
|
+
console.log(`\n ${bold(v.title)}`);
|
|
988
|
+
if (v.chapter_title) console.log(` ${magenta(`§ ${v.chapter_title}`)} ${dim(`@ ${formatTime(v.start_s)}`)}`);
|
|
989
|
+
console.log(` ${cyan(link(timeUrl))}\n`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (ctx.type === "properties" && ctx.results[idx]) {
|
|
993
|
+
const p = ctx.results[idx];
|
|
994
|
+
await doPage(String(p.page_id));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (ctx.type === "changelogs" && ctx.results[idx]) {
|
|
998
|
+
const c = ctx.results[idx];
|
|
999
|
+
console.log(`\n ${bold(c.version)} ${dim(c.released ?? "")} ${dim(c.category)}${c.is_breaking ? ` ${red("⚠ BREAKING")}` : ""}`);
|
|
1000
|
+
console.log(` ${c.description}\n`);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
938
1003
|
console.log(dim(` No item #${idx + 1} in current view.`));
|
|
939
1004
|
}
|
|
940
1005
|
|
|
@@ -987,7 +1052,7 @@ async function doPropsForPage(pageId: number, title: string): Promise<void> {
|
|
|
987
1052
|
return;
|
|
988
1053
|
}
|
|
989
1054
|
await paged(` ${bold("Properties for")} ${bold(title)}\n\n${renderProperties(pageProps)}`);
|
|
990
|
-
pushCtx({ type: "properties", query: title, pageId });
|
|
1055
|
+
pushCtx({ type: "properties", query: title, pageId, results: pageProps.map(p => ({ name: p.name, page_id: p.page_id ?? pageId, page_title: p.page_title })) });
|
|
991
1056
|
}
|
|
992
1057
|
|
|
993
1058
|
async function doLookupProperty(name: string): Promise<void> {
|
|
@@ -999,7 +1064,7 @@ async function doLookupProperty(name: string): Promise<void> {
|
|
|
999
1064
|
return;
|
|
1000
1065
|
}
|
|
1001
1066
|
await paged(renderProperties(results));
|
|
1002
|
-
pushCtx({ type: "properties", query: name });
|
|
1067
|
+
pushCtx({ type: "properties", query: name, results: results.map(p => ({ name: p.name, page_id: p.page_id, page_title: p.page_title })) });
|
|
1003
1068
|
}
|
|
1004
1069
|
|
|
1005
1070
|
async function doSearchProperties(query: string): Promise<void> {
|
|
@@ -1009,7 +1074,7 @@ async function doSearchProperties(query: string): Promise<void> {
|
|
|
1009
1074
|
return;
|
|
1010
1075
|
}
|
|
1011
1076
|
await paged(` ${bold(String(results.length))} properties matching ${cyan(`"${query}"`)}\n\n${renderProperties(results)}`);
|
|
1012
|
-
pushCtx({ type: "properties", query });
|
|
1077
|
+
pushCtx({ type: "properties", query, results: results.map(p => ({ name: p.name, page_id: p.page_id, page_title: p.page_title })) });
|
|
1013
1078
|
}
|
|
1014
1079
|
|
|
1015
1080
|
async function doCommandTree(path: string): Promise<void> {
|
|
@@ -1056,16 +1121,25 @@ async function doTests(argsStr: string): Promise<void> {
|
|
|
1056
1121
|
console.log(` ${dim("Modes:")} ${meta.modes.join(", ")}`);
|
|
1057
1122
|
console.log(` ${dim("Packet sizes:")} ${meta.packet_sizes.join(", ")}`);
|
|
1058
1123
|
console.log("");
|
|
1059
|
-
console.log(` ${dim("Usage: tests <type> [mode] [packet_size]")}`);
|
|
1124
|
+
console.log(` ${dim("Usage: tests [device] <type> [mode] [packet_size]")}`);
|
|
1060
1125
|
console.log(` ${dim("Example: tests ethernet Routing 1518")}`);
|
|
1126
|
+
console.log(` ${dim("Example: tests rb5009 ethernet 1518")}`);
|
|
1061
1127
|
return;
|
|
1062
1128
|
}
|
|
1063
1129
|
|
|
1064
1130
|
const parts = argsStr.split(/\s+/);
|
|
1065
1131
|
const filters: Record<string, string | number> = {};
|
|
1066
|
-
|
|
1067
|
-
if
|
|
1068
|
-
|
|
1132
|
+
|
|
1133
|
+
// Known test types — if parts[0] is not a known type, treat it as a device filter
|
|
1134
|
+
const knownTypes = ["ethernet", "ipsec"];
|
|
1135
|
+
let offset = 0;
|
|
1136
|
+
if (parts[0] && !knownTypes.includes(parts[0].toLowerCase())) {
|
|
1137
|
+
filters.device = parts[0];
|
|
1138
|
+
offset = 1;
|
|
1139
|
+
}
|
|
1140
|
+
if (parts[offset]) filters.test_type = parts[offset];
|
|
1141
|
+
if (parts[offset + 1]) filters.mode = parts[offset + 1];
|
|
1142
|
+
if (parts[offset + 2] && /^\d+$/.test(parts[offset + 2])) filters.packet_size = Number.parseInt(parts[offset + 2], 10);
|
|
1069
1143
|
|
|
1070
1144
|
const result = searchDeviceTests(filters);
|
|
1071
1145
|
if (result.results.length === 0) {
|
|
@@ -1094,7 +1168,7 @@ async function doSearchCallouts(query: string): Promise<void> {
|
|
|
1094
1168
|
return;
|
|
1095
1169
|
}
|
|
1096
1170
|
await paged(` ${bold(String(results.length))} callouts${type ? ` (${type})` : ""}\n\n${renderCallouts(results)}`);
|
|
1097
|
-
pushCtx({ type: "callouts", query });
|
|
1171
|
+
pushCtx({ type: "callouts", query, results });
|
|
1098
1172
|
}
|
|
1099
1173
|
|
|
1100
1174
|
async function doSearchChangelogs(query: string): Promise<void> {
|
|
@@ -1135,7 +1209,7 @@ async function doSearchChangelogs(query: string): Promise<void> {
|
|
|
1135
1209
|
return;
|
|
1136
1210
|
}
|
|
1137
1211
|
await paged(` ${bold("Changelogs")}${version ? ` for ${bold(version)}` : ""}${breakingOnly ? ` ${red("(breaking only)")}` : ""}\n\n${renderChangelogs(results)}`);
|
|
1138
|
-
pushCtx({ type: "changelogs" });
|
|
1212
|
+
pushCtx({ type: "changelogs", results });
|
|
1139
1213
|
}
|
|
1140
1214
|
|
|
1141
1215
|
async function doSearchVideos(query: string): Promise<void> {
|
|
@@ -1145,7 +1219,7 @@ async function doSearchVideos(query: string): Promise<void> {
|
|
|
1145
1219
|
return;
|
|
1146
1220
|
}
|
|
1147
1221
|
await paged(` ${bold(String(results.length))} video results for ${cyan(`"${query}"`)}\n\n${renderVideos(results)}`);
|
|
1148
|
-
pushCtx({ type: "videos", query });
|
|
1222
|
+
pushCtx({ type: "videos", query, results });
|
|
1149
1223
|
}
|
|
1150
1224
|
|
|
1151
1225
|
async function doDiff(from: string, to: string, pathPrefix?: string): Promise<void> {
|
|
@@ -1172,6 +1246,7 @@ async function doCurrentVersions(): Promise<void> {
|
|
|
1172
1246
|
const ch = pad(channel, 14);
|
|
1173
1247
|
out.push(` ${dim(ch)} ${bold(String(v))}`);
|
|
1174
1248
|
}
|
|
1249
|
+
out.push(` ${dim(pad("winbox 4", 14))} ${bold(String(result.winbox ?? dim("unavailable")))}`);
|
|
1175
1250
|
console.log(out.join("\n"));
|
|
1176
1251
|
}
|
|
1177
1252
|
|
|
@@ -1210,7 +1285,7 @@ async function main() {
|
|
|
1210
1285
|
const rl = readline.createInterface({
|
|
1211
1286
|
input: process.stdin,
|
|
1212
1287
|
output: process.stdout,
|
|
1213
|
-
prompt:
|
|
1288
|
+
prompt: buildPrompt(),
|
|
1214
1289
|
terminal: true,
|
|
1215
1290
|
});
|
|
1216
1291
|
|
|
@@ -1222,6 +1297,7 @@ async function main() {
|
|
|
1222
1297
|
} catch (err) {
|
|
1223
1298
|
console.error(red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1224
1299
|
}
|
|
1300
|
+
rl.setPrompt(buildPrompt());
|
|
1225
1301
|
rl.prompt();
|
|
1226
1302
|
});
|
|
1227
1303
|
|
package/src/db.ts
CHANGED
|
@@ -441,8 +441,24 @@ export function checkSchemaVersion(): { ok: boolean; actual: number; expected: n
|
|
|
441
441
|
export function getDbStats() {
|
|
442
442
|
const count = (sql: string) =>
|
|
443
443
|
Number((db.prepare(sql).get() as { c: number }).c ?? 0);
|
|
444
|
+
const dbSizeBytes = (() => {
|
|
445
|
+
try {
|
|
446
|
+
return Bun.file(DB_PATH).size;
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
})();
|
|
451
|
+
const schemaVersion = (() => {
|
|
452
|
+
try {
|
|
453
|
+
return (db.prepare("PRAGMA user_version").get() as { user_version: number }).user_version;
|
|
454
|
+
} catch {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
})();
|
|
444
458
|
return {
|
|
445
459
|
db_path: DB_PATH,
|
|
460
|
+
db_size_bytes: dbSizeBytes,
|
|
461
|
+
schema_version: schemaVersion,
|
|
446
462
|
pages: count("SELECT COUNT(*) AS c FROM pages"),
|
|
447
463
|
sections: count("SELECT COUNT(*) AS c FROM sections"),
|
|
448
464
|
properties: count("SELECT COUNT(*) AS c FROM properties"),
|
package/src/extract-devices.ts
CHANGED
|
@@ -120,7 +120,11 @@ if (lines.length < 2) {
|
|
|
120
120
|
// Skip header row
|
|
121
121
|
const dataLines = lines.slice(1);
|
|
122
122
|
|
|
123
|
-
// Idempotent: clear existing data (FTS triggers handle cleanup)
|
|
123
|
+
// Idempotent: clear existing data (FTS triggers handle cleanup).
|
|
124
|
+
// device_test_results FKs into devices — wipe it first to avoid a FOREIGN KEY
|
|
125
|
+
// constraint failure on re-run over a populated DB. extract-test-results runs
|
|
126
|
+
// later in the pipeline and repopulates it.
|
|
127
|
+
db.run("DELETE FROM device_test_results");
|
|
124
128
|
db.run("DELETE FROM devices");
|
|
125
129
|
|
|
126
130
|
const insert = db.prepare(`INSERT INTO devices (
|