@tikoci/rosetta 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/browse.ts +143 -188
- package/src/db.ts +146 -7
- package/src/extract-commands.ts +55 -10
- package/src/extract-devices.ts +5 -1
- package/src/extract-dude.ts +409 -0
- package/src/extract-html.test.ts +67 -0
- package/src/extract-html.ts +232 -112
- package/src/mcp-http.test.ts +7 -5
- package/src/mcp.ts +280 -15
- package/src/paths.ts +1 -1
- package/src/query.test.ts +124 -6
- package/src/query.ts +115 -20
- package/src/release.test.ts +20 -0
package/README.md
CHANGED
|
@@ -269,7 +269,7 @@ Ask your AI assistant questions like:
|
|
|
269
269
|
|
|
270
270
|
## MCP Tools
|
|
271
271
|
|
|
272
|
-
The server exposes
|
|
272
|
+
The server exposes 16 tools designed to work together — agents start with `routeros_search` and drill into specific data as needed:
|
|
273
273
|
|
|
274
274
|
| Tool | What it does |
|
|
275
275
|
|------|-------------|
|
|
@@ -285,6 +285,8 @@ The server exposes 14 tools designed to work together — agents start with `rou
|
|
|
285
285
|
| `routeros_device_lookup` | Hardware specs — filter by architecture, RAM, PoE, wireless, etc. |
|
|
286
286
|
| `routeros_search_tests` | Cross-device ethernet and IPSec benchmarks |
|
|
287
287
|
| `routeros_search_videos` | YouTube transcript search with chapter timestamps |
|
|
288
|
+
| `routeros_dude_search` | FTS across archived Dude wiki docs (separate from RouterOS search) |
|
|
289
|
+
| `routeros_dude_get_page` | Full Dude wiki page by ID or title, with screenshot metadata |
|
|
288
290
|
| `routeros_stats` | Database health and coverage stats |
|
|
289
291
|
| `routeros_current_versions` | Live-fetch current RouterOS versions from MikroTik |
|
|
290
292
|
|
package/package.json
CHANGED
package/src/browse.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
ChangelogResult,
|
|
20
20
|
DeviceResult,
|
|
21
21
|
DeviceTestRow,
|
|
22
|
+
DudeSearchResult,
|
|
22
23
|
SearchResponse,
|
|
23
24
|
SearchResult,
|
|
24
25
|
SectionTocEntry,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
checkCommandVersions,
|
|
30
31
|
diffCommandVersions,
|
|
31
32
|
fetchCurrentVersions,
|
|
33
|
+
getDudePage,
|
|
32
34
|
getPage,
|
|
33
35
|
getTestResultMeta,
|
|
34
36
|
lookupProperty,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
searchChangelogs,
|
|
37
39
|
searchDevices,
|
|
38
40
|
searchDeviceTests,
|
|
41
|
+
searchDude,
|
|
39
42
|
searchPages,
|
|
40
43
|
searchProperties,
|
|
41
44
|
searchVideos,
|
|
@@ -196,6 +199,7 @@ type Context =
|
|
|
196
199
|
| { type: "callouts"; query: string; results: CalloutResult[] }
|
|
197
200
|
| { type: "changelogs"; results: ChangelogResult[] }
|
|
198
201
|
| { type: "videos"; query: string; results: VideoSearchResult[] }
|
|
202
|
+
| { type: "dude"; query: string; results: DudeSearchResult[] }
|
|
199
203
|
| { type: "properties"; query: string; pageId?: number; results: Array<{ name: string; page_id: number; page_title: string }> }
|
|
200
204
|
| { type: "diff" }
|
|
201
205
|
| { type: "vcheck"; path: string };
|
|
@@ -235,6 +239,7 @@ function contextLabel(c: Context): string {
|
|
|
235
239
|
case "callouts": return c.query ? truncate(`cal: "${c.query}"`, 30) : "callouts";
|
|
236
240
|
case "changelogs": return "changelogs";
|
|
237
241
|
case "videos": return truncate(`vid: "${c.query}"`, 30);
|
|
242
|
+
case "dude": return truncate(`dude: "${c.query}"`, 30);
|
|
238
243
|
case "diff": return "diff";
|
|
239
244
|
case "vcheck": return truncate(`vc: ${c.path}`, 30);
|
|
240
245
|
}
|
|
@@ -281,7 +286,7 @@ function renderSearchResults(resp: SearchResponse): string {
|
|
|
281
286
|
|
|
282
287
|
// Navigation hints
|
|
283
288
|
const hints = [
|
|
284
|
-
`${cyan("
|
|
289
|
+
`${cyan("[N]")} view page`,
|
|
285
290
|
`${cyan("[s <query>]")} search`,
|
|
286
291
|
`${cyan("[p <query>]")} properties`,
|
|
287
292
|
`${cyan("[cmd <path>]")} commands`,
|
|
@@ -374,7 +379,7 @@ function renderPage(page: NonNullable<ReturnType<typeof getPage>>): string {
|
|
|
374
379
|
// Navigation hints
|
|
375
380
|
out.push("");
|
|
376
381
|
const hints: string[] = [];
|
|
377
|
-
if (page.sections && page.sections.length > 0) hints.push(`${cyan("
|
|
382
|
+
if (page.sections && page.sections.length > 0) hints.push(`${cyan("[N]")} section`);
|
|
378
383
|
hints.push(`${cyan("[p]")} properties`);
|
|
379
384
|
hints.push(`${cyan("[cmd]")} command tree`);
|
|
380
385
|
hints.push(`${cyan("[cal]")} callouts`);
|
|
@@ -480,7 +485,7 @@ function renderDeviceResults(results: DeviceResult[], mode: string, total: numbe
|
|
|
480
485
|
out.push("");
|
|
481
486
|
}
|
|
482
487
|
|
|
483
|
-
out.push(` ${cyan("
|
|
488
|
+
out.push(` ${cyan("[N]")} view device ${cyan("[tests]")} benchmarks ${cyan("[b]")} back`);
|
|
484
489
|
return out.join("\n");
|
|
485
490
|
}
|
|
486
491
|
|
|
@@ -529,24 +534,17 @@ function renderDeviceCard(d: DeviceResult): string {
|
|
|
529
534
|
out.push(` ${dim("Block diagram:")} ${cyan(link(d.block_diagram_url, "view"))}`);
|
|
530
535
|
}
|
|
531
536
|
|
|
532
|
-
// Test results (attached for exact matches)
|
|
537
|
+
// Test results (attached for exact matches)
|
|
533
538
|
if (d.test_results && d.test_results.length > 0) {
|
|
534
|
-
const pkt512 = d.test_results.filter((t) => t.packet_size === 512);
|
|
535
|
-
const displayTests = pkt512.length > 0 ? pkt512 : d.test_results.slice(0, 20);
|
|
536
|
-
const pktNote = pkt512.length > 0 ? " (512B baseline)" : "";
|
|
537
539
|
out.push("");
|
|
538
|
-
out.push(` ${bold("Benchmarks:")} ${dim(`(${
|
|
539
|
-
for (const t of
|
|
540
|
+
out.push(` ${bold("Benchmarks:")} ${dim(`(${d.test_results.length} tests)`)}`);
|
|
541
|
+
for (const t of d.test_results.slice(0, 12)) {
|
|
540
542
|
const mbps = t.throughput_mbps ? `${fmt(t.throughput_mbps)} Mbps` : "";
|
|
541
543
|
const kpps = t.throughput_kpps ? `${fmt(t.throughput_kpps)} Kpps` : "";
|
|
542
544
|
out.push(` ${dim(pad(t.test_type, 9))} ${pad(t.mode, 16)} ${dim(pad(t.configuration, 28))} ${pad(`${t.packet_size}B`, 6)} ${bold(mbps)} ${dim(kpps)}`);
|
|
543
545
|
}
|
|
544
|
-
if (d.test_results.length >
|
|
545
|
-
|
|
546
|
-
d.test_results.filter((t) => t.packet_size !== 512).map((t) => t.packet_size),
|
|
547
|
-
)].sort((a, b) => a - b);
|
|
548
|
-
const sizesStr = otherSizes.map((s) => `${s}B`).join(", ");
|
|
549
|
-
out.push(` ${dim(`Also at ${sizesStr} — use`)} ${cyan("tests")} ${dim("<type> <size> for other packet sizes")}`);
|
|
546
|
+
if (d.test_results.length > 12) {
|
|
547
|
+
out.push(` ${dim(`... and ${d.test_results.length - 12} more (use`)} ${cyan("tests")} ${dim("for full listing)")}`);
|
|
550
548
|
}
|
|
551
549
|
}
|
|
552
550
|
|
|
@@ -637,7 +635,7 @@ function renderChangelogs(results: ChangelogResult[]): string {
|
|
|
637
635
|
}
|
|
638
636
|
|
|
639
637
|
out.push("");
|
|
640
|
-
out.push(` ${cyan("[cl
|
|
638
|
+
out.push(` ${cyan("[cl breaking]")} breaking only ${cyan("[cl <ver>]")} specific version ${cyan("[b]")} back`);
|
|
641
639
|
return out.join("\n");
|
|
642
640
|
}
|
|
643
641
|
|
|
@@ -669,6 +667,65 @@ function renderVideos(results: VideoSearchResult[]): string {
|
|
|
669
667
|
return out.join("\n");
|
|
670
668
|
}
|
|
671
669
|
|
|
670
|
+
function renderDudeResults(results: DudeSearchResult[]): string {
|
|
671
|
+
const out: string[] = [];
|
|
672
|
+
if (results.length === 0) {
|
|
673
|
+
out.push(` ${dim("No Dude wiki results found.")}`);
|
|
674
|
+
return out.join("\n");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
for (let i = 0; i < results.length; i++) {
|
|
678
|
+
const d = results[i];
|
|
679
|
+
const num = dim(`${String(i + 1).padStart(3)} `);
|
|
680
|
+
const title = bold(truncate(d.title, 60));
|
|
681
|
+
const ver = dim(`[${d.version}]`);
|
|
682
|
+
const imgs = d.image_count > 0 ? dim(`📷 ${d.image_count}`) : "";
|
|
683
|
+
out.push(`${num}${title} ${ver} ${imgs}`);
|
|
684
|
+
out.push(` ${dim(d.path)}`);
|
|
685
|
+
const excerpt = d.excerpt.replace(/\*\*/g, `${ESC}[1m`);
|
|
686
|
+
out.push(` ${dim(truncate(excerpt, termWidth() - 8))}`);
|
|
687
|
+
out.push("");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
out.push(` ${cyan("[<n>]")} view page ${cyan("[s <query>]")} search docs ${cyan("[b]")} back`);
|
|
691
|
+
return out.join("\n");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function renderDudePage(page: import("./query.ts").DudePageResult): string {
|
|
695
|
+
const out: string[] = [];
|
|
696
|
+
out.push(` ${bold(page.title)} ${dim(`[${page.version}]`)}`);
|
|
697
|
+
out.push(` ${dim(page.path)}`);
|
|
698
|
+
out.push(` ${cyan(link(page.url))}`);
|
|
699
|
+
if (page.wayback_url) out.push(` ${dim(`Archived: ${page.wayback_url}`)}`);
|
|
700
|
+
out.push("");
|
|
701
|
+
|
|
702
|
+
if (page.images.length > 0) {
|
|
703
|
+
out.push(` ${bold("Screenshots:")} ${page.images.length}`);
|
|
704
|
+
for (const img of page.images) {
|
|
705
|
+
out.push(` ${dim("•")} ${img.filename}${img.caption ? ` ${dim(img.caption)}` : ""}`);
|
|
706
|
+
out.push(` ${dim(img.local_path)}`);
|
|
707
|
+
}
|
|
708
|
+
out.push("");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Truncate long pages for TUI display
|
|
712
|
+
const maxChars = 8000;
|
|
713
|
+
const text = page.text.length > maxChars
|
|
714
|
+
? `${page.text.slice(0, maxChars)}\n\n ${dim(`... truncated (${page.text.length} chars total)`)}`
|
|
715
|
+
: page.text;
|
|
716
|
+
out.push(text);
|
|
717
|
+
|
|
718
|
+
if (page.code) {
|
|
719
|
+
out.push("");
|
|
720
|
+
out.push(` ${bold("Code:")}`);
|
|
721
|
+
out.push(page.code.length > 2000 ? `${page.code.slice(0, 2000)}\n ${dim("... truncated")}` : page.code);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
out.push("");
|
|
725
|
+
out.push(` ${cyan("[dude <query>]")} search dude ${cyan("[s <query>]")} search docs ${cyan("[b]")} back`);
|
|
726
|
+
return out.join("\n");
|
|
727
|
+
}
|
|
728
|
+
|
|
672
729
|
function renderDiff(result: ReturnType<typeof diffCommandVersions>): string {
|
|
673
730
|
const out: string[] = [];
|
|
674
731
|
out.push(` ${bold("Command diff:")} ${result.from_version} → ${result.to_version}`);
|
|
@@ -728,6 +785,13 @@ function renderStats(): string {
|
|
|
728
785
|
const out: string[] = [];
|
|
729
786
|
out.push(` ${bold("Database Statistics")}`);
|
|
730
787
|
out.push(` ${dim("Path:")} ${stats.db_path}`);
|
|
788
|
+
if (stats.db_size_bytes != null) {
|
|
789
|
+
const mb = stats.db_size_bytes / (1024 * 1024);
|
|
790
|
+
out.push(` ${dim("Size:")} ${mb.toFixed(1)} MB`);
|
|
791
|
+
}
|
|
792
|
+
if (stats.schema_version != null) {
|
|
793
|
+
out.push(` ${dim("Schema:")} v${stats.schema_version}`);
|
|
794
|
+
}
|
|
731
795
|
out.push(` ${dim("Export:")} ${stats.doc_export}`);
|
|
732
796
|
out.push("");
|
|
733
797
|
|
|
@@ -771,10 +835,11 @@ function renderHelp(): string {
|
|
|
771
835
|
cmd("props <query>", "sp", "Search properties by FTS", "routeros_search_properties");
|
|
772
836
|
cmd("cmd [path]", "tree", "Browse command tree", "routeros_command_tree");
|
|
773
837
|
cmd("device <query>", "dev", "Look up device specs", "routeros_device_lookup");
|
|
774
|
-
cmd("tests [device]
|
|
838
|
+
cmd("tests [device] [type]", "", "Cross-device benchmarks", "routeros_search_tests");
|
|
775
839
|
cmd("callouts [query]", "cal", "Search callouts (type filter: cal warning)", "routeros_search_callouts");
|
|
776
840
|
cmd("changelog [query]", "cl", "Search changelogs (cl 7.22, cl breaking)", "routeros_search_changelogs");
|
|
777
841
|
cmd("videos <query>", "vid", "Search video transcripts", "routeros_search_videos");
|
|
842
|
+
cmd("dude <query>", "", "Search archived Dude wiki docs", "routeros_dude_search");
|
|
778
843
|
cmd("diff <from> <to> [path]", "", "Command tree diff between versions", "routeros_command_diff");
|
|
779
844
|
cmd("vcheck <path>", "vc", "Version range for a command path", "routeros_command_version_check");
|
|
780
845
|
cmd("versions", "ver", "Live-fetch current RouterOS versions", "routeros_current_versions");
|
|
@@ -791,69 +856,8 @@ function renderHelp(): string {
|
|
|
791
856
|
return out.join("\n");
|
|
792
857
|
}
|
|
793
858
|
|
|
794
|
-
/** Fetch the full transcript text for a specific video segment. */
|
|
795
|
-
function getVideoSegmentTranscript(youtubeVideoId: string, startS: number): string | null {
|
|
796
|
-
try {
|
|
797
|
-
const row = db
|
|
798
|
-
.prepare(
|
|
799
|
-
`SELECT vs.transcript FROM video_segments vs
|
|
800
|
-
JOIN videos v ON v.id = vs.video_id
|
|
801
|
-
WHERE v.video_id = ? AND vs.start_s = ? LIMIT 1`,
|
|
802
|
-
)
|
|
803
|
-
.get(youtubeVideoId, startS) as { transcript: string } | null;
|
|
804
|
-
return row?.transcript ?? null;
|
|
805
|
-
} catch {
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
859
|
// ── Command dispatcher ──
|
|
811
860
|
|
|
812
|
-
/** Re-render the current context after going back — uses cached results where possible. */
|
|
813
|
-
async function renderCurrentCtx(): Promise<void> {
|
|
814
|
-
switch (ctx.type) {
|
|
815
|
-
case "home":
|
|
816
|
-
console.log(renderWelcome());
|
|
817
|
-
return;
|
|
818
|
-
case "search":
|
|
819
|
-
await paged(renderSearchResults(ctx.response));
|
|
820
|
-
return;
|
|
821
|
-
case "commands": {
|
|
822
|
-
const children = browseCommands(ctx.path);
|
|
823
|
-
if (children.length > 0) await paged(renderCommandTree(ctx.path, children));
|
|
824
|
-
else console.log(dim(` No children at "${ctx.path}".`));
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
case "devices":
|
|
828
|
-
await paged(renderDeviceResults(ctx.results, "cached", ctx.results.length));
|
|
829
|
-
return;
|
|
830
|
-
case "device":
|
|
831
|
-
await paged(renderDeviceCard(ctx.device));
|
|
832
|
-
return;
|
|
833
|
-
case "callouts":
|
|
834
|
-
if (ctx.results.length > 0) await paged(renderCallouts(ctx.results));
|
|
835
|
-
return;
|
|
836
|
-
case "changelogs":
|
|
837
|
-
if (ctx.results.length > 0) await paged(renderChangelogs(ctx.results));
|
|
838
|
-
return;
|
|
839
|
-
case "videos":
|
|
840
|
-
if (ctx.results.length > 0) await paged(renderVideos(ctx.results));
|
|
841
|
-
return;
|
|
842
|
-
case "page": {
|
|
843
|
-
const page = getPage(ctx.pageId);
|
|
844
|
-
if (page) await paged(renderPage(page));
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
case "sections": {
|
|
848
|
-
const page = getPage(ctx.pageId);
|
|
849
|
-
if (page) await paged(renderPage(page));
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
default:
|
|
853
|
-
console.log(dim(` In ${ctx.type} context. Type a command or help.`));
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
861
|
async function dispatch(input: string): Promise<void> {
|
|
858
862
|
const trimmed = input.trim();
|
|
859
863
|
if (!trimmed) return;
|
|
@@ -888,8 +892,7 @@ async function dispatch(input: string): Promise<void> {
|
|
|
888
892
|
if (!popCtx()) {
|
|
889
893
|
console.log(dim(" Already at top."));
|
|
890
894
|
} else {
|
|
891
|
-
console.log(dim(` ← ${ctx.type}`));
|
|
892
|
-
await renderCurrentCtx();
|
|
895
|
+
console.log(dim(` ← back to ${ctx.type}`));
|
|
893
896
|
}
|
|
894
897
|
return;
|
|
895
898
|
|
|
@@ -904,20 +907,7 @@ async function dispatch(input: string): Promise<void> {
|
|
|
904
907
|
return;
|
|
905
908
|
|
|
906
909
|
case "page": {
|
|
907
|
-
if (!rest) {
|
|
908
|
-
// From commands context: navigate to the linked page
|
|
909
|
-
if (ctx.type === "commands") {
|
|
910
|
-
const row = db.prepare("SELECT page_id FROM commands WHERE path = ? LIMIT 1").get(ctx.path) as { page_id: number | null } | null;
|
|
911
|
-
if (row?.page_id) {
|
|
912
|
-
await doPage(String(row.page_id));
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
console.log(dim(` No linked page for ${ctx.path}.`));
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
console.log(dim(" Usage: page <id|title>"));
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
910
|
+
if (!rest) { console.log(dim(" Usage: page <id|title>")); return; }
|
|
921
911
|
await doPage(rest);
|
|
922
912
|
return;
|
|
923
913
|
}
|
|
@@ -925,12 +915,13 @@ async function dispatch(input: string): Promise<void> {
|
|
|
925
915
|
case "p":
|
|
926
916
|
case "prop": {
|
|
927
917
|
if (!rest) {
|
|
928
|
-
// Context-scoped: show properties for current page
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
918
|
+
// Context-scoped: show properties for current page
|
|
919
|
+
if (ctx.type === "page") {
|
|
920
|
+
const page = getPage(ctx.pageId, 0); // just get metadata
|
|
921
|
+
if (page) {
|
|
922
|
+
await doPropsForPage(ctx.pageId, ctx.title);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
934
925
|
}
|
|
935
926
|
console.log(dim(" Usage: prop <name> — or navigate to a page first"));
|
|
936
927
|
return;
|
|
@@ -948,15 +939,8 @@ async function dispatch(input: string): Promise<void> {
|
|
|
948
939
|
|
|
949
940
|
case "cmd":
|
|
950
941
|
case "tree": {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
// Re-display current commands context
|
|
954
|
-
cmdPath = ctx.path;
|
|
955
|
-
} else if (cmdPath && !cmdPath.startsWith("/") && ctx.type === "commands") {
|
|
956
|
-
// Relative path: resolve against current commands path
|
|
957
|
-
cmdPath = `${ctx.path}/${cmdPath}`;
|
|
958
|
-
}
|
|
959
|
-
await doCommandTree(cmdPath);
|
|
942
|
+
const path = rest || (ctx.type === "commands" ? ctx.path : "");
|
|
943
|
+
await doCommandTree(path);
|
|
960
944
|
return;
|
|
961
945
|
}
|
|
962
946
|
|
|
@@ -974,24 +958,13 @@ async function dispatch(input: string): Promise<void> {
|
|
|
974
958
|
|
|
975
959
|
case "cal":
|
|
976
960
|
case "callouts": {
|
|
977
|
-
if (!rest) {
|
|
978
|
-
//
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
`SELECT c.type, c.content, p.title as page_title, p.url as page_url,
|
|
985
|
-
c.page_id, c.content as excerpt
|
|
986
|
-
FROM callouts c JOIN pages p ON p.id = c.page_id
|
|
987
|
-
WHERE c.page_id = ? ORDER BY c.sort_order`,
|
|
988
|
-
).all(pageId) as CalloutResult[];
|
|
989
|
-
if (pageCallouts.length > 0) {
|
|
990
|
-
await paged(renderCallouts(pageCallouts));
|
|
991
|
-
pushCtx({ type: "callouts", query: "", results: pageCallouts });
|
|
992
|
-
} else {
|
|
993
|
-
console.log(dim(" No callouts for this page."));
|
|
994
|
-
}
|
|
961
|
+
if (!rest && ctx.type === "page") {
|
|
962
|
+
// Show callouts for current page
|
|
963
|
+
const results = searchCallouts("", undefined, 50);
|
|
964
|
+
const pageCallouts = results.filter((c) => c.page_id === (ctx as { pageId: number }).pageId);
|
|
965
|
+
if (pageCallouts.length > 0) {
|
|
966
|
+
await paged(renderCallouts(pageCallouts));
|
|
967
|
+
pushCtx({ type: "callouts", query: "", results: pageCallouts });
|
|
995
968
|
return;
|
|
996
969
|
}
|
|
997
970
|
}
|
|
@@ -1006,13 +979,18 @@ async function dispatch(input: string): Promise<void> {
|
|
|
1006
979
|
}
|
|
1007
980
|
|
|
1008
981
|
case "vid":
|
|
1009
|
-
case "video":
|
|
1010
982
|
case "videos": {
|
|
1011
983
|
if (!rest) { console.log(dim(" Usage: videos <query>")); return; }
|
|
1012
984
|
await doSearchVideos(rest);
|
|
1013
985
|
return;
|
|
1014
986
|
}
|
|
1015
987
|
|
|
988
|
+
case "dude": {
|
|
989
|
+
if (!rest) { console.log(dim(" Usage: dude <query>")); return; }
|
|
990
|
+
await doSearchDude(rest);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
1016
994
|
case "diff": {
|
|
1017
995
|
const diffParts = rest.split(/\s+/);
|
|
1018
996
|
if (diffParts.length < 2) {
|
|
@@ -1077,18 +1055,18 @@ async function handleNumberSelect(idx: number): Promise<void> {
|
|
|
1077
1055
|
if (ctx.type === "videos" && ctx.results[idx]) {
|
|
1078
1056
|
const v = ctx.results[idx];
|
|
1079
1057
|
const timeUrl = v.start_s > 0 ? `${v.url}&t=${v.start_s}` : v.url;
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1058
|
+
console.log(`\n ${bold(v.title)}`);
|
|
1059
|
+
if (v.chapter_title) console.log(` ${magenta(`§ ${v.chapter_title}`)} ${dim(`@ ${formatTime(v.start_s)}`)}`);
|
|
1060
|
+
console.log(` ${cyan(link(timeUrl))}\n`);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (ctx.type === "dude" && ctx.results[idx]) {
|
|
1064
|
+
const d = ctx.results[idx];
|
|
1065
|
+
const page = getDudePage(d.id);
|
|
1066
|
+
if (page) {
|
|
1067
|
+
await paged(renderDudePage(page));
|
|
1068
|
+
pushCtx({ type: "dude", query: ctx.query, results: ctx.results });
|
|
1090
1069
|
}
|
|
1091
|
-
await paged(out.join("\n"));
|
|
1092
1070
|
return;
|
|
1093
1071
|
}
|
|
1094
1072
|
if (ctx.type === "properties" && ctx.results[idx]) {
|
|
@@ -1215,64 +1193,33 @@ async function doDeviceLookup(query: string): Promise<void> {
|
|
|
1215
1193
|
}
|
|
1216
1194
|
|
|
1217
1195
|
async function doTests(argsStr: string): Promise<void> {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
// Parse args if provided
|
|
1222
|
-
if (argsStr) {
|
|
1223
|
-
const knownTypes = ["ethernet", "ipsec"];
|
|
1224
|
-
let offset = 0;
|
|
1225
|
-
if (parts[0] && !knownTypes.includes(parts[0].toLowerCase())) {
|
|
1226
|
-
filters.device = parts[0];
|
|
1227
|
-
offset = 1;
|
|
1228
|
-
}
|
|
1229
|
-
if (parts[offset]) filters.test_type = parts[offset];
|
|
1230
|
-
// packet_size is always numeric; mode can be multi-word — identify by type
|
|
1231
|
-
const remaining = parts.slice(offset + 1);
|
|
1232
|
-
const pktIdx = remaining.findIndex((p) => /^\d+$/.test(p));
|
|
1233
|
-
if (pktIdx !== -1) {
|
|
1234
|
-
filters.packet_size = Number.parseInt(remaining[pktIdx], 10);
|
|
1235
|
-
const modeParts = [...remaining.slice(0, pktIdx), ...remaining.slice(pktIdx + 1)];
|
|
1236
|
-
if (modeParts.length > 0) filters.mode = modeParts.join(" ");
|
|
1237
|
-
} else if (remaining.length > 0) {
|
|
1238
|
-
filters.mode = remaining.join(" ");
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// Auto-inject device name from context when in a device view and none specified
|
|
1243
|
-
if (!filters.device && ctx.type === "device") {
|
|
1244
|
-
filters.device = ctx.device.product_name;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// Show help if no filters could be determined
|
|
1248
|
-
if (!argsStr && !filters.device) {
|
|
1196
|
+
if (!argsStr) {
|
|
1197
|
+
// Show available filter values
|
|
1249
1198
|
const meta = getTestResultMeta();
|
|
1250
1199
|
console.log(` ${bold("Test filters:")}`);
|
|
1251
1200
|
console.log(` ${dim("Types:")} ${meta.test_types.join(", ")}`);
|
|
1252
1201
|
console.log(` ${dim("Modes:")} ${meta.modes.join(", ")}`);
|
|
1253
1202
|
console.log(` ${dim("Packet sizes:")} ${meta.packet_sizes.join(", ")}`);
|
|
1254
1203
|
console.log("");
|
|
1255
|
-
console.log(` ${dim("Usage: tests [device] <type> [
|
|
1204
|
+
console.log(` ${dim("Usage: tests [device] <type> [mode] [packet_size]")}`);
|
|
1256
1205
|
console.log(` ${dim("Example: tests ethernet Routing 1518")}`);
|
|
1257
1206
|
console.log(` ${dim("Example: tests rb5009 ethernet 1518")}`);
|
|
1258
1207
|
return;
|
|
1259
1208
|
}
|
|
1260
1209
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const validSizes = [64, 512, 1400, 1518];
|
|
1264
|
-
const size = Number(filters.packet_size);
|
|
1265
|
-
const rounded = validSizes.reduce((prev, curr) =>
|
|
1266
|
-
Math.abs(curr - size) < Math.abs(prev - size) ? curr : prev,
|
|
1267
|
-
);
|
|
1268
|
-
if (rounded !== size) {
|
|
1269
|
-
console.log(dim(` Rounding ${size}B → ${rounded}B (nearest test packet size)`));
|
|
1270
|
-
filters.packet_size = rounded;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1210
|
+
const parts = argsStr.split(/\s+/);
|
|
1211
|
+
const filters: Record<string, string | number> = {};
|
|
1273
1212
|
|
|
1274
|
-
//
|
|
1275
|
-
|
|
1213
|
+
// Known test types — if parts[0] is not a known type, treat it as a device filter
|
|
1214
|
+
const knownTypes = ["ethernet", "ipsec"];
|
|
1215
|
+
let offset = 0;
|
|
1216
|
+
if (parts[0] && !knownTypes.includes(parts[0].toLowerCase())) {
|
|
1217
|
+
filters.device = parts[0];
|
|
1218
|
+
offset = 1;
|
|
1219
|
+
}
|
|
1220
|
+
if (parts[offset]) filters.test_type = parts[offset];
|
|
1221
|
+
if (parts[offset + 1]) filters.mode = parts[offset + 1];
|
|
1222
|
+
if (parts[offset + 2] && /^\d+$/.test(parts[offset + 2])) filters.packet_size = Number.parseInt(parts[offset + 2], 10);
|
|
1276
1223
|
|
|
1277
1224
|
const result = searchDeviceTests(filters);
|
|
1278
1225
|
if (result.results.length === 0) {
|
|
@@ -1355,6 +1302,16 @@ async function doSearchVideos(query: string): Promise<void> {
|
|
|
1355
1302
|
pushCtx({ type: "videos", query, results });
|
|
1356
1303
|
}
|
|
1357
1304
|
|
|
1305
|
+
async function doSearchDude(query: string): Promise<void> {
|
|
1306
|
+
const results = searchDude(query, 10);
|
|
1307
|
+
if (results.length === 0) {
|
|
1308
|
+
console.log(dim(` No Dude wiki results for "${query}".`));
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
await paged(` ${bold(String(results.length))} Dude wiki results for ${cyan(`"${query}"`)}\n\n${renderDudeResults(results)}`);
|
|
1312
|
+
pushCtx({ type: "dude", query, results });
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1358
1315
|
async function doDiff(from: string, to: string, pathPrefix?: string): Promise<void> {
|
|
1359
1316
|
const result = diffCommandVersions(from, to, pathPrefix);
|
|
1360
1317
|
await paged(renderDiff(result));
|
|
@@ -1379,6 +1336,7 @@ async function doCurrentVersions(): Promise<void> {
|
|
|
1379
1336
|
const ch = pad(channel, 14);
|
|
1380
1337
|
out.push(` ${dim(ch)} ${bold(String(v))}`);
|
|
1381
1338
|
}
|
|
1339
|
+
out.push(` ${dim(pad("winbox 4", 14))} ${bold(String(result.winbox ?? dim("unavailable")))}`);
|
|
1382
1340
|
console.log(out.join("\n"));
|
|
1383
1341
|
}
|
|
1384
1342
|
|
|
@@ -1429,9 +1387,6 @@ async function main() {
|
|
|
1429
1387
|
} catch (err) {
|
|
1430
1388
|
console.error(red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1431
1389
|
}
|
|
1432
|
-
// Clear any keys that leaked from the pager into readline's internal line buffer
|
|
1433
|
-
// (the key pressed to exit paging — q/SPACE/ENTER — can appear in the next prompt)
|
|
1434
|
-
rl.write(null as unknown as string, { ctrl: true, name: "u" });
|
|
1435
1390
|
rl.setPrompt(buildPrompt());
|
|
1436
1391
|
rl.prompt();
|
|
1437
1392
|
});
|