@usex/mikrotik-mcp 4.14.0 → 4.16.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
@@ -45,6 +45,7 @@ import {
45
45
  devicesView,
46
46
  diffLines,
47
47
  executeMikrotikCommand,
48
+ fetchAllReleases,
48
49
  fetchDevices,
49
50
  fetchLatestRelease,
50
51
  getConfig,
@@ -89,6 +90,7 @@ import {
89
90
  resetRadiusCounters,
90
91
  resolveDeviceName,
91
92
  restoreLocalBackup,
93
+ riskOf,
92
94
  s3Target,
93
95
  sampleAllTraffic,
94
96
  sampleDeviceTraffic,
@@ -106,18 +108,18 @@ import {
106
108
  updateAaaEntity,
107
109
  updateSummaryLine,
108
110
  writeBackup
109
- } from "./shared/cli-bk79xfcq.js";
111
+ } from "./shared/cli-w6ecrhxa.js";
110
112
 
111
113
  // src/cli.ts
112
114
  import { existsSync as existsSync2 } from "fs";
113
115
 
114
116
  // src/observability/dashboard.ts
115
117
  import { spawn } from "child_process";
116
- import { readFileSync as readFileSync2 } from "fs";
118
+ import { readFileSync as readFileSync3 } from "fs";
117
119
  import { homedir, networkInterfaces } from "os";
118
- import { dirname as dirname4, join as join2 } from "path";
120
+ import { dirname as dirname4, join as join3 } from "path";
119
121
  var {serve } = globalThis.Bun;
120
- import { z } from "zod";
122
+ import { z as z2 } from "zod";
121
123
 
122
124
  // src/observability/modules.ts
123
125
  var lcSet = (xs) => new Set((xs ?? []).map((s) => s.toLowerCase()));
@@ -370,6 +372,149 @@ function historyBytes() {
370
372
  return total;
371
373
  }
372
374
 
375
+ // src/prompts/index.ts
376
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
377
+ import { join as join2 } from "path";
378
+ import { z } from "zod";
379
+ function parseFrontmatter(raw) {
380
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
381
+ if (!match)
382
+ return null;
383
+ const [, fm, body] = match;
384
+ const meta = {};
385
+ const args = [];
386
+ let cur = null;
387
+ let inArgs = false;
388
+ for (const line of fm.split(`
389
+ `)) {
390
+ if (/^arguments:\s*$/.test(line)) {
391
+ inArgs = true;
392
+ continue;
393
+ }
394
+ if (inArgs) {
395
+ const item = line.match(/^[ \t]*-[ \t]*name:[ \t]*(\S.*)$/);
396
+ if (item) {
397
+ if (cur)
398
+ args.push(cur);
399
+ cur = { name: item[1].trim() };
400
+ continue;
401
+ }
402
+ const prop = line.match(/^[ \t]+(description|required):[ \t]*(\S.*)$/);
403
+ if (prop && cur) {
404
+ if (prop[1] === "required")
405
+ cur.required = /^(true|yes)$/i.test(prop[2].trim());
406
+ else
407
+ cur.description = prop[2].trim().replace(/^["']|["']$/g, "");
408
+ continue;
409
+ }
410
+ if (/^\S/.test(line))
411
+ inArgs = false;
412
+ }
413
+ const kv = line.match(/^(\w+):[ \t]*(\S.*)?$/);
414
+ if (kv && !inArgs)
415
+ meta[kv[1]] = (kv[2] ?? "").trim().replace(/^["']|["']$/g, "");
416
+ }
417
+ if (cur)
418
+ args.push(cur);
419
+ if (!meta.name)
420
+ return null;
421
+ return {
422
+ name: meta.name,
423
+ title: meta.title ?? meta.name,
424
+ description: meta.description ?? "",
425
+ arguments: args,
426
+ body: body.trim()
427
+ };
428
+ }
429
+ function substitute(body, vars) {
430
+ return body.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => {
431
+ const v = vars[key];
432
+ if (v === undefined || v === null || v === "")
433
+ return whole;
434
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
435
+ });
436
+ }
437
+ function deviceArgDescription(directory) {
438
+ if (directory && directory.length > 0) {
439
+ const rows = directory.map((d) => `\u2022 ${d.key}${d.label && d.label !== d.key ? ` ("${d.label}")` : ""} \u2192 ${d.target}${d.isDefault ? " [default]" : ""}`).join(`
440
+ `);
441
+ return "Which configured MikroTik device to run this workflow on. " + `Pass the EXACT config key or its label. Configured devices:
442
+ ` + `${rows}
443
+ ` + "Omit to use the default device.";
444
+ }
445
+ return "Which configured MikroTik device to run this workflow on. Omit to use the default device.";
446
+ }
447
+ function listPrompts() {
448
+ let files;
449
+ try {
450
+ files = readdirSync2(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
451
+ } catch {
452
+ return [];
453
+ }
454
+ const out = [];
455
+ for (const file of files) {
456
+ try {
457
+ const parsed = parseFrontmatter(readFileSync2(join2(PROMPTS_DIR, file), "utf8"));
458
+ if (parsed) {
459
+ out.push({
460
+ name: parsed.name,
461
+ title: parsed.title,
462
+ description: parsed.description,
463
+ arguments: parsed.arguments,
464
+ body: parsed.body
465
+ });
466
+ }
467
+ } catch {}
468
+ }
469
+ return out.sort((a, b) => a.name.localeCompare(b.name));
470
+ }
471
+ function registerPrompts(server, opts = {}) {
472
+ let files;
473
+ try {
474
+ files = readdirSync2(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
475
+ } catch {
476
+ return 0;
477
+ }
478
+ const multiDevice = opts.deviceNames && opts.deviceNames.length > 1;
479
+ const selectorNames = multiDevice ? [...new Set([...opts.deviceNames, ...opts.deviceAliases ?? []])] : [];
480
+ let count = 0;
481
+ for (const file of files) {
482
+ let parsed;
483
+ try {
484
+ parsed = parseFrontmatter(readFileSync2(join2(PROMPTS_DIR, file), "utf8"));
485
+ } catch (e) {
486
+ logger.warn(`Skipping prompt ${file}: ${String(e)}`);
487
+ continue;
488
+ }
489
+ if (!parsed) {
490
+ logger.warn(`Skipping prompt ${file}: missing or invalid frontmatter`);
491
+ continue;
492
+ }
493
+ const argsSchema = {};
494
+ for (const arg of parsed.arguments) {
495
+ const s = z.string().describe(arg.description ?? "");
496
+ argsSchema[arg.name] = arg.required ? s : s.optional();
497
+ }
498
+ const hasOwnDevice = parsed.arguments.some((a) => a.name === "device" || a.name === "device_a");
499
+ if (multiDevice && !hasOwnDevice) {
500
+ argsSchema.device = z.enum(selectorNames).optional().describe(deviceArgDescription(opts.deviceDirectory));
501
+ }
502
+ server.registerPrompt(parsed.name, { title: parsed.title, description: parsed.description, argsSchema }, (args) => ({
503
+ messages: [
504
+ {
505
+ role: "user",
506
+ content: {
507
+ type: "text",
508
+ text: substitute(parsed.body, args)
509
+ }
510
+ }
511
+ ]
512
+ }));
513
+ count++;
514
+ }
515
+ return count;
516
+ }
517
+
373
518
  // src/observability/topology.ts
374
519
  function normMac(mac) {
375
520
  if (!mac)
@@ -1039,10 +1184,17 @@ async function sampleClients(store, device, ts) {
1039
1184
  }
1040
1185
  store.recordClientSamples(device, ts, samples);
1041
1186
  }
1187
+ var noUserManager = new Set;
1042
1188
  async function ingestSessions(store, device) {
1189
+ if (noUserManager.has(device))
1190
+ return;
1043
1191
  const ctx = createContext(undefined, device);
1044
1192
  const out = await executeMikrotikCommand("/user-manager session print detail", ctx);
1045
- if (isEmpty(out) || looksLikeError(out) || commandUnsupported(out))
1193
+ if (commandUnsupported(out)) {
1194
+ noUserManager.add(device);
1195
+ return;
1196
+ }
1197
+ if (isEmpty(out) || looksLikeError(out))
1046
1198
  return;
1047
1199
  const sessions = [];
1048
1200
  for (const row of parseRecords(out).rows) {
@@ -1492,9 +1644,35 @@ function restartProcess() {
1492
1644
  setTimeout(() => process.exit(0), 500);
1493
1645
  return ok;
1494
1646
  }
1647
+ function runUpgrade(spec) {
1648
+ return new Promise((resolve) => {
1649
+ let out = "";
1650
+ const child = spawn(process.execPath, ["i", "-g", spec], {
1651
+ cwd: process.cwd(),
1652
+ env: process.env,
1653
+ stdio: ["ignore", "pipe", "pipe"]
1654
+ });
1655
+ const timer3 = setTimeout(() => {
1656
+ child.kill();
1657
+ resolve({ ok: false, log: `${out}
1658
+ (timed out after 180s)` });
1659
+ }, 180000);
1660
+ child.stdout?.on("data", (d) => out += d.toString());
1661
+ child.stderr?.on("data", (d) => out += d.toString());
1662
+ child.on("error", (e) => {
1663
+ clearTimeout(timer3);
1664
+ resolve({ ok: false, log: `${out}
1665
+ spawn error: ${String(e)}` });
1666
+ });
1667
+ child.on("exit", (code) => {
1668
+ clearTimeout(timer3);
1669
+ resolve({ ok: code === 0, log: out.trim() || `(exit ${code})` });
1670
+ });
1671
+ });
1672
+ }
1495
1673
  function dashboardHtml() {
1496
1674
  try {
1497
- return readFileSync2(join2(UI_DIST_DIR, "observability.html"), "utf8");
1675
+ return readFileSync3(join3(UI_DIST_DIR, "observability.html"), "utf8");
1498
1676
  } catch {
1499
1677
  return `<!doctype html><meta charset=utf-8><body style="font:14px system-ui;padding:24px;background:#0b0d10;color:#e8eaed">
1500
1678
  <h2>MikroTik MCP \u2014 Observability Dashboard</h2>
@@ -1726,7 +1904,7 @@ function configSchemaJson() {
1726
1904
  return {
1727
1905
  $schema: "https://json-schema.org/draft/2020-12/schema",
1728
1906
  title: "MikrotikConfig",
1729
- ...z.toJSONSchema(MikrotikConfigSchema, { target: "draft-2020-12" })
1907
+ ...z2.toJSONSchema(MikrotikConfigSchema, { target: "draft-2020-12" })
1730
1908
  };
1731
1909
  }
1732
1910
  function clampRollback(v) {
@@ -2280,7 +2458,7 @@ async function featureRoutes(req, url) {
2280
2458
  const raw = b?.dir?.trim();
2281
2459
  if (!raw)
2282
2460
  return json3({ error: "dir required" }, 400);
2283
- const dir = raw === "~" || raw.startsWith("~/") ? join2(homedir(), raw.slice(1)) : raw;
2461
+ const dir = raw === "~" || raw.startsWith("~/") ? join3(homedir(), raw.slice(1)) : raw;
2284
2462
  const next = { ...getConfig(), backupDir: dir };
2285
2463
  setConfig(next);
2286
2464
  try {
@@ -2388,7 +2566,7 @@ async function runDashboard(cfg, transportLabel) {
2388
2566
  });
2389
2567
  startHealthChecks(30000);
2390
2568
  try {
2391
- usageStore = await openUsageStore(join2(dirname4(cfg.dbPath), "usage.db"));
2569
+ usageStore = await openUsageStore(join3(dirname4(cfg.dbPath), "usage.db"));
2392
2570
  startUsageSampler(usageStore);
2393
2571
  } catch (e) {
2394
2572
  logger.warn(`[${SERVER_TAG2}] usage history disabled: ${String(e)}`);
@@ -2405,7 +2583,7 @@ async function runDashboard(cfg, transportLabel) {
2405
2583
  source: getConfigSource,
2406
2584
  readFile: (pth) => {
2407
2585
  try {
2408
- return readFileSync2(pth, "utf8");
2586
+ return readFileSync3(pth, "utf8");
2409
2587
  } catch {
2410
2588
  return null;
2411
2589
  }
@@ -2554,6 +2732,63 @@ async function runDashboard(cfg, transportLabel) {
2554
2732
  return json3({ error: e instanceof Error ? e.message : "fetch failed" }, 502);
2555
2733
  }
2556
2734
  }
2735
+ if (url.pathname === "/api/catalog" && req.method === "GET") {
2736
+ const modules = moduleCatalog.map((m) => ({
2737
+ label: m.label,
2738
+ slug: m.slug,
2739
+ group: m.group,
2740
+ description: m.description,
2741
+ toolCount: m.tools.length,
2742
+ tools: m.tools.map((t) => ({
2743
+ name: t.name,
2744
+ title: t.title,
2745
+ description: t.description,
2746
+ risk: riskOf(t.annotations)
2747
+ }))
2748
+ }));
2749
+ const prompts = listPrompts();
2750
+ const groups = [...new Set(modules.map((m) => m.group))].sort();
2751
+ return json3({
2752
+ modules,
2753
+ prompts,
2754
+ groups,
2755
+ counts: {
2756
+ modules: modules.length,
2757
+ tools: modules.reduce((n, m) => n + m.toolCount, 0),
2758
+ prompts: prompts.length,
2759
+ groups: groups.length
2760
+ },
2761
+ version: VERSION
2762
+ });
2763
+ }
2764
+ if (url.pathname === "/api/releases" && req.method === "GET") {
2765
+ try {
2766
+ return json3(await fetchAllReleases());
2767
+ } catch (e) {
2768
+ return json3({ error: e instanceof Error ? e.message : "fetch failed" }, 502);
2769
+ }
2770
+ }
2771
+ if (url.pathname === "/api/upgrade" && req.method === "POST") {
2772
+ const body = await readJson(req);
2773
+ const version = String(body?.version ?? "latest");
2774
+ if (version !== "latest" && !/^\d+\.\d+\.\d+$/.test(version)) {
2775
+ return json3({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
2776
+ }
2777
+ const spec = `@usex/mikrotik-mcp@${version}`;
2778
+ logger.warn(`Dashboard requested upgrade: bun i -g ${spec}`);
2779
+ const { ok, log } = await runUpgrade(spec);
2780
+ if (!ok)
2781
+ return json3({ ok: false, version, log }, 500);
2782
+ const willRestart = body?.restart !== false;
2783
+ const relaunched = willRestart ? restartProcess() : false;
2784
+ return json3({
2785
+ ok: true,
2786
+ version,
2787
+ log,
2788
+ restarting: relaunched,
2789
+ note: relaunched ? "Installed. Restarting on the new version now \u2014 reconnect shortly." : "Installed. Restart the server (or use the restart button) for it to take effect."
2790
+ });
2791
+ }
2557
2792
  if (url.pathname === "/api/meta") {
2558
2793
  const f = facets(db);
2559
2794
  return json3({
@@ -2696,127 +2931,6 @@ function corsHeaders(origin, configured) {
2696
2931
  // src/server.ts
2697
2932
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2698
2933
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2699
-
2700
- // src/prompts/index.ts
2701
- import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
2702
- import { join as join3 } from "path";
2703
- import { z as z2 } from "zod";
2704
- function parseFrontmatter(raw) {
2705
- const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
2706
- if (!match)
2707
- return null;
2708
- const [, fm, body] = match;
2709
- const meta = {};
2710
- const args = [];
2711
- let cur = null;
2712
- let inArgs = false;
2713
- for (const line of fm.split(`
2714
- `)) {
2715
- if (/^arguments:\s*$/.test(line)) {
2716
- inArgs = true;
2717
- continue;
2718
- }
2719
- if (inArgs) {
2720
- const item = line.match(/^[ \t]*-[ \t]*name:[ \t]*(\S.*)$/);
2721
- if (item) {
2722
- if (cur)
2723
- args.push(cur);
2724
- cur = { name: item[1].trim() };
2725
- continue;
2726
- }
2727
- const prop = line.match(/^[ \t]+(description|required):[ \t]*(\S.*)$/);
2728
- if (prop && cur) {
2729
- if (prop[1] === "required")
2730
- cur.required = /^(true|yes)$/i.test(prop[2].trim());
2731
- else
2732
- cur.description = prop[2].trim().replace(/^["']|["']$/g, "");
2733
- continue;
2734
- }
2735
- if (/^\S/.test(line))
2736
- inArgs = false;
2737
- }
2738
- const kv = line.match(/^(\w+):[ \t]*(\S.*)?$/);
2739
- if (kv && !inArgs)
2740
- meta[kv[1]] = (kv[2] ?? "").trim().replace(/^["']|["']$/g, "");
2741
- }
2742
- if (cur)
2743
- args.push(cur);
2744
- if (!meta.name)
2745
- return null;
2746
- return {
2747
- name: meta.name,
2748
- title: meta.title ?? meta.name,
2749
- description: meta.description ?? "",
2750
- arguments: args,
2751
- body: body.trim()
2752
- };
2753
- }
2754
- function substitute(body, vars) {
2755
- return body.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => {
2756
- const v = vars[key];
2757
- if (v === undefined || v === null || v === "")
2758
- return whole;
2759
- return typeof v === "object" ? JSON.stringify(v) : String(v);
2760
- });
2761
- }
2762
- function deviceArgDescription(directory) {
2763
- if (directory && directory.length > 0) {
2764
- const rows = directory.map((d) => `\u2022 ${d.key}${d.label && d.label !== d.key ? ` ("${d.label}")` : ""} \u2192 ${d.target}${d.isDefault ? " [default]" : ""}`).join(`
2765
- `);
2766
- return "Which configured MikroTik device to run this workflow on. " + `Pass the EXACT config key or its label. Configured devices:
2767
- ` + `${rows}
2768
- ` + "Omit to use the default device.";
2769
- }
2770
- return "Which configured MikroTik device to run this workflow on. Omit to use the default device.";
2771
- }
2772
- function registerPrompts(server, opts = {}) {
2773
- let files;
2774
- try {
2775
- files = readdirSync2(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
2776
- } catch {
2777
- return 0;
2778
- }
2779
- const multiDevice = opts.deviceNames && opts.deviceNames.length > 1;
2780
- const selectorNames = multiDevice ? [...new Set([...opts.deviceNames, ...opts.deviceAliases ?? []])] : [];
2781
- let count = 0;
2782
- for (const file of files) {
2783
- let parsed;
2784
- try {
2785
- parsed = parseFrontmatter(readFileSync3(join3(PROMPTS_DIR, file), "utf8"));
2786
- } catch (e) {
2787
- logger.warn(`Skipping prompt ${file}: ${String(e)}`);
2788
- continue;
2789
- }
2790
- if (!parsed) {
2791
- logger.warn(`Skipping prompt ${file}: missing or invalid frontmatter`);
2792
- continue;
2793
- }
2794
- const argsSchema = {};
2795
- for (const arg of parsed.arguments) {
2796
- const s = z2.string().describe(arg.description ?? "");
2797
- argsSchema[arg.name] = arg.required ? s : s.optional();
2798
- }
2799
- const hasOwnDevice = parsed.arguments.some((a) => a.name === "device" || a.name === "device_a");
2800
- if (multiDevice && !hasOwnDevice) {
2801
- argsSchema.device = z2.enum(selectorNames).optional().describe(deviceArgDescription(opts.deviceDirectory));
2802
- }
2803
- server.registerPrompt(parsed.name, { title: parsed.title, description: parsed.description, argsSchema }, (args) => ({
2804
- messages: [
2805
- {
2806
- role: "user",
2807
- content: {
2808
- type: "text",
2809
- text: substitute(parsed.body, args)
2810
- }
2811
- }
2812
- ]
2813
- }));
2814
- count++;
2815
- }
2816
- return count;
2817
- }
2818
-
2819
- // src/server.ts
2820
2934
  var INSTRUCTIONS = `MikroTik RouterOS management over SSH.
2821
2935
 
2822
2936
  This server exposes RouterOS configuration as MCP tools grouped by subsystem:
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  selectToolModules,
30
30
  setConfig,
31
31
  updateSummaryLine
32
- } from "./shared/library-k3ygd0y8.js";
32
+ } from "./shared/library-212xbkwr.js";
33
33
  // src/server.ts
34
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
35
35
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-bk79xfcq.js";
7
+ } from "./cli-w6ecrhxa.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -8995,7 +8995,7 @@ var cache = null;
8995
8995
  async function gateway() {
8996
8996
  if (cache)
8997
8997
  return cache;
8998
- const { moduleCatalog } = await import("./cli-cb2zp05h.js");
8998
+ const { moduleCatalog } = await import("./cli-9s02dq5d.js");
8999
8999
  const forIndex = [];
9000
9000
  const byName = new Map;
9001
9001
  for (const mod of moduleCatalog) {
@@ -27043,12 +27043,12 @@ function saveFileCache(data) {
27043
27043
  }
27044
27044
  async function fetchLatestRelease() {
27045
27045
  if (memoryCache && Date.now() - memoryCache.fetchedAt < MEMORY_CACHE_TTL) {
27046
- return memoryCache.data;
27046
+ return withCurrentRelation(memoryCache.data);
27047
27047
  }
27048
27048
  const fileCached = loadFileCache();
27049
27049
  if (fileCached) {
27050
27050
  memoryCache = { data: fileCached, fetchedAt: Date.now() };
27051
- return fileCached;
27051
+ return withCurrentRelation(fileCached);
27052
27052
  }
27053
27053
  const res = await fetch(GITHUB_API, {
27054
27054
  headers: {
@@ -27073,7 +27073,11 @@ async function fetchLatestRelease() {
27073
27073
  };
27074
27074
  memoryCache = { data, fetchedAt: Date.now() };
27075
27075
  saveFileCache(data);
27076
- return data;
27076
+ return withCurrentRelation(data);
27077
+ }
27078
+ function withCurrentRelation(r) {
27079
+ const cmp = compareVersions(r.version, VERSION);
27080
+ return { ...r, currentVersion: VERSION, isNewer: cmp > 0, isAhead: cmp < 0 };
27077
27081
  }
27078
27082
  async function checkForUpdate() {
27079
27083
  try {
@@ -27082,7 +27086,7 @@ async function checkForUpdate() {
27082
27086
  } catch (e) {
27083
27087
  const stale = loadFileCache(Infinity);
27084
27088
  if (stale) {
27085
- return { release: stale, checkedAt: Date.now(), fromCache: true };
27089
+ return { release: withCurrentRelation(stale), checkedAt: Date.now(), fromCache: true };
27086
27090
  }
27087
27091
  return {
27088
27092
  release: null,
@@ -27113,6 +27117,45 @@ function updateSummaryLine(release) {
27113
27117
  return null;
27114
27118
  return `Server update available: MikroTik MCP v${release.version} ` + `(you are running v${VERSION}). ` + `Call check_server_pulse for release notes and upgrade commands, ` + `or upgrade directly: bun i -g @usex/mikrotik-mcp@latest`;
27115
27119
  }
27120
+ var RELEASES_API = "https://api.github.com/repos/mikrotik-mcp/mikrotik-mcp/releases?per_page=100";
27121
+ var releasesCache = null;
27122
+ async function fetchAllReleases() {
27123
+ if (releasesCache && Date.now() - releasesCache.fetchedAt < MEMORY_CACHE_TTL) {
27124
+ return releasesCache.data;
27125
+ }
27126
+ const res = await fetch(RELEASES_API, {
27127
+ headers: {
27128
+ accept: "application/vnd.github+json",
27129
+ "user-agent": `mikrotik-mcp/${VERSION}`
27130
+ }
27131
+ });
27132
+ if (!res.ok)
27133
+ throw new Error(`GitHub API ${res.status}`);
27134
+ const raw = await res.json();
27135
+ const releases = raw.filter((r) => !r.draft).map((r) => {
27136
+ const version = r.tag_name.replace(/^v/, "");
27137
+ const cmp = compareVersions(version, VERSION);
27138
+ return {
27139
+ version,
27140
+ name: r.name || `v${version}`,
27141
+ body: r.body || "",
27142
+ publishedAt: r.published_at,
27143
+ url: r.html_url,
27144
+ prerelease: r.prerelease,
27145
+ relation: cmp === 0 ? "current" : cmp > 0 ? "newer" : "older"
27146
+ };
27147
+ }).sort((a, b) => compareVersions(b.version, a.version));
27148
+ const latest = releases.find((r) => !r.prerelease) ?? releases[0] ?? null;
27149
+ const data = {
27150
+ currentVersion: VERSION,
27151
+ latestVersion: latest ? latest.version : null,
27152
+ updateAvailable: latest ? compareVersions(latest.version, VERSION) > 0 : false,
27153
+ releases,
27154
+ fetchedAt: Date.now()
27155
+ };
27156
+ releasesCache = { data, fetchedAt: Date.now() };
27157
+ return data;
27158
+ }
27116
27159
 
27117
27160
  // src/tools/server-pulse.ts
27118
27161
  var UPGRADE_COMMANDS = {
@@ -32231,4 +32274,4 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
32231
32274
  }).map((m) => m.tools);
32232
32275
  }
32233
32276
 
32234
- export { __require, DEFAULT_SNAPSHOT_DB, DEFAULT_CONFIG_HISTORY_DIR, DeviceConfigSchema, ToolFilterSchema, MikrotikConfigSchema, getConfigSource, loadConfig, logger, setConfig, getConfig, listDevices, deviceLabels, resolveDeviceName, deviceDirectory, isMacTelnetDevice, createDeviceClient, describeTransport, isPoolEnabled, closeAll, poolStatus, getMemoryStore, closeMemoryStore, reopenMemoryStore, executeMikrotikCommand, createContext, Cmd, isEmpty, looksLikeError, commandUnsupported, parseKeyValues, parseRouterosDate, parseSize, parseSystemResource, parseRecords, parseLeadingNumber, REDACTED, redact, configureRecorder, getEventStore, subscribe, subscriberCount, registerTools, fetchDevices, sampleDeviceTraffic, sampleAllTraffic, setDeviceLimits, blockDevice, allowDevice, makeDeviceStatic, setDeviceIp, setDeviceLabel, removeDeviceLease, devicesView, PROMPTS_DIR, UI_DIST_DIR, registerUiResources, backupDir, listBackups, readBackup, writeBackup, deleteBackup, renameBackup, createLocalBackup, restoreLocalBackup, isS3Configured, getS3Client, presignExpiresIn, s3Target, splitCommands, buildChangePlan, renderPlan, diffLines, normalizeExport, analyzeDrift, attributeChanges, openSnapshotStore, DEFAULT_TZSP_PORT, capture2 as capture, AAA_ENTITIES, listAaaEntity, addAaaEntity, updateAaaEntity, removeAaaEntity, toggleAaaEntity, getRadiusIncoming, setRadiusIncoming, resetRadiusCounters, getUmSettings, setUmSettings, VERSION, WEBSITE_URL, LOGO_URL, SERVER_TITLE, SERVER_DESCRIPTION, SERVER_NAME, PKG_META, loadFileCacheSync, fetchLatestRelease, checkForUpdate, updateSummaryLine, moduleCatalog, allToolModules, ALWAYS_ON_MODULES, selectToolModules };
32277
+ export { __require, DEFAULT_SNAPSHOT_DB, DEFAULT_CONFIG_HISTORY_DIR, DeviceConfigSchema, ToolFilterSchema, MikrotikConfigSchema, getConfigSource, loadConfig, logger, setConfig, getConfig, listDevices, deviceLabels, resolveDeviceName, deviceDirectory, isMacTelnetDevice, createDeviceClient, describeTransport, isPoolEnabled, closeAll, poolStatus, getMemoryStore, closeMemoryStore, reopenMemoryStore, executeMikrotikCommand, createContext, Cmd, isEmpty, looksLikeError, commandUnsupported, parseKeyValues, parseRouterosDate, parseSize, parseSystemResource, parseRecords, parseLeadingNumber, riskOf, REDACTED, redact, configureRecorder, getEventStore, subscribe, subscriberCount, registerTools, fetchDevices, sampleDeviceTraffic, sampleAllTraffic, setDeviceLimits, blockDevice, allowDevice, makeDeviceStatic, setDeviceIp, setDeviceLabel, removeDeviceLease, devicesView, PROMPTS_DIR, UI_DIST_DIR, registerUiResources, backupDir, listBackups, readBackup, writeBackup, deleteBackup, renameBackup, createLocalBackup, restoreLocalBackup, isS3Configured, getS3Client, presignExpiresIn, s3Target, splitCommands, buildChangePlan, renderPlan, diffLines, normalizeExport, analyzeDrift, attributeChanges, openSnapshotStore, DEFAULT_TZSP_PORT, capture2 as capture, AAA_ENTITIES, listAaaEntity, addAaaEntity, updateAaaEntity, removeAaaEntity, toggleAaaEntity, getRadiusIncoming, setRadiusIncoming, resetRadiusCounters, getUmSettings, setUmSettings, VERSION, WEBSITE_URL, LOGO_URL, SERVER_TITLE, SERVER_DESCRIPTION, SERVER_NAME, PKG_META, loadFileCacheSync, fetchLatestRelease, checkForUpdate, updateSummaryLine, fetchAllReleases, moduleCatalog, allToolModules, ALWAYS_ON_MODULES, selectToolModules };
@@ -8804,7 +8804,7 @@ var cache = null;
8804
8804
  async function gateway() {
8805
8805
  if (cache)
8806
8806
  return cache;
8807
- const { moduleCatalog } = await import("./library-sfdm0xh9.js");
8807
+ const { moduleCatalog } = await import("./library-c0fxxq85.js");
8808
8808
  const forIndex = [];
8809
8809
  const byName = new Map;
8810
8810
  for (const mod of moduleCatalog) {
@@ -26851,12 +26851,12 @@ function saveFileCache(data) {
26851
26851
  }
26852
26852
  async function fetchLatestRelease() {
26853
26853
  if (memoryCache && Date.now() - memoryCache.fetchedAt < MEMORY_CACHE_TTL) {
26854
- return memoryCache.data;
26854
+ return withCurrentRelation(memoryCache.data);
26855
26855
  }
26856
26856
  const fileCached = loadFileCache();
26857
26857
  if (fileCached) {
26858
26858
  memoryCache = { data: fileCached, fetchedAt: Date.now() };
26859
- return fileCached;
26859
+ return withCurrentRelation(fileCached);
26860
26860
  }
26861
26861
  const res = await fetch(GITHUB_API, {
26862
26862
  headers: {
@@ -26881,7 +26881,11 @@ async function fetchLatestRelease() {
26881
26881
  };
26882
26882
  memoryCache = { data, fetchedAt: Date.now() };
26883
26883
  saveFileCache(data);
26884
- return data;
26884
+ return withCurrentRelation(data);
26885
+ }
26886
+ function withCurrentRelation(r) {
26887
+ const cmp = compareVersions(r.version, VERSION);
26888
+ return { ...r, currentVersion: VERSION, isNewer: cmp > 0, isAhead: cmp < 0 };
26885
26889
  }
26886
26890
  async function checkForUpdate() {
26887
26891
  try {
@@ -26890,7 +26894,7 @@ async function checkForUpdate() {
26890
26894
  } catch (e) {
26891
26895
  const stale = loadFileCache(Infinity);
26892
26896
  if (stale) {
26893
- return { release: stale, checkedAt: Date.now(), fromCache: true };
26897
+ return { release: withCurrentRelation(stale), checkedAt: Date.now(), fromCache: true };
26894
26898
  }
26895
26899
  return {
26896
26900
  release: null,
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./library-k3ygd0y8.js";
7
+ } from "./library-212xbkwr.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,