@switchbot/openapi-cli 3.4.0 → 3.5.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.
Files changed (3) hide show
  1. package/README.md +110 -602
  2. package/dist/index.js +436 -77
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7221,7 +7221,7 @@ function emitJsonError(errorPayload) {
7221
7221
  function emitStreamHeader(opts) {
7222
7222
  console.log(
7223
7223
  JSON.stringify({
7224
- schemaVersion: SCHEMA_VERSION,
7224
+ schemaVersion: opts.schemaVersion ?? SCHEMA_VERSION,
7225
7225
  stream: true,
7226
7226
  eventKind: opts.eventKind,
7227
7227
  cadence: opts.cadence
@@ -7531,7 +7531,7 @@ var init_output = __esm({
7531
7531
  init_source();
7532
7532
  init_client();
7533
7533
  init_flags();
7534
- SCHEMA_VERSION = "1.1";
7534
+ SCHEMA_VERSION = "1.2";
7535
7535
  ASCII_BORDER_CHARS = {
7536
7536
  top: "-",
7537
7537
  "top-mid": "+",
@@ -8380,9 +8380,8 @@ function updateCacheFromDeviceList(body) {
8380
8380
  for (const d of body.deviceList) {
8381
8381
  if (!d.deviceId) continue;
8382
8382
  devices[d.deviceId] = {
8383
- // Some real devices omit deviceType entirely (for example AI accessories).
8384
- // Keep them in cache with an empty type string rather than dropping the row.
8385
- type: d.deviceType ?? "",
8383
+ type: d.deviceType || d.controlType || "Unknown Device",
8384
+ typeSource: d.deviceType ? "deviceType" : d.controlType ? "controlType" : "deviceType",
8386
8385
  name: d.deviceName,
8387
8386
  category: "physical",
8388
8387
  hubDeviceId: d.hubDeviceId,
@@ -8397,6 +8396,7 @@ function updateCacheFromDeviceList(body) {
8397
8396
  if (!d.deviceId) continue;
8398
8397
  devices[d.deviceId] = {
8399
8398
  type: d.remoteType,
8399
+ typeSource: "remoteType",
8400
8400
  name: d.deviceName,
8401
8401
  category: "ir",
8402
8402
  hubDeviceId: d.hubDeviceId,
@@ -8793,10 +8793,11 @@ async function fetchDeviceList(client, options = {}) {
8793
8793
  const infraredRemoteList = [];
8794
8794
  for (const [deviceId, entry] of Object.entries(cached2.devices)) {
8795
8795
  if (entry.category === "physical") {
8796
+ const cachedDeviceType = entry.typeSource === "deviceType" ? entry.type : entry.typeSource === void 0 && entry.type !== entry.controlType ? entry.type : void 0;
8796
8797
  deviceList.push({
8797
8798
  deviceId,
8798
8799
  deviceName: entry.name,
8799
- ...entry.type ? { deviceType: entry.type } : {},
8800
+ ...cachedDeviceType && cachedDeviceType !== "Unknown Device" ? { deviceType: cachedDeviceType } : {},
8800
8801
  enableCloudService: entry.enableCloudService ?? true,
8801
8802
  hubDeviceId: entry.hubDeviceId ?? "",
8802
8803
  roomID: entry.roomID,
@@ -8867,10 +8868,12 @@ async function executeCommand(deviceId, cmd, parameter, commandType, client, opt
8867
8868
  if (err instanceof Error && err.name === "DryRunSignal") {
8868
8869
  writeAudit({ ...baseAudit, result: "dry-run" });
8869
8870
  } else {
8871
+ const statusCode = err instanceof ApiError ? err.code : void 0;
8870
8872
  writeAudit({
8871
8873
  ...baseAudit,
8872
8874
  result: "error",
8873
- error: err instanceof Error ? err.message : String(err)
8875
+ error: err instanceof Error ? err.message : String(err),
8876
+ ...statusCode !== void 0 ? { statusCode } : {}
8874
8877
  });
8875
8878
  }
8876
8879
  throw err;
@@ -8991,7 +8994,23 @@ async function describeDevice(deviceId, options = {}, client) {
8991
8994
  ir = infraredRemoteList.find((d) => d.deviceId === deviceId);
8992
8995
  }
8993
8996
  if (!physical && !ir) throw new DeviceNotFoundError(deviceId);
8994
- const typeName = physical ? physical.deviceType ?? "" : ir.remoteType;
8997
+ let typeName;
8998
+ let typeSource;
8999
+ if (physical) {
9000
+ if (physical.deviceType) {
9001
+ typeName = physical.deviceType;
9002
+ typeSource = "deviceType";
9003
+ } else if (physical.controlType) {
9004
+ typeName = physical.controlType;
9005
+ typeSource = "controlType";
9006
+ } else {
9007
+ typeName = "Unknown Device";
9008
+ typeSource = "deviceType";
9009
+ }
9010
+ } else {
9011
+ typeName = ir.remoteType;
9012
+ typeSource = "remoteType";
9013
+ }
8995
9014
  const match = typeName ? findCatalogEntry(typeName) : null;
8996
9015
  const catalogEntry = !match || Array.isArray(match) ? null : match;
8997
9016
  let liveStatus;
@@ -9027,6 +9046,7 @@ async function describeDevice(deviceId, options = {}, client) {
9027
9046
  device: selectedDevice,
9028
9047
  isPhysical: Boolean(physical),
9029
9048
  typeName,
9049
+ typeSource,
9030
9050
  controlType: physical?.controlType ?? ir?.controlType ?? null,
9031
9051
  catalog: catalogEntry,
9032
9052
  capabilities,
@@ -28296,8 +28316,21 @@ function commandToJson(cmd, opts = {}) {
28296
28316
  }
28297
28317
  function resolveTargetCommand(root, argv) {
28298
28318
  let cmd = root;
28319
+ const rootOptions = root.options;
28320
+ let consumeNext = false;
28299
28321
  for (const token of argv) {
28300
- if (token.startsWith("-")) continue;
28322
+ if (consumeNext) {
28323
+ consumeNext = false;
28324
+ continue;
28325
+ }
28326
+ if (token.startsWith("-")) {
28327
+ if (!token.includes("=")) {
28328
+ const localOpts = cmd.options;
28329
+ const opt = localOpts.find((o) => o.short === token || o.long === token) || rootOptions.find((o) => o.short === token || o.long === token);
28330
+ if (opt && (opt.required || opt.optional)) consumeNext = true;
28331
+ }
28332
+ continue;
28333
+ }
28301
28334
  const sub = cmd.commands.find(
28302
28335
  (c) => c.name() === token || c.aliases().includes(token)
28303
28336
  );
@@ -31697,6 +31730,161 @@ function applyFilter(clauses, deviceList, infraredRemoteList, hubLocation) {
31697
31730
  // src/devices/param-validator.ts
31698
31731
  init_cjs_shim();
31699
31732
  init_output();
31733
+
31734
+ // src/devices/css-colors.ts
31735
+ init_cjs_shim();
31736
+ var CSS_COLORS = {
31737
+ aliceblue: [240, 248, 255],
31738
+ antiquewhite: [250, 235, 215],
31739
+ aqua: [0, 255, 255],
31740
+ aquamarine: [127, 255, 212],
31741
+ azure: [240, 255, 255],
31742
+ beige: [245, 245, 220],
31743
+ bisque: [255, 228, 196],
31744
+ black: [0, 0, 0],
31745
+ blanchedalmond: [255, 235, 205],
31746
+ blue: [0, 0, 255],
31747
+ blueviolet: [138, 43, 226],
31748
+ brown: [165, 42, 42],
31749
+ burlywood: [222, 184, 135],
31750
+ cadetblue: [95, 158, 160],
31751
+ chartreuse: [127, 255, 0],
31752
+ chocolate: [210, 105, 30],
31753
+ coral: [255, 127, 80],
31754
+ cornflowerblue: [100, 149, 237],
31755
+ cornsilk: [255, 248, 220],
31756
+ crimson: [220, 20, 60],
31757
+ cyan: [0, 255, 255],
31758
+ darkblue: [0, 0, 139],
31759
+ darkcyan: [0, 139, 139],
31760
+ darkgoldenrod: [184, 134, 11],
31761
+ darkgray: [169, 169, 169],
31762
+ darkgreen: [0, 100, 0],
31763
+ darkgrey: [169, 169, 169],
31764
+ darkkhaki: [189, 183, 107],
31765
+ darkmagenta: [139, 0, 139],
31766
+ darkolivegreen: [85, 107, 47],
31767
+ darkorange: [255, 140, 0],
31768
+ darkorchid: [153, 50, 204],
31769
+ darkred: [139, 0, 0],
31770
+ darksalmon: [233, 150, 122],
31771
+ darkseagreen: [143, 188, 143],
31772
+ darkslateblue: [72, 61, 139],
31773
+ darkslategray: [47, 79, 79],
31774
+ darkslategrey: [47, 79, 79],
31775
+ darkturquoise: [0, 206, 209],
31776
+ darkviolet: [148, 0, 211],
31777
+ deeppink: [255, 20, 147],
31778
+ deepskyblue: [0, 191, 255],
31779
+ dimgray: [105, 105, 105],
31780
+ dimgrey: [105, 105, 105],
31781
+ dodgerblue: [30, 144, 255],
31782
+ firebrick: [178, 34, 34],
31783
+ floralwhite: [255, 250, 240],
31784
+ forestgreen: [34, 139, 34],
31785
+ fuchsia: [255, 0, 255],
31786
+ gainsboro: [220, 220, 220],
31787
+ ghostwhite: [248, 248, 255],
31788
+ gold: [255, 215, 0],
31789
+ goldenrod: [218, 165, 32],
31790
+ gray: [128, 128, 128],
31791
+ green: [0, 128, 0],
31792
+ greenyellow: [173, 255, 47],
31793
+ grey: [128, 128, 128],
31794
+ honeydew: [240, 255, 240],
31795
+ hotpink: [255, 105, 180],
31796
+ indianred: [205, 92, 92],
31797
+ indigo: [75, 0, 130],
31798
+ ivory: [255, 255, 240],
31799
+ khaki: [240, 230, 140],
31800
+ lavender: [230, 230, 250],
31801
+ lavenderblush: [255, 240, 245],
31802
+ lawngreen: [124, 252, 0],
31803
+ lemonchiffon: [255, 250, 205],
31804
+ lightblue: [173, 216, 230],
31805
+ lightcoral: [240, 128, 128],
31806
+ lightcyan: [224, 255, 255],
31807
+ lightgoldenrodyellow: [250, 250, 210],
31808
+ lightgray: [211, 211, 211],
31809
+ lightgreen: [144, 238, 144],
31810
+ lightgrey: [211, 211, 211],
31811
+ lightpink: [255, 182, 193],
31812
+ lightsalmon: [255, 160, 122],
31813
+ lightseagreen: [32, 178, 170],
31814
+ lightskyblue: [135, 206, 250],
31815
+ lightslategray: [119, 136, 153],
31816
+ lightslategrey: [119, 136, 153],
31817
+ lightsteelblue: [176, 196, 222],
31818
+ lightyellow: [255, 255, 224],
31819
+ lime: [0, 255, 0],
31820
+ limegreen: [50, 205, 50],
31821
+ linen: [250, 240, 230],
31822
+ magenta: [255, 0, 255],
31823
+ maroon: [128, 0, 0],
31824
+ mediumaquamarine: [102, 205, 170],
31825
+ mediumblue: [0, 0, 205],
31826
+ mediumorchid: [186, 85, 211],
31827
+ mediumpurple: [147, 111, 219],
31828
+ mediumseagreen: [60, 179, 113],
31829
+ mediumslateblue: [123, 104, 238],
31830
+ mediumspringgreen: [0, 250, 154],
31831
+ mediumturquoise: [72, 209, 204],
31832
+ mediumvioletred: [199, 21, 133],
31833
+ midnightblue: [25, 25, 112],
31834
+ mintcream: [245, 255, 250],
31835
+ mistyrose: [255, 228, 225],
31836
+ moccasin: [255, 228, 181],
31837
+ navajowhite: [255, 222, 173],
31838
+ navy: [0, 0, 128],
31839
+ oldlace: [253, 245, 230],
31840
+ olive: [128, 128, 0],
31841
+ olivedrab: [107, 142, 35],
31842
+ orange: [255, 165, 0],
31843
+ orangered: [255, 69, 0],
31844
+ orchid: [218, 112, 214],
31845
+ palegoldenrod: [238, 232, 170],
31846
+ palegreen: [152, 251, 152],
31847
+ paleturquoise: [175, 238, 238],
31848
+ palevioletred: [219, 112, 147],
31849
+ papayawhip: [255, 239, 213],
31850
+ peachpuff: [255, 218, 185],
31851
+ peru: [205, 133, 63],
31852
+ pink: [255, 192, 203],
31853
+ plum: [221, 160, 221],
31854
+ powderblue: [176, 224, 230],
31855
+ purple: [128, 0, 128],
31856
+ rebeccapurple: [102, 51, 153],
31857
+ red: [255, 0, 0],
31858
+ rosybrown: [188, 143, 143],
31859
+ royalblue: [65, 105, 225],
31860
+ saddlebrown: [139, 69, 19],
31861
+ salmon: [250, 128, 114],
31862
+ sandybrown: [244, 164, 96],
31863
+ seagreen: [46, 139, 87],
31864
+ seashell: [255, 245, 238],
31865
+ sienna: [160, 82, 45],
31866
+ silver: [192, 192, 192],
31867
+ skyblue: [135, 206, 235],
31868
+ slateblue: [106, 90, 205],
31869
+ slategray: [112, 128, 144],
31870
+ slategrey: [112, 128, 144],
31871
+ snow: [255, 250, 250],
31872
+ springgreen: [0, 255, 127],
31873
+ steelblue: [70, 130, 180],
31874
+ tan: [210, 180, 140],
31875
+ teal: [0, 128, 128],
31876
+ thistle: [216, 191, 216],
31877
+ tomato: [255, 99, 71],
31878
+ turquoise: [64, 224, 208],
31879
+ violet: [238, 130, 238],
31880
+ wheat: [245, 222, 179],
31881
+ white: [255, 255, 255],
31882
+ whitesmoke: [245, 245, 245],
31883
+ yellow: [255, 255, 0],
31884
+ yellowgreen: [154, 205, 50]
31885
+ };
31886
+
31887
+ // src/devices/param-validator.ts
31700
31888
  var AC_MODE_MAP = { auto: 1, cool: 2, dry: 3, fan: 4, heat: 5 };
31701
31889
  var AC_FAN_MAP = { auto: 1, low: 2, mid: 3, high: 4 };
31702
31890
  var CURTAIN_MODE_MAP = { default: "ff", performance: "0", silent: "1" };
@@ -31763,6 +31951,26 @@ function buildRelaySetMode(opts) {
31763
31951
  }
31764
31952
  return `${ch};${modeInt}`;
31765
31953
  }
31954
+ function buildBrightnessSet(opts) {
31955
+ if (!opts.brightness) throw new UsageError("--brightness is required (1-100)");
31956
+ const b2 = parseInt(opts.brightness, 10);
31957
+ if (!Number.isFinite(b2) || b2 < 1 || b2 > 100) {
31958
+ throw new UsageError(`--brightness must be an integer between 1 and 100 (got "${opts.brightness}")`);
31959
+ }
31960
+ return String(b2);
31961
+ }
31962
+ function buildColorSet(opts) {
31963
+ if (!opts.color) throw new UsageError('--color is required (e.g. "255:0:0", "#FF0000", "red")');
31964
+ const result = validateSetColor(opts.color);
31965
+ if (!result.ok) throw new UsageError(result.error);
31966
+ return result.normalized ?? opts.color;
31967
+ }
31968
+ function buildColorTemperatureSet(opts) {
31969
+ if (!opts.colorTemp) throw new UsageError("--color-temp is required (2700-6500)");
31970
+ const result = validateSetColorTemperature(opts.colorTemp);
31971
+ if (!result.ok) throw new UsageError(result.error);
31972
+ return result.normalized ?? opts.colorTemp;
31973
+ }
31766
31974
  function validateParameter(deviceType, command, raw) {
31767
31975
  if (!deviceType) return { ok: true };
31768
31976
  if (deviceType === "Air Conditioner" && command === "setAll") {
@@ -31783,7 +31991,7 @@ function validateParameter(deviceType, command, raw) {
31783
31991
  if (command === "setColor" && isColorDevice(deviceType)) {
31784
31992
  return validateSetColor(raw);
31785
31993
  }
31786
- if (command === "setColorTemperature" && isColorDevice(deviceType)) {
31994
+ if (command === "setColorTemperature" && isBrightnessDevice(deviceType)) {
31787
31995
  return validateSetColorTemperature(raw);
31788
31996
  }
31789
31997
  return { ok: true };
@@ -31792,7 +32000,12 @@ function isBrightnessDevice(deviceType) {
31792
32000
  return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Ceiling Light" || deviceType === "Ceiling Light Pro" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Dimmer" || deviceType === "Fill Light";
31793
32001
  }
31794
32002
  function isColorDevice(deviceType) {
31795
- return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Ceiling Light" || deviceType === "Ceiling Light Pro" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Fill Light";
32003
+ return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Fill Light";
32004
+ }
32005
+ function isLightingCommandSupported(deviceType, command) {
32006
+ if (command === "setBrightness" || command === "setColorTemperature") return isBrightnessDevice(deviceType);
32007
+ if (command === "setColor") return isColorDevice(deviceType);
32008
+ return false;
31796
32009
  }
31797
32010
  function validateSetBrightness(raw) {
31798
32011
  if (raw === void 0 || raw === "" || raw === "default") {
@@ -31820,29 +32033,18 @@ function validateSetBrightness(raw) {
31820
32033
  function hintBrightnessRetry() {
31821
32034
  return `Ask the user whether they meant a percentage (1-100). Example: "50".`;
31822
32035
  }
31823
- var NAMED_COLORS = {
31824
- red: [255, 0, 0],
31825
- green: [0, 128, 0],
31826
- lime: [0, 255, 0],
31827
- blue: [0, 0, 255],
31828
- yellow: [255, 255, 0],
31829
- cyan: [0, 255, 255],
31830
- magenta: [255, 0, 255],
31831
- white: [255, 255, 255],
31832
- black: [0, 0, 0],
31833
- orange: [255, 165, 0],
31834
- purple: [128, 0, 128],
31835
- pink: [255, 192, 203],
31836
- brown: [165, 42, 42],
31837
- grey: [128, 128, 128],
31838
- gray: [128, 128, 128],
32036
+ var CUSTOM_COLORS = {
31839
32037
  warm: [255, 180, 100]
31840
32038
  };
32039
+ var NAMED_COLORS = {
32040
+ ...CSS_COLORS,
32041
+ ...CUSTOM_COLORS
32042
+ };
31841
32043
  function validateSetColor(raw) {
31842
32044
  if (raw === void 0 || raw === "" || raw === "default") {
31843
32045
  return {
31844
32046
  ok: false,
31845
- error: `setColor requires a color. Expected one of: "R:G:B" (e.g. "255:0:0"), "#RRGGBB" (e.g. "#FF0000"), "#RGB", "R,G,B", or a named color (${Object.keys(NAMED_COLORS).slice(0, 8).join(", ")}, ...).`
32047
+ error: `setColor requires a color. Use a CSS color name (e.g. coral, teal, salmon), hex (#RRGGBB / #RGB), or R:G:B format.`
31846
32048
  };
31847
32049
  }
31848
32050
  const trimmed = raw.trim();
@@ -32357,8 +32559,11 @@ Examples:
32357
32559
  }
32358
32560
  return;
32359
32561
  }
32562
+ const totalDevices = resolved.ids.length;
32563
+ const deviceIndices = new Map(resolved.ids.map((id, i) => [id, i + 1]));
32360
32564
  const startedAt = Date.now();
32361
32565
  const outcomes = await runPool(resolved.ids, concurrency, staggerMs, async (id) => {
32566
+ const stepIdx = deviceIndices.get(id);
32362
32567
  const stepStart = Date.now();
32363
32568
  const startedIso = new Date(stepStart).toISOString();
32364
32569
  try {
@@ -32370,7 +32575,7 @@ Examples:
32370
32575
  const durationMs = Date.now() - stepStart;
32371
32576
  const replayed = typeof result2 === "object" && result2 !== null && result2.replayed === true;
32372
32577
  if (!isJsonMode()) {
32373
- console.log(`\u2713 ${id}: ${cmd}${replayed ? " (replayed)" : ""}`);
32578
+ console.log(`[${stepIdx}/${totalDevices}] \u2713 ${id}: ${cmd}${replayed ? " (replayed)" : ""}`);
32374
32579
  }
32375
32580
  return {
32376
32581
  ok: true,
@@ -32393,7 +32598,7 @@ Examples:
32393
32598
  }
32394
32599
  const errorPayload = buildErrorPayload(err);
32395
32600
  if (!isJsonMode()) {
32396
- console.error(`\u2717 ${id}: ${errorPayload.message}`);
32601
+ console.error(`[${stepIdx}/${totalDevices}] \u2717 ${id}: ${errorPayload.message}`);
32397
32602
  }
32398
32603
  return {
32399
32604
  ok: false,
@@ -32643,7 +32848,7 @@ function registerWatchCommand(devices) {
32643
32848
  `Polling interval: "30s", "1m", "500ms", ... (default 30s, min ${MIN_INTERVAL_MS / 1e3}s)`,
32644
32849
  durationArg("--interval"),
32645
32850
  "30s"
32646
- ).option("--max <n>", "Stop after N ticks (default: run until Ctrl-C)", intArg("--max", { min: 1 })).option("--for <dur>", 'Stop after elapsed time (e.g. "5m", "30s"). Combines with --max: first limit wins.', durationArg("--for")).option("--include-unchanged", "Emit a tick even when no field changed").option("--initial <mode>", "How to handle the first poll: snapshot | emit | skip (default: snapshot)", enumArg("--initial", INITIAL_MODES), "snapshot").addHelpText(
32851
+ ).option("--max <n>", "Stop after N ticks (default: run until Ctrl-C)", intArg("--max", { min: 1 })).option("--once", "Stop after one tick (shorthand for --max 1)").option("--for <dur>", 'Stop after elapsed time (e.g. "5m", "30s"). Combines with --max: first limit wins.', durationArg("--for")).option("--include-unchanged", "Emit a tick even when no field changed").option("--initial <mode>", "How to handle the first poll: snapshot | emit | skip (default: snapshot)", enumArg("--initial", INITIAL_MODES), "snapshot").addHelpText(
32647
32852
  "after",
32648
32853
  `
32649
32854
  Default output is a human-readable table of field changes per tick; add --json
@@ -32672,6 +32877,12 @@ Examples:
32672
32877
  ).action(
32673
32878
  async (deviceIds, options) => {
32674
32879
  try {
32880
+ if (options.once && options.max !== void 0) {
32881
+ throw new UsageError("--once and --max are mutually exclusive.");
32882
+ }
32883
+ if (options.once) {
32884
+ options.max = "1";
32885
+ }
32675
32886
  const allIds = [...deviceIds];
32676
32887
  if (options.name) {
32677
32888
  const resolved = resolveDeviceId(void 0, options.name);
@@ -32923,10 +33134,11 @@ init_arg_parsers();
32923
33134
  init_output();
32924
33135
  init_cache();
32925
33136
  init_devices();
33137
+ init_catalog();
32926
33138
  init_flags();
32927
33139
  init_client();
32928
33140
  function registerExpandCommand(devices) {
32929
- devices.command("expand").description("Send a command with semantic flags instead of raw positional parameters").argument("[deviceId]", 'Target device ID from "devices list" (or use --name)').argument("[command]", "Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2)").option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: require-unique)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Curtain", "Air Conditioner")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--temp <celsius>", "AC setAll: temperature in Celsius (16-30)", intArg("--temp", { min: 16, max: 30 })).option("--mode <mode>", "AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary", stringArg("--mode")).option("--fan <speed>", "AC setAll: fan speed auto|low|mid|high", stringArg("--fan")).option("--power <state>", "AC setAll: on|off", stringArg("--power")).option("--position <percent>", "Curtain setPosition: 0-100 (0=open, 100=closed)", intArg("--position", { min: 0, max: 100 })).option("--direction <dir>", "Blind Tilt setPosition: up|down", stringArg("--direction")).option("--angle <percent>", "Blind Tilt setPosition: 0-100 (0=closed, 100=open)", intArg("--angle", { min: 0, max: 100 })).option("--channel <n>", "Relay Switch 2 setMode: channel 1 or 2", intArg("--channel", { min: 1, max: 2 })).option("--yes", "Confirm destructive commands").addHelpText("after", `
33141
+ devices.command("expand").description("Send a command with semantic flags instead of raw positional parameters").argument("[deviceId]", 'Target device ID from "devices list" (or use --name)').argument("[command]", "Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2), setBrightness/setColor/setColorTemperature (lighting)").option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: require-unique)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Curtain", "Air Conditioner")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--temp <celsius>", "AC setAll: temperature in Celsius (16-30)", intArg("--temp", { min: 16, max: 30 })).option("--mode <mode>", "AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary", stringArg("--mode")).option("--fan <speed>", "AC setAll: fan speed auto|low|mid|high", stringArg("--fan")).option("--power <state>", "AC setAll: on|off", stringArg("--power")).option("--position <percent>", "Curtain setPosition: 0-100 (0=open, 100=closed)", intArg("--position", { min: 0, max: 100 })).option("--direction <dir>", "Blind Tilt setPosition: up|down", stringArg("--direction")).option("--angle <percent>", "Blind Tilt setPosition: 0-100 (0=closed, 100=open)", intArg("--angle", { min: 0, max: 100 })).option("--channel <n>", "Relay Switch 2 setMode: channel 1 or 2", intArg("--channel", { min: 1, max: 2 })).option("--brightness <percent>", "setBrightness: 1-100 percent", intArg("--brightness", { min: 1, max: 100 })).option("--color <value>", "setColor: R:G:B, #RRGGBB, or named color (red, blue, etc.)", stringArg("--color")).option("--color-temp <kelvin>", "setColorTemperature: 2700-6500 Kelvin", intArg("--color-temp", { min: 2700, max: 6500 })).option("--yes", "Confirm destructive commands").addHelpText("after", `
32930
33142
  Translates semantic flags into the wire parameter format, then sends the command.
32931
33143
 
32932
33144
  Supported expansions:
@@ -32947,12 +33159,25 @@ Supported expansions:
32947
33159
  --channel 1 --mode edge \u2192 "1;1"
32948
33160
  --mode values: toggle (0) | edge (1) | detached (2) | momentary (3)
32949
33161
 
33162
+ Color Bulb / Strip Light / Ceiling Light \u2014 setBrightness
33163
+ --brightness 80 \u2192 "80"
33164
+
33165
+ Color Bulb / Strip Light / Floor Lamp \u2014 setColor
33166
+ --color "255:0:0" \u2192 "255:0:0"
33167
+ --color "#FF0000" \u2192 "255:0:0"
33168
+ --color red \u2192 "255:0:0"
33169
+
33170
+ Color Bulb / Strip Light / Ceiling Light \u2014 setColorTemperature
33171
+ --color-temp 4000 \u2192 "4000"
33172
+
32950
33173
  Examples:
32951
- $ switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
32952
- $ switchbot devices expand <curtainId> setPosition --position 50 --mode silent
32953
- $ switchbot devices expand <blindId> setPosition --direction up --angle 50
32954
- $ switchbot devices expand <relayId> setMode --channel 1 --mode edge
32955
- $ switchbot devices expand <acId> setAll --temp 22 --mode heat --fan auto --power on --dry-run
33174
+ $ switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
33175
+ $ switchbot devices expand <curtainId> setPosition --position 50 --mode silent
33176
+ $ switchbot devices expand <blindId> setPosition --direction up --angle 50
33177
+ $ switchbot devices expand <relayId> setMode --channel 1 --mode edge
33178
+ $ switchbot devices expand <stripId> setBrightness --brightness 80
33179
+ $ switchbot devices expand <bulbId> setColor --color "#FF0000"
33180
+ $ switchbot devices expand <bulbId> setColorTemperature --color-temp 4000
32956
33181
  $ switchbot devices expand --name "Living Room AC" setAll --temp 26 --mode cool --fan low --power on
32957
33182
  `).action(async (deviceIdArg, commandArg, options) => {
32958
33183
  let deviceId = "";
@@ -32970,12 +33195,22 @@ Examples:
32970
33195
  category: options.nameCategory,
32971
33196
  room: options.nameRoom
32972
33197
  });
32973
- if (!effectiveCommand) throw new UsageError("A command argument is required (setAll, setPosition, setMode).");
33198
+ if (!effectiveCommand) throw new UsageError("A command argument is required (setAll, setPosition, setMode, setBrightness, setColor, setColorTemperature).");
32974
33199
  command = effectiveCommand;
32975
33200
  const cached2 = getCachedDevice(deviceId);
32976
33201
  const deviceType = cached2?.type ?? "";
32977
33202
  let parameter;
32978
33203
  if (command === "setAll") {
33204
+ if (!cached2) {
33205
+ throw new UsageError(
33206
+ `Device ${deviceId} is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' can verify this is an Air Conditioner.`
33207
+ );
33208
+ }
33209
+ if (deviceType !== "Air Conditioner") {
33210
+ throw new UsageError(
33211
+ `"setAll" is only supported on Air Conditioner devices, but "${cached2.type}" was found.`
33212
+ );
33213
+ }
32979
33214
  parameter = buildAcSetAll(options);
32980
33215
  } else if (command === "setPosition") {
32981
33216
  if (!cached2) {
@@ -32983,13 +33218,56 @@ Examples:
32983
33218
  `Device ${deviceId} is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' knows whether this is a Curtain or a Blind Tilt.`
32984
33219
  );
32985
33220
  }
33221
+ const positionTypes = ["Curtain", "Curtain 3", "Roller Shade", "Blind Tilt"];
33222
+ if (!positionTypes.some((t) => deviceType.startsWith(t))) {
33223
+ throw new UsageError(
33224
+ `"setPosition" is only supported on Curtain, Roller Shade, and Blind Tilt devices, but "${cached2.type}" was found.`
33225
+ );
33226
+ }
32986
33227
  const isBlind = deviceType.startsWith("Blind Tilt");
32987
- parameter = isBlind ? buildBlindTiltSetPosition(options) : buildCurtainSetPosition(options);
33228
+ const isRollerShade = deviceType.startsWith("Roller Shade");
33229
+ if (isBlind) {
33230
+ parameter = buildBlindTiltSetPosition(options);
33231
+ } else if (isRollerShade) {
33232
+ if (!options.position) throw new UsageError("--position is required (0-100)");
33233
+ parameter = options.position;
33234
+ } else {
33235
+ parameter = buildCurtainSetPosition(options);
33236
+ }
32988
33237
  } else if (command === "setMode" && deviceType.startsWith("Relay Switch")) {
32989
33238
  parameter = buildRelaySetMode(options);
33239
+ } else if (command === "setBrightness" || command === "setColor" || command === "setColorTemperature") {
33240
+ if (!cached2) {
33241
+ throw new UsageError(
33242
+ `Device "${deviceId}" is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' can verify this device supports ${command}.`
33243
+ );
33244
+ }
33245
+ const catalogResult = findCatalogEntry(cached2.type);
33246
+ const catalogEntry = Array.isArray(catalogResult) ? catalogResult[0] : catalogResult;
33247
+ const supportedHint = command === "setColor" ? "Color Bulb, Strip Light, Floor Lamp, and similar RGB lighting devices" : "Color Bulb, Strip Light, Ceiling Light, Floor Lamp, and similar lighting devices";
33248
+ if (catalogEntry !== null) {
33249
+ if (!catalogEntry.commands.some((c) => c.command === command)) {
33250
+ throw new UsageError(
33251
+ `Device type "${cached2.type}" does not support ${command}. Supported on: ${supportedHint}.`
33252
+ );
33253
+ }
33254
+ } else {
33255
+ if (!isLightingCommandSupported(cached2.type, command)) {
33256
+ throw new UsageError(
33257
+ `Device type "${cached2.type}" does not support ${command}. Supported on: ${supportedHint}.`
33258
+ );
33259
+ }
33260
+ }
33261
+ if (command === "setBrightness") {
33262
+ parameter = buildBrightnessSet(options);
33263
+ } else if (command === "setColor") {
33264
+ parameter = buildColorSet(options);
33265
+ } else {
33266
+ parameter = buildColorTemperatureSet(options);
33267
+ }
32990
33268
  } else {
32991
33269
  throw new UsageError(
32992
- `'expand' does not support "${command}" for device type "${deviceType || "unknown"}". Use 'switchbot devices command' to send raw parameters instead.`
33270
+ `'expand' does not support "${command}" for device type "${deviceType || "unknown"}". Supported: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch), setBrightness/setColor/setColorTemperature (lighting). Use 'switchbot devices command' to send raw parameters instead.`
32993
33271
  );
32994
33272
  }
32995
33273
  if (!options.yes && !isDryRun() && isDestructiveCommand(deviceType, command, "command")) {
@@ -33153,13 +33431,16 @@ var EXPAND_HINTS = {
33153
33431
  "Relay Switch 2PM": { command: "setMode", flags: "--channel 1 --mode edge" }
33154
33432
  };
33155
33433
  function annotateStatusPayload(deviceId, body) {
33156
- const annotated = { ...body };
33434
+ const cached2 = getCachedDevice(deviceId);
33435
+ const deviceType = cached2?.type ?? "";
33436
+ const annotated = { deviceId, deviceType, ...body };
33437
+ annotated.deviceId = deviceId;
33438
+ annotated.deviceType = deviceType;
33157
33439
  if (Object.keys(body).length === 0) {
33158
33440
  annotated.supported = false;
33159
33441
  annotated.note = "this device does not expose cloud status";
33160
33442
  return annotated;
33161
33443
  }
33162
- const cached2 = getCachedDevice(deviceId);
33163
33444
  const looksLikeMeter = cached2?.type?.toLowerCase().includes("meter") ?? false;
33164
33445
  const staleZeroReading = looksLikeMeter && !Object.prototype.hasOwnProperty.call(body, "onlineStatus") && body.battery === 0 && body.temperature === 0 && body.humidity === 0;
33165
33446
  if (staleZeroReading) {
@@ -33316,7 +33597,7 @@ Examples:
33316
33597
  rows.push([
33317
33598
  d.deviceId,
33318
33599
  d.deviceName,
33319
- d.deviceType || "\u2014",
33600
+ d.deviceType || d.controlType || "Unknown Device",
33320
33601
  "physical",
33321
33602
  d.controlType || "\u2014",
33322
33603
  d.familyName || "\u2014",
@@ -33402,7 +33683,7 @@ Examples:
33402
33683
  const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
33403
33684
  const fetchedAt2 = (/* @__PURE__ */ new Date()).toISOString();
33404
33685
  const batch = results.map(
33405
- (r, i) => r.status === "fulfilled" ? { deviceId: ids[i], ok: true, _fetchedAt: fetchedAt2, ...annotateStatusPayload(ids[i], r.value) } : { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) }
33686
+ (r, i) => r.status === "fulfilled" ? { deviceId: ids[i], ok: true, fetchedAt: fetchedAt2, ...annotateStatusPayload(ids[i], r.value) } : { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) }
33406
33687
  );
33407
33688
  const batchFmt = resolveFormat();
33408
33689
  if (isJsonMode() || batchFmt === "json") {
@@ -33414,7 +33695,7 @@ Examples:
33414
33695
  } else {
33415
33696
  const rawFields = resolveFields();
33416
33697
  for (const entry of batch) {
33417
- const { deviceId: deviceId2, ok, error: error48, _fetchedAt: ts, ...status } = entry;
33698
+ const { deviceId: deviceId2, ok, error: error48, fetchedAt: ts, ...status } = entry;
33418
33699
  console.log(`
33419
33700
  \u2500\u2500\u2500 ${String(deviceId2)} \u2500\u2500\u2500`);
33420
33701
  if (!ok) {
@@ -33440,11 +33721,11 @@ Examples:
33440
33721
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
33441
33722
  const fmt = resolveFormat();
33442
33723
  if (fmt === "json" && process.argv.includes("--json")) {
33443
- printJson({ ...body, _fetchedAt: fetchedAt });
33724
+ printJson({ ...body, fetchedAt });
33444
33725
  return;
33445
33726
  }
33446
33727
  if (fmt !== "table") {
33447
- const statusWithTs = { ...body, _fetchedAt: fetchedAt };
33728
+ const statusWithTs = { ...body, fetchedAt };
33448
33729
  const allHeaders = Object.keys(statusWithTs);
33449
33730
  const allRows = [Object.values(statusWithTs)];
33450
33731
  const rawFields = resolveFields();
@@ -33773,7 +34054,7 @@ Examples:
33773
34054
  const joinedMatch = findCatalogEntry(joined);
33774
34055
  if (joinedMatch && !Array.isArray(joinedMatch)) {
33775
34056
  if (isJsonMode()) {
33776
- printJson(normalizeCatalogForJson(joinedMatch));
34057
+ printJson([normalizeCatalogForJson(joinedMatch)]);
33777
34058
  } else {
33778
34059
  renderCatalogEntry(joinedMatch);
33779
34060
  }
@@ -33865,12 +34146,19 @@ Examples:
33865
34146
  });
33866
34147
  return;
33867
34148
  }
34149
+ if (result.typeSource === "controlType") {
34150
+ const deviceName2 = device.deviceName ?? deviceId;
34151
+ console.error(`warning: ${deviceName2} (${deviceId}): deviceType not reported by API, using controlType "${result.controlType}". Capabilities may be limited.`);
34152
+ } else if (typeName === "Unknown Device") {
34153
+ const deviceName2 = device.deviceName ?? deviceId;
34154
+ console.error(`warning: ${deviceName2} (${deviceId}): neither deviceType nor controlType reported by API. Capabilities may be limited.`);
34155
+ }
33868
34156
  if (isPhysical) {
33869
34157
  const physical = device;
33870
34158
  printKeyValue({
33871
34159
  deviceId: physical.deviceId,
33872
34160
  deviceName: physical.deviceName,
33873
- deviceType: physical.deviceType || "\u2014",
34161
+ deviceType: physical.deviceType || physical.controlType || "Unknown Device",
33874
34162
  controlType: physical.controlType || "\u2014",
33875
34163
  family: physical.familyName || "\u2014",
33876
34164
  roomID: physical.roomID || "\u2014",
@@ -34049,7 +34337,7 @@ Examples:
34049
34337
  handleError(error48);
34050
34338
  }
34051
34339
  });
34052
- scenes.command("execute").description("Execute a manual scene by its ID").argument("<sceneId>", 'Scene ID from "scenes list"').addHelpText("after", `
34340
+ scenes.command("execute").alias("run").description("Execute a manual scene by its ID").argument("<sceneId>", 'Scene ID from "scenes list"').addHelpText("after", `
34053
34341
  Example:
34054
34342
  $ switchbot scenes execute T12345678
34055
34343
  `).action(async (sceneId) => {
@@ -48672,9 +48960,9 @@ var logLevel = process.env.LOG_LEVEL || "warn";
48672
48960
  var logFormat = process.env.LOG_FORMAT || "json";
48673
48961
  var pinoConfig = {
48674
48962
  level: logLevel,
48675
- transport: logFormat === "pretty" ? { target: "pino-pretty" } : void 0
48963
+ transport: logFormat === "pretty" ? { target: "pino-pretty", options: { destination: 2 } } : void 0
48676
48964
  };
48677
- var log = pino(pinoConfig);
48965
+ var log = logFormat === "pretty" ? pino(pinoConfig) : pino(pinoConfig, pino.destination(2));
48678
48966
 
48679
48967
  // src/mcp/device-history.ts
48680
48968
  init_cjs_shim();
@@ -49782,15 +50070,27 @@ against the live API without executing any mutations.
49782
50070
  handleError(err);
49783
50071
  }
49784
50072
  });
49785
- plan.command("run").description("Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default").argument("[file]", 'Path to plan.json, or "-" / omit to read stdin').option("--yes", "Authorize destructive commands (e.g. Smart Lock unlock, Garage open)").option("--require-approval", "Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)").option("--continue-on-error", "Keep running after a failed step (default: stop at first error)").action(
50073
+ plan.command("run").description("Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default").argument("[file]", 'Path to plan.json, or "-" / omit to read stdin').option("--yes", "Authorize destructive commands (e.g. Smart Lock unlock, Garage open)").option("--require-approval", "Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)").option("--continue-on-error", "Keep running after a failed step (default: stop at first error)").option("--plan <json>", "Inline plan JSON (alternative to file argument or stdin)").action(
49786
50074
  async (file2, options) => {
49787
50075
  if (options.requireApproval && isJsonMode()) {
49788
50076
  console.error("error: --require-approval cannot be used with --json (no TTY available for prompts)");
49789
50077
  process.exit(1);
49790
50078
  }
50079
+ if (options.plan !== void 0 && file2 !== void 0) {
50080
+ console.error("error: --plan and a file argument are mutually exclusive.");
50081
+ process.exit(2);
50082
+ }
49791
50083
  let raw;
49792
50084
  try {
49793
- raw = await readPlanSource(file2);
50085
+ if (options.plan !== void 0) {
50086
+ try {
50087
+ raw = JSON.parse(options.plan);
50088
+ } catch (err) {
50089
+ throw new UsageError(`--plan is not valid JSON: ${err.message}`);
50090
+ }
50091
+ } else {
50092
+ raw = await readPlanSource(file2);
50093
+ }
49794
50094
  } catch (err) {
49795
50095
  handleError(err);
49796
50096
  }
@@ -52513,12 +52813,27 @@ function listRegisteredTools(server) {
52513
52813
  if (!internal._registeredTools) return [];
52514
52814
  return Object.keys(internal._registeredTools).sort();
52515
52815
  }
52816
+ function listRegisteredToolsWithMeta(server) {
52817
+ const internal = server;
52818
+ if (!internal._registeredTools) return [];
52819
+ return Object.entries(internal._registeredTools).sort(([a], [b2]) => a.localeCompare(b2)).map(([name, reg]) => {
52820
+ const entry = { name };
52821
+ if (reg.description) entry.description = reg.description;
52822
+ if (reg.inputSchema) {
52823
+ try {
52824
+ entry.inputSchema = external_exports.toJSONSchema(reg.inputSchema);
52825
+ } catch {
52826
+ }
52827
+ }
52828
+ return entry;
52829
+ });
52830
+ }
52516
52831
  function listRegisteredResources() {
52517
52832
  return ["switchbot://events"];
52518
52833
  }
52519
52834
  function printMcpToolDirectory() {
52520
52835
  const server = createSwitchBotMcpServer();
52521
- const tools = listRegisteredTools(server).map((name) => ({ name }));
52836
+ const tools = listRegisteredToolsWithMeta(server);
52522
52837
  const resources = listRegisteredResources().map((uri) => ({ uri }));
52523
52838
  if (isJsonMode()) {
52524
52839
  printJson({ tools, resources });
@@ -52526,7 +52841,8 @@ function printMcpToolDirectory() {
52526
52841
  }
52527
52842
  console.log("Tools:");
52528
52843
  for (const tool of tools) {
52529
- console.log(` ${tool.name}`);
52844
+ const desc = tool.description ? ` \u2014 ${tool.description.slice(0, 80)}` : "";
52845
+ console.log(` ${tool.name}${desc}`);
52530
52846
  }
52531
52847
  console.log("");
52532
52848
  console.log("Resources:");
@@ -52538,7 +52854,7 @@ Total: ${tools.length} tool(s), ${resources.length} resource(s)`);
52538
52854
  }
52539
52855
  function registerMcpCommand(program3) {
52540
52856
  const mcp = program3.command("mcp").description("Run as a Model Context Protocol server so AI agents can call SwitchBot tools").addHelpText("after", `
52541
- The MCP server exposes twenty-one tools:
52857
+ The MCP server exposes twenty-four tools:
52542
52858
  - list_devices fetch all physical + IR devices
52543
52859
  - get_device_status live status for a physical device
52544
52860
  - send_command control a device (destructive commands need confirm:true)
@@ -52561,6 +52877,8 @@ function registerMcpCommand(program3) {
52561
52877
  - audit_stats aggregate audit counts by kind/result/device/rule
52562
52878
  - rule_notifications query rule notify action delivery history
52563
52879
  - rules_suggest draft an automation rule YAML from intent (heuristic, no LLM)
52880
+ - rules_explain show why a rule evaluation fired or was blocked
52881
+ - rules_simulate simulate a rule against historical events
52564
52882
  - policy_add_rule append a rule into automation.rules[] in policy.yaml
52565
52883
 
52566
52884
  Resource (read-only):
@@ -53003,7 +53321,7 @@ Examples:
53003
53321
  throw new UsageError(`"${match.type}" exists in the effective catalog but not in source "${source}".`);
53004
53322
  }
53005
53323
  if (isJsonMode()) {
53006
- printJson(picked);
53324
+ printJson([picked]);
53007
53325
  return;
53008
53326
  }
53009
53327
  renderEntry(picked);
@@ -53745,7 +54063,7 @@ Examples:
53745
54063
  let matchedCount = 0;
53746
54064
  const ac = new AbortController();
53747
54065
  const forTimer = forMs !== null && forMs > 0 ? setTimeout(() => ac.abort(), forMs) : null;
53748
- if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push" });
54066
+ if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push", schemaVersion: EVENTS_SCHEMA_VERSION });
53749
54067
  await new Promise((resolve2, reject) => {
53750
54068
  let server = null;
53751
54069
  try {
@@ -53897,7 +54215,7 @@ Examples:
53897
54215
  if (!isJsonMode()) {
53898
54216
  console.error("Fetching MQTT credentials from SwitchBot service\u2026");
53899
54217
  }
53900
- if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push" });
54218
+ if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push", schemaVersion: EVENTS_SCHEMA_VERSION });
53901
54219
  if (isJsonMode()) {
53902
54220
  const sessionStartAt = (/* @__PURE__ */ new Date()).toISOString();
53903
54221
  emitJsonStreamRecord({
@@ -55467,7 +55785,7 @@ function runSchemaExport(options) {
55467
55785
  payload.resources = RESOURCE_CATALOG;
55468
55786
  payload.cliAddedFields = [
55469
55787
  {
55470
- field: "_fetchedAt",
55788
+ field: "fetchedAt",
55471
55789
  appliesTo: ["devices status", "devices describe"],
55472
55790
  type: "string (ISO-8601)",
55473
55791
  description: "CLI-synthesized timestamp indicating when this status response was fetched or served from the cache. Not part of the upstream SwitchBot API."
@@ -55530,7 +55848,7 @@ Common top-level fields:
55530
55848
  schemaVersion CLI schema version (stable for agent contracts)
55531
55849
  data.version Catalog schema version
55532
55850
  data.types Array of SchemaEntry (or CompactSchemaEntry with --compact)
55533
- data._fetchedAt CLI-added; present on live-query responses ('devices status'),
55851
+ data.fetchedAt CLI-added; present on live-query responses ('devices status'),
55534
55852
  not on this offline export.
55535
55853
 
55536
55854
  Examples:
@@ -55566,7 +55884,7 @@ Examples:
55566
55884
  $ switchbot history show --limit 10
55567
55885
  $ switchbot history replay 3
55568
55886
  `);
55569
- history.command("show").description("Print recent audit entries").option("--file <path>", `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg("--file")).option("--limit <n>", "Show only the last N entries", intArg("--limit", { min: 1 })).action((options) => {
55887
+ history.command("show").alias("list").description("Print recent audit entries").option("--file <path>", `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg("--file")).option("--limit <n>", "Show only the last N entries", intArg("--limit", { min: 1 })).action((options) => {
55570
55888
  const file2 = options.file ?? DEFAULT_AUDIT;
55571
55889
  const entries = readAudit(file2);
55572
55890
  const limited = options.limit !== void 0 ? entries.slice(-Math.max(1, Number(options.limit) || 1)) : entries;
@@ -56841,13 +57159,12 @@ function registerRun(rules) {
56841
57159
  const loaded = loadAutomation(pathArg);
56842
57160
  if (!loaded) return;
56843
57161
  if (loaded.automation?.enabled !== true) {
56844
- const msg = "automation.enabled is not true \u2014 nothing to run.";
56845
- if (isJsonMode()) {
56846
- printJson({ kind: "control", controlKind: "disabled", message: msg });
56847
- } else {
56848
- console.error(msg);
56849
- }
56850
- process.exit(0);
57162
+ exitWithError({
57163
+ code: 1,
57164
+ kind: "runtime",
57165
+ message: "automation.enabled is not true \u2014 set it to true in your policy file to start the daemon.",
57166
+ hint: "Set automation.enabled: true in your policy file, then re-run."
57167
+ });
56851
57168
  }
56852
57169
  const lint = lintRules(loaded.automation);
56853
57170
  if (!lint.valid) {
@@ -59216,6 +59533,7 @@ import os25 from "node:os";
59216
59533
  import path27 from "node:path";
59217
59534
  var DEFAULT_AUDIT_PATH3 = path27.join(os25.homedir(), ".switchbot", "audit.log");
59218
59535
  var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
59536
+ var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set([161, 171, 190]);
59219
59537
  function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
59220
59538
  const now = /* @__PURE__ */ new Date();
59221
59539
  const procHealth = {
@@ -59236,20 +59554,46 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
59236
59554
  };
59237
59555
  let auditHealth;
59238
59556
  if (!fs31.existsSync(auditPath)) {
59239
- auditHealth = { present: false, recentErrors: 0, recentTotal: 0, errorRatePercent: 0, status: "ok" };
59557
+ auditHealth = {
59558
+ present: false,
59559
+ recentErrors: 0,
59560
+ recentTotal: 0,
59561
+ errorRatePercent: 0,
59562
+ expectedErrors: 0,
59563
+ unexpectedErrors: 0,
59564
+ unexpectedRatePercent: 0,
59565
+ breakdown: {},
59566
+ status: "ok"
59567
+ };
59240
59568
  } else {
59241
59569
  const entries = readAudit(auditPath);
59242
59570
  const windowStart = now.getTime() - AUDIT_ERROR_WINDOW_MS;
59243
59571
  const recent = entries.filter((e) => new Date(e.t).getTime() >= windowStart);
59244
- const errors = recent.filter((e) => e.result === "error").length;
59572
+ const errorEntries = recent.filter((e) => e.result === "error");
59245
59573
  const total = recent.length;
59574
+ const errors = errorEntries.length;
59246
59575
  const errorRate = total > 0 ? Math.round(errors / total * 100) : 0;
59576
+ const breakdown = {};
59577
+ let expectedErrors = 0;
59578
+ for (const e of errorEntries) {
59579
+ const code = e.statusCode !== void 0 ? String(e.statusCode) : "unknown";
59580
+ breakdown[code] = (breakdown[code] ?? 0) + 1;
59581
+ if (e.statusCode !== void 0 && EXPECTED_ERROR_CODES.has(e.statusCode)) {
59582
+ expectedErrors++;
59583
+ }
59584
+ }
59585
+ const unexpectedErrors = errors - expectedErrors;
59586
+ const unexpectedRatePercent = total > 0 ? Math.round(unexpectedErrors / total * 100 * 10) / 10 : 0;
59247
59587
  auditHealth = {
59248
59588
  present: true,
59249
59589
  recentErrors: errors,
59250
59590
  recentTotal: total,
59251
59591
  errorRatePercent: errorRate,
59252
- status: errorRate >= 30 ? "warn" : "ok"
59592
+ expectedErrors,
59593
+ unexpectedErrors,
59594
+ unexpectedRatePercent,
59595
+ breakdown,
59596
+ status: unexpectedRatePercent >= 30 ? "warn" : "ok"
59253
59597
  };
59254
59598
  }
59255
59599
  const cbStats = apiCircuitBreaker.getStats();
@@ -59658,7 +60002,7 @@ The daemon reads the same policy file as \`switchbot rules run\`.
59658
60002
  }
59659
60003
  }
59660
60004
  const thisFile = fileURLToPath3(import.meta.url);
59661
- const cliEntry = path28.resolve(path28.dirname(thisFile), "..", "index.js");
60005
+ const cliEntry = path28.basename(thisFile) === "index.js" ? thisFile : path28.resolve(path28.dirname(thisFile), "..", "index.js");
59662
60006
  const args = ["rules", "run"];
59663
60007
  if (opts.policy) args.push(opts.policy);
59664
60008
  fs32.mkdirSync(path28.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
@@ -60015,17 +60359,32 @@ try {
60015
60359
  } catch (err) {
60016
60360
  if (err instanceof CommanderError) {
60017
60361
  if (err.code === "commander.helpDisplayed") {
60362
+ const helpRequested = process.argv.includes("--help") || process.argv.includes("-h") || process.argv.includes("help");
60363
+ if (helpRequested) {
60364
+ if (isJsonMode()) {
60365
+ const target = resolveTargetCommand(program2, process.argv.slice(2));
60366
+ printJson(commandToJson(target, { includeIdentity: target === program2 }));
60367
+ }
60368
+ process.exit(0);
60369
+ }
60018
60370
  if (isJsonMode()) {
60019
60371
  const target = resolveTargetCommand(program2, process.argv.slice(2));
60020
- printJson(commandToJson(target, { includeIdentity: target === program2 }));
60372
+ const subNames = target.commands.map((c) => c.name()).join(", ");
60373
+ const usefulMessage = subNames ? `${target.name()}: a subcommand is required. Available: ${subNames}` : err.message;
60374
+ emitJsonError({ code: 2, kind: "usage", message: usefulMessage });
60021
60375
  }
60022
- process.exit(0);
60376
+ process.exit(2);
60023
60377
  }
60024
60378
  if (err.code === "commander.version") {
60025
60379
  process.exit(0);
60026
60380
  }
60027
60381
  if (isJsonMode()) {
60028
- emitJsonError({ code: 2, kind: "usage", message: err.message });
60382
+ const errorMessage = err.code === "commander.help" ? (() => {
60383
+ const target = resolveTargetCommand(program2, process.argv.slice(2));
60384
+ const subNames = target.commands.map((c) => c.name()).join(", ");
60385
+ return subNames ? `${target.name()}: a subcommand is required. Available: ${subNames}` : err.message;
60386
+ })() : err.message;
60387
+ emitJsonError({ code: 2, kind: "usage", message: errorMessage });
60029
60388
  }
60030
60389
  process.exit(2);
60031
60390
  }