@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 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
- - **317 documentation pages** from MikroTik's official help site (~515K words)
14
- - **4,860 property definitions** with types, defaults, and descriptions
15
- - **5,114 commands** in the RouterOS command hierarchy (551 directories, 34K arguments)
16
- - **1,034 callout blocks** warnings, notes, and tips with important caveats
17
- - **144 hardware products** — CPU, RAM, storage, ports, PoE, wireless, license level, pricing
18
- - **2,874 performance benchmarks** ethernet and IPSec throughput test results for 125 devices (64/512/1518-byte packets, multiple routing/bridging modes), plus block diagrams for 110
19
- - **518 YouTube video transcripts**chapter-level transcript segments from MikroTik's official YouTube channel, searchable by topic with timestamped deep links
20
- - **46 RouterOS versions tracked** (7.9 through 7.23beta2) for command history
21
- - **2 MCP CSV resources** for bulk reporting workflows: full benchmark dataset and full device catalog
22
- - Direct links to help.mikrotik.com for every page and section
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
- ### Or configure manually
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
- ### MCP Resources for Reporting
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
- - `rosetta://datasets/device-test-results.csv`
222
- - `rosetta://datasets/devices.csv`
227
+ ## Browse Without AI
223
228
 
224
- In VS Code Copilot, attach them via **Add Context > MCP Resources** or **MCP: Browse Resources**. Use tools for normal search and drill-down; use resources when you explicitly want the whole dataset as CSV.
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
- ## Install from Binary
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
- Download a compiled binary from [Releases](https://github.com/tikoci/rosetta/releases) — no Bun, Node.js, or other runtime needed.
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
- | Platform | File |
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
- ```sh
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
- > **macOS Gatekeeper:** `xattr -d com.apple.quarantine ./rosetta` or System Settings Privacy & Security Allow Anyway.
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 provides 15 tools, designed to work together:
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 with BM25 ranking |
268
- | `routeros_get_page` | Retrieve full page content by ID or title. Section-aware for large pages |
269
- | `routeros_lookup_property` | Look up a property by exact name (type, default, description) |
270
- | `routeros_search_properties` | Search across 4,860 property names and descriptions |
271
- | `routeros_command_tree` | Browse the `/ip/firewall/filter` style command hierarchy |
272
- | `routeros_search_callouts` | Search warnings, notes, and tips across all pages |
273
- | `routeros_search_changelogs` | Search parsed changelog entries — filter by version range, category, breaking changes |
274
- | `routeros_command_version_check` | Check which RouterOS versions include a command |
275
- | `routeros_command_diff` | Diff two RouterOS versions — which command paths were added or removed between them |
276
- | `routeros_device_lookup` | Hardware specs for 144 MikroTik products — filter by architecture, RAM, storage, PoE, wireless, LTE. Includes ethernet/IPSec benchmarks and block diagrams for most devices |
277
- | `routeros_search_tests` | Cross-device performance benchmarks filter by test type, mode, packet size; one call replaces 125+ individual lookups |
278
- | `routeros_search_videos` | Search MikroTik YouTube video transcripts chapter-level results with timestamps and excerpts |
279
- | `routeros_stats` | Database health: page/property/command counts, coverage stats |
280
- | `routeros_current_versions` | Fetch current RouterOS versions from MikroTik (live) |
281
-
282
- The AI assistant typically starts with `routeros_search`, then drills into specific pages, properties, or the command tree based on what it finds. Each tool's description includes workflow hints (e.g., "→ use `routeros_get_page` to read full content") and empty-result suggestions so the AI knows how to chain tools together — this is where most of the tuning effort goes.
283
-
284
- ## Troubleshooting
285
-
286
- | Issue | Solution |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/rosetta",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "RouterOS documentation as SQLite FTS5 — RAG search + command glossary via MCP",
5
5
  "type": "module",
6
6
  "license": "MIT",
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(`── ${remaining} more lines (Enter=next, q=stop) ──`));
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 (const c of results) {
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 breaking = c.is_breaking ? red("⚠ ") : " ";
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() - 22);
596
- out.push(` ${breaking}${cat} ${desc}`);
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
- out.push(` ${cyan(pad(name, 26))} ${dim(pad(alias, 6))} ${desc}`);
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 [type] [mode]", "", "Cross-device benchmarks");
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
- if (parts[0]) filters.test_type = parts[0];
1067
- if (parts[1]) filters.mode = parts[1];
1068
- if (parts[2] && /^\d+$/.test(parts[2])) filters.packet_size = Number.parseInt(parts[2], 10);
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: `${cyan("rosetta")}${dim(">")} `,
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"),
@@ -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 (