@usex/mikrotik-mcp 2.1.0 → 2.3.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/dist/cli.js CHANGED
@@ -29,6 +29,10 @@ var DeviceConfigSchema = z.object({
29
29
  privateKey: z.string().optional(),
30
30
  keyPassphrase: z.string().optional(),
31
31
  timeoutMs: z.coerce.number().int().positive().default(1e4),
32
+ mac: z.string().optional(),
33
+ sourceMac: z.string().optional(),
34
+ macHost: z.string().optional(),
35
+ macPort: z.coerce.number().int().positive().optional(),
32
36
  description: z.string().optional()
33
37
  });
34
38
  var S3ConfigSchema = z.object({
@@ -116,7 +120,11 @@ function loadConfig(argv = process.argv.slice(2)) {
116
120
  keyFilename: pick("key-filename", "MIKROTIK_KEY_FILENAME"),
117
121
  privateKey: pick("private-key", "MIKROTIK_PRIVATE_KEY"),
118
122
  keyPassphrase: pick("key-passphrase", "MIKROTIK_KEY_PASSPHRASE"),
119
- timeoutMs: pick("timeout-ms", "MIKROTIK_TIMEOUT_MS")
123
+ timeoutMs: pick("timeout-ms", "MIKROTIK_TIMEOUT_MS"),
124
+ mac: pick("mac", "MIKROTIK_MAC"),
125
+ sourceMac: pick("source-mac", "MIKROTIK_SOURCE_MAC"),
126
+ macHost: pick("mac-host", "MIKROTIK_MAC_HOST"),
127
+ macPort: pick("mac-port", "MIKROTIK_MAC_PORT")
120
128
  };
121
129
  const hasSingle = Object.values(single).some((v) => v !== undefined);
122
130
  const devices = {};
@@ -250,6 +258,340 @@ var logger = {
250
258
  error: (m) => emit("error", m)
251
259
  };
252
260
 
261
+ // src/mac-telnet/console.ts
262
+ import { MacTelnetSession } from "@tikoci/centrs/protocols";
263
+ var ESC = "\x1B";
264
+ var enc = new TextEncoder;
265
+ var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
266
+ var ANSI_ESC2 = new RegExp(`${ESC}[@-_]`, "g");
267
+ var ROUTEROS_PROMPT_RE = /\[[^\]@\r\n]+@[^\]\r\n]*\][^\r\n]*>\s*$/;
268
+ var TICK_INTERVAL_MS = 15;
269
+ var LICENSE_RE = /do you want to see the software license/i;
270
+ function emulateScreen(text) {
271
+ const clean = text.replace(ANSI_CSI, "").replace(ANSI_ESC2, "");
272
+ const lines = [[]];
273
+ let row = 0;
274
+ let col = 0;
275
+ for (const ch of clean) {
276
+ const code = ch.charCodeAt(0);
277
+ if (ch === `
278
+ `) {
279
+ row += 1;
280
+ if (!lines[row])
281
+ lines[row] = [];
282
+ col = 0;
283
+ } else if (ch === "\r") {
284
+ col = 0;
285
+ } else if (code === 8) {
286
+ col = Math.max(0, col - 1);
287
+ } else if (code >= 32 && code !== 127) {
288
+ const line = lines[row];
289
+ line[col] = ch;
290
+ col += 1;
291
+ }
292
+ }
293
+ return lines.map((line) => Array.from(line, (c) => c ?? " ").join("").replace(/\s+$/, ""));
294
+ }
295
+ function extractCommandOutput(raw, command) {
296
+ const lines = emulateScreen(raw);
297
+ let start = 1;
298
+ if (command !== undefined && lines.length > 0) {
299
+ const first = lines[0] ?? "";
300
+ const promptMatch = first.match(/^\[[^\]\r\n]*\][^\r\n]*?>\s?/);
301
+ const echoedOnFirst = promptMatch ? first.slice(promptMatch[0].length) : first;
302
+ let consumed = echoedOnFirst.length;
303
+ while (consumed < command.length && start < lines.length) {
304
+ consumed += (lines[start] ?? "").length;
305
+ start += 1;
306
+ }
307
+ }
308
+ const body = lines.slice(start);
309
+ while (body.length > 0) {
310
+ const last = body[body.length - 1];
311
+ if (last.length === 0 || ROUTEROS_PROMPT_RE.test(last)) {
312
+ body.pop();
313
+ continue;
314
+ }
315
+ break;
316
+ }
317
+ return body.join(`
318
+ `);
319
+ }
320
+
321
+ class MacTelnetConsole {
322
+ options;
323
+ session;
324
+ buffer = "";
325
+ ready = false;
326
+ closed = false;
327
+ closeError;
328
+ waiter;
329
+ tickTimer;
330
+ readyWaiters = [];
331
+ decoder = new TextDecoder;
332
+ probeTail = "";
333
+ constructor(options) {
334
+ this.options = {
335
+ rows: 9999,
336
+ cols: 512,
337
+ primeTimeoutMs: 30000,
338
+ commandTimeoutMs: 15000,
339
+ settleMs: 150,
340
+ acceptLicense: true,
341
+ ...options
342
+ };
343
+ this.session = new MacTelnetSession({
344
+ sink: options.sink,
345
+ sourceMac: options.sourceMac,
346
+ destinationMac: options.destinationMac,
347
+ username: options.username,
348
+ password: options.password,
349
+ sessionKey: options.sessionKey,
350
+ terminalType: "vt102",
351
+ terminalWidth: this.options.cols,
352
+ terminalHeight: this.options.rows,
353
+ onReady: () => this.onReady(),
354
+ onData: (bytes) => this.onData(bytes),
355
+ onClose: (error) => this.onClose(error)
356
+ });
357
+ }
358
+ handlePacket(bytes) {
359
+ try {
360
+ this.session.handlePacket(bytes);
361
+ } catch (error) {
362
+ this.onClose(error instanceof Error ? error : new Error("Failed to process a MAC-Telnet datagram."));
363
+ }
364
+ }
365
+ async open() {
366
+ this.tickTimer = setInterval(() => {
367
+ try {
368
+ this.session.tick(Date.now());
369
+ } catch {}
370
+ }, TICK_INTERVAL_MS);
371
+ this.tickTimer.unref?.();
372
+ this.session.start();
373
+ await this.waitReady(this.options.primeTimeoutMs);
374
+ await this.waitFor((buffer) => this.endsWithPrompt(buffer) || LICENSE_RE.test(buffer), this.options.primeTimeoutMs, "waiting for the RouterOS console prompt");
375
+ if (this.options.acceptLicense && LICENSE_RE.test(this.buffer)) {
376
+ this.buffer = "";
377
+ this.session.sendInput(enc.encode("n\r"));
378
+ await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.primeTimeoutMs, "waiting for the prompt after the license screen");
379
+ }
380
+ this.buffer = "";
381
+ this.session.sendInput(enc.encode("\r"));
382
+ await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.commandTimeoutMs, "waiting for a clean prompt");
383
+ this.buffer = "";
384
+ }
385
+ async run(cli) {
386
+ this.assertOpen();
387
+ this.buffer = "";
388
+ this.session.sendInput(enc.encode(`${cli}\r`));
389
+ await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.commandTimeoutMs, `running over mac-telnet: ${cli}`);
390
+ const raw = this.buffer;
391
+ return { output: extractCommandOutput(raw, cli), raw };
392
+ }
393
+ close() {
394
+ if (this.tickTimer) {
395
+ clearInterval(this.tickTimer);
396
+ this.tickTimer = undefined;
397
+ }
398
+ if (!this.closed)
399
+ this.session.end();
400
+ }
401
+ get isReady() {
402
+ return this.ready && !this.closed;
403
+ }
404
+ onReady() {
405
+ this.ready = true;
406
+ for (const w of this.readyWaiters.splice(0))
407
+ w.resolve();
408
+ }
409
+ onData(bytes) {
410
+ const chunk = this.decoder.decode(bytes, { stream: true });
411
+ const combined = `${this.probeTail}${chunk}`;
412
+ this.answerSizeProbe(combined);
413
+ this.probeTail = combined.slice(-3);
414
+ this.buffer += chunk;
415
+ this.checkWaiter();
416
+ }
417
+ onClose(error) {
418
+ this.closed = true;
419
+ this.closeError = error;
420
+ if (this.tickTimer) {
421
+ clearInterval(this.tickTimer);
422
+ this.tickTimer = undefined;
423
+ }
424
+ const failure = error ?? new Error("The MAC-Telnet console session closed.");
425
+ for (const w of this.readyWaiters.splice(0))
426
+ w.reject(failure);
427
+ if (this.waiter) {
428
+ const waiter = this.waiter;
429
+ this.waiter = undefined;
430
+ clearTimeout(waiter.timeout);
431
+ if (waiter.settle)
432
+ clearTimeout(waiter.settle);
433
+ waiter.reject(failure);
434
+ }
435
+ }
436
+ answerSizeProbe(chunk) {
437
+ if (chunk.includes(`${ESC}[6n`)) {
438
+ this.session.sendInput(enc.encode(`${ESC}[${this.options.rows};${this.options.cols}R`));
439
+ }
440
+ if (chunk.includes(`${ESC}Z`) || chunk.includes(`${ESC}[c`)) {
441
+ this.session.sendInput(enc.encode(`${ESC}[?6c`));
442
+ }
443
+ }
444
+ endsWithPrompt(buffer) {
445
+ const lines = emulateScreen(buffer).filter((line) => line.length > 0);
446
+ return ROUTEROS_PROMPT_RE.test(lines[lines.length - 1] ?? "");
447
+ }
448
+ waitReady(timeoutMs) {
449
+ if (this.ready)
450
+ return Promise.resolve();
451
+ if (this.closed) {
452
+ return Promise.reject(this.closeError ?? new Error("The MAC-Telnet session closed before login completed."));
453
+ }
454
+ return new Promise((resolve, reject) => {
455
+ let timer;
456
+ const entry = {
457
+ resolve: () => {
458
+ clearTimeout(timer);
459
+ resolve();
460
+ },
461
+ reject: (error) => {
462
+ clearTimeout(timer);
463
+ reject(error);
464
+ }
465
+ };
466
+ timer = setTimeout(() => {
467
+ this.readyWaiters = this.readyWaiters.filter((w) => w !== entry);
468
+ reject(new Error("MAC-Telnet login did not complete \u2014 no console response from the device. " + "Confirm the device is reachable over mac-telnet (mac-server interface list) and the credentials are correct."));
469
+ }, timeoutMs);
470
+ this.readyWaiters.push(entry);
471
+ });
472
+ }
473
+ waitFor(predicate, timeoutMs, label) {
474
+ if (this.closed) {
475
+ return Promise.reject(this.closeError ?? new Error(`MAC-Telnet session closed while ${label}.`));
476
+ }
477
+ return new Promise((resolve, reject) => {
478
+ const timeout = setTimeout(() => {
479
+ this.waiter = undefined;
480
+ reject(new Error(`Timed out ${label}. The RouterOS console did not return a prompt in time; ` + "raise the timeout or confirm the device is responsive over mac-telnet."));
481
+ }, timeoutMs);
482
+ this.waiter = { predicate, resolve, reject, timeout };
483
+ this.checkWaiter();
484
+ });
485
+ }
486
+ checkWaiter() {
487
+ const waiter = this.waiter;
488
+ if (!waiter)
489
+ return;
490
+ if (!waiter.predicate(this.buffer)) {
491
+ if (waiter.settle) {
492
+ clearTimeout(waiter.settle);
493
+ waiter.settle = undefined;
494
+ }
495
+ return;
496
+ }
497
+ if (waiter.settle)
498
+ clearTimeout(waiter.settle);
499
+ waiter.settle = setTimeout(() => {
500
+ if (this.waiter !== waiter)
501
+ return;
502
+ this.waiter = undefined;
503
+ clearTimeout(waiter.timeout);
504
+ waiter.resolve();
505
+ }, this.options.settleMs);
506
+ }
507
+ assertOpen() {
508
+ if (!this.ready || this.closed) {
509
+ throw new Error("The MAC-Telnet console is not open. Call open() and await it before running commands.");
510
+ }
511
+ }
512
+ }
513
+
514
+ // src/mac-telnet/client.ts
515
+ import {
516
+ DEFAULT_MAC_TELNET_BROADCAST,
517
+ isBroadcastHost,
518
+ MAC_TELNET_PORT,
519
+ createUdpMacTelnetTransport,
520
+ parseMac,
521
+ resolveMacTelnetRoute
522
+ } from "@tikoci/centrs/protocols";
523
+
524
+ class MikroTikMacTelnetClient {
525
+ transport = null;
526
+ console = null;
527
+ opts;
528
+ lastError;
529
+ constructor(opts) {
530
+ this.opts = {
531
+ port: MAC_TELNET_PORT,
532
+ timeoutMs: 1e4,
533
+ ...opts
534
+ };
535
+ }
536
+ async connect() {
537
+ this.lastError = undefined;
538
+ try {
539
+ const destinationMac = parseMac(this.opts.mac);
540
+ const explicitSourceMac = this.opts.sourceMac ? parseMac(this.opts.sourceMac) : undefined;
541
+ const host = this.opts.host ?? DEFAULT_MAC_TELNET_BROADCAST;
542
+ const route = await resolveMacTelnetRoute({
543
+ destinationMac,
544
+ host,
545
+ port: this.opts.port,
546
+ timeoutMs: this.opts.timeoutMs,
547
+ explicitSourceMac
548
+ });
549
+ const transport = createUdpMacTelnetTransport({
550
+ host: route.host,
551
+ port: this.opts.port,
552
+ broadcast: isBroadcastHost(route.host)
553
+ });
554
+ this.transport = transport;
555
+ await transport.ready();
556
+ const console = new MacTelnetConsole({
557
+ sink: transport,
558
+ sourceMac: route.sourceMac,
559
+ destinationMac,
560
+ username: this.opts.username,
561
+ password: this.opts.password ?? "",
562
+ primeTimeoutMs: Math.max(this.opts.timeoutMs, 30000),
563
+ commandTimeoutMs: Math.max(this.opts.timeoutMs, 15000)
564
+ });
565
+ this.console = console;
566
+ transport.onMessage((bytes) => console.handlePacket(bytes));
567
+ await console.open();
568
+ return true;
569
+ } catch (e) {
570
+ this.lastError = e instanceof Error ? e.message : String(e);
571
+ logger.error(`Failed to connect to MikroTik over MAC-Telnet: ${this.lastError}`);
572
+ this.disconnect();
573
+ return false;
574
+ }
575
+ }
576
+ async run(command) {
577
+ if (!this.console || !this.console.isReady) {
578
+ throw new Error("Not connected to MikroTik device (MAC-Telnet)");
579
+ }
580
+ const { output } = await this.console.run(command);
581
+ return output;
582
+ }
583
+ disconnect() {
584
+ try {
585
+ this.console?.close();
586
+ } catch {}
587
+ try {
588
+ this.transport?.close();
589
+ } catch {}
590
+ this.console = null;
591
+ this.transport = null;
592
+ }
593
+ }
594
+
253
595
  // src/ssh/client.ts
254
596
  import { readFileSync as readFileSync2 } from "fs";
255
597
  import { Client } from "ssh2";
@@ -352,6 +694,45 @@ class MikroTikSSHClient {
352
694
  }
353
695
  }
354
696
 
697
+ // src/core/transport.ts
698
+ function isMacTelnetDevice(dc) {
699
+ return Boolean(dc.mac);
700
+ }
701
+ function createDeviceClient(dc) {
702
+ if (isMacTelnetDevice(dc)) {
703
+ return new MikroTikMacTelnetClient({
704
+ mac: dc.mac,
705
+ username: dc.username,
706
+ password: dc.password,
707
+ sourceMac: dc.sourceMac,
708
+ host: dc.macHost,
709
+ port: dc.macPort,
710
+ timeoutMs: dc.timeoutMs
711
+ });
712
+ }
713
+ return new MikroTikSSHClient({
714
+ host: dc.host,
715
+ username: dc.username,
716
+ password: dc.password,
717
+ keyFilename: dc.keyFilename,
718
+ privateKey: dc.privateKey,
719
+ keyPassphrase: dc.keyPassphrase,
720
+ port: dc.port,
721
+ timeoutMs: dc.timeoutMs
722
+ });
723
+ }
724
+ function describeTransport(dc) {
725
+ return dc.mac ? `MAC ${dc.mac} (mac-telnet)` : `${dc.host}:${dc.port}`;
726
+ }
727
+ function connectErrorMessage(name, dc, lastError) {
728
+ const reason = lastError ? ` \u2014 ${lastError}` : "";
729
+ if (dc.mac) {
730
+ return `Failed to connect to MikroTik device '${name}' over MAC-Telnet at ${dc.mac}${reason}. ` + "Check you are on the same Layer-2 segment, MAC-Telnet is enabled (/tool mac-server) on a reachable interface, and the credentials are correct.";
731
+ }
732
+ const authMode = dc.keyFilename || dc.privateKey ? "SSH key" : dc.password ? "password" : "no credentials";
733
+ return `Failed to connect to MikroTik device '${name}' at ${dc.host}:${dc.port} (auth: ${authMode})${reason}. ` + "Check the host/port are reachable, the SSH service is enabled (/ip service), and the credentials are correct.";
734
+ }
735
+
355
736
  // src/tools/address-list.ts
356
737
  import { z as z3 } from "zod";
357
738
 
@@ -389,6 +770,9 @@ class SafeModeManager {
389
770
  if (this.active)
390
771
  return "Safe mode is already active.";
391
772
  const dc = getDevice(this.deviceName);
773
+ if (dc.mac) {
774
+ return "Error: Safe Mode is not supported for a MAC-Telnet device " + `('${this.deviceName}' is reached by MAC ${dc.mac}). ` + "Connect over SSH (configure host/credentials) to use Safe Mode.";
775
+ }
392
776
  const ssh = new MikroTikSSHClient({
393
777
  host: dc.host,
394
778
  username: dc.username,
@@ -523,25 +907,14 @@ function getSafeModeManager(deviceName) {
523
907
  async function runOnce(command, deviceName) {
524
908
  const name = resolveDeviceName(deviceName);
525
909
  const dc = getDevice(deviceName);
526
- const ssh = new MikroTikSSHClient({
527
- host: dc.host,
528
- username: dc.username,
529
- password: dc.password,
530
- keyFilename: dc.keyFilename,
531
- privateKey: dc.privateKey,
532
- keyPassphrase: dc.keyPassphrase,
533
- port: dc.port,
534
- timeoutMs: dc.timeoutMs
535
- });
910
+ const client = createDeviceClient(dc);
536
911
  try {
537
- if (!await ssh.connect()) {
538
- const authMode = dc.keyFilename || dc.privateKey ? "SSH key" : dc.password ? "password" : "no credentials";
539
- const reason = ssh.lastError ? ` \u2014 ${ssh.lastError}` : "";
540
- throw new Error(`Failed to connect to MikroTik device '${name}' at ${dc.host}:${dc.port} (auth: ${authMode})${reason}. ` + "Check the host/port are reachable, the SSH service is enabled (/ip service), and the credentials are correct.");
912
+ if (!await client.connect()) {
913
+ throw new Error(connectErrorMessage(name, dc, client.lastError));
541
914
  }
542
- return await ssh.run(command);
915
+ return await client.run(command);
543
916
  } finally {
544
- ssh.disconnect();
917
+ client.disconnect();
545
918
  }
546
919
  }
547
920
  async function executeMikrotikCommand(command, ctx) {
@@ -17276,30 +17649,34 @@ function getDeviceStatus(name) {
17276
17649
  return statuses.get(name) ?? UNKNOWN;
17277
17650
  }
17278
17651
  async function probeDevice(name, dc) {
17279
- const ssh = new MikroTikSSHClient({
17280
- host: dc.host,
17281
- username: dc.username,
17282
- password: dc.password,
17283
- port: dc.port,
17284
- keyFilename: dc.keyFilename,
17285
- privateKey: dc.privateKey,
17286
- keyPassphrase: dc.keyPassphrase,
17652
+ if (dc.mac) {
17653
+ const status2 = {
17654
+ reachable: null,
17655
+ checkedAt: Date.now(),
17656
+ latencyMs: null,
17657
+ error: "MAC-Telnet device \u2014 reachability is verified on tool use, not background-probed."
17658
+ };
17659
+ statuses.set(name, status2);
17660
+ return status2;
17661
+ }
17662
+ const client = createDeviceClient({
17663
+ ...dc,
17287
17664
  timeoutMs: Math.min(dc.timeoutMs ?? 1e4, 8000)
17288
17665
  });
17289
17666
  const t0 = Date.now();
17290
17667
  let status;
17291
17668
  try {
17292
- const ok = await ssh.connect();
17669
+ const ok = await client.connect();
17293
17670
  if (!ok) {
17294
17671
  status = {
17295
17672
  reachable: false,
17296
17673
  checkedAt: Date.now(),
17297
17674
  latencyMs: null,
17298
- error: ssh.lastError ?? "connection failed"
17675
+ error: client.lastError ?? "connection failed"
17299
17676
  };
17300
17677
  } else {
17301
- const identity = parseKeyValues(await ssh.run("/system identity print")).name;
17302
- const version = parseKeyValues(await ssh.run("/system resource print")).version;
17678
+ const identity = parseKeyValues(await client.run("/system identity print")).name;
17679
+ const version = parseKeyValues(await client.run("/system resource print")).version;
17303
17680
  status = {
17304
17681
  reachable: true,
17305
17682
  checkedAt: Date.now(),
@@ -17316,7 +17693,7 @@ async function probeDevice(name, dc) {
17316
17693
  error: e instanceof Error ? e.message : String(e)
17317
17694
  };
17318
17695
  } finally {
17319
- ssh.disconnect();
17696
+ client.disconnect();
17320
17697
  }
17321
17698
  statuses.set(name, status);
17322
17699
  return status;
@@ -17662,8 +18039,11 @@ function devicesPayload(store2) {
17662
18039
  name,
17663
18040
  host: dc.host,
17664
18041
  port: dc.port,
18042
+ mac: dc.mac,
18043
+ transport: dc.mac ? "mac-telnet" : "ssh",
18044
+ address: dc.mac ? dc.mac : `${dc.host}:${dc.port}`,
17665
18045
  username: dc.username,
17666
- authMode: dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
18046
+ authMode: dc.mac ? "mac-telnet" : dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
17667
18047
  isDefault: name === cfg.defaultDevice,
17668
18048
  description: dc.description,
17669
18049
  status: getDeviceStatus(name),
@@ -17687,10 +18067,10 @@ function sseResponse(transportLabel) {
17687
18067
  let ping;
17688
18068
  const stream = new ReadableStream({
17689
18069
  start(controller) {
17690
- const enc = new TextEncoder;
18070
+ const enc2 = new TextEncoder;
17691
18071
  const send = (text) => {
17692
18072
  try {
17693
- controller.enqueue(enc.encode(text));
18073
+ controller.enqueue(enc2.encode(text));
17694
18074
  } catch {}
17695
18075
  };
17696
18076
  send(`event: hello
@@ -17978,7 +18358,7 @@ function registerPrompts(server) {
17978
18358
  // package.json
17979
18359
  var package_default = {
17980
18360
  name: "@usex/mikrotik-mcp",
17981
- version: "2.1.0",
18361
+ version: "2.3.0",
17982
18362
  description: "Bun-native MCP server for MikroTik RouterOS \u2014 200+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
17983
18363
  keywords: [
17984
18364
  "ai",
@@ -18052,6 +18432,7 @@ var package_default = {
18052
18432
  dependencies: {
18053
18433
  "@modelcontextprotocol/ext-apps": "^1.7.4",
18054
18434
  "@modelcontextprotocol/sdk": "^1.29.0",
18435
+ "@tikoci/centrs": "^0.1.0",
18055
18436
  ssh2: "^1.17.0",
18056
18437
  zod: "^4.4.3"
18057
18438
  },
@@ -18310,9 +18691,9 @@ function listDevicesCli() {
18310
18691
  const cfg = loadConfig();
18311
18692
  for (const [name, d] of Object.entries(cfg.devices)) {
18312
18693
  const tag = name === cfg.defaultDevice ? " (default)" : "";
18313
- const auth = d.keyFilename || d.privateKey ? "key" : d.password ? "password" : "none";
18694
+ const auth = d.mac ? "mac-telnet" : d.keyFilename || d.privateKey ? "key" : d.password ? "password" : "none";
18314
18695
  const desc = d.description ? ` \u2014 ${d.description}` : "";
18315
- process.stdout.write(`${name}${tag} ${d.username}@${d.host}:${d.port} [auth: ${auth}]${desc}
18696
+ process.stdout.write(`${name}${tag} ${d.username}@${describeTransport(d)} [auth: ${auth}]${desc}
18316
18697
  `);
18317
18698
  }
18318
18699
  process.stdout.write(`
@@ -18320,31 +18701,22 @@ ${Object.keys(cfg.devices).length} device(s)
18320
18701
  `);
18321
18702
  }
18322
18703
  async function probeDevice2(name, d) {
18323
- const authMode = d.keyFilename || d.privateKey ? "SSH key" : "password";
18324
- logger.info(`[${name}] Connecting to ${d.username}@${d.host}:${d.port} (auth: ${authMode}) \u2026`);
18325
- const ssh = new MikroTikSSHClient({
18326
- host: d.host,
18327
- username: d.username,
18328
- password: d.password,
18329
- keyFilename: d.keyFilename,
18330
- privateKey: d.privateKey,
18331
- keyPassphrase: d.keyPassphrase,
18332
- port: d.port,
18333
- timeoutMs: d.timeoutMs
18334
- });
18335
- if (!await ssh.connect()) {
18336
- logger.error(`[${name}] Connection FAILED. Check host/credentials/reachability.`);
18704
+ const authMode = d.mac ? "mac-telnet" : d.keyFilename || d.privateKey ? "SSH key" : "password";
18705
+ logger.info(`[${name}] Connecting to ${d.username}@${describeTransport(d)} (auth: ${authMode}) \u2026`);
18706
+ const client = createDeviceClient(d);
18707
+ if (!await client.connect()) {
18708
+ logger.error(`[${name}] Connection FAILED. ${client.lastError ?? "Check credentials/reachability."}`);
18337
18709
  return false;
18338
18710
  }
18339
18711
  try {
18340
- const identity = (await ssh.run("/system identity print")).trim();
18712
+ const identity = (await client.run("/system identity print")).trim();
18341
18713
  process.stdout.write(`
18342
18714
  [${name}] Connection OK.
18343
18715
  ${identity}
18344
18716
  `);
18345
18717
  return true;
18346
18718
  } finally {
18347
- ssh.disconnect();
18719
+ client.disconnect();
18348
18720
  }
18349
18721
  }
18350
18722
  async function authCheck() {