@usex/mikrotik-mcp 4.16.0 → 4.17.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 CHANGED
@@ -385,6 +385,7 @@ source** (`bun run gen:schemas`) so they can never drift:
385
385
  | **[Firewall Audit](docs/firewall-audit.md)** | Shadowed/broad/dead rules, risk-scored |
386
386
  | **[Security Hardening](docs/security-hardening.md)** | Per-category audit+remediate, fix by finding_id, snapshot + Safe-Mode |
387
387
  | **[Port-Scan Detection](docs/port-scan-detection.md)** | Detect+tag six scan signatures behind a trust-excluding jump-gate |
388
+ | **[CAPsMAN Orchestrator](docs/capsman.md)** | Wi-Fi fabric audit: coverage/co-channel, weak signal, load, FT & HA |
388
389
  | **[Packet Capture Studio](docs/packet-capture.md)** | Live TZSP capture + pcap export |
389
390
  | **[Discovery](docs/discovery.md)** | `bun run discover`, MNDP neighbours, topology map |
390
391
  | **[Config Studio](docs/config-studio.md)** | Edit config in the dashboard with autocomplete |
package/dist/cli.js CHANGED
@@ -25,11 +25,19 @@ import {
25
25
  allToolModules,
26
26
  allowDevice,
27
27
  analyzeDrift,
28
+ applyWritesSafely,
28
29
  attributeChanges,
29
30
  backupDir,
30
31
  blockDevice,
31
32
  buildChangePlan,
33
+ buildChannelPlanCommands,
34
+ buildFtCommands,
35
+ buildHaCommands,
36
+ buildLoadBalanceCommands,
37
+ buildSteerCommands,
38
+ capsmanOverview,
32
39
  capture,
40
+ captureSnapshot,
33
41
  checkForUpdate,
34
42
  closeAll,
35
43
  closeMemoryStore,
@@ -46,6 +54,7 @@ import {
46
54
  diffLines,
47
55
  executeMikrotikCommand,
48
56
  fetchAllReleases,
57
+ fetchCapsmanState,
49
58
  fetchDevices,
50
59
  fetchLatestRelease,
51
60
  getConfig,
@@ -55,6 +64,7 @@ import {
55
64
  getRadiusIncoming,
56
65
  getS3Client,
57
66
  getUmSettings,
67
+ haGuidance,
58
68
  isEmpty,
59
69
  isMacTelnetDevice,
60
70
  isPoolEnabled,
@@ -62,6 +72,7 @@ import {
62
72
  listAaaEntity,
63
73
  listBackups,
64
74
  listDevices,
75
+ loadBalancePlan,
65
76
  loadConfig,
66
77
  loadFileCacheSync,
67
78
  logger,
@@ -87,10 +98,12 @@ import {
87
98
  renameBackup,
88
99
  renderPlan,
89
100
  reopenMemoryStore,
101
+ reportWeakClients,
90
102
  resetRadiusCounters,
91
103
  resolveDeviceName,
92
104
  restoreLocalBackup,
93
105
  riskOf,
106
+ runCapsmanAudit,
94
107
  s3Target,
95
108
  sampleAllTraffic,
96
109
  sampleDeviceTraffic,
@@ -102,13 +115,14 @@ import {
102
115
  setRadiusIncoming,
103
116
  setUmSettings,
104
117
  splitCommands,
118
+ steerAlreadyPresent,
105
119
  subscribe,
106
120
  subscriberCount,
107
121
  toggleAaaEntity,
108
122
  updateAaaEntity,
109
123
  updateSummaryLine,
110
124
  writeBackup
111
- } from "./shared/cli-w6ecrhxa.js";
125
+ } from "./shared/cli-v75jtv6p.js";
112
126
 
113
127
  // src/cli.ts
114
128
  import { existsSync as existsSync2 } from "fs";
@@ -117,7 +131,7 @@ import { existsSync as existsSync2 } from "fs";
117
131
  import { spawn } from "child_process";
118
132
  import { readFileSync as readFileSync3 } from "fs";
119
133
  import { homedir, networkInterfaces } from "os";
120
- import { dirname as dirname4, join as join3 } from "path";
134
+ import { dirname as dirname5, join as join3 } from "path";
121
135
  var {serve } = globalThis.Bun;
122
136
  import { z as z2 } from "zod";
123
137
 
@@ -1018,9 +1032,172 @@ async function openSqliteStore(path) {
1018
1032
  return new SqliteEventStore(db);
1019
1033
  }
1020
1034
 
1021
- // src/observability/usage-store.ts
1035
+ // src/observability/capsman-store.ts
1022
1036
  import { mkdirSync as mkdirSync4 } from "fs";
1023
1037
  import { dirname as dirname3 } from "path";
1038
+ function summariseRadioSamples(rows) {
1039
+ const byRadio = new Map;
1040
+ for (const r of rows) {
1041
+ const list = byRadio.get(r.radioId);
1042
+ if (list)
1043
+ list.push(r);
1044
+ else
1045
+ byRadio.set(r.radioId, [r]);
1046
+ }
1047
+ const series = [];
1048
+ for (const [radioId, list] of byRadio) {
1049
+ list.sort((a, b) => a.ts - b.ts);
1050
+ const last = list[list.length - 1];
1051
+ const points = list.map((r) => ({ ts: r.ts, clients: r.clients, channel: r.channel }));
1052
+ const peak = points.reduce((m, p) => Math.max(m, p.clients), 0);
1053
+ const total = points.reduce((s, p) => s + p.clients, 0);
1054
+ series.push({
1055
+ radioId,
1056
+ cap: last.cap,
1057
+ band: last.band,
1058
+ points,
1059
+ peak,
1060
+ avg: points.length ? Math.round(total / points.length) : 0
1061
+ });
1062
+ }
1063
+ return series.sort((a, b) => a.radioId.localeCompare(b.radioId));
1064
+ }
1065
+ var SCHEMA_STATEMENTS2 = [
1066
+ `CREATE TABLE IF NOT EXISTS capsman_samples (
1067
+ device TEXT NOT NULL,
1068
+ radio_id TEXT NOT NULL,
1069
+ cap TEXT NOT NULL,
1070
+ band TEXT NOT NULL,
1071
+ ts INTEGER NOT NULL,
1072
+ clients INTEGER NOT NULL,
1073
+ channel INTEGER
1074
+ )`,
1075
+ "CREATE INDEX IF NOT EXISTS idx_capsman_radio ON capsman_samples(device, radio_id, ts)",
1076
+ "CREATE INDEX IF NOT EXISTS idx_capsman_ts ON capsman_samples(ts)"
1077
+ ];
1078
+
1079
+ class SqliteCapsmanStore {
1080
+ db;
1081
+ constructor(db) {
1082
+ this.db = db;
1083
+ db.run("PRAGMA journal_mode = WAL");
1084
+ db.run("PRAGMA synchronous = NORMAL");
1085
+ for (const stmt of SCHEMA_STATEMENTS2)
1086
+ db.run(stmt);
1087
+ }
1088
+ recordRadioSamples(device, ts, samples) {
1089
+ if (samples.length === 0)
1090
+ return;
1091
+ const insert = this.db.query(`INSERT INTO capsman_samples (device, radio_id, cap, band, ts, clients, channel)
1092
+ VALUES ($d,$r,$cap,$band,$ts,$c,$ch)`);
1093
+ const tx = this.db.transaction((rows) => {
1094
+ for (const r of rows) {
1095
+ insert.run({
1096
+ $d: device,
1097
+ $r: r.radioId,
1098
+ $cap: r.cap,
1099
+ $band: r.band,
1100
+ $ts: ts,
1101
+ $c: r.clients,
1102
+ $ch: r.channel ?? null
1103
+ });
1104
+ }
1105
+ });
1106
+ tx(samples);
1107
+ }
1108
+ radioSeries(device, sinceTs) {
1109
+ const rows = this.db.query(`SELECT radio_id AS radioId, cap, band, ts, clients, channel
1110
+ FROM capsman_samples WHERE device=$d AND ts>=$since ORDER BY ts ASC`).all({ $d: device, $since: sinceTs });
1111
+ return summariseRadioSamples(rows);
1112
+ }
1113
+ pruneSamples(olderThanTs) {
1114
+ const res = this.db.query("DELETE FROM capsman_samples WHERE ts < $t").run({ $t: olderThanTs });
1115
+ return Number(res.changes ?? 0);
1116
+ }
1117
+ close() {
1118
+ this.db.close();
1119
+ }
1120
+ }
1121
+ async function openCapsmanStore(path) {
1122
+ if (path !== ":memory:") {
1123
+ try {
1124
+ mkdirSync4(dirname3(path), { recursive: true });
1125
+ } catch {}
1126
+ }
1127
+ const { Database } = await import("bun:sqlite");
1128
+ const db = new Database(path, { create: true });
1129
+ return new SqliteCapsmanStore(db);
1130
+ }
1131
+
1132
+ // src/observability/capsman-sampler.ts
1133
+ var SERVER_TAG = "mikrotik-mcp";
1134
+ var CAPSMAN_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
1135
+ var DEFAULT_CAPSMAN_INTERVAL_MS = 5 * 60000;
1136
+ var MIN_CAPSMAN_INTERVAL_MS = 60000;
1137
+ var MAX_CAPSMAN_INTERVAL_MS = 6 * 60 * 60000;
1138
+ var timer2 = null;
1139
+ var inFlight2 = false;
1140
+ var noCapsman = new Set;
1141
+ function clampInterval(ms) {
1142
+ if (!Number.isFinite(ms))
1143
+ return DEFAULT_CAPSMAN_INTERVAL_MS;
1144
+ return Math.max(MIN_CAPSMAN_INTERVAL_MS, Math.min(MAX_CAPSMAN_INTERVAL_MS, Math.round(ms)));
1145
+ }
1146
+ async function sampleDevice(store, device, ts) {
1147
+ if (noCapsman.has(device))
1148
+ return;
1149
+ const ctx = createContext(undefined, device);
1150
+ const state = await fetchCapsmanState(ctx);
1151
+ if (state.path === null) {
1152
+ noCapsman.add(device);
1153
+ return;
1154
+ }
1155
+ const overview = capsmanOverview(state);
1156
+ const samples = overview.radios.map((r) => ({
1157
+ radioId: r.radioId,
1158
+ cap: r.cap,
1159
+ band: r.band,
1160
+ channel: r.channel,
1161
+ clients: r.clientCount
1162
+ }));
1163
+ store.recordRadioSamples(device, ts, samples);
1164
+ }
1165
+ async function sampleCapsmanOnce(store) {
1166
+ if (inFlight2)
1167
+ return;
1168
+ inFlight2 = true;
1169
+ const ts = Date.now();
1170
+ try {
1171
+ const cfg = getConfig();
1172
+ await Promise.all(Object.entries(cfg.devices).map(async ([name, dc]) => {
1173
+ if (dc.mac)
1174
+ return;
1175
+ try {
1176
+ await sampleDevice(store, name, ts);
1177
+ } catch (e) {
1178
+ logger.warn(`[${SERVER_TAG}] capsman sample failed for '${name}': ${String(e)}`);
1179
+ }
1180
+ }));
1181
+ store.pruneSamples(ts - CAPSMAN_RETENTION_MS);
1182
+ } finally {
1183
+ inFlight2 = false;
1184
+ }
1185
+ }
1186
+ function startCapsmanSampler(store, intervalMs = DEFAULT_CAPSMAN_INTERVAL_MS) {
1187
+ const ms = clampInterval(intervalMs);
1188
+ sampleCapsmanOnce(store);
1189
+ timer2 = setInterval(() => void sampleCapsmanOnce(store), ms);
1190
+ }
1191
+ function stopCapsmanSampler() {
1192
+ if (timer2) {
1193
+ clearInterval(timer2);
1194
+ timer2 = null;
1195
+ }
1196
+ }
1197
+
1198
+ // src/observability/usage-store.ts
1199
+ import { mkdirSync as mkdirSync5 } from "fs";
1200
+ import { dirname as dirname4 } from "path";
1024
1201
  function dayOf(ts) {
1025
1202
  return new Date(ts).toISOString().slice(0, 10);
1026
1203
  }
@@ -1039,7 +1216,7 @@ function dailyUsageFromSamples(samples) {
1039
1216
  }
1040
1217
  return [...byDay.entries()].map(([day, v]) => ({ day, rx: v.rx, tx: v.tx })).sort((a, b) => a.day.localeCompare(b.day));
1041
1218
  }
1042
- var SCHEMA_STATEMENTS2 = [
1219
+ var SCHEMA_STATEMENTS3 = [
1043
1220
  `CREATE TABLE IF NOT EXISTS usage_samples (
1044
1221
  device TEXT NOT NULL,
1045
1222
  subject TEXT NOT NULL,
@@ -1071,7 +1248,7 @@ class SqliteUsageStore {
1071
1248
  this.db = db;
1072
1249
  db.run("PRAGMA journal_mode = WAL");
1073
1250
  db.run("PRAGMA synchronous = NORMAL");
1074
- for (const stmt of SCHEMA_STATEMENTS2)
1251
+ for (const stmt of SCHEMA_STATEMENTS3)
1075
1252
  db.run(stmt);
1076
1253
  }
1077
1254
  recordClientSamples(device, ts, samples) {
@@ -1140,7 +1317,7 @@ class SqliteUsageStore {
1140
1317
  async function openUsageStore(path) {
1141
1318
  if (path !== ":memory:") {
1142
1319
  try {
1143
- mkdirSync4(dirname3(path), { recursive: true });
1320
+ mkdirSync5(dirname4(path), { recursive: true });
1144
1321
  } catch {}
1145
1322
  }
1146
1323
  const { Database } = await import("bun:sqlite");
@@ -1149,16 +1326,16 @@ async function openUsageStore(path) {
1149
1326
  }
1150
1327
 
1151
1328
  // src/observability/usage-sampler.ts
1152
- var SERVER_TAG = "mikrotik-mcp";
1329
+ var SERVER_TAG2 = "mikrotik-mcp";
1153
1330
  var USAGE_RETENTION_MS = 93 * 24 * 60 * 60 * 1000;
1154
1331
  var DEFAULT_USAGE_INTERVAL_MS = 60000;
1155
1332
  var MIN_USAGE_INTERVAL_MS = 30000;
1156
1333
  var MAX_USAGE_INTERVAL_MS = 6 * 60 * 60000;
1157
- var timer2 = null;
1158
- var inFlight2 = false;
1334
+ var timer3 = null;
1335
+ var inFlight3 = false;
1159
1336
  var currentStore = null;
1160
1337
  var currentIntervalMs = DEFAULT_USAGE_INTERVAL_MS;
1161
- function clampInterval(ms) {
1338
+ function clampInterval2(ms) {
1162
1339
  if (!Number.isFinite(ms))
1163
1340
  return DEFAULT_USAGE_INTERVAL_MS;
1164
1341
  return Math.max(MIN_USAGE_INTERVAL_MS, Math.min(MAX_USAGE_INTERVAL_MS, Math.round(ms)));
@@ -1216,9 +1393,9 @@ async function ingestSessions(store, device) {
1216
1393
  store.upsertSessions(device, sessions);
1217
1394
  }
1218
1395
  async function sampleUsageOnce(store) {
1219
- if (inFlight2)
1396
+ if (inFlight3)
1220
1397
  return;
1221
- inFlight2 = true;
1398
+ inFlight3 = true;
1222
1399
  const ts = Date.now();
1223
1400
  try {
1224
1401
  const cfg = getConfig();
@@ -1229,37 +1406,37 @@ async function sampleUsageOnce(store) {
1229
1406
  await sampleClients(store, name, ts);
1230
1407
  await ingestSessions(store, name);
1231
1408
  } catch (e) {
1232
- logger.warn(`[${SERVER_TAG}] usage sample failed for '${name}': ${String(e)}`);
1409
+ logger.warn(`[${SERVER_TAG2}] usage sample failed for '${name}': ${String(e)}`);
1233
1410
  }
1234
1411
  }));
1235
1412
  store.pruneSamples(ts - USAGE_RETENTION_MS);
1236
1413
  } finally {
1237
- inFlight2 = false;
1414
+ inFlight3 = false;
1238
1415
  }
1239
1416
  }
1240
1417
  function startUsageSampler(store, intervalMs = DEFAULT_USAGE_INTERVAL_MS) {
1241
1418
  currentStore = store;
1242
- currentIntervalMs = clampInterval(intervalMs);
1419
+ currentIntervalMs = clampInterval2(intervalMs);
1243
1420
  sampleUsageOnce(store);
1244
- timer2 = setInterval(() => void sampleUsageOnce(store), currentIntervalMs);
1421
+ timer3 = setInterval(() => void sampleUsageOnce(store), currentIntervalMs);
1245
1422
  }
1246
1423
  function getUsageSamplerInterval() {
1247
1424
  return currentIntervalMs;
1248
1425
  }
1249
1426
  function setUsageSamplerInterval(intervalMs) {
1250
- currentIntervalMs = clampInterval(intervalMs);
1251
- if (timer2)
1252
- clearInterval(timer2);
1427
+ currentIntervalMs = clampInterval2(intervalMs);
1428
+ if (timer3)
1429
+ clearInterval(timer3);
1253
1430
  if (currentStore) {
1254
1431
  const store = currentStore;
1255
- timer2 = setInterval(() => void sampleUsageOnce(store), currentIntervalMs);
1432
+ timer3 = setInterval(() => void sampleUsageOnce(store), currentIntervalMs);
1256
1433
  }
1257
1434
  return currentIntervalMs;
1258
1435
  }
1259
1436
  function stopUsageSampler() {
1260
- if (timer2) {
1261
- clearInterval(timer2);
1262
- timer2 = null;
1437
+ if (timer3) {
1438
+ clearInterval(timer3);
1439
+ timer3 = null;
1263
1440
  }
1264
1441
  currentStore = null;
1265
1442
  }
@@ -1621,7 +1798,7 @@ async function memoryRoutes(req, url) {
1621
1798
  }
1622
1799
 
1623
1800
  // src/observability/dashboard.ts
1624
- var SERVER_TAG2 = "mikrotik-mcp";
1801
+ var SERVER_TAG3 = "mikrotik-mcp";
1625
1802
  var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
1626
1803
  function json3(body, status = 200) {
1627
1804
  return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
@@ -1652,7 +1829,7 @@ function runUpgrade(spec) {
1652
1829
  env: process.env,
1653
1830
  stdio: ["ignore", "pipe", "pipe"]
1654
1831
  });
1655
- const timer3 = setTimeout(() => {
1832
+ const timer4 = setTimeout(() => {
1656
1833
  child.kill();
1657
1834
  resolve({ ok: false, log: `${out}
1658
1835
  (timed out after 180s)` });
@@ -1660,12 +1837,12 @@ function runUpgrade(spec) {
1660
1837
  child.stdout?.on("data", (d) => out += d.toString());
1661
1838
  child.stderr?.on("data", (d) => out += d.toString());
1662
1839
  child.on("error", (e) => {
1663
- clearTimeout(timer3);
1840
+ clearTimeout(timer4);
1664
1841
  resolve({ ok: false, log: `${out}
1665
1842
  spawn error: ${String(e)}` });
1666
1843
  });
1667
1844
  child.on("exit", (code) => {
1668
- clearTimeout(timer3);
1845
+ clearTimeout(timer4);
1669
1846
  resolve({ ok: code === 0, log: out.trim() || `(exit ${code})` });
1670
1847
  });
1671
1848
  });
@@ -1782,7 +1959,7 @@ function devicesPayload(store) {
1782
1959
  }
1783
1960
  };
1784
1961
  });
1785
- return { server: SERVER_TAG2, defaultDevice: cfg.defaultDevice, devices };
1962
+ return { server: SERVER_TAG3, defaultDevice: cfg.defaultDevice, devices };
1786
1963
  }
1787
1964
  function topologyPayload() {
1788
1965
  const cfg = getConfig();
@@ -1795,7 +1972,7 @@ function topologyPayload() {
1795
1972
  for (const { name } of devices)
1796
1973
  neighborsByDevice[name] = getDeviceNeighbors(name);
1797
1974
  return {
1798
- server: SERVER_TAG2,
1975
+ server: SERVER_TAG3,
1799
1976
  defaultDevice: cfg.defaultDevice,
1800
1977
  generatedAt: Date.now(),
1801
1978
  ...buildTopology({ devices, neighborsByDevice })
@@ -2178,6 +2355,99 @@ async function captureRoutes(req, url) {
2178
2355
  }
2179
2356
  return null;
2180
2357
  }
2358
+ var capsmanStore = null;
2359
+ async function capsmanRoutes(req, url) {
2360
+ const p = url.pathname;
2361
+ if (!p.startsWith("/api/capsman"))
2362
+ return null;
2363
+ if (req.method === "GET") {
2364
+ const ctx = createContext(undefined, url.searchParams.get("device") ?? undefined);
2365
+ if (p === "/api/capsman/overview") {
2366
+ return json3(capsmanOverview(await fetchCapsmanState(ctx)));
2367
+ }
2368
+ if (p === "/api/capsman/clients") {
2369
+ const state = await fetchCapsmanState(ctx);
2370
+ const weakDbm = Number.parseInt(url.searchParams.get("weak_dbm") ?? "", 10);
2371
+ const weak = reportWeakClients(state, Number.isFinite(weakDbm) ? weakDbm : undefined);
2372
+ return json3({ clients: state.clients, weak });
2373
+ }
2374
+ if (p === "/api/capsman/audit") {
2375
+ return json3(runCapsmanAudit(await fetchCapsmanState(ctx)));
2376
+ }
2377
+ if (p === "/api/capsman/trends") {
2378
+ if (!capsmanStore)
2379
+ return json3({ error: "capsman store not active" }, 503);
2380
+ const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
2381
+ const days = daysParam(url, 7, 30);
2382
+ const series = capsmanStore.radioSeries(device, Date.now() - days * 86400000);
2383
+ return json3({ series, days });
2384
+ }
2385
+ return null;
2386
+ }
2387
+ if (req.method === "POST") {
2388
+ const body = await readJson(req);
2389
+ const ctx = createContext(undefined, body?.device);
2390
+ const state = await fetchCapsmanState(ctx);
2391
+ if (p === "/api/capsman/apply/steer") {
2392
+ const client = state.clients.find((c) => c.mac.toLowerCase() === (body.mac ?? "").toLowerCase());
2393
+ if (!client)
2394
+ return json3({ ok: false, error: "client not associated" }, 400);
2395
+ if (steerAlreadyPresent(state, body.mac ?? "")) {
2396
+ return json3({ ok: true, message: "already steered (no-op)" });
2397
+ }
2398
+ const mode = body.mode === "soft" ? "soft" : "hard";
2399
+ const commands = buildSteerCommands(state, body.mac ?? "", client.radioId, mode);
2400
+ if (!body.confirm)
2401
+ return json3({ ok: true, preview: commands });
2402
+ return json3(await applyCapsmanWrites(ctx, commands, `pre-steer-${body.mac}`));
2403
+ }
2404
+ if (p === "/api/capsman/apply/load-balance") {
2405
+ const plan = loadBalancePlan(state);
2406
+ const commands = buildLoadBalanceCommands(state, plan);
2407
+ if (!body.confirm)
2408
+ return json3({ ok: true, preview: commands, plan });
2409
+ return json3(await applyCapsmanWrites(ctx, commands, "pre-load-balance"));
2410
+ }
2411
+ if (p === "/api/capsman/apply/channel-plan") {
2412
+ if (state.path === "/caps-man") {
2413
+ return json3({ ok: false, error: "channel-plan apply is v7 /interface wifi only" }, 400);
2414
+ }
2415
+ const commands = buildChannelPlanCommands(state);
2416
+ if (!body.confirm)
2417
+ return json3({ ok: true, preview: commands });
2418
+ return json3(await applyCapsmanWrites(ctx, commands, "pre-channel-plan"));
2419
+ }
2420
+ if (p === "/api/capsman/apply/ft") {
2421
+ const commands = buildFtCommands(state);
2422
+ if (!body.confirm)
2423
+ return json3({ ok: true, preview: commands });
2424
+ return json3(await applyCapsmanWrites(ctx, commands, "pre-ft"));
2425
+ }
2426
+ if (p === "/api/capsman/apply/ha") {
2427
+ const commands = buildHaCommands(state);
2428
+ const guidance = haGuidance(state);
2429
+ if (!body.confirm)
2430
+ return json3({ ok: true, preview: commands, guidance });
2431
+ const res = await applyCapsmanWrites(ctx, commands, "pre-ha");
2432
+ return json3({ ...res, guidance });
2433
+ }
2434
+ }
2435
+ return null;
2436
+ }
2437
+ async function applyCapsmanWrites(ctx, commands, label) {
2438
+ if (commands.length === 0)
2439
+ return { ok: true, applied: 0 };
2440
+ const snapshotId = await captureSnapshot(ctx, label);
2441
+ const device = resolveDeviceName(ctx.device);
2442
+ const outcome = await applyWritesSafely(ctx, device, commands, { allowDirectFallback: false });
2443
+ return {
2444
+ ok: outcome.committed && !outcome.error,
2445
+ snapshotId,
2446
+ safeMode: outcome.safeMode,
2447
+ applied: outcome.applied,
2448
+ error: outcome.error
2449
+ };
2450
+ }
2181
2451
  async function clientsRoutes(req, url) {
2182
2452
  const p = url.pathname;
2183
2453
  if (!p.startsWith("/api/clients"))
@@ -2566,16 +2836,22 @@ async function runDashboard(cfg, transportLabel) {
2566
2836
  });
2567
2837
  startHealthChecks(30000);
2568
2838
  try {
2569
- usageStore = await openUsageStore(join3(dirname4(cfg.dbPath), "usage.db"));
2839
+ usageStore = await openUsageStore(join3(dirname5(cfg.dbPath), "usage.db"));
2570
2840
  startUsageSampler(usageStore);
2571
2841
  } catch (e) {
2572
- logger.warn(`[${SERVER_TAG2}] usage history disabled: ${String(e)}`);
2842
+ logger.warn(`[${SERVER_TAG3}] usage history disabled: ${String(e)}`);
2843
+ }
2844
+ try {
2845
+ capsmanStore = await openCapsmanStore(join3(dirname5(cfg.dbPath), "capsman.db"));
2846
+ startCapsmanSampler(capsmanStore);
2847
+ } catch (e) {
2848
+ logger.warn(`[${SERVER_TAG3}] capsman trends disabled: ${String(e)}`);
2573
2849
  }
2574
2850
  try {
2575
2851
  if (isEmpty2())
2576
2852
  recordVersion(getConfig(), "auto", Date.now(), "baseline");
2577
2853
  } catch (e) {
2578
- logger.warn(`[${SERVER_TAG2}] could not seed config history baseline: ${String(e)}`);
2854
+ logger.warn(`[${SERVER_TAG3}] could not seed config history baseline: ${String(e)}`);
2579
2855
  }
2580
2856
  const configAdmin = createConfigAdmin({
2581
2857
  getConfig,
@@ -2622,6 +2898,9 @@ async function runDashboard(cfg, transportLabel) {
2622
2898
  const captureResp = await captureRoutes(req, url);
2623
2899
  if (captureResp)
2624
2900
  return captureResp;
2901
+ const capsmanResp = await capsmanRoutes(req, url);
2902
+ if (capsmanResp)
2903
+ return capsmanResp;
2625
2904
  const clientsResp = await clientsRoutes(req, url);
2626
2905
  if (clientsResp)
2627
2906
  return clientsResp;
@@ -2875,10 +3154,13 @@ async function runDashboard(cfg, transportLabel) {
2875
3154
  stop() {
2876
3155
  stopHealthChecks();
2877
3156
  stopUsageSampler();
3157
+ stopCapsmanSampler();
2878
3158
  server.stop(true);
2879
3159
  store.close();
2880
3160
  usageStore?.close();
2881
3161
  usageStore = null;
3162
+ capsmanStore?.close();
3163
+ capsmanStore = null;
2882
3164
  closeMemoryStore();
2883
3165
  }
2884
3166
  };
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  selectToolModules,
30
30
  setConfig,
31
31
  updateSummaryLine
32
- } from "./shared/library-212xbkwr.js";
32
+ } from "./shared/library-qma8metz.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-w6ecrhxa.js";
7
+ } from "./cli-v75jtv6p.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,