@switchbot/openapi-cli 3.6.0 → 3.6.2

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/index.js CHANGED
@@ -989,8 +989,8 @@ var require_command = __commonJS({
989
989
  init_cjs_shim();
990
990
  var EventEmitter = __require("node:events").EventEmitter;
991
991
  var childProcess = __require("node:child_process");
992
- var path29 = __require("node:path");
993
- var fs33 = __require("node:fs");
992
+ var path31 = __require("node:path");
993
+ var fs36 = __require("node:fs");
994
994
  var process4 = __require("node:process");
995
995
  var { Argument: Argument2, humanReadableArgName } = require_argument();
996
996
  var { CommanderError: CommanderError2 } = require_error();
@@ -1922,11 +1922,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1922
1922
  let launchWithNode = false;
1923
1923
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1924
1924
  function findFile(baseDir, baseName) {
1925
- const localBin = path29.resolve(baseDir, baseName);
1926
- if (fs33.existsSync(localBin)) return localBin;
1927
- if (sourceExt.includes(path29.extname(baseName))) return void 0;
1925
+ const localBin = path31.resolve(baseDir, baseName);
1926
+ if (fs36.existsSync(localBin)) return localBin;
1927
+ if (sourceExt.includes(path31.extname(baseName))) return void 0;
1928
1928
  const foundExt = sourceExt.find(
1929
- (ext) => fs33.existsSync(`${localBin}${ext}`)
1929
+ (ext) => fs36.existsSync(`${localBin}${ext}`)
1930
1930
  );
1931
1931
  if (foundExt) return `${localBin}${foundExt}`;
1932
1932
  return void 0;
@@ -1938,21 +1938,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1938
1938
  if (this._scriptPath) {
1939
1939
  let resolvedScriptPath;
1940
1940
  try {
1941
- resolvedScriptPath = fs33.realpathSync(this._scriptPath);
1941
+ resolvedScriptPath = fs36.realpathSync(this._scriptPath);
1942
1942
  } catch (err) {
1943
1943
  resolvedScriptPath = this._scriptPath;
1944
1944
  }
1945
- executableDir = path29.resolve(
1946
- path29.dirname(resolvedScriptPath),
1945
+ executableDir = path31.resolve(
1946
+ path31.dirname(resolvedScriptPath),
1947
1947
  executableDir
1948
1948
  );
1949
1949
  }
1950
1950
  if (executableDir) {
1951
1951
  let localFile = findFile(executableDir, executableFile);
1952
1952
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1953
- const legacyName = path29.basename(
1953
+ const legacyName = path31.basename(
1954
1954
  this._scriptPath,
1955
- path29.extname(this._scriptPath)
1955
+ path31.extname(this._scriptPath)
1956
1956
  );
1957
1957
  if (legacyName !== this._name) {
1958
1958
  localFile = findFile(
@@ -1963,7 +1963,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1963
1963
  }
1964
1964
  executableFile = localFile || executableFile;
1965
1965
  }
1966
- launchWithNode = sourceExt.includes(path29.extname(executableFile));
1966
+ launchWithNode = sourceExt.includes(path31.extname(executableFile));
1967
1967
  let proc;
1968
1968
  if (process4.platform !== "win32") {
1969
1969
  if (launchWithNode) {
@@ -2803,7 +2803,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2803
2803
  * @return {Command}
2804
2804
  */
2805
2805
  nameFromFilename(filename) {
2806
- this._name = path29.basename(filename, path29.extname(filename));
2806
+ this._name = path31.basename(filename, path31.extname(filename));
2807
2807
  return this;
2808
2808
  }
2809
2809
  /**
@@ -2817,9 +2817,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2817
2817
  * @param {string} [path]
2818
2818
  * @return {(string|null|Command)}
2819
2819
  */
2820
- executableDir(path30) {
2821
- if (path30 === void 0) return this._executableDir;
2822
- this._executableDir = path30;
2820
+ executableDir(path32) {
2821
+ if (path32 === void 0) return this._executableDir;
2822
+ this._executableDir = path32;
2823
2823
  return this;
2824
2824
  }
2825
2825
  /**
@@ -4340,7 +4340,7 @@ var require_supports_colors = __commonJS({
4340
4340
  "node_modules/@colors/colors/lib/system/supports-colors.js"(exports, module) {
4341
4341
  "use strict";
4342
4342
  init_cjs_shim();
4343
- var os26 = __require("os");
4343
+ var os27 = __require("os");
4344
4344
  var hasFlag2 = require_has_flag();
4345
4345
  var env2 = process.env;
4346
4346
  var forceColor = void 0;
@@ -4378,7 +4378,7 @@ var require_supports_colors = __commonJS({
4378
4378
  }
4379
4379
  var min = forceColor ? 1 : 0;
4380
4380
  if (process.platform === "win32") {
4381
- var osRelease = os26.release().split(".");
4381
+ var osRelease = os27.release().split(".");
4382
4382
  if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
4383
4383
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
4384
4384
  }
@@ -7797,7 +7797,9 @@ var init_catalog = __esm({
7797
7797
  moveDetected: "Motion detected (boolean)",
7798
7798
  openState: "Contact sensor open/closed",
7799
7799
  status: "Device-specific status word",
7800
- lightLevel: "Ambient light level"
7800
+ lightLevel: "Ambient light level",
7801
+ detected: "Presence detected (boolean)",
7802
+ hubDeviceId: "Hub device identifier for hub-bound devices"
7801
7803
  };
7802
7804
  onOff = [
7803
7805
  { command: "turnOn", parameter: "\u2014", description: "Power on", idempotent: true },
@@ -7851,9 +7853,9 @@ var init_catalog = __esm({
7851
7853
  {
7852
7854
  type: "Smart Lock",
7853
7855
  category: "physical",
7854
- description: "Bluetooth/Wi-Fi electronic deadbolt that locks and unlocks a door via cloud API.",
7856
+ description: 'Bluetooth/Wi-Fi electronic deadbolt that locks and unlocks a door via cloud API. Pro models support the Matter protocol when configured via the SwitchBot app (deviceType "Smart Lock Pro Wifi").',
7855
7857
  role: "security",
7856
- aliases: ["Lock", "Smart Lock Pro", "Lock Pro"],
7858
+ aliases: ["Lock", "Smart Lock Pro", "Lock Pro", "Smart Lock Pro Wifi"],
7857
7859
  commands: [
7858
7860
  { command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
7859
7861
  { command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." },
@@ -7886,6 +7888,29 @@ var init_catalog = __esm({
7886
7888
  ],
7887
7889
  statusFields: ["battery", "version", "lockState", "doorState", "calibrate"]
7888
7890
  },
7891
+ {
7892
+ type: "Lock Vision",
7893
+ category: "physical",
7894
+ description: "Video smart lock with built-in camera; supports lock and unlock control plus status reporting.",
7895
+ role: "security",
7896
+ commands: [
7897
+ { command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
7898
+ { command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." }
7899
+ ],
7900
+ statusFields: ["lockState", "doorState", "battery", "keyList", "version"]
7901
+ },
7902
+ {
7903
+ type: "Lock Vision Pro",
7904
+ category: "physical",
7905
+ description: "Pro-tier video smart lock with camera; supports lock, unlock, and deadbolt control.",
7906
+ role: "security",
7907
+ commands: [
7908
+ { command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
7909
+ { command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." },
7910
+ { command: "deadbolt", parameter: "\u2014", description: "Engage deadbolt", idempotent: true }
7911
+ ],
7912
+ statusFields: ["lockState", "doorState", "battery", "keyList", "version"]
7913
+ },
7889
7914
  {
7890
7915
  type: "Plug",
7891
7916
  category: "physical",
@@ -7918,12 +7943,12 @@ var init_catalog = __esm({
7918
7943
  {
7919
7944
  type: "Relay Switch 2PM",
7920
7945
  category: "physical",
7921
- description: "Dual-channel relay switch with per-channel on/off/toggle and optional roller-shade mode.",
7946
+ description: 'Dual-channel relay switch with per-channel on/off/toggle and optional roller-shade mode. IMPORTANT: turnOn/turnOff/toggle require a channel parameter ("1" or "2") \u2014 omitting it is a usage error.',
7922
7947
  role: "power",
7923
7948
  commands: [
7924
- { command: "turnOn", parameter: "1 | 2 (channel)", description: "Turn on channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7925
- { command: "turnOff", parameter: "1 | 2 (channel)", description: "Turn off channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7926
- { command: "toggle", parameter: "1 | 2 (channel)", description: "Toggle channel 1 or 2", idempotent: false, exampleParams: ["1", "2"] },
7949
+ { command: "turnOn", parameter: "1 | 2 (channel \u2014 required)", description: "Turn on channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7950
+ { command: "turnOff", parameter: "1 | 2 (channel \u2014 required)", description: "Turn off channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7951
+ { command: "toggle", parameter: "1 | 2 (channel \u2014 required)", description: "Toggle channel 1 or 2", idempotent: false, exampleParams: ["1", "2"] },
7927
7952
  { command: "setMode", parameter: '"<channel>;<mode>" e.g. "1;0"', description: "Per-channel mode (see Relay Switch 1 modes)", idempotent: true, exampleParams: ["1;0", "2;3"] },
7928
7953
  { command: "setPosition", parameter: "0-100 (roller percentage)", description: "Roller-shade-pair mode only", idempotent: true, exampleParams: ["0", "50", "100"] }
7929
7954
  ],
@@ -7945,7 +7970,7 @@ var init_catalog = __esm({
7945
7970
  category: "physical",
7946
7971
  description: "Evaporative humidifier with multiple speed/auto/sleep/humidity modes and child lock.",
7947
7972
  role: "climate",
7948
- aliases: ["Evaporative Humidifier"],
7973
+ aliases: ["Evaporative Humidifier", "Evaporative Humidifier (Auto-refill)"],
7949
7974
  commands: [
7950
7975
  ...onOff,
7951
7976
  { command: "setMode", parameter: `'{"mode":1-8,"targetHumidify":0-100}'`, description: "mode: 1=lv4 2=lv3 3=lv2 4=lv1 5=humidity 6=sleep 7=auto 8=drying", idempotent: true, exampleParams: ['{"mode":7,"targetHumidify":50}'] },
@@ -8064,7 +8089,7 @@ var init_catalog = __esm({
8064
8089
  category: "physical",
8065
8090
  description: "Entry-level robot vacuum with start/stop/dock and four suction power levels.",
8066
8091
  role: "cleaning",
8067
- aliases: ["Robot Vacuum", "Robot Vacuum Cleaner S1 Plus", "K10+", "K10+ Pro"],
8092
+ aliases: ["Robot Vacuum", "S1", "S1 Plus", "Robot Vacuum Cleaner S1 Plus", "K10+", "K10+ Pro"],
8068
8093
  commands: [
8069
8094
  { command: "start", parameter: "\u2014", description: "Start cleaning", idempotent: true },
8070
8095
  { command: "stop", parameter: "\u2014", description: "Stop cleaning", idempotent: true },
@@ -8078,7 +8103,7 @@ var init_catalog = __esm({
8078
8103
  category: "physical",
8079
8104
  description: "Compact robot vacuum and mop combo with sweep/mop sessions, fan level, and water level.",
8080
8105
  role: "cleaning",
8081
- aliases: ["K10+ Pro Combo", "K20+ Pro", "K11+", "Robot Vacuum Cleaner K11+"],
8106
+ aliases: ["K10+ Pro Combo", "K20+ Pro", "Robot Vacuum Cleaner K20 Plus Pro", "K11+", "Robot Vacuum Cleaner K11+"],
8082
8107
  commands: [
8083
8108
  { command: "startClean", parameter: `'{"action":"sweep"|"mop","param":{"fanLevel":1-4,"times":1-2639999}}'`, description: "Begin a cleaning session", idempotent: false, exampleParams: ['{"action":"sweep","param":{"fanLevel":2,"times":1}}'] },
8084
8109
  { command: "pause", parameter: "\u2014", description: "Pause cleaning", idempotent: true },
@@ -8093,7 +8118,7 @@ var init_catalog = __esm({
8093
8118
  category: "physical",
8094
8119
  description: "Advanced floor cleaning robot with sweep/mop modes, self-wash dock, and humidifier refill.",
8095
8120
  role: "cleaning",
8096
- aliases: ["Robot Vacuum Cleaner S10", "Robot Vacuum Cleaner S20", "S20"],
8121
+ aliases: ["S10", "Robot Vacuum Cleaner S10", "Robot Vacuum Cleaner S20", "S20"],
8097
8122
  commands: [
8098
8123
  { command: "startClean", parameter: `'{"action":"sweep"|"sweep_mop","param":{"fanLevel":1-4,"waterLevel":1-2,"times":1-2639999}}'`, description: "Begin a cleaning session", idempotent: false, exampleParams: ['{"action":"sweep","param":{"fanLevel":2,"waterLevel":1,"times":1}}'] },
8099
8124
  { command: "pause", parameter: "\u2014", description: "Pause cleaning", idempotent: true },
@@ -8193,11 +8218,12 @@ var init_catalog = __esm({
8193
8218
  {
8194
8219
  type: "AI Art Frame",
8195
8220
  category: "physical",
8196
- description: "Digital art frame that can switch to the next or previous image.",
8221
+ description: "Digital art frame that can switch images and accept new artwork uploads.",
8197
8222
  role: "other",
8198
8223
  commands: [
8199
8224
  { command: "next", parameter: "\u2014", description: "Switch to the next image", idempotent: false },
8200
- { command: "previous", parameter: "\u2014", description: "Switch to the previous image", idempotent: false }
8225
+ { command: "previous", parameter: "\u2014", description: "Switch to the previous image", idempotent: false },
8226
+ { command: "uploadImage", parameter: "<imageUrl>", description: "Upload a new image from an https:// URL to display on the frame", idempotent: true, exampleParams: ["https://example.com/art.jpg"] }
8201
8227
  ],
8202
8228
  statusFields: ["version"]
8203
8229
  },
@@ -8208,10 +8234,31 @@ var init_catalog = __esm({
8208
8234
  description: "Battery-powered temperature and humidity sensor; read-only, no control commands.",
8209
8235
  role: "sensor",
8210
8236
  readOnly: true,
8211
- aliases: ["Meter Plus", "MeterPro", "MeterPro(CO2)", "WoIOSensor"],
8237
+ aliases: [
8238
+ "MeterPlus",
8239
+ "Meter Plus",
8240
+ "Meter Plus (JP)",
8241
+ "Meter Plus (US)",
8242
+ "Outdoor Meter",
8243
+ "MeterPro",
8244
+ "Meter Pro",
8245
+ "MeterPro(CO2)",
8246
+ "Meter Pro (CO2 Monitor)",
8247
+ "WoIOSensor"
8248
+ ],
8212
8249
  commands: [],
8213
8250
  statusFields: ["temperature", "humidity", "battery", "version"]
8214
8251
  },
8252
+ {
8253
+ type: "WeatherStation",
8254
+ category: "physical",
8255
+ description: "Outdoor weather station reporting temperature, humidity, and atmospheric pressure; read-only.",
8256
+ role: "sensor",
8257
+ readOnly: true,
8258
+ aliases: ["Weather Station"],
8259
+ commands: [],
8260
+ statusFields: ["temperature", "humidity", "atmosphericPressure", "battery", "version"]
8261
+ },
8215
8262
  {
8216
8263
  type: "Motion Sensor",
8217
8264
  category: "physical",
@@ -8221,6 +8268,16 @@ var init_catalog = __esm({
8221
8268
  commands: [],
8222
8269
  statusFields: ["battery", "version", "moveDetected", "brightness", "openState"]
8223
8270
  },
8271
+ {
8272
+ type: "Presence Sensor",
8273
+ category: "physical",
8274
+ description: "Presence detector that reports occupancy and ambient light through a paired hub; read-only.",
8275
+ role: "sensor",
8276
+ readOnly: true,
8277
+ aliases: ["Human Presence Sensor", "Prensence Sensor"],
8278
+ commands: [],
8279
+ statusFields: ["version", "battery", "lightLevel", "detected", "hubDeviceId"]
8280
+ },
8224
8281
  {
8225
8282
  type: "Contact Sensor",
8226
8283
  category: "physical",
@@ -8256,7 +8313,7 @@ var init_catalog = __esm({
8256
8313
  description: "IR hub that bridges BLE devices to the cloud and learns IR remotes; no direct control commands.",
8257
8314
  role: "hub",
8258
8315
  readOnly: true,
8259
- aliases: ["Hub Mini2"],
8316
+ aliases: ["Hub", "Hub Plus", "Hub Mini2"],
8260
8317
  commands: [],
8261
8318
  statusFields: ["version"]
8262
8319
  },
@@ -8305,6 +8362,41 @@ var init_catalog = __esm({
8305
8362
  commands: [],
8306
8363
  statusFields: ["battery", "version"]
8307
8364
  },
8365
+ {
8366
+ type: "Indoor Cam",
8367
+ category: "physical",
8368
+ description: "Indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
8369
+ role: "security",
8370
+ readOnly: true,
8371
+ commands: []
8372
+ },
8373
+ {
8374
+ type: "Pan/Tilt Cam",
8375
+ category: "physical",
8376
+ description: "Pan/tilt indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
8377
+ role: "security",
8378
+ readOnly: true,
8379
+ aliases: ["Pan/Tilt Cam 2K"],
8380
+ commands: []
8381
+ },
8382
+ {
8383
+ type: "Pan/Tilt Cam Plus 3K",
8384
+ category: "physical",
8385
+ description: "Pan/tilt indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
8386
+ role: "security",
8387
+ readOnly: true,
8388
+ aliases: ["Pan/Tilt Cam Plus 2K", "Pan/Tilt Cam Plus", "Pan Tilt Cam Plus 3K"],
8389
+ commands: []
8390
+ },
8391
+ {
8392
+ type: "Remote",
8393
+ category: "physical",
8394
+ description: "Bluetooth wireless button remote paired to a hub or device; listed by the cloud API but exposes no cloud status or control commands.",
8395
+ role: "other",
8396
+ readOnly: true,
8397
+ aliases: ["SwitchBot Remote", "Remote Button", "Wireless Remote"],
8398
+ commands: []
8399
+ },
8308
8400
  // ---------- Virtual IR remotes ----------
8309
8401
  {
8310
8402
  type: "Air Conditioner",
@@ -9463,6 +9555,161 @@ var init_credential = __esm({
9463
9555
  }
9464
9556
  });
9465
9557
 
9558
+ // src/devices/history-query.ts
9559
+ import fs10 from "node:fs";
9560
+ import path10 from "node:path";
9561
+ import os11 from "node:os";
9562
+ import readline2 from "node:readline";
9563
+ function historyDir2() {
9564
+ return path10.join(os11.homedir(), ".switchbot", "device-history");
9565
+ }
9566
+ function parseDurationToMs2(spec) {
9567
+ const m2 = spec.trim().match(/^(\d+)(ms|s|m|h|d)$/i);
9568
+ if (!m2) return null;
9569
+ const n = Number(m2[1]);
9570
+ const unit = m2[2].toLowerCase();
9571
+ const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
9572
+ return n * factor;
9573
+ }
9574
+ function parseInstantToMs(spec) {
9575
+ const ms = Date.parse(spec);
9576
+ return Number.isFinite(ms) ? ms : null;
9577
+ }
9578
+ function resolveRange(opts) {
9579
+ let fromMs = Number.NEGATIVE_INFINITY;
9580
+ let toMs = Number.POSITIVE_INFINITY;
9581
+ if (opts.since && (opts.from || opts.to)) {
9582
+ throw new Error("--since is mutually exclusive with --from/--to.");
9583
+ }
9584
+ if (opts.since) {
9585
+ const durMs = parseDurationToMs2(opts.since);
9586
+ if (durMs === null) {
9587
+ throw new Error(`Invalid --since value "${opts.since}". Expected e.g. "30s", "15m", "1h", "7d".`);
9588
+ }
9589
+ fromMs = Date.now() - durMs;
9590
+ } else {
9591
+ if (opts.from) {
9592
+ const parsed = parseInstantToMs(opts.from);
9593
+ if (parsed === null) throw new Error(`Invalid --from value "${opts.from}". Expected ISO-8601 timestamp.`);
9594
+ fromMs = parsed;
9595
+ }
9596
+ if (opts.to) {
9597
+ const parsed = parseInstantToMs(opts.to);
9598
+ if (parsed === null) throw new Error(`Invalid --to value "${opts.to}". Expected ISO-8601 timestamp.`);
9599
+ toMs = parsed;
9600
+ }
9601
+ if (fromMs > toMs) throw new Error("--from must be <= --to.");
9602
+ }
9603
+ return { fromMs, toMs };
9604
+ }
9605
+ function jsonlFilesForDevice(deviceId, baseDir = historyDir2()) {
9606
+ const out = [];
9607
+ if (!fs10.existsSync(baseDir)) return out;
9608
+ for (let i = 3; i >= 1; i--) {
9609
+ const p2 = path10.join(baseDir, `${deviceId}.jsonl.${i}`);
9610
+ if (fs10.existsSync(p2)) out.push(p2);
9611
+ }
9612
+ const current = path10.join(baseDir, `${deviceId}.jsonl`);
9613
+ if (fs10.existsSync(current)) out.push(current);
9614
+ return out;
9615
+ }
9616
+ function projectFields(record2, fields) {
9617
+ if (fields.length === 0) return record2;
9618
+ const projected = {};
9619
+ const payload = record2.payload ?? {};
9620
+ for (const f2 of fields) {
9621
+ if (f2 in payload) projected[f2] = payload[f2];
9622
+ }
9623
+ return { t: record2.t, topic: record2.topic, deviceType: record2.deviceType, payload: projected };
9624
+ }
9625
+ async function queryDeviceHistory(deviceId, opts = {}) {
9626
+ const { fromMs, toMs } = resolveRange(opts);
9627
+ const limit = Math.max(0, opts.limit ?? DEFAULT_LIMIT);
9628
+ const fields = opts.fields ?? [];
9629
+ const files = jsonlFilesForDevice(deviceId);
9630
+ const out = [];
9631
+ for (const file2 of files) {
9632
+ try {
9633
+ const stat = fs10.statSync(file2);
9634
+ if (stat.mtimeMs < fromMs) continue;
9635
+ } catch {
9636
+ continue;
9637
+ }
9638
+ const stream = fs10.createReadStream(file2, { encoding: "utf-8" });
9639
+ const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
9640
+ for await (const line of rl) {
9641
+ if (!line) continue;
9642
+ let rec;
9643
+ try {
9644
+ rec = JSON.parse(line);
9645
+ } catch {
9646
+ continue;
9647
+ }
9648
+ const tMs = Date.parse(rec.t);
9649
+ if (!Number.isFinite(tMs)) continue;
9650
+ if (tMs < fromMs || tMs > toMs) continue;
9651
+ out.push(projectFields(rec, fields));
9652
+ if (out.length >= limit) {
9653
+ rl.close();
9654
+ stream.destroy();
9655
+ return out;
9656
+ }
9657
+ }
9658
+ }
9659
+ return out;
9660
+ }
9661
+ function queryDeviceHistoryStats(deviceId) {
9662
+ const dir = historyDir2();
9663
+ const files = jsonlFilesForDevice(deviceId);
9664
+ let totalBytes = 0;
9665
+ let oldest = null;
9666
+ let newest = null;
9667
+ let count = 0;
9668
+ for (const file2 of files) {
9669
+ try {
9670
+ totalBytes += fs10.statSync(file2).size;
9671
+ } catch {
9672
+ }
9673
+ }
9674
+ for (const file2 of files) {
9675
+ try {
9676
+ const lines = fs10.readFileSync(file2, "utf-8").split("\n");
9677
+ for (const line of lines) {
9678
+ if (!line) continue;
9679
+ count += 1;
9680
+ try {
9681
+ const rec = JSON.parse(line);
9682
+ const tMs = Date.parse(rec.t);
9683
+ if (Number.isFinite(tMs)) {
9684
+ if (oldest === null || tMs < oldest) oldest = tMs;
9685
+ if (newest === null || tMs > newest) newest = tMs;
9686
+ }
9687
+ } catch {
9688
+ }
9689
+ }
9690
+ } catch {
9691
+ }
9692
+ }
9693
+ return {
9694
+ deviceId,
9695
+ fileCount: files.length,
9696
+ totalBytes,
9697
+ recordCount: count,
9698
+ oldest: oldest !== null ? new Date(oldest).toISOString() : void 0,
9699
+ newest: newest !== null ? new Date(newest).toISOString() : void 0,
9700
+ jsonlFiles: files.map((f2) => path10.basename(f2)),
9701
+ historyDir: dir
9702
+ };
9703
+ }
9704
+ var DEFAULT_LIMIT;
9705
+ var init_history_query = __esm({
9706
+ "src/devices/history-query.ts"() {
9707
+ "use strict";
9708
+ init_cjs_shim();
9709
+ DEFAULT_LIMIT = 1e3;
9710
+ }
9711
+ });
9712
+
9466
9713
  // node_modules/yaml/dist/nodes/identity.js
9467
9714
  var require_identity = __commonJS({
9468
9715
  "node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -9542,17 +9789,17 @@ var require_visit = __commonJS({
9542
9789
  visit.BREAK = BREAK;
9543
9790
  visit.SKIP = SKIP;
9544
9791
  visit.REMOVE = REMOVE;
9545
- function visit_(key, node, visitor, path29) {
9546
- const ctrl = callVisitor(key, node, visitor, path29);
9792
+ function visit_(key, node, visitor, path31) {
9793
+ const ctrl = callVisitor(key, node, visitor, path31);
9547
9794
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
9548
- replaceNode(key, path29, ctrl);
9549
- return visit_(key, ctrl, visitor, path29);
9795
+ replaceNode(key, path31, ctrl);
9796
+ return visit_(key, ctrl, visitor, path31);
9550
9797
  }
9551
9798
  if (typeof ctrl !== "symbol") {
9552
9799
  if (identity.isCollection(node)) {
9553
- path29 = Object.freeze(path29.concat(node));
9800
+ path31 = Object.freeze(path31.concat(node));
9554
9801
  for (let i = 0; i < node.items.length; ++i) {
9555
- const ci = visit_(i, node.items[i], visitor, path29);
9802
+ const ci = visit_(i, node.items[i], visitor, path31);
9556
9803
  if (typeof ci === "number")
9557
9804
  i = ci - 1;
9558
9805
  else if (ci === BREAK)
@@ -9563,13 +9810,13 @@ var require_visit = __commonJS({
9563
9810
  }
9564
9811
  }
9565
9812
  } else if (identity.isPair(node)) {
9566
- path29 = Object.freeze(path29.concat(node));
9567
- const ck = visit_("key", node.key, visitor, path29);
9813
+ path31 = Object.freeze(path31.concat(node));
9814
+ const ck = visit_("key", node.key, visitor, path31);
9568
9815
  if (ck === BREAK)
9569
9816
  return BREAK;
9570
9817
  else if (ck === REMOVE)
9571
9818
  node.key = null;
9572
- const cv = visit_("value", node.value, visitor, path29);
9819
+ const cv = visit_("value", node.value, visitor, path31);
9573
9820
  if (cv === BREAK)
9574
9821
  return BREAK;
9575
9822
  else if (cv === REMOVE)
@@ -9590,17 +9837,17 @@ var require_visit = __commonJS({
9590
9837
  visitAsync.BREAK = BREAK;
9591
9838
  visitAsync.SKIP = SKIP;
9592
9839
  visitAsync.REMOVE = REMOVE;
9593
- async function visitAsync_(key, node, visitor, path29) {
9594
- const ctrl = await callVisitor(key, node, visitor, path29);
9840
+ async function visitAsync_(key, node, visitor, path31) {
9841
+ const ctrl = await callVisitor(key, node, visitor, path31);
9595
9842
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
9596
- replaceNode(key, path29, ctrl);
9597
- return visitAsync_(key, ctrl, visitor, path29);
9843
+ replaceNode(key, path31, ctrl);
9844
+ return visitAsync_(key, ctrl, visitor, path31);
9598
9845
  }
9599
9846
  if (typeof ctrl !== "symbol") {
9600
9847
  if (identity.isCollection(node)) {
9601
- path29 = Object.freeze(path29.concat(node));
9848
+ path31 = Object.freeze(path31.concat(node));
9602
9849
  for (let i = 0; i < node.items.length; ++i) {
9603
- const ci = await visitAsync_(i, node.items[i], visitor, path29);
9850
+ const ci = await visitAsync_(i, node.items[i], visitor, path31);
9604
9851
  if (typeof ci === "number")
9605
9852
  i = ci - 1;
9606
9853
  else if (ci === BREAK)
@@ -9611,13 +9858,13 @@ var require_visit = __commonJS({
9611
9858
  }
9612
9859
  }
9613
9860
  } else if (identity.isPair(node)) {
9614
- path29 = Object.freeze(path29.concat(node));
9615
- const ck = await visitAsync_("key", node.key, visitor, path29);
9861
+ path31 = Object.freeze(path31.concat(node));
9862
+ const ck = await visitAsync_("key", node.key, visitor, path31);
9616
9863
  if (ck === BREAK)
9617
9864
  return BREAK;
9618
9865
  else if (ck === REMOVE)
9619
9866
  node.key = null;
9620
- const cv = await visitAsync_("value", node.value, visitor, path29);
9867
+ const cv = await visitAsync_("value", node.value, visitor, path31);
9621
9868
  if (cv === BREAK)
9622
9869
  return BREAK;
9623
9870
  else if (cv === REMOVE)
@@ -9644,23 +9891,23 @@ var require_visit = __commonJS({
9644
9891
  }
9645
9892
  return visitor;
9646
9893
  }
9647
- function callVisitor(key, node, visitor, path29) {
9894
+ function callVisitor(key, node, visitor, path31) {
9648
9895
  if (typeof visitor === "function")
9649
- return visitor(key, node, path29);
9896
+ return visitor(key, node, path31);
9650
9897
  if (identity.isMap(node))
9651
- return visitor.Map?.(key, node, path29);
9898
+ return visitor.Map?.(key, node, path31);
9652
9899
  if (identity.isSeq(node))
9653
- return visitor.Seq?.(key, node, path29);
9900
+ return visitor.Seq?.(key, node, path31);
9654
9901
  if (identity.isPair(node))
9655
- return visitor.Pair?.(key, node, path29);
9902
+ return visitor.Pair?.(key, node, path31);
9656
9903
  if (identity.isScalar(node))
9657
- return visitor.Scalar?.(key, node, path29);
9904
+ return visitor.Scalar?.(key, node, path31);
9658
9905
  if (identity.isAlias(node))
9659
- return visitor.Alias?.(key, node, path29);
9906
+ return visitor.Alias?.(key, node, path31);
9660
9907
  return void 0;
9661
9908
  }
9662
- function replaceNode(key, path29, node) {
9663
- const parent = path29[path29.length - 1];
9909
+ function replaceNode(key, path31, node) {
9910
+ const parent = path31[path31.length - 1];
9664
9911
  if (identity.isCollection(parent)) {
9665
9912
  parent.items[key] = node;
9666
9913
  } else if (identity.isPair(parent)) {
@@ -10277,10 +10524,10 @@ var require_Collection = __commonJS({
10277
10524
  var createNode = require_createNode();
10278
10525
  var identity = require_identity();
10279
10526
  var Node = require_Node();
10280
- function collectionFromPath(schema2, path29, value) {
10527
+ function collectionFromPath(schema2, path31, value) {
10281
10528
  let v2 = value;
10282
- for (let i = path29.length - 1; i >= 0; --i) {
10283
- const k2 = path29[i];
10529
+ for (let i = path31.length - 1; i >= 0; --i) {
10530
+ const k2 = path31[i];
10284
10531
  if (typeof k2 === "number" && Number.isInteger(k2) && k2 >= 0) {
10285
10532
  const a = [];
10286
10533
  a[k2] = v2;
@@ -10299,7 +10546,7 @@ var require_Collection = __commonJS({
10299
10546
  sourceObjects: /* @__PURE__ */ new Map()
10300
10547
  });
10301
10548
  }
10302
- var isEmptyPath = (path29) => path29 == null || typeof path29 === "object" && !!path29[Symbol.iterator]().next().done;
10549
+ var isEmptyPath = (path31) => path31 == null || typeof path31 === "object" && !!path31[Symbol.iterator]().next().done;
10303
10550
  var Collection = class extends Node.NodeBase {
10304
10551
  constructor(type2, schema2) {
10305
10552
  super(type2);
@@ -10329,11 +10576,11 @@ var require_Collection = __commonJS({
10329
10576
  * be a Pair instance or a `{ key, value }` object, which may not have a key
10330
10577
  * that already exists in the map.
10331
10578
  */
10332
- addIn(path29, value) {
10333
- if (isEmptyPath(path29))
10579
+ addIn(path31, value) {
10580
+ if (isEmptyPath(path31))
10334
10581
  this.add(value);
10335
10582
  else {
10336
- const [key, ...rest] = path29;
10583
+ const [key, ...rest] = path31;
10337
10584
  const node = this.get(key, true);
10338
10585
  if (identity.isCollection(node))
10339
10586
  node.addIn(rest, value);
@@ -10347,8 +10594,8 @@ var require_Collection = __commonJS({
10347
10594
  * Removes a value from the collection.
10348
10595
  * @returns `true` if the item was found and removed.
10349
10596
  */
10350
- deleteIn(path29) {
10351
- const [key, ...rest] = path29;
10597
+ deleteIn(path31) {
10598
+ const [key, ...rest] = path31;
10352
10599
  if (rest.length === 0)
10353
10600
  return this.delete(key);
10354
10601
  const node = this.get(key, true);
@@ -10362,8 +10609,8 @@ var require_Collection = __commonJS({
10362
10609
  * scalar values from their surrounding node; to disable set `keepScalar` to
10363
10610
  * `true` (collections are always returned intact).
10364
10611
  */
10365
- getIn(path29, keepScalar) {
10366
- const [key, ...rest] = path29;
10612
+ getIn(path31, keepScalar) {
10613
+ const [key, ...rest] = path31;
10367
10614
  const node = this.get(key, true);
10368
10615
  if (rest.length === 0)
10369
10616
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -10381,8 +10628,8 @@ var require_Collection = __commonJS({
10381
10628
  /**
10382
10629
  * Checks if the collection includes a value with the key `key`.
10383
10630
  */
10384
- hasIn(path29) {
10385
- const [key, ...rest] = path29;
10631
+ hasIn(path31) {
10632
+ const [key, ...rest] = path31;
10386
10633
  if (rest.length === 0)
10387
10634
  return this.has(key);
10388
10635
  const node = this.get(key, true);
@@ -10392,8 +10639,8 @@ var require_Collection = __commonJS({
10392
10639
  * Sets a value in this collection. For `!!set`, `value` needs to be a
10393
10640
  * boolean to add/remove the item from the set.
10394
10641
  */
10395
- setIn(path29, value) {
10396
- const [key, ...rest] = path29;
10642
+ setIn(path31, value) {
10643
+ const [key, ...rest] = path31;
10397
10644
  if (rest.length === 0) {
10398
10645
  this.set(key, value);
10399
10646
  } else {
@@ -12940,9 +13187,9 @@ var require_Document = __commonJS({
12940
13187
  this.contents.add(value);
12941
13188
  }
12942
13189
  /** Adds a value to the document. */
12943
- addIn(path29, value) {
13190
+ addIn(path31, value) {
12944
13191
  if (assertCollection(this.contents))
12945
- this.contents.addIn(path29, value);
13192
+ this.contents.addIn(path31, value);
12946
13193
  }
12947
13194
  /**
12948
13195
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -13017,14 +13264,14 @@ var require_Document = __commonJS({
13017
13264
  * Removes a value from the document.
13018
13265
  * @returns `true` if the item was found and removed.
13019
13266
  */
13020
- deleteIn(path29) {
13021
- if (Collection.isEmptyPath(path29)) {
13267
+ deleteIn(path31) {
13268
+ if (Collection.isEmptyPath(path31)) {
13022
13269
  if (this.contents == null)
13023
13270
  return false;
13024
13271
  this.contents = null;
13025
13272
  return true;
13026
13273
  }
13027
- return assertCollection(this.contents) ? this.contents.deleteIn(path29) : false;
13274
+ return assertCollection(this.contents) ? this.contents.deleteIn(path31) : false;
13028
13275
  }
13029
13276
  /**
13030
13277
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -13039,10 +13286,10 @@ var require_Document = __commonJS({
13039
13286
  * scalar values from their surrounding node; to disable set `keepScalar` to
13040
13287
  * `true` (collections are always returned intact).
13041
13288
  */
13042
- getIn(path29, keepScalar) {
13043
- if (Collection.isEmptyPath(path29))
13289
+ getIn(path31, keepScalar) {
13290
+ if (Collection.isEmptyPath(path31))
13044
13291
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
13045
- return identity.isCollection(this.contents) ? this.contents.getIn(path29, keepScalar) : void 0;
13292
+ return identity.isCollection(this.contents) ? this.contents.getIn(path31, keepScalar) : void 0;
13046
13293
  }
13047
13294
  /**
13048
13295
  * Checks if the document includes a value with the key `key`.
@@ -13053,10 +13300,10 @@ var require_Document = __commonJS({
13053
13300
  /**
13054
13301
  * Checks if the document includes a value at `path`.
13055
13302
  */
13056
- hasIn(path29) {
13057
- if (Collection.isEmptyPath(path29))
13303
+ hasIn(path31) {
13304
+ if (Collection.isEmptyPath(path31))
13058
13305
  return this.contents !== void 0;
13059
- return identity.isCollection(this.contents) ? this.contents.hasIn(path29) : false;
13306
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path31) : false;
13060
13307
  }
13061
13308
  /**
13062
13309
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -13073,13 +13320,13 @@ var require_Document = __commonJS({
13073
13320
  * Sets a value in this document. For `!!set`, `value` needs to be a
13074
13321
  * boolean to add/remove the item from the set.
13075
13322
  */
13076
- setIn(path29, value) {
13077
- if (Collection.isEmptyPath(path29)) {
13323
+ setIn(path31, value) {
13324
+ if (Collection.isEmptyPath(path31)) {
13078
13325
  this.contents = value;
13079
13326
  } else if (this.contents == null) {
13080
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path29), value);
13327
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path31), value);
13081
13328
  } else if (assertCollection(this.contents)) {
13082
- this.contents.setIn(path29, value);
13329
+ this.contents.setIn(path31, value);
13083
13330
  }
13084
13331
  }
13085
13332
  /**
@@ -15056,9 +15303,9 @@ var require_cst_visit = __commonJS({
15056
15303
  visit.BREAK = BREAK;
15057
15304
  visit.SKIP = SKIP;
15058
15305
  visit.REMOVE = REMOVE;
15059
- visit.itemAtPath = (cst, path29) => {
15306
+ visit.itemAtPath = (cst, path31) => {
15060
15307
  let item = cst;
15061
- for (const [field, index] of path29) {
15308
+ for (const [field, index] of path31) {
15062
15309
  const tok = item?.[field];
15063
15310
  if (tok && "items" in tok) {
15064
15311
  item = tok.items[index];
@@ -15067,23 +15314,23 @@ var require_cst_visit = __commonJS({
15067
15314
  }
15068
15315
  return item;
15069
15316
  };
15070
- visit.parentCollection = (cst, path29) => {
15071
- const parent = visit.itemAtPath(cst, path29.slice(0, -1));
15072
- const field = path29[path29.length - 1][0];
15317
+ visit.parentCollection = (cst, path31) => {
15318
+ const parent = visit.itemAtPath(cst, path31.slice(0, -1));
15319
+ const field = path31[path31.length - 1][0];
15073
15320
  const coll = parent?.[field];
15074
15321
  if (coll && "items" in coll)
15075
15322
  return coll;
15076
15323
  throw new Error("Parent collection not found");
15077
15324
  };
15078
- function _visit(path29, item, visitor) {
15079
- let ctrl = visitor(item, path29);
15325
+ function _visit(path31, item, visitor) {
15326
+ let ctrl = visitor(item, path31);
15080
15327
  if (typeof ctrl === "symbol")
15081
15328
  return ctrl;
15082
15329
  for (const field of ["key", "value"]) {
15083
15330
  const token = item[field];
15084
15331
  if (token && "items" in token) {
15085
15332
  for (let i = 0; i < token.items.length; ++i) {
15086
- const ci = _visit(Object.freeze(path29.concat([[field, i]])), token.items[i], visitor);
15333
+ const ci = _visit(Object.freeze(path31.concat([[field, i]])), token.items[i], visitor);
15087
15334
  if (typeof ci === "number")
15088
15335
  i = ci - 1;
15089
15336
  else if (ci === BREAK)
@@ -15094,10 +15341,10 @@ var require_cst_visit = __commonJS({
15094
15341
  }
15095
15342
  }
15096
15343
  if (typeof ctrl === "function" && field === "key")
15097
- ctrl = ctrl(item, path29);
15344
+ ctrl = ctrl(item, path31);
15098
15345
  }
15099
15346
  }
15100
- return typeof ctrl === "function" ? ctrl(item, path29) : ctrl;
15347
+ return typeof ctrl === "function" ? ctrl(item, path31) : ctrl;
15101
15348
  }
15102
15349
  exports.visit = visit;
15103
15350
  }
@@ -16386,14 +16633,14 @@ var require_parser = __commonJS({
16386
16633
  case "scalar":
16387
16634
  case "single-quoted-scalar":
16388
16635
  case "double-quoted-scalar": {
16389
- const fs33 = this.flowScalar(this.type);
16636
+ const fs36 = this.flowScalar(this.type);
16390
16637
  if (atNextItem || it.value) {
16391
- map3.items.push({ start, key: fs33, sep: [] });
16638
+ map3.items.push({ start, key: fs36, sep: [] });
16392
16639
  this.onKeyLine = true;
16393
16640
  } else if (it.sep) {
16394
- this.stack.push(fs33);
16641
+ this.stack.push(fs36);
16395
16642
  } else {
16396
- Object.assign(it, { key: fs33, sep: [] });
16643
+ Object.assign(it, { key: fs36, sep: [] });
16397
16644
  this.onKeyLine = true;
16398
16645
  }
16399
16646
  return;
@@ -16521,13 +16768,13 @@ var require_parser = __commonJS({
16521
16768
  case "scalar":
16522
16769
  case "single-quoted-scalar":
16523
16770
  case "double-quoted-scalar": {
16524
- const fs33 = this.flowScalar(this.type);
16771
+ const fs36 = this.flowScalar(this.type);
16525
16772
  if (!it || it.value)
16526
- fc.items.push({ start: [], key: fs33, sep: [] });
16773
+ fc.items.push({ start: [], key: fs36, sep: [] });
16527
16774
  else if (it.sep)
16528
- this.stack.push(fs33);
16775
+ this.stack.push(fs36);
16529
16776
  else
16530
- Object.assign(it, { key: fs33, sep: [] });
16777
+ Object.assign(it, { key: fs36, sep: [] });
16531
16778
  return;
16532
16779
  }
16533
16780
  case "flow-map-end":
@@ -20126,8 +20373,8 @@ var require_utils2 = __commonJS({
20126
20373
  }
20127
20374
  return ind;
20128
20375
  }
20129
- function removeDotSegments(path29) {
20130
- let input = path29;
20376
+ function removeDotSegments(path31) {
20377
+ let input = path31;
20131
20378
  const output = [];
20132
20379
  let nextSlash = -1;
20133
20380
  let len = 0;
@@ -20327,8 +20574,8 @@ var require_schemes = __commonJS({
20327
20574
  wsComponent.secure = void 0;
20328
20575
  }
20329
20576
  if (wsComponent.resourceName) {
20330
- const [path29, query] = wsComponent.resourceName.split("?");
20331
- wsComponent.path = path29 && path29 !== "/" ? path29 : void 0;
20577
+ const [path31, query] = wsComponent.resourceName.split("?");
20578
+ wsComponent.path = path31 && path31 !== "/" ? path31 : void 0;
20332
20579
  wsComponent.query = query;
20333
20580
  wsComponent.resourceName = void 0;
20334
20581
  }
@@ -20387,7 +20634,7 @@ var require_schemes = __commonJS({
20387
20634
  urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
20388
20635
  return urnComponent;
20389
20636
  }
20390
- var http6 = (
20637
+ var http8 = (
20391
20638
  /** @type {SchemeHandler} */
20392
20639
  {
20393
20640
  scheme: "http",
@@ -20396,11 +20643,11 @@ var require_schemes = __commonJS({
20396
20643
  serialize: httpSerialize
20397
20644
  }
20398
20645
  );
20399
- var https5 = (
20646
+ var https7 = (
20400
20647
  /** @type {SchemeHandler} */
20401
20648
  {
20402
20649
  scheme: "https",
20403
- domainHost: http6.domainHost,
20650
+ domainHost: http8.domainHost,
20404
20651
  parse: httpParse,
20405
20652
  serialize: httpSerialize
20406
20653
  }
@@ -20444,8 +20691,8 @@ var require_schemes = __commonJS({
20444
20691
  var SCHEMES = (
20445
20692
  /** @type {Record<SchemeName, SchemeHandler>} */
20446
20693
  {
20447
- http: http6,
20448
- https: https5,
20694
+ http: http8,
20695
+ https: https7,
20449
20696
  ws,
20450
20697
  wss,
20451
20698
  urn,
@@ -24135,6 +24382,10 @@ function isNotCondition(c) {
24135
24382
  function isLlmCondition(c) {
24136
24383
  return c.llm !== void 0 && typeof c.llm === "object";
24137
24384
  }
24385
+ function isEventCountCondition(c) {
24386
+ const ec = c.event_count;
24387
+ return ec !== void 0 && typeof ec === "object" && typeof ec.device === "string";
24388
+ }
24138
24389
  var init_types = __esm({
24139
24390
  "src/rules/types.ts"() {
24140
24391
  "use strict";
@@ -24418,7 +24669,7 @@ function locateError(doc, lineCounter, err) {
24418
24669
  return { line: pos.line, col: pos.col };
24419
24670
  }
24420
24671
  function humanMessage(err) {
24421
- const path29 = err.instancePath || "(root)";
24672
+ const path31 = err.instancePath || "(root)";
24422
24673
  switch (err.keyword) {
24423
24674
  case "required":
24424
24675
  return `missing required property "${err.params.missingProperty}"`;
@@ -24426,21 +24677,21 @@ function humanMessage(err) {
24426
24677
  return `unknown property "${err.params.additionalProperty}"`;
24427
24678
  case "dependentRequired": {
24428
24679
  const { property, missingProperty } = err.params;
24429
- const parent = path29 === "(root)" ? "" : `${path29}: `;
24680
+ const parent = path31 === "(root)" ? "" : `${path31}: `;
24430
24681
  return `${parent}when "${property}" is set, "${missingProperty}" is also required`;
24431
24682
  }
24432
24683
  case "pattern":
24433
- return `${path29} does not match pattern ${err.params.pattern}`;
24684
+ return `${path31} does not match pattern ${err.params.pattern}`;
24434
24685
  case "const":
24435
- return `${path29} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
24686
+ return `${path31} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
24436
24687
  case "enum":
24437
- return `${path29} must be one of ${JSON.stringify(err.params.allowedValues)}`;
24688
+ return `${path31} must be one of ${JSON.stringify(err.params.allowedValues)}`;
24438
24689
  case "type":
24439
- return `${path29} must be ${err.params.type}`;
24690
+ return `${path31} must be ${err.params.type}`;
24440
24691
  case "not":
24441
- return `${path29} is not allowed here`;
24692
+ return `${path31} is not allowed here`;
24442
24693
  default:
24443
- return `${path29} ${err.message ?? "is invalid"}`;
24694
+ return `${path31} ${err.message ?? "is invalid"}`;
24444
24695
  }
24445
24696
  }
24446
24697
  function hintFor(err) {
@@ -24496,8 +24747,8 @@ function escapeJsonPointerSegment(segment) {
24496
24747
  function isPlausibleDeviceId(value) {
24497
24748
  return HEX_MAC_DEVICE_ID_RE.test(value) || HYPHENATED_DEVICE_ID_RE.test(value);
24498
24749
  }
24499
- function hasErrorAtPath(errors, path29) {
24500
- return errors.some((err) => err.path === path29);
24750
+ function hasErrorAtPath(errors, path31) {
24751
+ return errors.some((err) => err.path === path31);
24501
24752
  }
24502
24753
  function resolvePolicyDeviceRef(raw, aliases) {
24503
24754
  if (!raw) return { ok: false, reason: "missing-device" };
@@ -24520,25 +24771,25 @@ function isDeviceStateConditionLike(value) {
24520
24771
  const candidate = value;
24521
24772
  return typeof candidate.device === "string" && typeof candidate.field === "string" && typeof candidate.op === "string";
24522
24773
  }
24523
- function collectConditionDeviceRefs(condition, path29) {
24774
+ function collectConditionDeviceRefs(condition, path31) {
24524
24775
  if (!condition || typeof condition !== "object" || Array.isArray(condition)) return [];
24525
24776
  const out = [];
24526
24777
  if (isDeviceStateConditionLike(condition)) {
24527
- out.push({ path: `${path29}/device`, ref: condition.device });
24778
+ out.push({ path: `${path31}/device`, ref: condition.device });
24528
24779
  }
24529
24780
  const candidate = condition;
24530
24781
  if (Array.isArray(candidate.all)) {
24531
24782
  for (let i = 0; i < candidate.all.length; i++) {
24532
- out.push(...collectConditionDeviceRefs(candidate.all[i], `${path29}/all/${i}`));
24783
+ out.push(...collectConditionDeviceRefs(candidate.all[i], `${path31}/all/${i}`));
24533
24784
  }
24534
24785
  }
24535
24786
  if (Array.isArray(candidate.any)) {
24536
24787
  for (let i = 0; i < candidate.any.length; i++) {
24537
- out.push(...collectConditionDeviceRefs(candidate.any[i], `${path29}/any/${i}`));
24788
+ out.push(...collectConditionDeviceRefs(candidate.any[i], `${path31}/any/${i}`));
24538
24789
  }
24539
24790
  }
24540
24791
  if (candidate.not !== void 0) {
24541
- out.push(...collectConditionDeviceRefs(candidate.not, `${path29}/not`));
24792
+ out.push(...collectConditionDeviceRefs(candidate.not, `${path31}/not`));
24542
24793
  }
24543
24794
  return out;
24544
24795
  }
@@ -24585,12 +24836,12 @@ function collectOfflineSemanticErrors(loaded, existingErrors) {
24585
24836
  const out = [];
24586
24837
  const aliases = collectAliasMap(data);
24587
24838
  for (const [aliasName, deviceId] of Object.entries(aliases)) {
24588
- const path29 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24589
- if (hasErrorAtPath(existingErrors, path29)) continue;
24839
+ const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24840
+ if (hasErrorAtPath(existingErrors, path31)) continue;
24590
24841
  if (isPlausibleDeviceId(deviceId)) continue;
24591
- const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
24842
+ const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
24592
24843
  out.push({
24593
- path: path29,
24844
+ path: path31,
24594
24845
  line,
24595
24846
  col,
24596
24847
  keyword: "alias-device-id",
@@ -24724,12 +24975,12 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
24724
24975
  inventoryById.set(remote.deviceId, { typeName: remote.remoteType });
24725
24976
  }
24726
24977
  for (const [aliasName, deviceId] of Object.entries(aliases)) {
24727
- const path29 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24728
- if (hasErrorAtPath(errors, path29)) continue;
24978
+ const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24979
+ if (hasErrorAtPath(errors, path31)) continue;
24729
24980
  if (!inventoryById.has(deviceId)) {
24730
- const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
24981
+ const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
24731
24982
  errors.push({
24732
- path: path29,
24983
+ path: path31,
24733
24984
  line,
24734
24985
  col,
24735
24986
  keyword: "alias-live-device-not-found",
@@ -24793,10 +25044,10 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
24793
25044
  if (!effectiveDeviceId) continue;
24794
25045
  const target = inventoryById.get(effectiveDeviceId);
24795
25046
  if (!target) {
24796
- const path29 = typeof action?.device === "string" ? devicePath : commandPath;
24797
- const { line: line2, col: col2 } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
25047
+ const path31 = typeof action?.device === "string" ? devicePath : commandPath;
25048
+ const { line: line2, col: col2 } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
24798
25049
  errors.push({
24799
- path: path29,
25050
+ path: path31,
24800
25051
  line: line2,
24801
25052
  col: col2,
24802
25053
  keyword: "rule-live-device-not-found",
@@ -24902,6 +25153,32 @@ var init_validate = __esm({
24902
25153
  }
24903
25154
  });
24904
25155
 
25156
+ // src/llm/pricing.ts
25157
+ function calculateCostUsd(model, tokensIn, tokensOut) {
25158
+ const pricing = PRICING[model];
25159
+ if (!pricing) return void 0;
25160
+ const inputCost = tokensIn / 1e6 * pricing.inUsdPer1M;
25161
+ const outputCost = tokensOut / 1e6 * pricing.outUsdPer1M;
25162
+ return inputCost + outputCost;
25163
+ }
25164
+ var PRICING;
25165
+ var init_pricing = __esm({
25166
+ "src/llm/pricing.ts"() {
25167
+ "use strict";
25168
+ init_cjs_shim();
25169
+ PRICING = {
25170
+ // OpenAI — https://openai.com/api/pricing/
25171
+ "gpt-4o-mini": { inUsdPer1M: 0.15, outUsdPer1M: 0.6 },
25172
+ "gpt-4o": { inUsdPer1M: 2.5, outUsdPer1M: 10 },
25173
+ "gpt-4-turbo": { inUsdPer1M: 10, outUsdPer1M: 30 },
25174
+ // Anthropic — https://www.anthropic.com/pricing
25175
+ "claude-haiku-4-5-20251001": { inUsdPer1M: 1, outUsdPer1M: 5 },
25176
+ "claude-sonnet-4-6": { inUsdPer1M: 3, outUsdPer1M: 15 },
25177
+ "claude-opus-4-7": { inUsdPer1M: 15, outUsdPer1M: 75 }
25178
+ };
25179
+ }
25180
+ });
25181
+
24905
25182
  // src/llm/providers/openai.ts
24906
25183
  import https from "node:https";
24907
25184
  import http from "node:http";
@@ -24910,9 +25187,11 @@ var init_openai = __esm({
24910
25187
  "src/llm/providers/openai.ts"() {
24911
25188
  "use strict";
24912
25189
  init_cjs_shim();
25190
+ init_pricing();
24913
25191
  OpenAIProvider = class {
24914
25192
  name = "openai";
24915
25193
  model;
25194
+ capabilities = { toolUse: true };
24916
25195
  apiKey;
24917
25196
  baseUrl;
24918
25197
  timeoutMs;
@@ -25037,7 +25316,10 @@ var init_openai = __esm({
25037
25316
  if (!toolCall) throw new Error("OpenAI decide: no tool call in response");
25038
25317
  const args = JSON.parse(toolCall.function.arguments);
25039
25318
  if (typeof args.pass !== "boolean") throw new Error("OpenAI decide: malformed function-call response");
25040
- return { pass: args.pass, reason: String(args.reason ?? "").slice(0, 200) };
25319
+ const tokensIn = json3.usage?.prompt_tokens ?? 0;
25320
+ const tokensOut = json3.usage?.completion_tokens ?? 0;
25321
+ const usage = json3.usage ? { tokensIn, tokensOut, costUsd: calculateCostUsd(this.model, tokensIn, tokensOut) } : void 0;
25322
+ return { pass: args.pass, reason: String(args.reason ?? "").slice(0, 200), usage };
25041
25323
  }
25042
25324
  };
25043
25325
  }
@@ -25050,9 +25332,11 @@ var init_anthropic = __esm({
25050
25332
  "src/llm/providers/anthropic.ts"() {
25051
25333
  "use strict";
25052
25334
  init_cjs_shim();
25335
+ init_pricing();
25053
25336
  AnthropicProvider = class {
25054
25337
  name = "anthropic";
25055
25338
  model;
25339
+ capabilities = { toolUse: true };
25056
25340
  apiKey;
25057
25341
  timeoutMs;
25058
25342
  maxTokens;
@@ -25167,7 +25451,352 @@ var init_anthropic = __esm({
25167
25451
  if (!toolUse?.input || typeof toolUse.input.pass !== "boolean") {
25168
25452
  throw new Error("Anthropic decide: malformed tool-use response");
25169
25453
  }
25170
- return { pass: toolUse.input.pass, reason: String(toolUse.input.reason ?? "").slice(0, 200) };
25454
+ const tokensIn = json3.usage?.input_tokens ?? 0;
25455
+ const tokensOut = json3.usage?.output_tokens ?? 0;
25456
+ const usage = json3.usage ? { tokensIn, tokensOut, costUsd: calculateCostUsd(this.model, tokensIn, tokensOut) } : void 0;
25457
+ return { pass: toolUse.input.pass, reason: String(toolUse.input.reason ?? "").slice(0, 200), usage };
25458
+ }
25459
+ };
25460
+ }
25461
+ });
25462
+
25463
+ // src/llm/providers/structured-output-fallback.ts
25464
+ import https3 from "node:https";
25465
+ import http2 from "node:http";
25466
+ async function decideViaStructuredOutput(opts) {
25467
+ const messages = [
25468
+ { role: "system", content: `${SYSTEM_INSTRUCTION} ${FEW_SHOT_EXAMPLE}` },
25469
+ { role: "user", content: opts.prompt }
25470
+ ];
25471
+ const first = await chatCompletion(opts, messages);
25472
+ const parsed = tryParseDecision(first.text);
25473
+ if (parsed) {
25474
+ return finalize3(parsed, first.usage, opts);
25475
+ }
25476
+ messages.push({ role: "assistant", content: first.text });
25477
+ messages.push({ role: "user", content: REPAIR_INSTRUCTION });
25478
+ const second = await chatCompletion(opts, messages);
25479
+ const repaired = tryParseDecision(second.text);
25480
+ if (repaired) {
25481
+ const merged = mergeUsage(first.usage, second.usage);
25482
+ return finalize3(repaired, merged, opts);
25483
+ }
25484
+ throw new Error(`Structured output fallback could not parse a JSON decision after repair retry. Last response: ${second.text.slice(0, 200)}`);
25485
+ }
25486
+ async function chatCompletion(opts, messages) {
25487
+ const body = JSON.stringify({
25488
+ model: opts.model,
25489
+ max_tokens: opts.maxTokens ?? 256,
25490
+ temperature: 0,
25491
+ messages
25492
+ });
25493
+ const url2 = new URL(`${opts.baseUrl.replace(/\/+$/, "")}/v1/chat/completions`);
25494
+ const isHttps = url2.protocol === "https:";
25495
+ const responseBody = await new Promise((resolve2, reject) => {
25496
+ const req = (isHttps ? https3 : http2).request(
25497
+ {
25498
+ hostname: url2.hostname,
25499
+ port: url2.port || (isHttps ? 443 : 80),
25500
+ path: url2.pathname,
25501
+ method: "POST",
25502
+ headers: {
25503
+ ...opts.apiKey ? { "Authorization": `Bearer ${opts.apiKey}` } : {},
25504
+ "Content-Type": "application/json",
25505
+ "Content-Length": Buffer.byteLength(body)
25506
+ },
25507
+ timeout: opts.timeoutMs
25508
+ },
25509
+ (res) => {
25510
+ const chunks = [];
25511
+ res.on("data", (c) => chunks.push(c));
25512
+ res.on("end", () => {
25513
+ const text2 = Buffer.concat(chunks).toString("utf-8");
25514
+ if (res.statusCode !== void 0 && res.statusCode >= 400) {
25515
+ reject(new Error(`Local LLM API error ${res.statusCode}: ${text2.slice(0, 200)}`));
25516
+ } else {
25517
+ resolve2(text2);
25518
+ }
25519
+ });
25520
+ }
25521
+ );
25522
+ req.on("error", reject);
25523
+ req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
25524
+ req.write(body);
25525
+ req.end();
25526
+ });
25527
+ const json3 = JSON.parse(responseBody);
25528
+ const text = json3.choices?.[0]?.message?.content ?? "";
25529
+ if (!text) throw new Error("Local LLM returned empty content");
25530
+ const usage = json3.usage ? { input_tokens: json3.usage.prompt_tokens ?? 0, output_tokens: json3.usage.completion_tokens ?? 0 } : void 0;
25531
+ return { text, usage };
25532
+ }
25533
+ function tryParseDecision(text) {
25534
+ const stripped = text.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
25535
+ const candidates = [stripped, ...extractAllJsonObjects(stripped)];
25536
+ for (const candidate of candidates) {
25537
+ if (!candidate) continue;
25538
+ try {
25539
+ const obj = JSON.parse(candidate);
25540
+ if (typeof obj.pass !== "boolean") continue;
25541
+ const reason = typeof obj.reason === "string" ? obj.reason : "";
25542
+ return { pass: obj.pass, reason: reason.slice(0, 200) };
25543
+ } catch {
25544
+ }
25545
+ }
25546
+ return null;
25547
+ }
25548
+ function extractAllJsonObjects(text) {
25549
+ const out = [];
25550
+ let i = 0;
25551
+ while (i < text.length) {
25552
+ const start = text.indexOf("{", i);
25553
+ if (start === -1) break;
25554
+ const block = readBalancedBraces(text, start);
25555
+ if (!block) {
25556
+ i = start + 1;
25557
+ continue;
25558
+ }
25559
+ out.push(block);
25560
+ i = start + block.length;
25561
+ }
25562
+ return out;
25563
+ }
25564
+ function readBalancedBraces(text, start) {
25565
+ let depth = 0;
25566
+ let inString = false;
25567
+ let escape2 = false;
25568
+ for (let i = start; i < text.length; i++) {
25569
+ const ch = text[i];
25570
+ if (escape2) {
25571
+ escape2 = false;
25572
+ continue;
25573
+ }
25574
+ if (ch === "\\" && inString) {
25575
+ escape2 = true;
25576
+ continue;
25577
+ }
25578
+ if (ch === '"') {
25579
+ inString = !inString;
25580
+ continue;
25581
+ }
25582
+ if (inString) continue;
25583
+ if (ch === "{") depth++;
25584
+ else if (ch === "}") {
25585
+ depth--;
25586
+ if (depth === 0) return text.slice(start, i + 1);
25587
+ }
25588
+ }
25589
+ return null;
25590
+ }
25591
+ function mergeUsage(a, b2) {
25592
+ if (!a && !b2) return void 0;
25593
+ return {
25594
+ input_tokens: (a?.input_tokens ?? 0) + (b2?.input_tokens ?? 0),
25595
+ output_tokens: (a?.output_tokens ?? 0) + (b2?.output_tokens ?? 0)
25596
+ };
25597
+ }
25598
+ function finalize3(decision, rawUsage, opts) {
25599
+ let usage;
25600
+ if (rawUsage) {
25601
+ const tokensIn = rawUsage.input_tokens;
25602
+ const tokensOut = rawUsage.output_tokens;
25603
+ const costUsd = opts.computeCostUsd ? opts.computeCostUsd(opts.model, tokensIn, tokensOut) : void 0;
25604
+ usage = { tokensIn, tokensOut, costUsd };
25605
+ }
25606
+ return { pass: decision.pass, reason: decision.reason, usage };
25607
+ }
25608
+ var SYSTEM_INSTRUCTION, FEW_SHOT_EXAMPLE, REPAIR_INSTRUCTION;
25609
+ var init_structured_output_fallback = __esm({
25610
+ "src/llm/providers/structured-output-fallback.ts"() {
25611
+ "use strict";
25612
+ init_cjs_shim();
25613
+ SYSTEM_INSTRUCTION = [
25614
+ "You are a yes/no decision endpoint for a smart-home rule engine.",
25615
+ "Read the user prompt and reply with ONLY a JSON object in this exact shape:",
25616
+ '{"pass": <true|false>, "reason": "<brief explanation, \u2264200 chars>"}',
25617
+ "No prose, no markdown fences, no extra fields. The JSON must be valid and parseable."
25618
+ ].join(" ");
25619
+ FEW_SHOT_EXAMPLE = [
25620
+ 'Example input: "Is the front door locked? Status: lockState=lock"',
25621
+ 'Example output: {"pass": true, "reason": "lockState reports lock"}'
25622
+ ].join(" ");
25623
+ REPAIR_INSTRUCTION = [
25624
+ "Your previous response was not valid JSON.",
25625
+ 'Reply with ONLY the JSON object: {"pass": <true|false>, "reason": "<short>"}.',
25626
+ "No prose, no markdown."
25627
+ ].join(" ");
25628
+ }
25629
+ });
25630
+
25631
+ // src/llm/providers/local.ts
25632
+ import https4 from "node:https";
25633
+ import http3 from "node:http";
25634
+ function stripTrailingV1(url2) {
25635
+ return url2.replace(/\/v1\/?$/, "").replace(/\/+$/, "");
25636
+ }
25637
+ function parseBoolEnv(v2) {
25638
+ if (!v2) return void 0;
25639
+ const lower = v2.trim().toLowerCase();
25640
+ if (["1", "true", "yes", "on"].includes(lower)) return true;
25641
+ if (["0", "false", "no", "off"].includes(lower)) return false;
25642
+ return void 0;
25643
+ }
25644
+ var DEFAULT_LOCAL_BASE_URL, LocalProvider;
25645
+ var init_local = __esm({
25646
+ "src/llm/providers/local.ts"() {
25647
+ "use strict";
25648
+ init_cjs_shim();
25649
+ init_pricing();
25650
+ init_structured_output_fallback();
25651
+ DEFAULT_LOCAL_BASE_URL = "http://localhost:11434/v1";
25652
+ LocalProvider = class {
25653
+ name = "local";
25654
+ model;
25655
+ capabilities;
25656
+ apiKey;
25657
+ baseUrl;
25658
+ timeoutMs;
25659
+ maxTokens;
25660
+ constructor(opts = {}) {
25661
+ this.apiKey = process.env.SWITCHBOT_LOCAL_LLM_API_KEY ?? process.env.LOCAL_LLM_API_KEY ?? "";
25662
+ this.model = opts.model ?? process.env.SWITCHBOT_LOCAL_LLM_MODEL ?? "llama3.2";
25663
+ this.baseUrl = stripTrailingV1(opts.baseUrl ?? process.env.SWITCHBOT_LOCAL_LLM_URL ?? DEFAULT_LOCAL_BASE_URL);
25664
+ this.timeoutMs = opts.timeoutMs ?? 6e4;
25665
+ this.maxTokens = opts.maxTokens ?? 1024;
25666
+ const envToolUse = parseBoolEnv(process.env.SWITCHBOT_LOCAL_LLM_TOOL_USE);
25667
+ const toolUse = opts.toolUse ?? envToolUse ?? false;
25668
+ this.capabilities = { toolUse };
25669
+ }
25670
+ async generateYaml(systemPrompt, userIntent) {
25671
+ const body = JSON.stringify({
25672
+ model: this.model,
25673
+ messages: [
25674
+ { role: "system", content: systemPrompt },
25675
+ { role: "user", content: userIntent }
25676
+ ],
25677
+ max_tokens: this.maxTokens,
25678
+ temperature: 0
25679
+ });
25680
+ const url2 = new URL(`${this.baseUrl}/v1/chat/completions`);
25681
+ const isHttps = url2.protocol === "https:";
25682
+ const responseBody = await new Promise((resolve2, reject) => {
25683
+ const req = (isHttps ? https4 : http3).request(
25684
+ {
25685
+ hostname: url2.hostname,
25686
+ port: url2.port || (isHttps ? 443 : 80),
25687
+ path: url2.pathname,
25688
+ method: "POST",
25689
+ headers: {
25690
+ ...this.apiKey ? { "Authorization": `Bearer ${this.apiKey}` } : {},
25691
+ "Content-Type": "application/json",
25692
+ "Content-Length": Buffer.byteLength(body)
25693
+ },
25694
+ timeout: this.timeoutMs
25695
+ },
25696
+ (res) => {
25697
+ const chunks = [];
25698
+ res.on("data", (c) => chunks.push(c));
25699
+ res.on("end", () => {
25700
+ const text = Buffer.concat(chunks).toString("utf-8");
25701
+ if (res.statusCode !== void 0 && res.statusCode >= 400) {
25702
+ reject(new Error(`Local LLM API error ${res.statusCode}: ${text.slice(0, 200)}`));
25703
+ } else {
25704
+ resolve2(text);
25705
+ }
25706
+ });
25707
+ }
25708
+ );
25709
+ req.on("error", reject);
25710
+ req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
25711
+ req.write(body);
25712
+ req.end();
25713
+ });
25714
+ const json3 = JSON.parse(responseBody);
25715
+ const content = json3.choices?.[0]?.message?.content;
25716
+ if (!content) throw new Error("Local LLM returned empty content");
25717
+ return content.replace(/^```ya?ml\n?/i, "").replace(/\n?```\s*$/i, "").trim();
25718
+ }
25719
+ async decide(prompt2, opts = {}) {
25720
+ const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
25721
+ if (!this.capabilities.toolUse) {
25722
+ return decideViaStructuredOutput({
25723
+ prompt: prompt2,
25724
+ apiKey: this.apiKey,
25725
+ baseUrl: this.baseUrl,
25726
+ model: this.model,
25727
+ timeoutMs,
25728
+ maxTokens: 256,
25729
+ computeCostUsd: calculateCostUsd
25730
+ });
25731
+ }
25732
+ const body = JSON.stringify({
25733
+ model: this.model,
25734
+ max_tokens: 256,
25735
+ tools: [{
25736
+ type: "function",
25737
+ function: {
25738
+ name: "decide",
25739
+ description: "Return a boolean pass/fail decision with a brief reason.",
25740
+ parameters: {
25741
+ type: "object",
25742
+ properties: {
25743
+ pass: { type: "boolean" },
25744
+ reason: { type: "string" }
25745
+ },
25746
+ required: ["pass", "reason"]
25747
+ }
25748
+ }
25749
+ }],
25750
+ tool_choice: { type: "function", function: { name: "decide" } },
25751
+ messages: [{ role: "user", content: prompt2 }]
25752
+ });
25753
+ const url2 = new URL(`${this.baseUrl}/v1/chat/completions`);
25754
+ const isHttps = url2.protocol === "https:";
25755
+ const responseBody = await new Promise((resolve2, reject) => {
25756
+ const req = (isHttps ? https4 : http3).request(
25757
+ {
25758
+ hostname: url2.hostname,
25759
+ port: url2.port || (isHttps ? 443 : 80),
25760
+ path: url2.pathname,
25761
+ method: "POST",
25762
+ headers: {
25763
+ ...this.apiKey ? { "Authorization": `Bearer ${this.apiKey}` } : {},
25764
+ "Content-Type": "application/json",
25765
+ "Content-Length": Buffer.byteLength(body)
25766
+ },
25767
+ timeout: timeoutMs
25768
+ },
25769
+ (res) => {
25770
+ const chunks = [];
25771
+ res.on("data", (c) => chunks.push(c));
25772
+ res.on("end", () => {
25773
+ const text = Buffer.concat(chunks).toString("utf-8");
25774
+ if (res.statusCode !== void 0 && res.statusCode >= 400) {
25775
+ reject(new Error(`Local LLM API error ${res.statusCode}: ${text.slice(0, 200)}`));
25776
+ } else {
25777
+ resolve2(text);
25778
+ }
25779
+ });
25780
+ }
25781
+ );
25782
+ req.on("error", reject);
25783
+ req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
25784
+ req.write(body);
25785
+ req.end();
25786
+ });
25787
+ const json3 = JSON.parse(responseBody);
25788
+ const toolCall = json3.choices?.[0]?.message?.tool_calls?.find((tc) => tc.function.name === "decide");
25789
+ if (!toolCall) throw new Error("Local LLM decide: no tool call in response");
25790
+ const args = JSON.parse(toolCall.function.arguments);
25791
+ if (typeof args.pass !== "boolean") throw new Error("Local LLM decide: malformed function-call response");
25792
+ const tokensIn = json3.usage?.prompt_tokens ?? 0;
25793
+ const tokensOut = json3.usage?.completion_tokens ?? 0;
25794
+ const usage = json3.usage ? { tokensIn, tokensOut, costUsd: calculateCostUsd(this.model, tokensIn, tokensOut) } : void 0;
25795
+ return { pass: args.pass, reason: String(args.reason ?? "").slice(0, 200), usage };
25796
+ }
25797
+ /** Exposed for doctor `local-llm-reachable` check. */
25798
+ getEndpoint() {
25799
+ return this.baseUrl;
25171
25800
  }
25172
25801
  };
25173
25802
  }
@@ -25209,8 +25838,9 @@ __export(llm_exports, {
25209
25838
  scoreIntentComplexity: () => scoreIntentComplexity
25210
25839
  });
25211
25840
  function createLLMProvider(backend, opts = {}) {
25212
- if (backend === "openai" || backend === "local") return new OpenAIProvider(opts);
25841
+ if (backend === "openai") return new OpenAIProvider(opts);
25213
25842
  if (backend === "anthropic") return new AnthropicProvider(opts);
25843
+ if (backend === "local") return new LocalProvider(opts);
25214
25844
  throw new Error(`Unknown LLM backend: ${backend}`);
25215
25845
  }
25216
25846
  var LLM_AUTO_THRESHOLD;
@@ -25220,6 +25850,7 @@ var init_llm = __esm({
25220
25850
  init_cjs_shim();
25221
25851
  init_openai();
25222
25852
  init_anthropic();
25853
+ init_local();
25223
25854
  init_complexity();
25224
25855
  LLM_AUTO_THRESHOLD = 4;
25225
25856
  }
@@ -25424,9 +26055,10 @@ async function evaluateSingle(c, now, ctx) {
25424
26055
  };
25425
26056
  }
25426
26057
  try {
26058
+ const recent = await fetchRecentEvents(c.llm.recent_events, ctx);
25427
26059
  const res = await ctx.llmEvaluator.evaluate(
25428
26060
  c.llm,
25429
- { event: ctx.event },
26061
+ { event: ctx.event, recentEvents: recent },
25430
26062
  ctx.ruleVersion ?? "unknown",
25431
26063
  ctx.globalLlmMaxCallsPerHour
25432
26064
  );
@@ -25438,6 +26070,44 @@ async function evaluateSingle(c, now, ctx) {
25438
26070
  return fail(`llm condition error: ${err instanceof Error ? err.message : String(err)}`);
25439
26071
  }
25440
26072
  }
26073
+ if (isEventCountCondition(c)) {
26074
+ if (!ctx.eventWindowFetcher) {
26075
+ return {
26076
+ matched: false,
26077
+ failures: [],
26078
+ unsupported: [{ keyword: "event_count", hint: "event_count evaluation requires a history fetcher; this call site did not provide one." }]
26079
+ };
26080
+ }
26081
+ const ec = c.event_count;
26082
+ const resolved = resolveDeviceRef(ec.device, ctx.aliases);
26083
+ if (!resolved) return fail(`event_count: could not resolve device "${ec.device}" to an id (no matching alias).`);
26084
+ const windowMs = parseDurationOrNull(ec.window);
26085
+ if (windowMs === null || windowMs <= 0) {
26086
+ return fail(`event_count: invalid window "${ec.window}" \u2014 expected e.g. "30s", "5m", "1h".`);
26087
+ }
26088
+ const untilMs = now.getTime();
26089
+ const sinceMs = untilMs - windowMs;
26090
+ try {
26091
+ const events = await ctx.eventWindowFetcher(resolved, {
26092
+ sinceMs,
26093
+ untilMs,
26094
+ limit: Math.max(ec.min, ec.max ?? ec.min) + 1,
26095
+ eventName: ec.event
26096
+ });
26097
+ const count = events.length;
26098
+ const min = ec.min;
26099
+ const max = ec.max;
26100
+ const ceilingViolated = max !== void 0 && count > max;
26101
+ const floorViolated = count < min;
26102
+ if (floorViolated || ceilingViolated) {
26103
+ const range = max !== void 0 ? `${min}\u2013${max}` : `\u2265 ${min}`;
26104
+ return fail(`event_count ${ec.device}${ec.event ? ` ${ec.event}` : ""} in ${ec.window}: ${count} (expected ${range})`);
26105
+ }
26106
+ return ok;
26107
+ } catch (err) {
26108
+ return fail(`event_count ${ec.device}: fetch failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
26109
+ }
26110
+ }
25441
26111
  return {
25442
26112
  matched: false,
25443
26113
  failures: [],
@@ -25504,12 +26174,14 @@ function conditionKind(c) {
25504
26174
  if (isTimeBetween(c)) return "time_between";
25505
26175
  if (isDeviceState(c)) return "device_state";
25506
26176
  if (isLlmCondition(c)) return "llm";
26177
+ if (isEventCountCondition(c)) return "event_count";
25507
26178
  return "unknown";
25508
26179
  }
25509
26180
  function conditionConfig(c) {
25510
26181
  if (isTimeBetween(c)) return c.time_between;
25511
26182
  if (isDeviceState(c)) return { device: c.device, field: c.field, op: c.op, value: c.value };
25512
26183
  if (isLlmCondition(c)) return { prompt: c.llm.prompt.slice(0, 80) };
26184
+ if (isEventCountCondition(c)) return c.event_count;
25513
26185
  return void 0;
25514
26186
  }
25515
26187
  function pushConditionTrace(trace, c, sub) {
@@ -25519,6 +26191,31 @@ function pushConditionTrace(trace, c, sub) {
25519
26191
  passed: sub.unsupported.length > 0 ? false : sub.matched
25520
26192
  });
25521
26193
  }
26194
+ function parseDurationOrNull(spec) {
26195
+ const m2 = String(spec ?? "").trim().match(/^(\d+)(ms|s|m|h|d)$/i);
26196
+ if (!m2) return null;
26197
+ const n = Number(m2[1]);
26198
+ const unit = m2[2].toLowerCase();
26199
+ const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
26200
+ return n * factor;
26201
+ }
26202
+ async function fetchRecentEvents(count, ctx) {
26203
+ if (!count || count <= 0) return void 0;
26204
+ if (!ctx.eventWindowFetcher || !ctx.event?.deviceId) return void 0;
26205
+ const untilMs = ctx.event.t.getTime();
26206
+ const sinceMs = untilMs - 24 * 60 * 60 * 1e3;
26207
+ try {
26208
+ const events = await ctx.eventWindowFetcher(ctx.event.deviceId, {
26209
+ sinceMs,
26210
+ untilMs,
26211
+ limit: count,
26212
+ eventName: ctx.event.event
26213
+ });
26214
+ return events.slice(-count);
26215
+ } catch {
26216
+ return void 0;
26217
+ }
26218
+ }
25522
26219
  var EVENT_CLASSIFIERS;
25523
26220
  var init_matcher = __esm({
25524
26221
  "src/rules/matcher.ts"() {
@@ -26444,7 +27141,7 @@ var init_cron_scheduler = __esm({
26444
27141
  });
26445
27142
 
26446
27143
  // src/rules/webhook-listener.ts
26447
- import http2 from "node:http";
27144
+ import http4 from "node:http";
26448
27145
  import { timingSafeEqual } from "node:crypto";
26449
27146
  function normalisePath(p2) {
26450
27147
  if (!p2) return "/";
@@ -26500,7 +27197,7 @@ var init_webhook_listener = __esm({
26500
27197
  /** Start listening. Resolves once the server has bound a port. */
26501
27198
  async start() {
26502
27199
  if (this.server) return;
26503
- const server = http2.createServer((req, res) => {
27200
+ const server = http4.createServer((req, res) => {
26504
27201
  this.handle(req, res).catch((err) => {
26505
27202
  if (!res.headersSent) {
26506
27203
  res.writeHead(500);
@@ -26640,8 +27337,8 @@ var init_webhook_listener = __esm({
26640
27337
  // src/rules/notify.ts
26641
27338
  import fs14 from "node:fs";
26642
27339
  import path12 from "node:path";
26643
- import http3 from "node:http";
26644
- import https3 from "node:https";
27340
+ import http5 from "node:http";
27341
+ import https5 from "node:https";
26645
27342
  function renderNotifyTemplate(template, vars) {
26646
27343
  return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_2, key) => {
26647
27344
  const val = vars[key];
@@ -26724,7 +27421,7 @@ async function sendWebhook(url2, body) {
26724
27421
  "User-Agent": "switchbot-cli/notify"
26725
27422
  }
26726
27423
  };
26727
- const req = (isHttps ? https3 : http3).request(options, (res) => {
27424
+ const req = (isHttps ? https5 : http5).request(options, (res) => {
26728
27425
  res.resume();
26729
27426
  const status = res.statusCode ?? 0;
26730
27427
  if (status >= 200 && status < 300) {
@@ -26911,10 +27608,250 @@ var init_trace = __esm({
26911
27608
  }
26912
27609
  });
26913
27610
 
27611
+ // src/rules/llm-condition.ts
27612
+ import { createHash as createHash4 } from "node:crypto";
27613
+ function buildCacheKey(ruleVersion2, promptTemplate, context) {
27614
+ const contextSnapshot = {
27615
+ event: { source: context.event.source, event: context.event.event, deviceId: context.event.deviceId },
27616
+ recentEvents: (context.recentEvents ?? []).map((e) => ({
27617
+ source: e.source,
27618
+ event: e.event,
27619
+ deviceId: e.deviceId
27620
+ }))
27621
+ };
27622
+ const serialized = JSON.stringify([ruleVersion2, promptTemplate, deepSortedJson(contextSnapshot)]);
27623
+ return createHash4("sha256").update(serialized).digest("hex");
27624
+ }
27625
+ function buildPrompt(template, context) {
27626
+ const eventDesc = `Event: ${context.event.source} ${context.event.event}${context.event.deviceId ? ` on ${context.event.deviceId}` : ""}`;
27627
+ return `${template}
27628
+
27629
+ ${eventDesc}`;
27630
+ }
27631
+ function parseCacheTtl(ttl) {
27632
+ if (ttl === "none") return 0;
27633
+ const match = /^(\d+)(s|m|h)$/.exec(ttl);
27634
+ if (!match) return 5 * 60 * 1e3;
27635
+ const n = parseInt(match[1], 10);
27636
+ if (match[2] === "s") return n * 1e3;
27637
+ if (match[2] === "m") return n * 60 * 1e3;
27638
+ return n * 60 * 60 * 1e3;
27639
+ }
27640
+ function resolveProvider(provider) {
27641
+ if (provider === "auto") {
27642
+ if (process.env.ANTHROPIC_API_KEY) return "anthropic";
27643
+ if (process.env.OPENAI_API_KEY || process.env.LLM_API_KEY) return "openai";
27644
+ throw new Error("No LLM API key found for llm condition. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.");
27645
+ }
27646
+ return provider;
27647
+ }
27648
+ function onErrorResult(onError, reason) {
27649
+ const pass = onError === "pass";
27650
+ return {
27651
+ pass,
27652
+ traceFields: {
27653
+ provider: "error",
27654
+ model: "error",
27655
+ latencyMs: 0,
27656
+ cacheHit: false,
27657
+ reason: reason.slice(0, 200),
27658
+ promptDigest: ""
27659
+ }
27660
+ };
27661
+ }
27662
+ var HOUR_MS, DAY_MS, LlmConditionEvaluator;
27663
+ var init_llm_condition = __esm({
27664
+ "src/rules/llm-condition.ts"() {
27665
+ "use strict";
27666
+ init_cjs_shim();
27667
+ init_trace();
27668
+ init_audit();
27669
+ HOUR_MS = 60 * 60 * 1e3;
27670
+ DAY_MS = 24 * HOUR_MS;
27671
+ LlmConditionEvaluator = class {
27672
+ cache = /* @__PURE__ */ new Map();
27673
+ budgetCounters = /* @__PURE__ */ new Map();
27674
+ async evaluate(condition, context, ruleVersion2, budgetCaps) {
27675
+ const caps = typeof budgetCaps === "number" ? { maxCallsPerHour: budgetCaps } : { ...budgetCaps ?? {} };
27676
+ if (condition.budget?.max_calls_per_hour !== void 0) caps.maxCallsPerHour = condition.budget.max_calls_per_hour;
27677
+ if (condition.budget?.max_tokens_per_hour !== void 0) caps.maxTokensPerHour = condition.budget.max_tokens_per_hour;
27678
+ if (condition.budget?.max_cost_per_day_usd !== void 0) caps.maxCostPerDayUsd = condition.budget.max_cost_per_day_usd;
27679
+ const cacheKey2 = buildCacheKey(ruleVersion2, condition.prompt, context);
27680
+ const ttlMs = parseCacheTtl(condition.cache_ttl ?? "5m");
27681
+ if (ttlMs > 0) {
27682
+ const cached2 = this.cache.get(cacheKey2);
27683
+ if (cached2 && Date.now() < cached2.expiresAt) {
27684
+ return {
27685
+ pass: cached2.result,
27686
+ traceFields: {
27687
+ provider: "cached",
27688
+ model: "cached",
27689
+ latencyMs: 0,
27690
+ cacheHit: true,
27691
+ reason: cached2.reason,
27692
+ promptDigest: cacheKey2.slice(0, 8)
27693
+ }
27694
+ };
27695
+ }
27696
+ }
27697
+ const budgetKey = `${ruleVersion2}:${condition.prompt.slice(0, 32)}`;
27698
+ const counter = this.rollCounter(budgetKey);
27699
+ const callViolation = this.checkPreCallBudget(counter, caps);
27700
+ if (callViolation) {
27701
+ this.emitBudgetExceeded(callViolation, context, condition);
27702
+ return onErrorResult(condition.on_error ?? "fail", `Budget exceeded (${callViolation.dimension})`);
27703
+ }
27704
+ const backend = resolveProvider(condition.provider ?? "auto");
27705
+ const { createLLMProvider: createLLMProvider2 } = await Promise.resolve().then(() => (init_llm(), llm_exports));
27706
+ const provider = createLLMProvider2(backend, {
27707
+ timeoutMs: condition.timeout_ms ?? 5e3
27708
+ });
27709
+ const prompt2 = buildPrompt(condition.prompt, context);
27710
+ const start = Date.now();
27711
+ try {
27712
+ const result = await provider.decide(prompt2, { timeoutMs: condition.timeout_ms ?? 5e3 });
27713
+ const latencyMs = Date.now() - start;
27714
+ counter.calls += 1;
27715
+ if (result.usage) {
27716
+ counter.tokens += result.usage.tokensIn + result.usage.tokensOut;
27717
+ if (result.usage.costUsd !== void 0) {
27718
+ counter.costUsd += result.usage.costUsd;
27719
+ }
27720
+ }
27721
+ if (ttlMs > 0) {
27722
+ this.cache.set(cacheKey2, {
27723
+ result: result.pass,
27724
+ reason: result.reason,
27725
+ expiresAt: Date.now() + ttlMs,
27726
+ usage: result.usage
27727
+ });
27728
+ }
27729
+ return {
27730
+ pass: result.pass,
27731
+ traceFields: {
27732
+ provider: provider.name,
27733
+ model: provider.model,
27734
+ latencyMs,
27735
+ cacheHit: false,
27736
+ reason: String(result.reason ?? "").slice(0, 200),
27737
+ promptDigest: cacheKey2.slice(0, 8),
27738
+ usage: result.usage
27739
+ }
27740
+ };
27741
+ } catch (err) {
27742
+ return onErrorResult(condition.on_error ?? "fail", String(err));
27743
+ }
27744
+ }
27745
+ rollCounter(key) {
27746
+ const now = Date.now();
27747
+ const existing = this.budgetCounters.get(key);
27748
+ if (!existing) {
27749
+ const fresh = { hourlyStart: now, calls: 0, tokens: 0, dailyStart: now, costUsd: 0 };
27750
+ this.budgetCounters.set(key, fresh);
27751
+ return fresh;
27752
+ }
27753
+ if (now - existing.hourlyStart >= HOUR_MS) {
27754
+ existing.hourlyStart = now;
27755
+ existing.calls = 0;
27756
+ existing.tokens = 0;
27757
+ }
27758
+ if (now - existing.dailyStart >= DAY_MS) {
27759
+ existing.dailyStart = now;
27760
+ existing.costUsd = 0;
27761
+ }
27762
+ return existing;
27763
+ }
27764
+ checkPreCallBudget(counter, caps) {
27765
+ if (caps.maxCallsPerHour !== void 0 && caps.maxCallsPerHour >= 0 && counter.calls >= caps.maxCallsPerHour) {
27766
+ return { dimension: "calls", limit: caps.maxCallsPerHour, observed: counter.calls };
27767
+ }
27768
+ if (caps.maxTokensPerHour !== void 0 && caps.maxTokensPerHour >= 0 && counter.tokens >= caps.maxTokensPerHour) {
27769
+ return { dimension: "tokens", limit: caps.maxTokensPerHour, observed: counter.tokens };
27770
+ }
27771
+ if (caps.maxCostPerDayUsd !== void 0 && caps.maxCostPerDayUsd >= 0 && counter.costUsd >= caps.maxCostPerDayUsd) {
27772
+ return { dimension: "cost", limit: caps.maxCostPerDayUsd, observed: counter.costUsd };
27773
+ }
27774
+ return null;
27775
+ }
27776
+ emitBudgetExceeded(violation, context, _condition) {
27777
+ writeAudit({
27778
+ auditVersion: 2,
27779
+ t: (/* @__PURE__ */ new Date()).toISOString(),
27780
+ kind: "llm-budget-exceeded",
27781
+ deviceId: context.event.deviceId ?? "",
27782
+ command: "llm-condition",
27783
+ parameter: null,
27784
+ commandType: "command",
27785
+ dryRun: false,
27786
+ budgetDimension: violation.dimension,
27787
+ budgetLimit: violation.limit,
27788
+ budgetObserved: violation.observed
27789
+ });
27790
+ }
27791
+ };
27792
+ }
27793
+ });
27794
+
27795
+ // src/devices/history-window.ts
27796
+ import fs15 from "node:fs";
27797
+ import readline5 from "node:readline";
27798
+ async function queryEventWindow(deviceId, opts) {
27799
+ const { sinceMs, untilMs } = opts;
27800
+ if (!Number.isFinite(sinceMs) || !Number.isFinite(untilMs)) return [];
27801
+ if (sinceMs > untilMs) return [];
27802
+ const limit = Math.max(0, opts.limit ?? Number.POSITIVE_INFINITY);
27803
+ if (limit === 0) return [];
27804
+ const files = jsonlFilesForDevice(deviceId).slice().reverse();
27805
+ const out = [];
27806
+ for (const file2 of files) {
27807
+ let mtimeMs;
27808
+ try {
27809
+ mtimeMs = fs15.statSync(file2).mtimeMs;
27810
+ } catch {
27811
+ continue;
27812
+ }
27813
+ if (mtimeMs < sinceMs) break;
27814
+ const records = await readWindowFromFile(file2, sinceMs, untilMs, opts.eventFilter);
27815
+ out.push(...records);
27816
+ if (out.length >= limit) {
27817
+ return out.slice(0, limit);
27818
+ }
27819
+ }
27820
+ return out;
27821
+ }
27822
+ async function readWindowFromFile(file2, sinceMs, untilMs, eventFilter) {
27823
+ const stream = fs15.createReadStream(file2, { encoding: "utf-8" });
27824
+ const rl = readline5.createInterface({ input: stream, crlfDelay: Infinity });
27825
+ const out = [];
27826
+ for await (const line of rl) {
27827
+ if (!line) continue;
27828
+ let rec;
27829
+ try {
27830
+ rec = JSON.parse(line);
27831
+ } catch {
27832
+ continue;
27833
+ }
27834
+ const tMs = Date.parse(rec.t);
27835
+ if (!Number.isFinite(tMs)) continue;
27836
+ if (tMs < sinceMs || tMs > untilMs) continue;
27837
+ if (eventFilter && !eventFilter(rec)) continue;
27838
+ out.push(rec);
27839
+ }
27840
+ return out;
27841
+ }
27842
+ var init_history_window = __esm({
27843
+ "src/devices/history-window.ts"() {
27844
+ "use strict";
27845
+ init_cjs_shim();
27846
+ init_history_query();
27847
+ }
27848
+ });
27849
+
26914
27850
  // src/rules/engine.ts
26915
27851
  var engine_exports = {};
26916
27852
  __export(engine_exports, {
26917
27853
  RulesEngine: () => RulesEngine,
27854
+ defaultEventWindowFetcher: () => defaultEventWindowFetcher,
26918
27855
  lintRules: () => lintRules
26919
27856
  });
26920
27857
  import { randomUUID as randomUUID4 } from "node:crypto";
@@ -27132,6 +28069,25 @@ function lintRules(automation) {
27132
28069
  message: "llm condition budget.max_calls_per_hour is 0 \u2014 condition will always take the on_error path."
27133
28070
  });
27134
28071
  }
28072
+ if (llm.budget?.max_tokens_per_hour === 0) {
28073
+ issues.push({
28074
+ rule: r.name,
28075
+ severity: "warning",
28076
+ code: "condition-llm-tokens-budget-zero",
28077
+ message: "llm condition budget.max_tokens_per_hour is 0 \u2014 condition will always take the on_error path."
28078
+ });
28079
+ }
28080
+ if (llm.budget?.max_cost_per_day_usd !== void 0 && llm.budget.max_cost_per_day_usd > 0) {
28081
+ const provider = llm.provider ?? "auto";
28082
+ if (provider === "auto") {
28083
+ issues.push({
28084
+ rule: r.name,
28085
+ severity: "warning",
28086
+ code: "condition-llm-cost-without-known-model",
28087
+ message: 'llm condition sets max_cost_per_day_usd but provider is "auto" \u2014 cost dimension is skipped for any model not in the pricing table.'
28088
+ });
28089
+ }
28090
+ }
27135
28091
  if (llm.on_error === "pass") {
27136
28092
  issues.push({
27137
28093
  rule: r.name,
@@ -27141,6 +28097,26 @@ function lintRules(automation) {
27141
28097
  });
27142
28098
  }
27143
28099
  }
28100
+ for (const c of r.conditions ?? []) {
28101
+ if (!isEventCountCondition(c)) continue;
28102
+ const ec = c.event_count;
28103
+ if (!/^\d+(ms|s|m|h|d)$/.test(String(ec.window ?? ""))) {
28104
+ issues.push({
28105
+ rule: r.name,
28106
+ severity: "error",
28107
+ code: "condition-event-count-bad-window",
28108
+ message: `event_count.window "${ec.window}" must match \`<digits>(ms|s|m|h|d)\` \u2014 e.g. "5m", "1h".`
28109
+ });
28110
+ }
28111
+ if (ec.max !== void 0 && ec.max < ec.min) {
28112
+ issues.push({
28113
+ rule: r.name,
28114
+ severity: "error",
28115
+ code: "condition-event-count-max-below-min",
28116
+ message: `event_count.max (${ec.max}) must be \u2265 min (${ec.min}).`
28117
+ });
28118
+ }
28119
+ }
27144
28120
  const enabled = r.enabled !== false;
27145
28121
  const hasError = issues.some((i) => i.severity === "error");
27146
28122
  const hasUnsupported = issues.some((i) => i.code === "trigger-unsupported");
@@ -27153,6 +28129,21 @@ function lintRules(automation) {
27153
28129
  unsupportedCount
27154
28130
  };
27155
28131
  }
28132
+ async function defaultEventWindowFetcher(deviceId, opts) {
28133
+ const records = await queryEventWindow(deviceId, {
28134
+ sinceMs: opts.sinceMs,
28135
+ untilMs: opts.untilMs,
28136
+ limit: opts.limit,
28137
+ eventFilter: opts.eventName ? (rec) => classifyMqttPayload(rec.payload).event === opts.eventName : void 0
28138
+ });
28139
+ return records.map((rec) => ({
28140
+ source: "mqtt",
28141
+ event: classifyMqttPayload(rec.payload).event,
28142
+ t: new Date(rec.t),
28143
+ deviceId,
28144
+ payload: rec.payload
28145
+ }));
28146
+ }
27156
28147
  var RulesEngine;
27157
28148
  var init_engine = __esm({
27158
28149
  "src/rules/engine.ts"() {
@@ -27170,6 +28161,8 @@ var init_engine = __esm({
27170
28161
  init_croner();
27171
28162
  init_audit();
27172
28163
  init_trace();
28164
+ init_llm_condition();
28165
+ init_history_window();
27173
28166
  RulesEngine = class {
27174
28167
  opts;
27175
28168
  rules;
@@ -27190,6 +28183,8 @@ var init_engine = __esm({
27190
28183
  * keeps the semantics of `max_per` honest.
27191
28184
  */
27192
28185
  pendingChain = Promise.resolve();
28186
+ llmEvaluator = new LlmConditionEvaluator();
28187
+ eventWindowFetcher = defaultEventWindowFetcher;
27193
28188
  stats = {
27194
28189
  started: false,
27195
28190
  rulesLoaded: 0,
@@ -27519,7 +28514,12 @@ var init_engine = __esm({
27519
28514
  const cond = await evaluateConditions(rule.conditions, event.t, {
27520
28515
  aliases: this.aliases,
27521
28516
  fetchStatus,
27522
- trace
28517
+ trace,
28518
+ event,
28519
+ llmEvaluator: this.llmEvaluator,
28520
+ ruleVersion: ruleVersion(rule),
28521
+ globalLlmMaxCallsPerHour: this.opts.automation?.llm_budget?.max_calls_per_hour,
28522
+ eventWindowFetcher: this.opts.eventWindowFetcher ?? this.eventWindowFetcher
27523
28523
  });
27524
28524
  if (!cond.matched) {
27525
28525
  const hasHysteresis = rule.hysteresis ?? rule.requires_stable_for;
@@ -27720,6 +28720,162 @@ var init_engine = __esm({
27720
28720
  }
27721
28721
  });
27722
28722
 
28723
+ // src/daemon/socket-path.ts
28724
+ import os17 from "node:os";
28725
+ import fs23 from "node:fs";
28726
+ import path18 from "node:path";
28727
+ import { execSync } from "node:child_process";
28728
+ function getDaemonSocketPath() {
28729
+ if (process.platform === "win32") {
28730
+ return `\\\\.\\pipe\\switchbot-daemon-${getCurrentUserKey()}`;
28731
+ }
28732
+ return path18.join(os17.homedir(), ".switchbot", "daemon.sock");
28733
+ }
28734
+ function getCurrentUserKey() {
28735
+ if (cachedUserKey) return cachedUserKey;
28736
+ const fromEnv = process.env.USERNAME ?? process.env.USER;
28737
+ if (fromEnv) {
28738
+ cachedUserKey = sanitize(fromEnv);
28739
+ return cachedUserKey;
28740
+ }
28741
+ try {
28742
+ const userInfo = os17.userInfo();
28743
+ cachedUserKey = sanitize(userInfo.username);
28744
+ return cachedUserKey;
28745
+ } catch {
28746
+ }
28747
+ try {
28748
+ const out = execSync("whoami", { encoding: "utf-8", timeout: 2e3 }).trim();
28749
+ cachedUserKey = sanitize(out.split(/[\\\/]/).pop() ?? "unknown");
28750
+ return cachedUserKey;
28751
+ } catch {
28752
+ cachedUserKey = "unknown";
28753
+ return cachedUserKey;
28754
+ }
28755
+ }
28756
+ function sanitize(name) {
28757
+ return name.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "unknown";
28758
+ }
28759
+ var cachedUserKey;
28760
+ var init_socket_path = __esm({
28761
+ "src/daemon/socket-path.ts"() {
28762
+ "use strict";
28763
+ init_cjs_shim();
28764
+ cachedUserKey = null;
28765
+ }
28766
+ });
28767
+
28768
+ // src/daemon/client.ts
28769
+ var client_exports2 = {};
28770
+ __export(client_exports2, {
28771
+ IpcDaemonClient: () => IpcDaemonClient,
28772
+ IpcDaemonClientError: () => IpcDaemonClientError
28773
+ });
28774
+ import net from "node:net";
28775
+ import { randomUUID as randomUUID6 } from "node:crypto";
28776
+ var IpcDaemonClientError, IpcDaemonClient;
28777
+ var init_client3 = __esm({
28778
+ "src/daemon/client.ts"() {
28779
+ "use strict";
28780
+ init_cjs_shim();
28781
+ init_socket_path();
28782
+ IpcDaemonClientError = class extends Error {
28783
+ constructor(message, code, data) {
28784
+ super(message);
28785
+ this.code = code;
28786
+ this.data = data;
28787
+ this.name = "IpcDaemonClientError";
28788
+ }
28789
+ code;
28790
+ data;
28791
+ };
28792
+ IpcDaemonClient = class {
28793
+ socketPath;
28794
+ timeoutMs;
28795
+ connectTimeoutMs;
28796
+ constructor(opts = {}) {
28797
+ this.socketPath = opts.socketPath ?? getDaemonSocketPath();
28798
+ this.timeoutMs = opts.timeoutMs ?? 5e3;
28799
+ this.connectTimeoutMs = opts.connectTimeoutMs ?? 2e3;
28800
+ }
28801
+ getSocketPath() {
28802
+ return this.socketPath;
28803
+ }
28804
+ /**
28805
+ * Sends a JSON-RPC request and resolves with the `result` field. Throws
28806
+ * IpcDaemonClientError on transport failure, parse failure, timeout, or
28807
+ * server-side error.
28808
+ */
28809
+ async call(method, params) {
28810
+ const id = randomUUID6();
28811
+ const request = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
28812
+ return new Promise((resolve2, reject) => {
28813
+ const socket = net.createConnection(this.socketPath);
28814
+ let buffer = "";
28815
+ let settled = false;
28816
+ const finish = (err, value) => {
28817
+ if (settled) return;
28818
+ settled = true;
28819
+ clearTimeout(callTimer);
28820
+ clearTimeout(connectTimer);
28821
+ socket.destroy();
28822
+ if (err) reject(err);
28823
+ else resolve2(value);
28824
+ };
28825
+ const callTimer = setTimeout(() => {
28826
+ finish(new IpcDaemonClientError(`IPC call to ${method} timed out after ${this.timeoutMs}ms`));
28827
+ }, this.timeoutMs);
28828
+ const connectTimer = setTimeout(() => {
28829
+ finish(new IpcDaemonClientError(`IPC connect timed out after ${this.connectTimeoutMs}ms`));
28830
+ }, this.connectTimeoutMs);
28831
+ socket.setEncoding("utf-8");
28832
+ socket.on("connect", () => {
28833
+ clearTimeout(connectTimer);
28834
+ socket.write(request);
28835
+ });
28836
+ socket.on("data", (chunk) => {
28837
+ buffer += chunk;
28838
+ const newlineIdx = buffer.indexOf("\n");
28839
+ if (newlineIdx === -1) return;
28840
+ const line = buffer.slice(0, newlineIdx).trim();
28841
+ if (!line) return;
28842
+ try {
28843
+ const response = JSON.parse(line);
28844
+ if ("error" in response) {
28845
+ finish(new IpcDaemonClientError(
28846
+ response.error.message,
28847
+ response.error.code,
28848
+ response.error.data
28849
+ ));
28850
+ return;
28851
+ }
28852
+ finish(null, response.result);
28853
+ } catch (err) {
28854
+ finish(new IpcDaemonClientError(`Malformed JSON-RPC response: ${err instanceof Error ? err.message : String(err)}`));
28855
+ }
28856
+ });
28857
+ socket.on("error", (err) => {
28858
+ const code = err.code === "ENOENT" || err.code === "ECONNREFUSED" ? `IPC daemon not listening at ${this.socketPath} (${err.code})` : `IPC socket error: ${err.message}`;
28859
+ finish(new IpcDaemonClientError(code));
28860
+ });
28861
+ socket.on("end", () => {
28862
+ if (!settled) finish(new IpcDaemonClientError("IPC server closed connection before responding"));
28863
+ });
28864
+ });
28865
+ }
28866
+ /**
28867
+ * Quick reachability probe. Resolves with the latency in milliseconds when
28868
+ * the daemon responds to `daemon.status`; rejects otherwise.
28869
+ */
28870
+ async ping() {
28871
+ const start = Date.now();
28872
+ const status = await this.call("daemon.status");
28873
+ return { latencyMs: Date.now() - start, status };
28874
+ }
28875
+ };
28876
+ }
28877
+ });
28878
+
27723
28879
  // src/devices/resources.ts
27724
28880
  var COMMON_WEBHOOK_FIELDS, RESOURCE_CATALOG;
27725
28881
  var init_resources = __esm({
@@ -28353,6 +29509,128 @@ var init_capabilities = __esm({
28353
29509
  }
28354
29510
  });
28355
29511
 
29512
+ // src/daemon/server.ts
29513
+ var server_exports = {};
29514
+ __export(server_exports, {
29515
+ startIpcServer: () => startIpcServer
29516
+ });
29517
+ import net2 from "node:net";
29518
+ import fs26 from "node:fs";
29519
+ import path22 from "node:path";
29520
+ async function startIpcServer(opts) {
29521
+ const socketPath = opts.socketPath ?? getDaemonSocketPath();
29522
+ if (process.platform !== "win32") {
29523
+ await ensureParentDir(socketPath);
29524
+ await removeStaleSocket(socketPath);
29525
+ }
29526
+ const server = net2.createServer((socket) => {
29527
+ let buffer = "";
29528
+ socket.setEncoding("utf-8");
29529
+ socket.on("data", (chunk) => {
29530
+ buffer += chunk;
29531
+ let newlineIdx = buffer.indexOf("\n");
29532
+ while (newlineIdx !== -1) {
29533
+ const line = buffer.slice(0, newlineIdx).trim();
29534
+ buffer = buffer.slice(newlineIdx + 1);
29535
+ newlineIdx = buffer.indexOf("\n");
29536
+ if (!line) continue;
29537
+ void handleLine(line, socket, opts);
29538
+ }
29539
+ });
29540
+ socket.on("error", (err) => opts.onClientError?.(err));
29541
+ });
29542
+ await new Promise((resolve2, reject) => {
29543
+ server.once("error", reject);
29544
+ server.listen(socketPath, () => {
29545
+ server.removeListener("error", reject);
29546
+ resolve2();
29547
+ });
29548
+ });
29549
+ if (process.platform !== "win32") {
29550
+ try {
29551
+ fs26.chmodSync(socketPath, 384);
29552
+ } catch {
29553
+ }
29554
+ }
29555
+ return {
29556
+ socketPath,
29557
+ isListening: () => server.listening,
29558
+ close: () => new Promise((resolve2) => {
29559
+ server.close(() => {
29560
+ if (process.platform !== "win32") {
29561
+ try {
29562
+ fs26.unlinkSync(socketPath);
29563
+ } catch {
29564
+ }
29565
+ }
29566
+ resolve2();
29567
+ });
29568
+ })
29569
+ };
29570
+ }
29571
+ async function handleLine(line, socket, opts) {
29572
+ let req;
29573
+ let id = null;
29574
+ try {
29575
+ const parsed = JSON.parse(line);
29576
+ req = parsed;
29577
+ id = parsed.id ?? null;
29578
+ } catch (err) {
29579
+ send(socket, errorResponse(null, ERR_PARSE, "Parse error", String(err)));
29580
+ return;
29581
+ }
29582
+ if (req.jsonrpc !== "2.0" || typeof req.method !== "string") {
29583
+ send(socket, errorResponse(id, ERR_INVALID_REQUEST, 'Invalid Request: missing jsonrpc:"2.0" or method'));
29584
+ return;
29585
+ }
29586
+ const handler = opts.handlers[req.method];
29587
+ if (!handler) {
29588
+ send(socket, errorResponse(id, ERR_METHOD_NOT_FOUND, `Method not found: ${req.method}`));
29589
+ return;
29590
+ }
29591
+ try {
29592
+ const result = await handler(req.params);
29593
+ if (req.id !== void 0) {
29594
+ send(socket, { jsonrpc: "2.0", id, result });
29595
+ }
29596
+ } catch (err) {
29597
+ const message = err instanceof Error ? err.message : String(err);
29598
+ send(socket, errorResponse(id, ERR_INTERNAL, message));
29599
+ }
29600
+ }
29601
+ function send(socket, msg) {
29602
+ if (!socket.writable) return;
29603
+ socket.write(JSON.stringify(msg) + "\n");
29604
+ }
29605
+ function errorResponse(id, code, message, data) {
29606
+ return { jsonrpc: "2.0", id, error: data === void 0 ? { code, message } : { code, message, data } };
29607
+ }
29608
+ async function ensureParentDir(socketPath) {
29609
+ const parent = path22.dirname(socketPath);
29610
+ await fs26.promises.mkdir(parent, { recursive: true });
29611
+ }
29612
+ async function removeStaleSocket(socketPath) {
29613
+ try {
29614
+ await fs26.promises.unlink(socketPath);
29615
+ } catch (err) {
29616
+ if (err.code !== "ENOENT") {
29617
+ throw err;
29618
+ }
29619
+ }
29620
+ }
29621
+ var ERR_PARSE, ERR_INVALID_REQUEST, ERR_METHOD_NOT_FOUND, ERR_INTERNAL;
29622
+ var init_server = __esm({
29623
+ "src/daemon/server.ts"() {
29624
+ "use strict";
29625
+ init_cjs_shim();
29626
+ init_socket_path();
29627
+ ERR_PARSE = -32700;
29628
+ ERR_INVALID_REQUEST = -32600;
29629
+ ERR_METHOD_NOT_FOUND = -32601;
29630
+ ERR_INTERNAL = -32603;
29631
+ }
29632
+ });
29633
+
28356
29634
  // src/index.ts
28357
29635
  init_cjs_shim();
28358
29636
  init_esm();
@@ -32221,15 +33499,15 @@ function validateSetBrightness(raw, deviceType) {
32221
33499
  error: `setBrightness requires an integer ${min}-${max} (percent). Example: "50".`
32222
33500
  };
32223
33501
  }
32224
- const trimmed = stripQuotes(raw.trim());
32225
- if (!/^-?\d+$/.test(trimmed)) {
33502
+ const parsed = parseStrictInt(raw);
33503
+ if (!parsed.ok) {
32226
33504
  return {
32227
33505
  ok: false,
32228
33506
  error: `setBrightness must be an integer ${min}-${max}, got ${JSON.stringify(raw)}. ${hintBrightnessRetry(min, max)}`
32229
33507
  };
32230
33508
  }
32231
- const n = Number(trimmed);
32232
- if (!Number.isInteger(n) || n < min || n > max) {
33509
+ const n = parsed.value;
33510
+ if (n < min || n > max) {
32233
33511
  return {
32234
33512
  ok: false,
32235
33513
  error: `setBrightness must be an integer ${min}-${max}, got "${raw}". ${hintBrightnessRetry(min, max)}`
@@ -32321,15 +33599,15 @@ function validateSetColorTemperature(raw) {
32321
33599
  error: `setColorTemperature requires an integer Kelvin value 2700-6500. Example: "4000".`
32322
33600
  };
32323
33601
  }
32324
- const trimmed = stripQuotes(raw.trim());
32325
- if (!/^-?\d+$/.test(trimmed)) {
33602
+ const parsed = parseStrictInt(raw);
33603
+ if (!parsed.ok) {
32326
33604
  return {
32327
33605
  ok: false,
32328
33606
  error: `setColorTemperature must be an integer 2700-6500, got ${JSON.stringify(raw)}.`
32329
33607
  };
32330
33608
  }
32331
- const n = Number(trimmed);
32332
- if (!Number.isInteger(n) || n < 2700 || n > 6500) {
33609
+ const n = parsed.value;
33610
+ if (n < 2700 || n > 6500) {
32333
33611
  return {
32334
33612
  ok: false,
32335
33613
  error: `setColorTemperature must be an integer 2700-6500, got "${raw}".`
@@ -32359,27 +33637,30 @@ function validateAcSetAll(raw) {
32359
33637
  };
32360
33638
  }
32361
33639
  const [tempStr, modeStr, fanStr, powerStr] = parts.map((s2) => s2.trim());
32362
- const temp = Number(tempStr);
32363
- if (!Number.isInteger(temp) || temp < 16 || temp > 30) {
33640
+ const tempP = parseStrictInt(tempStr);
33641
+ if (!tempP.ok || tempP.value < 16 || tempP.value > 30) {
32364
33642
  return {
32365
33643
  ok: false,
32366
33644
  error: `setAll field 1 (temp) must be an integer 16-30, got "${tempStr}". Example: "26,2,2,on".`
32367
33645
  };
32368
33646
  }
32369
- const mode = Number(modeStr);
32370
- if (!Number.isInteger(mode) || mode < 1 || mode > 5) {
33647
+ const temp = tempP.value;
33648
+ const modeP = parseStrictInt(modeStr);
33649
+ if (!modeP.ok || modeP.value < 1 || modeP.value > 5) {
32371
33650
  return {
32372
33651
  ok: false,
32373
33652
  error: `setAll field 2 (mode) must be 1-5 (1=auto 2=cool 3=dry 4=fan 5=heat), got "${modeStr}". Example: "26,2,2,on".`
32374
33653
  };
32375
33654
  }
32376
- const fan = Number(fanStr);
32377
- if (!Number.isInteger(fan) || fan < 1 || fan > 4) {
33655
+ const mode = modeP.value;
33656
+ const fanP = parseStrictInt(fanStr);
33657
+ if (!fanP.ok || fanP.value < 1 || fanP.value > 4) {
32378
33658
  return {
32379
33659
  ok: false,
32380
33660
  error: `setAll field 3 (fan) must be 1-4 (1=auto 2=low 3=mid 4=high), got "${fanStr}". Example: "26,2,2,on".`
32381
33661
  };
32382
33662
  }
33663
+ const fan = fanP.value;
32383
33664
  const power = powerStr.toLowerCase();
32384
33665
  if (power !== "on" && power !== "off") {
32385
33666
  return {
@@ -32398,14 +33679,14 @@ function validateCurtainSetPosition(raw) {
32398
33679
  }
32399
33680
  const stripped = stripQuotes(raw.trim());
32400
33681
  if (!stripped.includes(",")) {
32401
- const pos2 = Number(stripped);
32402
- if (!Number.isInteger(pos2) || pos2 < 0 || pos2 > 100) {
33682
+ const posP2 = parseStrictInt(stripped);
33683
+ if (!posP2.ok || posP2.value < 0 || posP2.value > 100) {
32403
33684
  return {
32404
33685
  ok: false,
32405
33686
  error: `setPosition must be an integer 0-100, got "${raw}". Example: "50".`
32406
33687
  };
32407
33688
  }
32408
- return { ok: true, normalized: String(pos2) };
33689
+ return { ok: true, normalized: String(posP2.value) };
32409
33690
  }
32410
33691
  const parts = stripped.split(",").map((s2) => s2.trim());
32411
33692
  if (parts.length !== 3) {
@@ -32415,13 +33696,14 @@ function validateCurtainSetPosition(raw) {
32415
33696
  };
32416
33697
  }
32417
33698
  const [idxStr, modeStr, posStr] = parts;
32418
- const idx = Number(idxStr);
32419
- if (!Number.isInteger(idx) || idx < 0) {
33699
+ const idxP = parseStrictInt(idxStr);
33700
+ if (!idxP.ok || idxP.value < 0) {
32420
33701
  return {
32421
33702
  ok: false,
32422
33703
  error: `setPosition field 1 (index) must be a non-negative integer, got "${idxStr}".`
32423
33704
  };
32424
33705
  }
33706
+ const idx = idxP.value;
32425
33707
  const modeLower = modeStr.toLowerCase();
32426
33708
  if (!["ff", "0", "1"].includes(modeLower)) {
32427
33709
  return {
@@ -32429,13 +33711,14 @@ function validateCurtainSetPosition(raw) {
32429
33711
  error: `setPosition field 2 (mode) must be "ff", "0", or "1", got "${modeStr}". (ff=default, 0=performance, 1=silent)`
32430
33712
  };
32431
33713
  }
32432
- const pos = Number(posStr);
32433
- if (!Number.isInteger(pos) || pos < 0 || pos > 100) {
33714
+ const posP = parseStrictInt(posStr);
33715
+ if (!posP.ok || posP.value < 0 || posP.value > 100) {
32434
33716
  return {
32435
33717
  ok: false,
32436
33718
  error: `setPosition field 3 (position) must be an integer 0-100, got "${posStr}".`
32437
33719
  };
32438
33720
  }
33721
+ const pos = posP.value;
32439
33722
  return { ok: true, normalized: `${idx},${modeLower},${pos}` };
32440
33723
  }
32441
33724
  function validateBlindTiltSetPosition(raw) {
@@ -32460,13 +33743,14 @@ function validateBlindTiltSetPosition(raw) {
32460
33743
  error: `Blind Tilt setPosition direction must be "up" or "down", got "${parts[0]}".`
32461
33744
  };
32462
33745
  }
32463
- const angle = Number(parts[1]);
32464
- if (!Number.isInteger(angle) || angle < 0 || angle > 100) {
33746
+ const angleP = parseStrictInt(parts[1]);
33747
+ if (!angleP.ok || angleP.value < 0 || angleP.value > 100) {
32465
33748
  return {
32466
33749
  ok: false,
32467
33750
  error: `Blind Tilt setPosition angle must be an integer 0-100, got "${parts[1]}".`
32468
33751
  };
32469
33752
  }
33753
+ const angle = angleP.value;
32470
33754
  if (angle % 2 !== 0) {
32471
33755
  return {
32472
33756
  ok: false,
@@ -32490,21 +33774,22 @@ function validateRelay2PmSetMode(raw) {
32490
33774
  error: `Relay Switch setMode expects "<channel>;<mode>", got ${JSON.stringify(raw)}. Example: "1;1".`
32491
33775
  };
32492
33776
  }
32493
- const ch = Number(parts[0]);
32494
- if (ch !== 1 && ch !== 2) {
33777
+ const chP = parseStrictInt(parts[0]);
33778
+ if (!chP.ok || chP.value !== 1 && chP.value !== 2) {
32495
33779
  return {
32496
33780
  ok: false,
32497
33781
  error: `Relay Switch setMode channel must be 1 or 2, got "${parts[0]}".`
32498
33782
  };
32499
33783
  }
32500
- const mode = Number(parts[1]);
32501
- if (!Number.isInteger(mode) || mode < 0 || mode > 3) {
33784
+ const ch = chP.value;
33785
+ const modeP = parseStrictInt(parts[1]);
33786
+ if (!modeP.ok || modeP.value < 0 || modeP.value > 3) {
32502
33787
  return {
32503
33788
  ok: false,
32504
33789
  error: `Relay Switch setMode mode must be 0-3 (0=toggle 1=edge 2=detached 3=momentary), got "${parts[1]}".`
32505
33790
  };
32506
33791
  }
32507
- return { ok: true, normalized: `${ch};${mode}` };
33792
+ return { ok: true, normalized: `${ch};${modeP.value}` };
32508
33793
  }
32509
33794
  function validateRelayChannel(raw) {
32510
33795
  if (raw === void 0 || raw === "" || raw === "default") {
@@ -32528,6 +33813,11 @@ function stripQuotes(s2) {
32528
33813
  }
32529
33814
  return s2;
32530
33815
  }
33816
+ function parseStrictInt(raw) {
33817
+ const cleaned = stripQuotes(raw);
33818
+ if (!/^[+-]?(?:0|[1-9]\d*)$/.test(cleaned)) return { ok: false };
33819
+ return { ok: true, value: Number(cleaned) };
33820
+ }
32531
33821
  function validateIntRange(raw, command, min, max, label) {
32532
33822
  if (raw === void 0 || raw === "" || raw === "default") {
32533
33823
  return {
@@ -32535,14 +33825,14 @@ function validateIntRange(raw, command, min, max, label) {
32535
33825
  error: `${command} requires an integer ${min}-${max} (${label}). Example: "${Math.round((min + max) / 2)}".`
32536
33826
  };
32537
33827
  }
32538
- const trimmed = stripQuotes(raw.trim());
32539
- if (!/^-?\d+$/.test(trimmed)) {
33828
+ const parsed = parseStrictInt(raw);
33829
+ if (!parsed.ok) {
32540
33830
  return {
32541
33831
  ok: false,
32542
33832
  error: `${command} must be an integer ${min}-${max} (${label}), got ${JSON.stringify(raw)}.`
32543
33833
  };
32544
33834
  }
32545
- const n = Number(trimmed);
33835
+ const n = parsed.value;
32546
33836
  if (n < min || n > max) {
32547
33837
  return {
32548
33838
  ok: false,
@@ -33205,9 +34495,9 @@ Examples:
33205
34495
  printJson(result);
33206
34496
  } else {
33207
34497
  if (dryRunned.length > 0) {
33208
- console.log(`
34498
+ console.error(`
33209
34499
  Planned (dry-run): ${dryRunned.length} device(s)`);
33210
- for (const d of dryRunned) console.log(` - ${d.deviceId}`);
34500
+ for (const d of dryRunned) console.error(` - ${d.deviceId}`);
33211
34501
  }
33212
34502
  if (preSkipped.length > 0) {
33213
34503
  console.log(`
@@ -33842,7 +35132,7 @@ Examples:
33842
35132
  if (isJsonMode()) {
33843
35133
  printJson({ ok: true, dryRun: true, command, deviceId });
33844
35134
  } else {
33845
- console.log(`\u25E6 dry-run: ${command} would be sent to ${deviceId}`);
35135
+ console.error(`\u25E6 dry-run: ${command} would be sent to ${deviceId}`);
33846
35136
  }
33847
35137
  return;
33848
35138
  }
@@ -34204,7 +35494,7 @@ Total: ${totalLabel}`);
34204
35494
  handleError(error48);
34205
35495
  }
34206
35496
  });
34207
- devices.command("status").description("Query the real-time status of a specific device").argument("[deviceId]", 'Device ID from "devices list" (or use --name or --ids)').option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: fuzzy)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', 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("--ids <list>", "Comma-separated device IDs for batch status (incompatible with --name)", stringArg("--ids")).addHelpText("after", `
35497
+ devices.command("status").description("Query the real-time status of a specific device").argument("[deviceId...]", 'Device ID(s) from "devices list" (or use --name or --ids)').option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: fuzzy)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', 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("--ids <list>", "Comma-separated device IDs for batch status (incompatible with --name)", stringArg("--ids")).addHelpText("after", `
34208
35498
  Status fields vary by device type. To discover them without a live call:
34209
35499
 
34210
35500
  switchbot devices commands <type> (prints the "Status fields" section)
@@ -34214,6 +35504,7 @@ all field names returned by your specific device, then narrow with --fields.
34214
35504
 
34215
35505
  Examples:
34216
35506
  $ switchbot devices status ABC123DEF456
35507
+ $ switchbot devices status ABC123 DEF456 GHI789
34217
35508
  $ switchbot devices status --name "Living Room AC"
34218
35509
  $ switchbot devices status ABC123DEF456 --json
34219
35510
  $ switchbot devices status ABC123DEF456 --format yaml
@@ -34221,12 +35512,13 @@ Examples:
34221
35512
  $ switchbot devices status ABC123DEF456 --json | jq '.data.battery'
34222
35513
  $ switchbot devices status --ids ABC123,DEF456,GHI789
34223
35514
  $ switchbot devices status --ids ABC123,DEF456 --fields power,battery
34224
- `).action(async (deviceIdArg, options) => {
35515
+ `).action(async (deviceIdArgs, options) => {
34225
35516
  try {
34226
- if (options.ids) {
35517
+ const batchIds = options.ids ? options.ids.split(",").map((s2) => s2.trim()).filter(Boolean) : deviceIdArgs.length > 1 ? deviceIdArgs : void 0;
35518
+ if (batchIds) {
34227
35519
  if (options.name) throw new UsageError("--ids and --name cannot be used together.");
34228
- const ids = options.ids.split(",").map((s2) => s2.trim()).filter(Boolean);
34229
- if (ids.length === 0) throw new UsageError("--ids requires at least one device ID.");
35520
+ if (batchIds.length === 0) throw new UsageError("--ids requires at least one device ID.");
35521
+ const ids = batchIds;
34230
35522
  const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
34231
35523
  const fetchedAt2 = (/* @__PURE__ */ new Date()).toISOString();
34232
35524
  const batch = results.map(
@@ -34258,7 +35550,7 @@ Examples:
34258
35550
  }
34259
35551
  return;
34260
35552
  }
34261
- const deviceId = resolveDeviceId(deviceIdArg, options.name, {
35553
+ const deviceId = resolveDeviceId(deviceIdArgs[0], options.name, {
34262
35554
  strategy: options.nameStrategy ?? "fuzzy",
34263
35555
  type: options.nameType,
34264
35556
  category: options.nameCategory,
@@ -34536,7 +35828,7 @@ ${extra}` : extra;
34536
35828
  if (isJsonMode()) {
34537
35829
  printJson({ dryRun: true, wouldSend });
34538
35830
  } else {
34539
- console.log(`\u25E6 dry-run intercepted for ${_cmd} on ${_deviceId}; see stderr preview for the HTTP request.`);
35831
+ console.error(`\u25E6 dry-run intercepted for ${_cmd} on ${_deviceId}; see stderr preview for the HTTP request.`);
34540
35832
  }
34541
35833
  return;
34542
35834
  }
@@ -34787,12 +36079,15 @@ function renderCatalogEntry(entry) {
34787
36079
  console.log(`Type: ${entry.type}`);
34788
36080
  console.log(`Category: ${entry.category === "ir" ? "IR remote" : "Physical device"}`);
34789
36081
  if (entry.role) console.log(`Role: ${entry.role}`);
34790
- if (entry.readOnly) console.log(`ReadOnly: yes (status-only device, no control commands)`);
36082
+ const hasStatusFields = (entry.statusFields?.length ?? 0) > 0;
36083
+ if (entry.readOnly) {
36084
+ console.log(hasStatusFields ? `ReadOnly: yes (status-only device, no control commands)` : `ReadOnly: yes (no cloud control commands cataloged)`);
36085
+ }
34791
36086
  if (entry.aliases && entry.aliases.length > 0) {
34792
36087
  console.log(`Aliases: ${entry.aliases.join(", ")}`);
34793
36088
  }
34794
36089
  if (entry.commands.length === 0) {
34795
- console.log("\nCommands: (none \u2014 status-only device)");
36090
+ console.log(hasStatusFields ? "\nCommands: (none \u2014 status-only device)" : "\nCommands: (none \u2014 no cloud control commands cataloged)");
34796
36091
  } else {
34797
36092
  console.log("\nCommands:");
34798
36093
  const hasExamples = entry.commands.some((c) => c.exampleParams && c.exampleParams.length > 0);
@@ -34897,7 +36192,7 @@ Example:
34897
36192
  if (isJsonMode()) {
34898
36193
  printJson({ dryRun: true, wouldSend });
34899
36194
  } else {
34900
- console.log(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
36195
+ console.error(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
34901
36196
  }
34902
36197
  return;
34903
36198
  }
@@ -35055,7 +36350,7 @@ Example:
35055
36350
  console.log(`idempotent: unknown (scene steps not exposed by API)`);
35056
36351
  console.log(`toExecute: ${explanation.toExecute}`);
35057
36352
  if (explanation.dryRun) {
35058
- console.log(`dryRun: true (pass --dry-run to execute would be a no-op)`);
36353
+ console.error(`dryRun: true (pass --dry-run to execute would be a no-op)`);
35059
36354
  }
35060
36355
  console.log(`note: ${explanation.note}`);
35061
36356
  } catch (error48) {
@@ -36391,10 +37686,10 @@ function mergeDefs(...defs) {
36391
37686
  function cloneDef(schema2) {
36392
37687
  return mergeDefs(schema2._zod.def);
36393
37688
  }
36394
- function getElementAtPath(obj, path29) {
36395
- if (!path29)
37689
+ function getElementAtPath(obj, path31) {
37690
+ if (!path31)
36396
37691
  return obj;
36397
- return path29.reduce((acc, key) => acc?.[key], obj);
37692
+ return path31.reduce((acc, key) => acc?.[key], obj);
36398
37693
  }
36399
37694
  function promiseAllObject(promisesObj) {
36400
37695
  const keys = Object.keys(promisesObj);
@@ -36777,11 +38072,11 @@ function aborted(x2, startIndex = 0) {
36777
38072
  }
36778
38073
  return false;
36779
38074
  }
36780
- function prefixIssues(path29, issues) {
38075
+ function prefixIssues(path31, issues) {
36781
38076
  return issues.map((iss) => {
36782
38077
  var _a2;
36783
38078
  (_a2 = iss).path ?? (_a2.path = []);
36784
- iss.path.unshift(path29);
38079
+ iss.path.unshift(path31);
36785
38080
  return iss;
36786
38081
  });
36787
38082
  }
@@ -36964,7 +38259,7 @@ function formatError2(error48, mapper = (issue2) => issue2.message) {
36964
38259
  }
36965
38260
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
36966
38261
  const result = { errors: [] };
36967
- const processError = (error49, path29 = []) => {
38262
+ const processError = (error49, path31 = []) => {
36968
38263
  var _a2, _b;
36969
38264
  for (const issue2 of error49.issues) {
36970
38265
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -36974,7 +38269,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
36974
38269
  } else if (issue2.code === "invalid_element") {
36975
38270
  processError({ issues: issue2.issues }, issue2.path);
36976
38271
  } else {
36977
- const fullpath = [...path29, ...issue2.path];
38272
+ const fullpath = [...path31, ...issue2.path];
36978
38273
  if (fullpath.length === 0) {
36979
38274
  result.errors.push(mapper(issue2));
36980
38275
  continue;
@@ -37006,8 +38301,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
37006
38301
  }
37007
38302
  function toDotPath(_path) {
37008
38303
  const segs = [];
37009
- const path29 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
37010
- for (const seg of path29) {
38304
+ const path31 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
38305
+ for (const seg of path31) {
37011
38306
  if (typeof seg === "number")
37012
38307
  segs.push(`[${seg}]`);
37013
38308
  else if (typeof seg === "symbol")
@@ -49062,13 +50357,13 @@ function resolveRef(ref, ctx) {
49062
50357
  if (!ref.startsWith("#")) {
49063
50358
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
49064
50359
  }
49065
- const path29 = ref.slice(1).split("/").filter(Boolean);
49066
- if (path29.length === 0) {
50360
+ const path31 = ref.slice(1).split("/").filter(Boolean);
50361
+ if (path31.length === 0) {
49067
50362
  return ctx.rootSchema;
49068
50363
  }
49069
50364
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
49070
- if (path29[0] === defsKey) {
49071
- const key = path29[1];
50365
+ if (path31[0] === defsKey) {
50366
+ const key = path31[1];
49072
50367
  if (!key || !ctx.defs[key]) {
49073
50368
  throw new Error(`Reference not found: ${ref}`);
49074
50369
  }
@@ -49851,157 +51146,12 @@ function resolveToolProfile(name) {
49851
51146
  throw new Error(`Unknown tool profile "${name}". Valid profiles: ${valid}`);
49852
51147
  }
49853
51148
 
49854
- // src/devices/history-query.ts
49855
- init_cjs_shim();
49856
- import fs10 from "node:fs";
49857
- import path10 from "node:path";
49858
- import os11 from "node:os";
49859
- import readline2 from "node:readline";
49860
- var DEFAULT_LIMIT = 1e3;
49861
- function historyDir2() {
49862
- return path10.join(os11.homedir(), ".switchbot", "device-history");
49863
- }
49864
- function parseDurationToMs2(spec) {
49865
- const m2 = spec.trim().match(/^(\d+)(ms|s|m|h|d)$/i);
49866
- if (!m2) return null;
49867
- const n = Number(m2[1]);
49868
- const unit = m2[2].toLowerCase();
49869
- const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
49870
- return n * factor;
49871
- }
49872
- function parseInstantToMs(spec) {
49873
- const ms = Date.parse(spec);
49874
- return Number.isFinite(ms) ? ms : null;
49875
- }
49876
- function resolveRange(opts) {
49877
- let fromMs = Number.NEGATIVE_INFINITY;
49878
- let toMs = Number.POSITIVE_INFINITY;
49879
- if (opts.since && (opts.from || opts.to)) {
49880
- throw new Error("--since is mutually exclusive with --from/--to.");
49881
- }
49882
- if (opts.since) {
49883
- const durMs = parseDurationToMs2(opts.since);
49884
- if (durMs === null) {
49885
- throw new Error(`Invalid --since value "${opts.since}". Expected e.g. "30s", "15m", "1h", "7d".`);
49886
- }
49887
- fromMs = Date.now() - durMs;
49888
- } else {
49889
- if (opts.from) {
49890
- const parsed = parseInstantToMs(opts.from);
49891
- if (parsed === null) throw new Error(`Invalid --from value "${opts.from}". Expected ISO-8601 timestamp.`);
49892
- fromMs = parsed;
49893
- }
49894
- if (opts.to) {
49895
- const parsed = parseInstantToMs(opts.to);
49896
- if (parsed === null) throw new Error(`Invalid --to value "${opts.to}". Expected ISO-8601 timestamp.`);
49897
- toMs = parsed;
49898
- }
49899
- if (fromMs > toMs) throw new Error("--from must be <= --to.");
49900
- }
49901
- return { fromMs, toMs };
49902
- }
49903
- function jsonlFilesForDevice(deviceId, baseDir = historyDir2()) {
49904
- const out = [];
49905
- if (!fs10.existsSync(baseDir)) return out;
49906
- for (let i = 3; i >= 1; i--) {
49907
- const p2 = path10.join(baseDir, `${deviceId}.jsonl.${i}`);
49908
- if (fs10.existsSync(p2)) out.push(p2);
49909
- }
49910
- const current = path10.join(baseDir, `${deviceId}.jsonl`);
49911
- if (fs10.existsSync(current)) out.push(current);
49912
- return out;
49913
- }
49914
- function projectFields(record2, fields) {
49915
- if (fields.length === 0) return record2;
49916
- const projected = {};
49917
- const payload = record2.payload ?? {};
49918
- for (const f2 of fields) {
49919
- if (f2 in payload) projected[f2] = payload[f2];
49920
- }
49921
- return { t: record2.t, topic: record2.topic, deviceType: record2.deviceType, payload: projected };
49922
- }
49923
- async function queryDeviceHistory(deviceId, opts = {}) {
49924
- const { fromMs, toMs } = resolveRange(opts);
49925
- const limit = Math.max(0, opts.limit ?? DEFAULT_LIMIT);
49926
- const fields = opts.fields ?? [];
49927
- const files = jsonlFilesForDevice(deviceId);
49928
- const out = [];
49929
- for (const file2 of files) {
49930
- try {
49931
- const stat = fs10.statSync(file2);
49932
- if (stat.mtimeMs < fromMs) continue;
49933
- } catch {
49934
- continue;
49935
- }
49936
- const stream = fs10.createReadStream(file2, { encoding: "utf-8" });
49937
- const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
49938
- for await (const line of rl) {
49939
- if (!line) continue;
49940
- let rec;
49941
- try {
49942
- rec = JSON.parse(line);
49943
- } catch {
49944
- continue;
49945
- }
49946
- const tMs = Date.parse(rec.t);
49947
- if (!Number.isFinite(tMs)) continue;
49948
- if (tMs < fromMs || tMs > toMs) continue;
49949
- out.push(projectFields(rec, fields));
49950
- if (out.length >= limit) {
49951
- rl.close();
49952
- stream.destroy();
49953
- return out;
49954
- }
49955
- }
49956
- }
49957
- return out;
49958
- }
49959
- function queryDeviceHistoryStats(deviceId) {
49960
- const dir = historyDir2();
49961
- const files = jsonlFilesForDevice(deviceId);
49962
- let totalBytes = 0;
49963
- let oldest = null;
49964
- let newest = null;
49965
- let count = 0;
49966
- for (const file2 of files) {
49967
- try {
49968
- totalBytes += fs10.statSync(file2).size;
49969
- } catch {
49970
- }
49971
- }
49972
- for (const file2 of files) {
49973
- try {
49974
- const lines = fs10.readFileSync(file2, "utf-8").split("\n");
49975
- for (const line of lines) {
49976
- if (!line) continue;
49977
- count += 1;
49978
- try {
49979
- const rec = JSON.parse(line);
49980
- const tMs = Date.parse(rec.t);
49981
- if (Number.isFinite(tMs)) {
49982
- if (oldest === null || tMs < oldest) oldest = tMs;
49983
- if (newest === null || tMs > newest) newest = tMs;
49984
- }
49985
- } catch {
49986
- }
49987
- }
49988
- } catch {
49989
- }
49990
- }
49991
- return {
49992
- deviceId,
49993
- fileCount: files.length,
49994
- totalBytes,
49995
- recordCount: count,
49996
- oldest: oldest !== null ? new Date(oldest).toISOString() : void 0,
49997
- newest: newest !== null ? new Date(newest).toISOString() : void 0,
49998
- jsonlFiles: files.map((f2) => path10.basename(f2)),
49999
- historyDir: dir
50000
- };
50001
- }
51149
+ // src/commands/mcp.ts
51150
+ init_history_query();
50002
51151
 
50003
51152
  // src/devices/history-agg.ts
50004
51153
  init_cjs_shim();
51154
+ init_history_query();
50005
51155
  import fs11 from "node:fs";
50006
51156
  import readline3 from "node:readline";
50007
51157
  var ALL_AGG_FNS = ["count", "min", "max", "avg", "sum", "p50", "p95"];
@@ -50545,7 +51695,7 @@ async function executePlanSteps(plan, planId, options) {
50545
51695
  if (err instanceof Error && err.name === "DryRunSignal") {
50546
51696
  out.results.push({ step: idx, type: "command", deviceId: resolvedDeviceId, command: step.command, status: "dry-run" });
50547
51697
  out.summary.dryRun++;
50548
- if (!isJsonMode()) console.log(` ${idx}. \u25E6 dry-run ${step.command} on ${resolvedDeviceId}`);
51698
+ if (!isJsonMode()) console.error(` ${idx}. \u25E6 dry-run ${step.command} on ${resolvedDeviceId}`);
50549
51699
  continue;
50550
51700
  }
50551
51701
  const msg = err instanceof Error ? err.message : String(err);
@@ -51125,7 +52275,7 @@ var import_yaml5 = __toESM(require_dist(), 1);
51125
52275
  var import_yaml6 = __toESM(require_dist(), 1);
51126
52276
  init_load();
51127
52277
  init_validate();
51128
- import fs15 from "node:fs";
52278
+ import fs16 from "node:fs";
51129
52279
  var AddRuleError = class extends Error {
51130
52280
  constructor(message, code) {
51131
52281
  super(message);
@@ -51241,7 +52391,7 @@ ${msgs}`,
51241
52391
  function addRuleToPolicyFile(opts) {
51242
52392
  const result = addRuleToPolicySource(opts);
51243
52393
  if (!opts.dryRun) {
51244
- fs15.writeFileSync(opts.policyPath, result.nextSource, "utf8");
52394
+ fs16.writeFileSync(opts.policyPath, result.nextSource, "utf8");
51245
52395
  return { ...result, written: true };
51246
52396
  }
51247
52397
  return { ...result, written: false };
@@ -51250,15 +52400,15 @@ function addRuleToPolicyFile(opts) {
51250
52400
  // src/rules/explain.ts
51251
52401
  init_cjs_shim();
51252
52402
  init_trace();
51253
- import fs16 from "node:fs";
52403
+ import fs17 from "node:fs";
51254
52404
  function loadTraceRecords(auditFile, opts = {}) {
51255
- if (!fs16.existsSync(auditFile)) return [];
51256
- const lines = fs16.readFileSync(auditFile, "utf-8").split(/\r?\n/);
52405
+ if (!fs17.existsSync(auditFile)) return [];
52406
+ const lines = fs17.readFileSync(auditFile, "utf-8").split(/\r?\n/);
51257
52407
  return filterTraceRecords(lines, opts);
51258
52408
  }
51259
52409
  function loadRelatedAudit(auditFile, fireId) {
51260
- if (!fs16.existsSync(auditFile)) return [];
51261
- const raw = fs16.readFileSync(auditFile, "utf-8");
52410
+ if (!fs17.existsSync(auditFile)) return [];
52411
+ const raw = fs17.readFileSync(auditFile, "utf-8");
51262
52412
  const out = [];
51263
52413
  for (const line of raw.split(/\r?\n/)) {
51264
52414
  const trimmed = line.trim();
@@ -51342,17 +52492,18 @@ init_throttle();
51342
52492
  init_trace();
51343
52493
  init_trace();
51344
52494
  init_matcher();
51345
- import fs17 from "node:fs";
52495
+ init_engine();
52496
+ import fs18 from "node:fs";
51346
52497
  import path14 from "node:path";
51347
52498
  import os13 from "node:os";
51348
52499
  import { randomUUID as randomUUID5 } from "node:crypto";
51349
- var HOUR_MS = 60 * 60 * 1e3;
52500
+ var HOUR_MS2 = 60 * 60 * 1e3;
51350
52501
  var DEVICE_HISTORY_DIR = path14.join(os13.homedir(), ".switchbot", "device-history");
51351
52502
  async function simulateRule(opts) {
51352
52503
  const { rule, aliases = {}, liveLlm = false } = opts;
51353
52504
  const rv = ruleVersion(rule);
51354
52505
  const events = loadSourceEvents(opts);
51355
- const windowStart = events.length > 0 ? new Date(Math.min(...events.map((e) => e.t.getTime()))) : new Date(Date.now() - 24 * HOUR_MS);
52506
+ const windowStart = events.length > 0 ? new Date(Math.min(...events.map((e) => e.t.getTime()))) : new Date(Date.now() - 24 * HOUR_MS2);
51356
52507
  const windowEnd = events.length > 0 ? new Date(Math.max(...events.map((e) => e.t.getTime()))) : /* @__PURE__ */ new Date();
51357
52508
  const counts = { wouldFire: 0, blocked: 0, throttled: 0, errored: 0, skippedLlm: 0 };
51358
52509
  const blockReasons = /* @__PURE__ */ new Map();
@@ -51402,7 +52553,8 @@ async function simulateRule(opts) {
51402
52553
  aliases,
51403
52554
  fetchStatus: statusFetcher,
51404
52555
  event,
51405
- ruleVersion: rv
52556
+ ruleVersion: rv,
52557
+ eventWindowFetcher: defaultEventWindowFetcher
51406
52558
  });
51407
52559
  } catch (err) {
51408
52560
  counts.errored++;
@@ -51451,8 +52603,8 @@ async function simulateRule(opts) {
51451
52603
  }
51452
52604
  function loadSourceEvents(opts) {
51453
52605
  if (opts.against) {
51454
- if (!fs17.existsSync(opts.against)) return [];
51455
- const lines2 = fs17.readFileSync(opts.against, "utf-8").split(/\r?\n/);
52606
+ if (!fs18.existsSync(opts.against)) return [];
52607
+ const lines2 = fs18.readFileSync(opts.against, "utf-8").split(/\r?\n/);
51456
52608
  const events = [];
51457
52609
  for (const line of lines2) {
51458
52610
  const trimmed = line.trim();
@@ -51472,10 +52624,10 @@ function loadSourceEvents(opts) {
51472
52624
  return events;
51473
52625
  }
51474
52626
  const auditLog = opts.auditLog;
51475
- if (!auditLog || !fs17.existsSync(auditLog)) return [];
51476
- const sinceMs = opts.since ? parseSince(opts.since) : Date.now() - 24 * HOUR_MS;
52627
+ if (!auditLog || !fs18.existsSync(auditLog)) return [];
52628
+ const sinceMs = opts.since ? parseSince(opts.since) : Date.now() - 24 * HOUR_MS2;
51477
52629
  const sinceIso = new Date(sinceMs).toISOString();
51478
- const lines = fs17.readFileSync(auditLog, "utf-8").split(/\r?\n/);
52630
+ const lines = fs18.readFileSync(auditLog, "utf-8").split(/\r?\n/);
51479
52631
  const traceRecords = filterTraceRecords(lines, {
51480
52632
  ruleName: opts.rule.name,
51481
52633
  since: sinceIso
@@ -51499,13 +52651,13 @@ function parseSince(since) {
51499
52651
  const unitMs = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
51500
52652
  return Date.now() - n * unitMs;
51501
52653
  }
51502
- return Date.now() - 24 * HOUR_MS;
52654
+ return Date.now() - 24 * HOUR_MS2;
51503
52655
  }
51504
52656
  function buildStatusFetcher(asOf) {
51505
52657
  return async (deviceId) => {
51506
52658
  const histFile = path14.join(DEVICE_HISTORY_DIR, `${deviceId}.jsonl`);
51507
- if (!fs17.existsSync(histFile)) return {};
51508
- const lines = fs17.readFileSync(histFile, "utf-8").split(/\r?\n/);
52659
+ if (!fs18.existsSync(histFile)) return {};
52660
+ const lines = fs18.readFileSync(histFile, "utf-8").split(/\r?\n/);
51509
52661
  const asOfMs = asOf.getTime();
51510
52662
  let best;
51511
52663
  for (const line of lines) {
@@ -51541,13 +52693,13 @@ function collectPolicyDiff(left, right, at, out, limit) {
51541
52693
  const maxLen = Math.max(left.length, right.length);
51542
52694
  for (let i = 0; i < maxLen; i++) {
51543
52695
  if (out.length >= limit) return;
51544
- const path29 = `${at}[${i}]`;
52696
+ const path31 = `${at}[${i}]`;
51545
52697
  if (i >= left.length) {
51546
- out.push({ path: path29, kind: "added", after: right[i] });
52698
+ out.push({ path: path31, kind: "added", after: right[i] });
51547
52699
  } else if (i >= right.length) {
51548
- out.push({ path: path29, kind: "removed", before: left[i] });
52700
+ out.push({ path: path31, kind: "removed", before: left[i] });
51549
52701
  } else {
51550
- collectPolicyDiff(left[i], right[i], path29, out, limit);
52702
+ collectPolicyDiff(left[i], right[i], path31, out, limit);
51551
52703
  }
51552
52704
  }
51553
52705
  return;
@@ -51556,15 +52708,15 @@ function collectPolicyDiff(left, right, at, out, limit) {
51556
52708
  const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
51557
52709
  for (const key of [...keys].sort()) {
51558
52710
  if (out.length >= limit) return;
51559
- const path29 = at === "$" ? `$.${key}` : `${at}.${key}`;
52711
+ const path31 = at === "$" ? `$.${key}` : `${at}.${key}`;
51560
52712
  const leftHas = Object.prototype.hasOwnProperty.call(left, key);
51561
52713
  const rightHas = Object.prototype.hasOwnProperty.call(right, key);
51562
52714
  if (!leftHas && rightHas) {
51563
- out.push({ path: path29, kind: "added", after: right[key] });
52715
+ out.push({ path: path31, kind: "added", after: right[key] });
51564
52716
  } else if (leftHas && !rightHas) {
51565
- out.push({ path: path29, kind: "removed", before: left[key] });
52717
+ out.push({ path: path31, kind: "removed", before: left[key] });
51566
52718
  } else {
51567
- collectPolicyDiff(left[key], right[key], path29, out, limit);
52719
+ collectPolicyDiff(left[key], right[key], path31, out, limit);
51568
52720
  }
51569
52721
  }
51570
52722
  return;
@@ -51618,7 +52770,7 @@ function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges
51618
52770
  init_embedded_assets();
51619
52771
  import { dirname as pathDirname, join as pathJoin } from "node:path";
51620
52772
  import os14 from "node:os";
51621
- import fs18 from "node:fs";
52773
+ import fs19 from "node:fs";
51622
52774
  var LATEST_SUPPORTED_VERSION = SUPPORTED_POLICY_SCHEMA_VERSIONS[SUPPORTED_POLICY_SCHEMA_VERSIONS.length - 1];
51623
52775
  function mcpError(kind, code, message, options) {
51624
52776
  const obj = { code, kind, message };
@@ -52594,15 +53746,15 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
52594
53746
  async ({ path: pathArg, force }) => {
52595
53747
  const policyPath = resolvePolicyPath({ flag: pathArg });
52596
53748
  const doForce = force === true;
52597
- if (fs18.existsSync(policyPath) && !doForce) {
53749
+ if (fs19.existsSync(policyPath) && !doForce) {
52598
53750
  return mcpError("guard", 5, `refusing to overwrite existing policy at ${policyPath}`, {
52599
53751
  hint: "pass force=true to overwrite, or choose a different path",
52600
53752
  context: { policyPath }
52601
53753
  });
52602
53754
  }
52603
53755
  const template = readPolicyExampleYaml();
52604
- fs18.mkdirSync(pathDirname(policyPath), { recursive: true });
52605
- fs18.writeFileSync(policyPath, template, { encoding: "utf-8" });
53756
+ fs19.mkdirSync(pathDirname(policyPath), { recursive: true });
53757
+ fs19.writeFileSync(policyPath, template, { encoding: "utf-8" });
52606
53758
  const structured = {
52607
53759
  policyPath,
52608
53760
  schemaVersion: CURRENT_POLICY_SCHEMA_VERSION,
@@ -52791,7 +53943,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
52791
53943
  let leftSource = "";
52792
53944
  let rightSource = "";
52793
53945
  try {
52794
- leftSource = fs18.readFileSync(left_path, "utf-8");
53946
+ leftSource = fs19.readFileSync(left_path, "utf-8");
52795
53947
  } catch (err) {
52796
53948
  if (err?.code === "ENOENT") {
52797
53949
  return mcpError("usage", 2, `policy file not found: ${left_path}`, {
@@ -52801,7 +53953,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
52801
53953
  return mcpError("runtime", 1, `failed to read ${left_path}: ${String(err)}`);
52802
53954
  }
52803
53955
  try {
52804
- rightSource = fs18.readFileSync(right_path, "utf-8");
53956
+ rightSource = fs19.readFileSync(right_path, "utf-8");
52805
53957
  } catch (err) {
52806
53958
  if (err?.code === "ENOENT") {
52807
53959
  return mcpError("usage", 2, `policy file not found: ${right_path}`, {
@@ -53515,21 +54667,21 @@ Example Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
53515
54667
  Inspect locally:
53516
54668
  $ npx @modelcontextprotocol/inspector switchbot mcp serve
53517
54669
  `);
53518
- mcp.command("tools").description("Print the registered MCP tools in human or JSON form").option("--tools <profile>", "Tool profile: default, readonly, all (default: all)", stringArg("--tools"), "all").action((opts) => {
54670
+ mcp.command("tools").description("Print the registered MCP tools in human or JSON form").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24). Lists all when omitted', stringArg("--tools"), "all").action((opts) => {
53519
54671
  try {
53520
54672
  printMcpToolDirectory(resolveToolProfile(opts.tools));
53521
54673
  } catch (e) {
53522
54674
  handleError(e);
53523
54675
  }
53524
54676
  });
53525
- mcp.command("list-tools").description("Alias of `mcp tools`").option("--tools <profile>", "Tool profile: default, readonly, all (default: all)", stringArg("--tools"), "all").action((opts) => {
54677
+ mcp.command("list-tools").description("Alias of `mcp tools`").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24). Lists all when omitted', stringArg("--tools"), "all").action((opts) => {
53526
54678
  try {
53527
54679
  printMcpToolDirectory(resolveToolProfile(opts.tools));
53528
54680
  } catch (e) {
53529
54681
  handleError(e);
53530
54682
  }
53531
54683
  });
53532
- mcp.command("serve").description("Start the MCP server on stdio (default) or HTTP (--port)").option("--port <n>", "Listen on HTTP instead of stdio (Streamable HTTP transport)", intArg("--port", { min: 1, max: 65535 })).option("--bind <host>", "IP address to bind (default 127.0.0.1; use 0.0.0.0 to accept external connections)", stringArg("--bind"), "127.0.0.1").option("--auth-token <token>", "Bearer token for HTTP requests (required for --bind 0.0.0.0; falls back to SWITCHBOT_MCP_TOKEN env var)", stringArg("--auth-token")).option("--cors-origin <url>", "Allowed CORS origin(s) for HTTP (repeatable)", stringArg("--cors-origin")).option("--rate-limit <n>", "Max requests per minute per profile (default 60)", intArg("--rate-limit", { min: 1 }), "60").option("--tools <profile>", "Tool profile: default, readonly, all (default: default)", stringArg("--tools"), "default").addHelpText("after", `
54684
+ mcp.command("serve").description("Start the MCP server on stdio (default) or HTTP (--port)").option("--port <n>", "Listen on HTTP instead of stdio (Streamable HTTP transport)", intArg("--port", { min: 1, max: 65535 })).option("--bind <host>", "IP address to bind (default 127.0.0.1; use 0.0.0.0 to accept external connections)", stringArg("--bind"), "127.0.0.1").option("--auth-token <token>", "Bearer token for HTTP requests (required for --bind 0.0.0.0; falls back to SWITCHBOT_MCP_TOKEN env var)", stringArg("--auth-token")).option("--cors-origin <url>", "Allowed CORS origin(s) for HTTP (repeatable)", stringArg("--cors-origin")).option("--rate-limit <n>", "Max requests per minute per profile (default 60)", intArg("--rate-limit", { min: 1 }), "60").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24)', stringArg("--tools"), "default").addHelpText("after", `
53533
54685
  Examples:
53534
54686
  $ switchbot mcp serve
53535
54687
  $ switchbot mcp serve --tools all
@@ -53676,7 +54828,7 @@ process_uptime_seconds ${Math.floor(process.uptime())}
53676
54828
  }
53677
54829
  if (profile) {
53678
54830
  const envCredsPresent = !!(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
53679
- if (!envCredsPresent && !fs18.existsSync(profileFilePath(profile))) {
54831
+ if (!envCredsPresent && !fs19.existsSync(profileFilePath(profile))) {
53680
54832
  res.writeHead(401, { "Content-Type": "application/json" });
53681
54833
  res.end(JSON.stringify({
53682
54834
  jsonrpc: "2.0",
@@ -53977,6 +55129,7 @@ Total: ${entries.length} device type(s) (source: ${source})`);
53977
55129
  });
53978
55130
  catalog.command("search").description("Fuzzy search the effective catalog by type name, alias, role, or command name").argument("<keyword>", "Substring to match (case-insensitive) against type, alias, role, or command").option("--strict", "Only return entries whose type name matches (skip alias/role/command fallbacks)").action((keyword, options) => {
53979
55131
  try {
55132
+ if (!keyword.trim()) throw new UsageError("catalog search requires a non-empty keyword.");
53980
55133
  const q = keyword.toLowerCase();
53981
55134
  const entries = getEffectiveCatalog();
53982
55135
  const strict = options.strict === true;
@@ -54281,7 +55434,7 @@ init_cjs_shim();
54281
55434
  init_output();
54282
55435
  init_arg_parsers();
54283
55436
  init_flags();
54284
- import http4 from "node:http";
55437
+ import http6 from "node:http";
54285
55438
  import crypto4 from "node:crypto";
54286
55439
  init_client2();
54287
55440
  init_credential();
@@ -54312,18 +55465,18 @@ var StdoutSink = class {
54312
55465
 
54313
55466
  // src/sinks/file.ts
54314
55467
  init_cjs_shim();
54315
- import fs19 from "node:fs";
55468
+ import fs20 from "node:fs";
54316
55469
  import path15 from "node:path";
54317
55470
  var FileSink = class {
54318
55471
  filePath;
54319
55472
  constructor(filePath) {
54320
55473
  this.filePath = path15.resolve(filePath);
54321
55474
  const dir = path15.dirname(this.filePath);
54322
- if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
55475
+ if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
54323
55476
  }
54324
55477
  async write(event) {
54325
55478
  try {
54326
- fs19.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
55479
+ fs20.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
54327
55480
  } catch {
54328
55481
  }
54329
55482
  }
@@ -54577,7 +55730,7 @@ function parseFilter2(flag) {
54577
55730
  }
54578
55731
  }
54579
55732
  function startReceiver(port, pathMatch, filter, onEvent) {
54580
- const server = http4.createServer((req, res) => {
55733
+ const server = http6.createServer((req, res) => {
54581
55734
  if (req.method !== "POST") {
54582
55735
  res.statusCode = 405;
54583
55736
  res.end("method not allowed");
@@ -54992,10 +56145,10 @@ init_catalog();
54992
56145
  init_config();
54993
56146
  init_cache();
54994
56147
  init_quota();
54995
- import fs22 from "node:fs";
54996
- import os17 from "node:os";
54997
- import path18 from "node:path";
54998
- import { execSync } from "node:child_process";
56148
+ import fs24 from "node:fs";
56149
+ import os18 from "node:os";
56150
+ import path19 from "node:path";
56151
+ import { execSync as execSync2 } from "node:child_process";
54999
56152
 
55000
56153
  // src/commands/agent-bootstrap.ts
55001
56154
  init_cjs_shim();
@@ -55230,7 +56383,7 @@ init_request_context();
55230
56383
 
55231
56384
  // src/lib/daemon-state.ts
55232
56385
  init_cjs_shim();
55233
- import fs20 from "node:fs";
56386
+ import fs21 from "node:fs";
55234
56387
  import os15 from "node:os";
55235
56388
  import path16 from "node:path";
55236
56389
  function getStateDir() {
@@ -55253,15 +56406,15 @@ var DAEMON_LOG_FILE = getDaemonLogFile();
55253
56406
  var DAEMON_STATE_FILE = getDaemonStateFile();
55254
56407
  var HEALTHZ_PID_FILE = getHealthzPidFile();
55255
56408
  function ensureStateDir() {
55256
- fs20.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
56409
+ fs21.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
55257
56410
  }
55258
56411
  function writeDaemonState(state) {
55259
56412
  ensureStateDir();
55260
- fs20.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
56413
+ fs21.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
55261
56414
  }
55262
56415
  function readDaemonState() {
55263
56416
  try {
55264
- const raw = fs20.readFileSync(getDaemonStateFile(), "utf-8");
56417
+ const raw = fs21.readFileSync(getDaemonStateFile(), "utf-8");
55265
56418
  return JSON.parse(raw);
55266
56419
  } catch {
55267
56420
  return null;
@@ -55270,7 +56423,7 @@ function readDaemonState() {
55270
56423
 
55271
56424
  // src/rules/pid-file.ts
55272
56425
  init_cjs_shim();
55273
- import fs21 from "node:fs";
56426
+ import fs22 from "node:fs";
55274
56427
  import os16 from "node:os";
55275
56428
  import path17 from "node:path";
55276
56429
  var DEFAULT_DIR = path17.join(os16.homedir(), ".switchbot");
@@ -55283,13 +56436,13 @@ function getDefaultPidFilePaths() {
55283
56436
  }
55284
56437
  function writePidFile(pidFile, pid = process.pid) {
55285
56438
  const dir = path17.dirname(pidFile);
55286
- fs21.mkdirSync(dir, { recursive: true, mode: 448 });
55287
- fs21.writeFileSync(pidFile, `${pid}
56439
+ fs22.mkdirSync(dir, { recursive: true, mode: 448 });
56440
+ fs22.writeFileSync(pidFile, `${pid}
55288
56441
  `, { mode: 384 });
55289
56442
  }
55290
56443
  function readPidFile(pidFile) {
55291
56444
  try {
55292
- const raw = fs21.readFileSync(pidFile, "utf-8").trim();
56445
+ const raw = fs22.readFileSync(pidFile, "utf-8").trim();
55293
56446
  const n = Number(raw);
55294
56447
  return Number.isInteger(n) && n > 0 ? n : null;
55295
56448
  } catch {
@@ -55299,20 +56452,20 @@ function readPidFile(pidFile) {
55299
56452
  function clearPidFile(pidFile, pid = process.pid) {
55300
56453
  try {
55301
56454
  const existing = readPidFile(pidFile);
55302
- if (existing === pid) fs21.unlinkSync(pidFile);
56455
+ if (existing === pid) fs22.unlinkSync(pidFile);
55303
56456
  } catch {
55304
56457
  }
55305
56458
  }
55306
56459
  function writeReloadSentinel(reloadFile) {
55307
56460
  const dir = path17.dirname(reloadFile);
55308
- fs21.mkdirSync(dir, { recursive: true, mode: 448 });
55309
- fs21.writeFileSync(reloadFile, `${Date.now()}
56461
+ fs22.mkdirSync(dir, { recursive: true, mode: 448 });
56462
+ fs22.writeFileSync(reloadFile, `${Date.now()}
55310
56463
  `, { mode: 384 });
55311
56464
  }
55312
56465
  function consumeReloadSentinel(reloadFile) {
55313
56466
  try {
55314
- if (!fs21.existsSync(reloadFile)) return false;
55315
- fs21.unlinkSync(reloadFile);
56467
+ if (!fs22.existsSync(reloadFile)) return false;
56468
+ fs22.unlinkSync(reloadFile);
55316
56469
  return true;
55317
56470
  } catch {
55318
56471
  return false;
@@ -55383,7 +56536,7 @@ async function checkCredentials() {
55383
56536
  };
55384
56537
  }
55385
56538
  const file2 = configFilePath();
55386
- if (!fs22.existsSync(file2)) {
56539
+ if (!fs24.existsSync(file2)) {
55387
56540
  return {
55388
56541
  name: "credentials",
55389
56542
  status: "fail",
@@ -55398,7 +56551,7 @@ async function checkCredentials() {
55398
56551
  };
55399
56552
  }
55400
56553
  try {
55401
- const raw = fs22.readFileSync(file2, "utf-8");
56554
+ const raw = fs24.readFileSync(file2, "utf-8");
55402
56555
  const cfg = JSON.parse(raw);
55403
56556
  if (!cfg.token || !cfg.secret) {
55404
56557
  return {
@@ -55445,8 +56598,8 @@ async function checkCredentials() {
55445
56598
  }
55446
56599
  }
55447
56600
  function checkProfiles() {
55448
- const dir = path18.join(os17.homedir(), ".switchbot", "profiles");
55449
- if (!fs22.existsSync(dir)) {
56601
+ const dir = path19.join(os18.homedir(), ".switchbot", "profiles");
56602
+ if (!fs24.existsSync(dir)) {
55450
56603
  return { name: "profiles", status: "ok", detail: "no profile dir (default profile only)" };
55451
56604
  }
55452
56605
  const profiles = listProfiles();
@@ -55541,6 +56694,25 @@ function checkCatalog() {
55541
56694
  detail: `${catalog.length} types loaded${missingRole > 0 ? `, ${missingRole} missing role` : ""}`
55542
56695
  };
55543
56696
  }
56697
+ function checkCatalogCoverage() {
56698
+ const cache2 = loadCache();
56699
+ if (!cache2) {
56700
+ return { name: "catalog-coverage", status: "ok", detail: 'no device cache \u2014 run "switchbot devices list" first' };
56701
+ }
56702
+ const catalog = getEffectiveCatalog();
56703
+ const catalogTypes = new Set(catalog.map((e) => e.type.toLowerCase()));
56704
+ const aliases = new Set(catalog.flatMap((e) => (e.aliases ?? []).map((a) => a.toLowerCase())));
56705
+ const deviceTypes = [...new Set(Object.values(cache2.devices).map((d) => d.type))];
56706
+ const missing = deviceTypes.filter((t) => !catalogTypes.has(t.toLowerCase()) && !aliases.has(t.toLowerCase()));
56707
+ if (missing.length === 0) {
56708
+ return { name: "catalog-coverage", status: "ok", detail: `all ${deviceTypes.length} device types have catalog entries` };
56709
+ }
56710
+ return {
56711
+ name: "catalog-coverage",
56712
+ status: "warn",
56713
+ detail: { missing, message: `${missing.length} device type(s) without catalog entry` }
56714
+ };
56715
+ }
55544
56716
  function checkCache() {
55545
56717
  try {
55546
56718
  const info = describeCache();
@@ -55553,8 +56725,8 @@ function checkCache() {
55553
56725
  }
55554
56726
  }
55555
56727
  function checkQuotaFile() {
55556
- const p2 = path18.join(os17.homedir(), ".switchbot", "quota.json");
55557
- if (!fs22.existsSync(p2)) {
56728
+ const p2 = path19.join(os18.homedir(), ".switchbot", "quota.json");
56729
+ if (!fs24.existsSync(p2)) {
55558
56730
  return {
55559
56731
  name: "quota",
55560
56732
  status: "ok",
@@ -55567,7 +56739,7 @@ function checkQuotaFile() {
55567
56739
  };
55568
56740
  }
55569
56741
  try {
55570
- const raw = fs22.readFileSync(p2, "utf-8");
56742
+ const raw = fs24.readFileSync(p2, "utf-8");
55571
56743
  JSON.parse(raw);
55572
56744
  } catch {
55573
56745
  return {
@@ -55642,13 +56814,14 @@ function checkInventoryConsistency() {
55642
56814
  status: "warn",
55643
56815
  detail: {
55644
56816
  message: `${dangling.length} device(s) reference a hubDeviceId that is not present in the current inventory`,
56817
+ hint: "This usually means the hub was removed or replaced. Re-pair affected devices in the SwitchBot app, or ignore if the devices still work.",
55645
56818
  dangling: dangling.slice(0, 10)
55646
56819
  }
55647
56820
  };
55648
56821
  }
55649
56822
  function checkAudit() {
55650
- const p2 = path18.join(os17.homedir(), ".switchbot", "audit.log");
55651
- if (!fs22.existsSync(p2)) {
56823
+ const p2 = path19.join(os18.homedir(), ".switchbot", "audit.log");
56824
+ if (!fs24.existsSync(p2)) {
55652
56825
  return {
55653
56826
  name: "audit",
55654
56827
  status: "ok",
@@ -55660,7 +56833,7 @@ function checkAudit() {
55660
56833
  };
55661
56834
  }
55662
56835
  try {
55663
- const raw = fs22.readFileSync(p2, "utf-8");
56836
+ const raw = fs24.readFileSync(p2, "utf-8");
55664
56837
  const since = Date.now() - 24 * 60 * 60 * 1e3;
55665
56838
  const recent = [];
55666
56839
  let total = 0;
@@ -55859,14 +57032,14 @@ function checkPathDiscoverability() {
55859
57032
  const binaryName = isWindows ? "switchbot.cmd" : "switchbot";
55860
57033
  let npmBinDir = null;
55861
57034
  try {
55862
- const prefix = execSync("npm prefix -g", { timeout: 4e3, encoding: "utf-8" }).trim();
55863
- npmBinDir = isWindows ? prefix : path18.join(prefix, "bin");
57035
+ const prefix = execSync2("npm prefix -g", { timeout: 4e3, encoding: "utf-8" }).trim();
57036
+ npmBinDir = isWindows ? prefix : path19.join(prefix, "bin");
55864
57037
  } catch {
55865
57038
  }
55866
57039
  let binaryOnPath = false;
55867
57040
  let resolvedPath = null;
55868
57041
  try {
55869
- const which = execSync(
57042
+ const which = execSync2(
55870
57043
  isWindows ? `where ${binaryName}` : `which ${binaryName}`,
55871
57044
  { timeout: 3e3, encoding: "utf-8" }
55872
57045
  ).trim().split(/\r?\n/)[0];
@@ -55890,7 +57063,7 @@ function checkPathDiscoverability() {
55890
57063
  };
55891
57064
  }
55892
57065
  const currentPath = process.env.PATH ?? "";
55893
- const missingSegment = npmBinDir && !currentPath.split(path18.delimiter).includes(npmBinDir) ? npmBinDir : null;
57066
+ const missingSegment = npmBinDir && !currentPath.split(path19.delimiter).includes(npmBinDir) ? npmBinDir : null;
55894
57067
  const currentShell = detectShellFlavor();
55895
57068
  const shellFix = buildPathFix(currentShell, missingSegment, npmBinDir);
55896
57069
  return {
@@ -55991,9 +57164,9 @@ function checkMqtt() {
55991
57164
  };
55992
57165
  }
55993
57166
  const file2 = configFilePath();
55994
- if (fs22.existsSync(file2)) {
57167
+ if (fs24.existsSync(file2)) {
55995
57168
  try {
55996
- const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
57169
+ const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
55997
57170
  if (cfg.token && cfg.secret) {
55998
57171
  return {
55999
57172
  name: "mqtt",
@@ -56020,9 +57193,9 @@ async function checkMqttProbe() {
56020
57193
  creds = { token, secret };
56021
57194
  } else {
56022
57195
  const file2 = configFilePath();
56023
- if (fs22.existsSync(file2)) {
57196
+ if (fs24.existsSync(file2)) {
56024
57197
  try {
56025
- const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
57198
+ const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
56026
57199
  if (cfg.token && cfg.secret) {
56027
57200
  creds = { token: cfg.token, secret: cfg.secret };
56028
57201
  }
@@ -56136,6 +57309,112 @@ function checkNotifyConnectivity() {
56136
57309
  detail: { webhookCount: webhookUrls.length, message: `${webhookUrls.length} webhook URL(s) configured (live probe not run \u2014 use --probe to test connectivity)` }
56137
57310
  };
56138
57311
  }
57312
+ async function checkLocalLlmReachable() {
57313
+ const policyPath = resolvePolicyPath();
57314
+ let loaded;
57315
+ try {
57316
+ loaded = loadPolicyFile(policyPath);
57317
+ } catch {
57318
+ return { name: "local-llm-reachable", status: "ok", detail: { present: false, message: "no policy or policy unreadable \u2014 check skipped" } };
57319
+ }
57320
+ const policy = loaded.data;
57321
+ const automation = policy?.automation;
57322
+ const ruleArr = Array.isArray(automation?.rules) ? automation.rules : [];
57323
+ const usesLocal = ruleArr.some((rule) => {
57324
+ const conds = Array.isArray(rule.conditions) ? rule.conditions : [];
57325
+ return conds.some((c) => {
57326
+ const llm = c.llm;
57327
+ return llm && llm.provider === "local";
57328
+ });
57329
+ });
57330
+ if (!usesLocal) {
57331
+ return { name: "local-llm-reachable", status: "ok", detail: { applicable: false, message: "no policy reference to provider:local \u2014 check skipped" } };
57332
+ }
57333
+ const baseUrl = (process.env.SWITCHBOT_LOCAL_LLM_URL ?? "http://localhost:11434/v1").replace(/\/v1\/?$/, "").replace(/\/+$/, "");
57334
+ const start = Date.now();
57335
+ try {
57336
+ const reachable = await probeLocalLlmEndpoint(baseUrl);
57337
+ const latencyMs = Date.now() - start;
57338
+ if (!reachable) {
57339
+ return {
57340
+ name: "local-llm-reachable",
57341
+ status: "fail",
57342
+ detail: { baseUrl, latencyMs, message: "endpoint did not respond \u2014 start your local LLM server (e.g. `ollama serve`)" }
57343
+ };
57344
+ }
57345
+ return { name: "local-llm-reachable", status: "ok", detail: { baseUrl, latencyMs } };
57346
+ } catch (err) {
57347
+ return {
57348
+ name: "local-llm-reachable",
57349
+ status: "fail",
57350
+ detail: { baseUrl, message: err instanceof Error ? err.message : String(err) }
57351
+ };
57352
+ }
57353
+ }
57354
+ async function probeLocalLlmEndpoint(baseUrl) {
57355
+ const httpMod = await import("node:http");
57356
+ const httpsMod = await import("node:https");
57357
+ return new Promise((resolve2) => {
57358
+ let url2;
57359
+ try {
57360
+ url2 = new URL(baseUrl);
57361
+ } catch {
57362
+ resolve2(false);
57363
+ return;
57364
+ }
57365
+ const isHttps = url2.protocol === "https:";
57366
+ const lib = isHttps ? httpsMod.default : httpMod.default;
57367
+ const req = lib.request(
57368
+ {
57369
+ hostname: url2.hostname,
57370
+ port: url2.port || (isHttps ? 443 : 80),
57371
+ path: url2.pathname || "/",
57372
+ method: "GET",
57373
+ timeout: 3e3
57374
+ },
57375
+ (res) => {
57376
+ res.on("data", () => {
57377
+ });
57378
+ res.on("end", () => resolve2(true));
57379
+ res.resume();
57380
+ }
57381
+ );
57382
+ req.on("error", () => resolve2(false));
57383
+ req.on("timeout", () => {
57384
+ req.destroy();
57385
+ resolve2(false);
57386
+ });
57387
+ req.end();
57388
+ });
57389
+ }
57390
+ async function checkDaemonIpc() {
57391
+ const daemonPid = readPidFile(DAEMON_PID_FILE);
57392
+ if (!daemonPid || !isPidAlive(daemonPid)) {
57393
+ return { name: "daemon-ipc", status: "ok", detail: { applicable: false, message: "daemon not running \u2014 check skipped" } };
57394
+ }
57395
+ try {
57396
+ const { IpcDaemonClient: IpcDaemonClient2 } = await Promise.resolve().then(() => (init_client3(), client_exports2));
57397
+ const client = new IpcDaemonClient2({ timeoutMs: 1500, connectTimeoutMs: 500 });
57398
+ const start = Date.now();
57399
+ const result = await client.ping();
57400
+ const latencyMs = Date.now() - start;
57401
+ return {
57402
+ name: "daemon-ipc",
57403
+ status: "ok",
57404
+ detail: {
57405
+ socketPath: client.getSocketPath(),
57406
+ latencyMs,
57407
+ ipcStatus: result.status
57408
+ }
57409
+ };
57410
+ } catch (err) {
57411
+ return {
57412
+ name: "daemon-ipc",
57413
+ status: "fail",
57414
+ detail: { message: err instanceof Error ? err.message : String(err) }
57415
+ };
57416
+ }
57417
+ }
56139
57418
  var CHECK_REGISTRY = [
56140
57419
  { name: "node", description: "Node.js version compatibility", run: () => checkNodeVersion() },
56141
57420
  { name: "path", description: "switchbot binary reachable on PATH", run: () => checkPathDiscoverability() },
@@ -56143,6 +57422,7 @@ var CHECK_REGISTRY = [
56143
57422
  { name: "keychain", description: "OS keychain backend availability and usage", run: () => checkKeychain() },
56144
57423
  { name: "profiles", description: "profile definitions valid", run: () => checkProfiles() },
56145
57424
  { name: "catalog", description: "catalog loads", run: () => checkCatalog() },
57425
+ { name: "catalog-coverage", description: "all cached device types have catalog entries", run: () => checkCatalogCoverage() },
56146
57426
  { name: "catalog-schema", description: "catalog vs agent-bootstrap version aligned", run: () => checkCatalogSchema() },
56147
57427
  { name: "inventory", description: "cached inventory graph consistency (hubDeviceId references)", run: () => checkInventoryConsistency() },
56148
57428
  { name: "cache", description: "device cache state", run: () => checkCache() },
@@ -56159,7 +57439,9 @@ var CHECK_REGISTRY = [
56159
57439
  { name: "audit", description: "recent command errors (last 24h)", run: () => checkAudit() },
56160
57440
  { name: "daemon", description: "daemon state file + runtime status", run: () => checkDaemon() },
56161
57441
  { name: "health", description: "health endpoint availability (daemon --healthz-port)", run: () => checkHealthEndpoint() },
56162
- { name: "notify-connectivity", description: "webhook URLs from notify actions in policy.yaml", run: () => checkNotifyConnectivity() }
57442
+ { name: "notify-connectivity", description: "webhook URLs from notify actions in policy.yaml", run: () => checkNotifyConnectivity() },
57443
+ { name: "local-llm-reachable", description: "local LLM endpoint reachable (only when policy uses provider:local)", run: () => checkLocalLlmReachable() },
57444
+ { name: "daemon-ipc", description: "daemon JSON-RPC IPC socket reachable (only when daemon is running)", run: () => checkDaemonIpc() }
56163
57445
  ];
56164
57446
  function applyFixes(checks, writeOk) {
56165
57447
  const results = [];
@@ -56204,7 +57486,7 @@ function applyFixes(checks, writeOk) {
56204
57486
  return results;
56205
57487
  }
56206
57488
  function registerDoctorCommand(program3) {
56207
- program3.command("doctor").description("Self-check the SwitchBot CLI setup: credentials, catalog, cache, quota, MQTT, daemon, health, MCP").option("--section <names>", "Comma-separated list of checks to run (see --list for names)").option("--list", "Print the registered check names and exit 0 without running any check").option("--fix", "Apply safe, reversible remediations for failing checks (e.g. clear stale cache)").option("--yes", "Required together with --fix to confirm write actions").option("--probe", "Perform live-probe variant of checks that support it (mqtt)").addHelpText("after", `
57489
+ program3.command("doctor").description("Self-check the SwitchBot CLI setup: credentials, catalog, cache, quota, MQTT, daemon, health, MCP").option("--section <names>", "Comma-separated list of checks to run (see --list for names)").option("--list", "Print the registered check names and exit 0 without running any check").option("--fix", "Apply safe, reversible remediations for failing checks (e.g. clear stale cache)").option("--yes", "Required together with --fix to confirm write actions").option("--probe", "Perform live-probe variant of checks that support it (mqtt)").option("-q, --quiet", "Only show warn/fail checks, hide passing checks").addHelpText("after", `
56208
57490
  Runs a battery of local sanity checks and exits with code 0 only when every
56209
57491
  check is 'ok'. 'warn' \u2192 exit 0 (informational); 'fail' \u2192 exit 1.
56210
57492
 
@@ -56281,7 +57563,9 @@ Examples:
56281
57563
  if (fixes !== void 0) payload.fixes = fixes;
56282
57564
  printJson(payload);
56283
57565
  } else {
57566
+ const quiet = Boolean(opts.quiet);
56284
57567
  for (const c of checks) {
57568
+ if (quiet && c.status === "ok") continue;
56285
57569
  const icon2 = c.status === "ok" ? "\u2713" : c.status === "warn" ? "!" : "\u2717";
56286
57570
  const detailStr = typeof c.detail === "string" ? c.detail : typeof c.detail.message === "string" ? c.detail.message : JSON.stringify(c.detail);
56287
57571
  console.log(`${icon2} ${c.name.padEnd(12)} ${detailStr}`);
@@ -56495,9 +57779,10 @@ init_arg_parsers();
56495
57779
  init_output();
56496
57780
  init_audit();
56497
57781
  init_devices();
56498
- import path19 from "node:path";
56499
- import os18 from "node:os";
56500
- var DEFAULT_AUDIT = path19.join(os18.homedir(), ".switchbot", "audit.log");
57782
+ init_history_query();
57783
+ import path20 from "node:path";
57784
+ import os19 from "node:os";
57785
+ var DEFAULT_AUDIT = path20.join(os19.homedir(), ".switchbot", "audit.log");
56501
57786
  function registerHistoryCommand(program3) {
56502
57787
  const history = program3.command("history").description("View and replay SwitchBot commands recorded via --audit-log").addHelpText("after", `
56503
57788
  Every 'devices command' run with --audit-log is appended as JSONL to the
@@ -57114,8 +58399,8 @@ Examples:
57114
58399
  if (opts.dryRun) {
57115
58400
  if (isJsonMode()) printJson(finalPayload);
57116
58401
  else {
57117
- console.log(`\u2022 dry-run: would upgrade ${policyPath} (v${plan.fromVersion} \u2192 v${plan.toVersion})`);
57118
- console.log(` bytes: ${bytesWritten}`);
58402
+ console.error(`\u2022 dry-run: would upgrade ${policyPath} (v${plan.fromVersion} \u2192 v${plan.toVersion})`);
58403
+ console.error(` bytes: ${bytesWritten}`);
57119
58404
  console.log(` precheck: valid against v${target}`);
57120
58405
  }
57121
58406
  return;
@@ -57232,7 +58517,7 @@ Reads rule YAML from stdin. Combine with 'rules suggest' for a full pipeline:
57232
58517
  if (result.written) {
57233
58518
  console.log(`\u2713 rule "${result.ruleName}" added to ${policyPath}`);
57234
58519
  } else {
57235
- console.log(`\u2022 dry-run: rule "${result.ruleName}" not written`);
58520
+ console.error(`\u2022 dry-run: rule "${result.ruleName}" not written`);
57236
58521
  }
57237
58522
  }
57238
58523
  } catch (err) {
@@ -57355,9 +58640,9 @@ init_load();
57355
58640
  init_validate();
57356
58641
  init_types();
57357
58642
  init_engine();
57358
- import fs24 from "node:fs";
57359
- import os20 from "node:os";
57360
- import path21 from "node:path";
58643
+ import fs27 from "node:fs";
58644
+ import os21 from "node:os";
58645
+ import path23 from "node:path";
57361
58646
 
57362
58647
  // src/rules/conflict-analyzer.ts
57363
58648
  init_cjs_shim();
@@ -57520,9 +58805,9 @@ init_client2();
57520
58805
 
57521
58806
  // src/rules/webhook-token.ts
57522
58807
  init_cjs_shim();
57523
- import fs23 from "node:fs";
57524
- import os19 from "node:os";
57525
- import path20 from "node:path";
58808
+ import fs25 from "node:fs";
58809
+ import os20 from "node:os";
58810
+ import path21 from "node:path";
57526
58811
  import { randomBytes } from "node:crypto";
57527
58812
  var ENV_TOKEN = "SWITCHBOT_WEBHOOK_TOKEN";
57528
58813
  var DEFAULT_FILE = ".switchbot/webhook-token";
@@ -57530,7 +58815,7 @@ var WebhookTokenStore = class {
57530
58815
  filePath;
57531
58816
  envLookup;
57532
58817
  constructor(opts = {}) {
57533
- this.filePath = opts.filePath ?? path20.join(os19.homedir(), DEFAULT_FILE);
58818
+ this.filePath = opts.filePath ?? path21.join(os20.homedir(), DEFAULT_FILE);
57534
58819
  this.envLookup = opts.envLookup ?? (() => process.env[ENV_TOKEN]);
57535
58820
  }
57536
58821
  /**
@@ -57554,7 +58839,7 @@ var WebhookTokenStore = class {
57554
58839
  */
57555
58840
  readFromDisk() {
57556
58841
  try {
57557
- const raw = fs23.readFileSync(this.filePath, "utf-8").trim();
58842
+ const raw = fs25.readFileSync(this.filePath, "utf-8").trim();
57558
58843
  return raw.length > 0 ? raw : null;
57559
58844
  } catch (err) {
57560
58845
  if (err.code === "ENOENT") return null;
@@ -57571,12 +58856,12 @@ var WebhookTokenStore = class {
57571
58856
  return this.filePath;
57572
58857
  }
57573
58858
  writeToDisk(token) {
57574
- const dir = path20.dirname(this.filePath);
57575
- fs23.mkdirSync(dir, { recursive: true });
57576
- fs23.writeFileSync(this.filePath, `${token}
58859
+ const dir = path21.dirname(this.filePath);
58860
+ fs25.mkdirSync(dir, { recursive: true });
58861
+ fs25.writeFileSync(this.filePath, `${token}
57577
58862
  `, { mode: 384 });
57578
58863
  try {
57579
- fs23.chmodSync(this.filePath, 384);
58864
+ fs25.chmodSync(this.filePath, 384);
57580
58865
  } catch {
57581
58866
  }
57582
58867
  }
@@ -57661,18 +58946,19 @@ function aggregateRuleAudits(entries) {
57661
58946
  }
57662
58947
 
57663
58948
  // src/commands/rules.ts
57664
- var DEFAULT_AUDIT_PATH2 = path21.join(os20.homedir(), ".switchbot", "audit.log");
58949
+ init_history_query();
58950
+ var DEFAULT_AUDIT_PATH2 = path23.join(os21.homedir(), ".switchbot", "audit.log");
57665
58951
  function loadAutomation(policyPathFlag) {
57666
- const path29 = resolvePolicyPath({ flag: policyPathFlag });
58952
+ const path31 = resolvePolicyPath({ flag: policyPathFlag });
57667
58953
  let loaded;
57668
58954
  try {
57669
- loaded = loadPolicyFile(path29);
58955
+ loaded = loadPolicyFile(path31);
57670
58956
  } catch (err) {
57671
58957
  if (err instanceof PolicyFileNotFoundError) {
57672
58958
  exitWithError({
57673
58959
  code: 2,
57674
58960
  kind: "usage",
57675
- message: `policy file not found: ${path29}`,
58961
+ message: `policy file not found: ${path31}`,
57676
58962
  extra: { subKind: "file-not-found" }
57677
58963
  });
57678
58964
  }
@@ -57680,7 +58966,7 @@ function loadAutomation(policyPathFlag) {
57680
58966
  exitWithError({
57681
58967
  code: 3,
57682
58968
  kind: "runtime",
57683
- message: `YAML parse error in ${path29}: ${err.message}`,
58969
+ message: `YAML parse error in ${path31}: ${err.message}`,
57684
58970
  extra: { subKind: "yaml-parse", errors: err.yamlErrors }
57685
58971
  });
57686
58972
  }
@@ -57692,7 +58978,7 @@ function loadAutomation(policyPathFlag) {
57692
58978
  code: 4,
57693
58979
  kind: "runtime",
57694
58980
  message: "policy file failed schema validation. Run `switchbot policy validate` for details.",
57695
- extra: { subKind: "invalid-policy", path: path29 }
58981
+ extra: { subKind: "invalid-policy", path: path31 }
57696
58982
  });
57697
58983
  }
57698
58984
  const data = loaded.data ?? {};
@@ -57706,7 +58992,7 @@ function loadAutomation(policyPathFlag) {
57706
58992
  }
57707
58993
  const rawQH = data.quiet_hours;
57708
58994
  const quietHours = rawQH && typeof rawQH.start === "string" && typeof rawQH.end === "string" ? { start: rawQH.start, end: rawQH.end } : null;
57709
- return { path: path29, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
58995
+ return { path: path31, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
57710
58996
  }
57711
58997
  function describeTrigger(rule) {
57712
58998
  const t = rule.when;
@@ -57841,6 +59127,8 @@ function registerRun(rules) {
57841
59127
  let stopping = false;
57842
59128
  const pidPaths = getDefaultPidFilePaths();
57843
59129
  writePidFile(pidPaths.pidFile);
59130
+ const ipcStartedAt = /* @__PURE__ */ new Date();
59131
+ let ipcServerHandle = null;
57844
59132
  const cleanup = () => {
57845
59133
  clearPidFile(pidPaths.pidFile);
57846
59134
  consumeReloadSentinel(pidPaths.reloadFile);
@@ -57849,6 +59137,12 @@ function registerRun(rules) {
57849
59137
  if (stopping) return;
57850
59138
  stopping = true;
57851
59139
  try {
59140
+ if (ipcServerHandle) {
59141
+ try {
59142
+ await ipcServerHandle.close();
59143
+ } catch {
59144
+ }
59145
+ }
57852
59146
  await engine.stop();
57853
59147
  await client.disconnect();
57854
59148
  } finally {
@@ -57907,6 +59201,34 @@ function registerRun(rules) {
57907
59201
  }
57908
59202
  }, 2e3);
57909
59203
  reloadPoll.unref();
59204
+ try {
59205
+ const { startIpcServer: startIpcServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
59206
+ ipcServerHandle = await startIpcServer2({
59207
+ handlers: {
59208
+ "daemon.status": () => ({
59209
+ status: "running",
59210
+ pid: process.pid,
59211
+ startedAt: ipcStartedAt.toISOString(),
59212
+ rulesActive: engine.getStats().rulesActive,
59213
+ globalDryRun: opts.dryRun === true
59214
+ }),
59215
+ "daemon.ping": () => ({ ok: true, t: (/* @__PURE__ */ new Date()).toISOString() }),
59216
+ "daemon.reload": async () => {
59217
+ await doReload("ipc");
59218
+ return { ok: true, rulesActive: engine.getStats().rulesActive };
59219
+ }
59220
+ },
59221
+ onClientError: () => {
59222
+ }
59223
+ });
59224
+ if (!isJsonMode()) {
59225
+ console.error(`IPC: listening on ${ipcServerHandle.socketPath}`);
59226
+ }
59227
+ } catch (err) {
59228
+ if (!isJsonMode()) {
59229
+ console.error(`IPC: failed to start (${err instanceof Error ? err.message : String(err)}) \u2014 daemon will run without IPC`);
59230
+ }
59231
+ }
57910
59232
  if (!isJsonMode()) {
57911
59233
  console.error(
57912
59234
  `Rules engine started \u2014 ${engine.getStats().rulesActive} active rule(s), ${opts.dryRun ? "global dry-run" : "live"}.`
@@ -57969,7 +59291,7 @@ function registerTail(rules) {
57969
59291
  rules.command("tail").description("Stream rule-* entries from the audit log.").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--since <duration>", "Only entries newer than this window (e.g. 1h, 30m, 7d).").option("--rule <name>", "Filter to a single rule name.").option("-f, --follow", "Keep the process open and stream new lines as they arrive.").action(async (opts) => {
57970
59292
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
57971
59293
  const sinceMs = resolveSinceMs(opts.since);
57972
- const existing = fs24.existsSync(file2) ? readAudit(file2) : [];
59294
+ const existing = fs27.existsSync(file2) ? readAudit(file2) : [];
57973
59295
  const filtered = filterRuleAudits(existing, { sinceMs, ruleName: opts.rule });
57974
59296
  if (isJsonMode()) {
57975
59297
  for (const e of filtered) console.log(JSON.stringify(e));
@@ -57981,7 +59303,7 @@ function registerTail(rules) {
57981
59303
  for (const e of filtered) console.log(formatAuditLine(e));
57982
59304
  }
57983
59305
  if (!opts.follow) return;
57984
- let offset = fs24.existsSync(file2) ? fs24.statSync(file2).size : 0;
59306
+ let offset = fs27.existsSync(file2) ? fs27.statSync(file2).size : 0;
57985
59307
  let buffer = "";
57986
59308
  const emit = (line) => {
57987
59309
  const trimmed = line.trim();
@@ -57998,21 +59320,21 @@ function registerTail(rules) {
57998
59320
  else console.log(formatAuditLine(entry));
57999
59321
  };
58000
59322
  const poll = setInterval(() => {
58001
- if (!fs24.existsSync(file2)) return;
58002
- const size = fs24.statSync(file2).size;
59323
+ if (!fs27.existsSync(file2)) return;
59324
+ const size = fs27.statSync(file2).size;
58003
59325
  if (size < offset) {
58004
59326
  offset = 0;
58005
59327
  buffer = "";
58006
59328
  }
58007
59329
  if (size === offset) return;
58008
- const fd = fs24.openSync(file2, "r");
59330
+ const fd = fs27.openSync(file2, "r");
58009
59331
  try {
58010
59332
  const chunk = Buffer.alloc(size - offset);
58011
- fs24.readSync(fd, chunk, 0, chunk.length, offset);
59333
+ fs27.readSync(fd, chunk, 0, chunk.length, offset);
58012
59334
  offset = size;
58013
59335
  buffer += chunk.toString("utf-8");
58014
59336
  } finally {
58015
- fs24.closeSync(fd);
59337
+ fs27.closeSync(fd);
58016
59338
  }
58017
59339
  let newline = buffer.indexOf("\n");
58018
59340
  while (newline !== -1) {
@@ -58052,7 +59374,7 @@ function formatReplayTable(report) {
58052
59374
  function registerReplay(rules) {
58053
59375
  rules.command("replay").description("Aggregate rule-* audit entries per rule (fire/throttle/error counts).").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--since <duration>", "Only entries newer than this window (e.g. 1h, 7d).").option("--rule <name>", "Filter to a single rule name.").action((opts) => {
58054
59376
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
58055
- const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
59377
+ const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
58056
59378
  const sinceMs = resolveSinceMs(opts.since);
58057
59379
  const filtered = filterRuleAudits(entries, {
58058
59380
  sinceMs,
@@ -58172,7 +59494,7 @@ function registerSuggest(rules) {
58172
59494
  for (const w2 of warnings) process.stderr.write(`warning: ${w2}
58173
59495
  `);
58174
59496
  if (opts.out) {
58175
- fs24.writeFileSync(opts.out, ruleYaml, "utf8");
59497
+ fs27.writeFileSync(opts.out, ruleYaml, "utf8");
58176
59498
  if (!isJsonMode()) console.log(`\u2713 rule YAML written to ${opts.out}`);
58177
59499
  } else if (isJsonMode()) {
58178
59500
  printJson({ rule, rule_yaml: ruleYaml, warnings });
@@ -58247,7 +59569,7 @@ overall: ${overall ? "ok" : "issues found"}`);
58247
59569
  function registerSummary(rules) {
58248
59570
  rules.command("summary").description("Aggregate rule-* audit entries per rule over a time window (fires, throttled, errors).").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--since <duration>", "Only entries newer than this window (default: 24h). E.g. 1h, 7d.").option("--rule <name>", "Filter to a single rule name.").action((opts) => {
58249
59571
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
58250
- const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
59572
+ const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
58251
59573
  const sinceMs = resolveSinceMs(opts.since ?? "24h");
58252
59574
  const filtered = filterRuleAudits(entries, { sinceMs, ruleName: opts.rule });
58253
59575
  const report = aggregateRuleAudits(filtered);
@@ -58278,7 +59600,7 @@ function registerLastFired(rules) {
58278
59600
  rules.command("last-fired").description("Show the N most recently fired rule-fire entries from the audit log.").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--rule <name>", "Filter to a single rule name.").option("-n <count>", "Number of entries to show (default: 10).", (v2) => Number.parseInt(v2, 10)).action((opts) => {
58279
59601
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
58280
59602
  const n = opts.n ?? 10;
58281
- const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
59603
+ const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
58282
59604
  const fires = filterRuleAudits(entries, {
58283
59605
  ruleName: opts.rule,
58284
59606
  kinds: ["rule-fire", "rule-fire-dry"]
@@ -58320,7 +59642,7 @@ function registerExplain(rules) {
58320
59642
  return;
58321
59643
  }
58322
59644
  const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
58323
- const entries = fs24.existsSync(auditFile) ? readAudit(auditFile) : [];
59645
+ const entries = fs27.existsSync(auditFile) ? readAudit(auditFile) : [];
58324
59646
  const fires = filterRuleAudits(entries, { ruleName: name, kinds: ["rule-fire", "rule-fire-dry"] });
58325
59647
  const lastFired = fires.length > 0 ? fires[fires.length - 1].t : null;
58326
59648
  const detail = {
@@ -58361,7 +59683,7 @@ function registerTraceExplain(rules) {
58361
59683
  rules.command("trace-explain [fireId]").description("Show why a rule evaluation fired or was blocked (reads rule-evaluate trace records).").option("--rule <name>", "Filter to a specific rule name.").option("--last", "Show the most recent evaluation for the rule (requires --rule).").option("--since <duration>", "Show evaluations in this window (e.g. 1h, 7d).").option("--all", "Include evaluations that fired (default: show all evaluations).").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2}).`).action(
58362
59684
  (fireIdArg, opts) => {
58363
59685
  const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
58364
- if (!fs24.existsSync(auditFile)) {
59686
+ if (!fs27.existsSync(auditFile)) {
58365
59687
  exitWithError({ code: 1, kind: "usage", message: `Audit log not found: ${auditFile}. Make sure trace recording is enabled (automation.audit.evaluate_trace: sampled or full).` });
58366
59688
  return;
58367
59689
  }
@@ -58396,14 +59718,14 @@ function registerSimulate(rules) {
58396
59718
  async (ruleOrPolicy, opts) => {
58397
59719
  let rule;
58398
59720
  const auditLog = opts.auditLog ?? DEFAULT_AUDIT_PATH2;
58399
- if (!fs24.existsSync(ruleOrPolicy)) {
59721
+ if (!fs27.existsSync(ruleOrPolicy)) {
58400
59722
  exitWithError({ code: 2, kind: "usage", message: `File not found: ${ruleOrPolicy}` });
58401
59723
  return;
58402
59724
  }
58403
59725
  let parsed;
58404
59726
  try {
58405
59727
  const { parse: yamlParse5 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
58406
- parsed = yamlParse5(fs24.readFileSync(ruleOrPolicy, "utf-8"));
59728
+ parsed = yamlParse5(fs27.readFileSync(ruleOrPolicy, "utf-8"));
58407
59729
  } catch {
58408
59730
  exitWithError({ code: 2, kind: "usage", message: `Could not parse YAML file: ${ruleOrPolicy}` });
58409
59731
  return;
@@ -58434,7 +59756,7 @@ function registerSimulate(rules) {
58434
59756
  liveLlm: opts.liveLlm ?? false
58435
59757
  });
58436
59758
  if (opts.reportOut) {
58437
- fs24.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
59759
+ fs27.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
58438
59760
  console.log(`Report written to ${opts.reportOut}`);
58439
59761
  }
58440
59762
  if (isJsonMode()) {
@@ -58525,10 +59847,10 @@ init_output();
58525
59847
  init_arg_parsers();
58526
59848
  init_request_context();
58527
59849
  init_keychain();
58528
- import fs25 from "node:fs";
58529
- import path22 from "node:path";
58530
- import os21 from "node:os";
58531
- import readline5 from "node:readline";
59850
+ import fs28 from "node:fs";
59851
+ import path24 from "node:path";
59852
+ import os22 from "node:os";
59853
+ import readline6 from "node:readline";
58532
59854
  function activeProfile() {
58533
59855
  return getActiveProfile() ?? "default";
58534
59856
  }
@@ -58540,7 +59862,7 @@ function maskValue2(value) {
58540
59862
  return `${head}${"*".repeat(Math.max(4, value.length - 4))}${tail}`;
58541
59863
  }
58542
59864
  async function promptSecret2(question) {
58543
- const rl = readline5.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
59865
+ const rl = readline6.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
58544
59866
  return new Promise((resolve2) => {
58545
59867
  process.stderr.write(question);
58546
59868
  const stdin = process.stdin;
@@ -58573,7 +59895,7 @@ async function promptSecret2(question) {
58573
59895
  });
58574
59896
  }
58575
59897
  function readStdinFile(filePath) {
58576
- if (!fs25.existsSync(filePath)) {
59898
+ if (!fs28.existsSync(filePath)) {
58577
59899
  exitWithError({
58578
59900
  code: 2,
58579
59901
  kind: "usage",
@@ -58582,7 +59904,7 @@ function readStdinFile(filePath) {
58582
59904
  }
58583
59905
  let parsed;
58584
59906
  try {
58585
- parsed = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
59907
+ parsed = JSON.parse(fs28.readFileSync(filePath, "utf-8"));
58586
59908
  } catch (err) {
58587
59909
  exitWithError({
58588
59910
  code: 2,
@@ -58612,10 +59934,10 @@ function cleanupMigratedSourceFile(sourceFile, parsed) {
58612
59934
  delete next.token;
58613
59935
  delete next.secret;
58614
59936
  if (Object.keys(next).length === 0) {
58615
- fs25.unlinkSync(sourceFile);
59937
+ fs28.unlinkSync(sourceFile);
58616
59938
  return "deleted";
58617
59939
  }
58618
- fs25.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
59940
+ fs28.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
58619
59941
  return "scrubbed";
58620
59942
  }
58621
59943
  function registerAuthCommand(program3) {
@@ -58746,8 +60068,8 @@ function registerAuthCommand(program3) {
58746
60068
  message: `backend "${store.name}" is not writable on this machine`
58747
60069
  });
58748
60070
  }
58749
- const sourceFile = profile === "default" ? path22.join(os21.homedir(), ".switchbot", "config.json") : path22.join(os21.homedir(), ".switchbot", "profiles", `${profile}.json`);
58750
- if (!fs25.existsSync(sourceFile)) {
60071
+ const sourceFile = profile === "default" ? path24.join(os22.homedir(), ".switchbot", "config.json") : path24.join(os22.homedir(), ".switchbot", "profiles", `${profile}.json`);
60072
+ if (!fs28.existsSync(sourceFile)) {
58751
60073
  exitWithError({
58752
60074
  code: 2,
58753
60075
  kind: "usage",
@@ -58757,7 +60079,7 @@ function registerAuthCommand(program3) {
58757
60079
  }
58758
60080
  let parsed;
58759
60081
  try {
58760
- const raw = JSON.parse(fs25.readFileSync(sourceFile, "utf-8"));
60082
+ const raw = JSON.parse(fs28.readFileSync(sourceFile, "utf-8"));
58761
60083
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
58762
60084
  throw new Error("expected a JSON object");
58763
60085
  }
@@ -58819,8 +60141,8 @@ function registerAuthCommand(program3) {
58819
60141
  init_cjs_shim();
58820
60142
  init_esm();
58821
60143
  init_load();
58822
- import fs28 from "node:fs";
58823
- import path25 from "node:path";
60144
+ import fs31 from "node:fs";
60145
+ import path27 from "node:path";
58824
60146
 
58825
60147
  // src/install/steps.ts
58826
60148
  init_cjs_shim();
@@ -58872,9 +60194,9 @@ init_cjs_shim();
58872
60194
  init_load();
58873
60195
  init_validate();
58874
60196
  init_keychain();
58875
- import fs26 from "node:fs";
58876
- import path23 from "node:path";
58877
- import os22 from "node:os";
60197
+ import fs29 from "node:fs";
60198
+ import path25 from "node:path";
60199
+ import os23 from "node:os";
58878
60200
  function parseMajor(version2) {
58879
60201
  const m2 = /^v?(\d+)\./.exec(version2);
58880
60202
  if (!m2) return null;
@@ -58964,10 +60286,10 @@ async function checkKeychain2() {
58964
60286
  }
58965
60287
  }
58966
60288
  function checkHomeDirWritable() {
58967
- const home = os22.homedir();
58968
- const switchbotDir = path23.join(home, ".switchbot");
60289
+ const home = os23.homedir();
60290
+ const switchbotDir = path25.join(home, ".switchbot");
58969
60291
  try {
58970
- const homeStat = fs26.statSync(home);
60292
+ const homeStat = fs29.statSync(home);
58971
60293
  if (!homeStat.isDirectory()) {
58972
60294
  return {
58973
60295
  name: "home",
@@ -58976,8 +60298,8 @@ function checkHomeDirWritable() {
58976
60298
  hint: "check your HOME/USERPROFILE environment configuration"
58977
60299
  };
58978
60300
  }
58979
- if (fs26.existsSync(switchbotDir)) {
58980
- const sbStat = fs26.statSync(switchbotDir);
60301
+ if (fs29.existsSync(switchbotDir)) {
60302
+ const sbStat = fs29.statSync(switchbotDir);
58981
60303
  if (!sbStat.isDirectory()) {
58982
60304
  return {
58983
60305
  name: "home",
@@ -58986,10 +60308,10 @@ function checkHomeDirWritable() {
58986
60308
  hint: "move the file aside and re-run install"
58987
60309
  };
58988
60310
  }
58989
- fs26.accessSync(switchbotDir, fs26.constants.W_OK);
60311
+ fs29.accessSync(switchbotDir, fs29.constants.W_OK);
58990
60312
  return { name: "home", status: "ok", message: `writable: ${switchbotDir}` };
58991
60313
  }
58992
- fs26.accessSync(home, fs26.constants.W_OK);
60314
+ fs29.accessSync(home, fs29.constants.W_OK);
58993
60315
  return { name: "home", status: "ok", message: `writable: ${home}` };
58994
60316
  } catch (err) {
58995
60317
  return {
@@ -59003,8 +60325,8 @@ function checkHomeDirWritable() {
59003
60325
  function nearestExistingPath(target) {
59004
60326
  let cur = target;
59005
60327
  while (true) {
59006
- if (fs26.existsSync(cur)) return cur;
59007
- const parent = path23.dirname(cur);
60328
+ if (fs29.existsSync(cur)) return cur;
60329
+ const parent = path25.dirname(cur);
59008
60330
  if (parent === cur) return null;
59009
60331
  cur = parent;
59010
60332
  }
@@ -59012,8 +60334,8 @@ function nearestExistingPath(target) {
59012
60334
  function checkAgentSkillDirWritable(opts) {
59013
60335
  const shouldCheck = opts.agent === "claude-code" && (opts.expectSkillLink ?? true);
59014
60336
  if (!shouldCheck) return null;
59015
- const home = os22.homedir();
59016
- const target = path23.join(home, ".claude", "skills");
60337
+ const home = os23.homedir();
60338
+ const target = path25.join(home, ".claude", "skills");
59017
60339
  try {
59018
60340
  const existing = nearestExistingPath(target);
59019
60341
  if (!existing) {
@@ -59024,7 +60346,7 @@ function checkAgentSkillDirWritable(opts) {
59024
60346
  hint: "check your home directory path and permissions"
59025
60347
  };
59026
60348
  }
59027
- const stat = fs26.statSync(existing);
60349
+ const stat = fs29.statSync(existing);
59028
60350
  if (!stat.isDirectory()) {
59029
60351
  return {
59030
60352
  name: "agent-skills-dir",
@@ -59033,7 +60355,7 @@ function checkAgentSkillDirWritable(opts) {
59033
60355
  hint: "move the blocking file aside and re-run install"
59034
60356
  };
59035
60357
  }
59036
- fs26.accessSync(existing, fs26.constants.W_OK);
60358
+ fs29.accessSync(existing, fs29.constants.W_OK);
59037
60359
  return { name: "agent-skills-dir", status: "ok", message: `writable: ${target}` };
59038
60360
  } catch (err) {
59039
60361
  return {
@@ -59058,9 +60380,9 @@ async function runPreflight(options = {}) {
59058
60380
 
59059
60381
  // src/install/default-steps.ts
59060
60382
  init_cjs_shim();
59061
- import fs27 from "node:fs";
59062
- import path24 from "node:path";
59063
- import os23 from "node:os";
60383
+ import fs30 from "node:fs";
60384
+ import path26 from "node:path";
60385
+ import os24 from "node:os";
59064
60386
  import { spawnSync } from "node:child_process";
59065
60387
  init_keychain();
59066
60388
  function stepPromptCredentials() {
@@ -59135,15 +60457,15 @@ function stepScaffoldPolicy() {
59135
60457
  const r = ctx.policyScaffoldResult;
59136
60458
  if (!r || r.skipped) return;
59137
60459
  try {
59138
- fs27.unlinkSync(r.policyPath);
60460
+ fs30.unlinkSync(r.policyPath);
59139
60461
  } catch {
59140
60462
  }
59141
60463
  }
59142
60464
  };
59143
60465
  }
59144
- function skillLinkPathFor(agent, home = os23.homedir()) {
60466
+ function skillLinkPathFor(agent, home = os24.homedir()) {
59145
60467
  if (agent === "claude-code") {
59146
- return path24.join(home, ".claude", "skills", "switchbot");
60468
+ return path26.join(home, ".claude", "skills", "switchbot");
59147
60469
  }
59148
60470
  return null;
59149
60471
  }
@@ -59157,11 +60479,11 @@ function stepSymlinkSkill(opts = {}) {
59157
60479
  ctx.skillRecipePrinted = true;
59158
60480
  return;
59159
60481
  }
59160
- const target = path24.resolve(ctx.skillPath);
59161
- if (!fs27.existsSync(target)) {
60482
+ const target = path26.resolve(ctx.skillPath);
60483
+ if (!fs30.existsSync(target)) {
59162
60484
  throw new Error(`--skill-path does not exist: ${target}`);
59163
60485
  }
59164
- const stat = fs27.statSync(target);
60486
+ const stat = fs30.statSync(target);
59165
60487
  if (!stat.isDirectory()) {
59166
60488
  throw new Error(`--skill-path is not a directory: ${target}`);
59167
60489
  }
@@ -59170,17 +60492,17 @@ function stepSymlinkSkill(opts = {}) {
59170
60492
  ctx.skillRecipePrinted = true;
59171
60493
  return;
59172
60494
  }
59173
- if (!opts.force && !fs27.existsSync(path24.join(target, "SKILL.md"))) {
60495
+ if (!opts.force && !fs30.existsSync(path26.join(target, "SKILL.md"))) {
59174
60496
  throw new Error(
59175
60497
  `${target} does not look like a skill (no SKILL.md at the root). Pass --force if you really mean to link this directory.`
59176
60498
  );
59177
60499
  }
59178
- if (fs27.existsSync(linkPath)) {
59179
- const st = fs27.lstatSync(linkPath);
60500
+ if (fs30.existsSync(linkPath)) {
60501
+ const st = fs30.lstatSync(linkPath);
59180
60502
  if (st.isSymbolicLink()) {
59181
60503
  let existingTarget = null;
59182
60504
  try {
59183
- existingTarget = path24.resolve(path24.dirname(linkPath), fs27.readlinkSync(linkPath));
60505
+ existingTarget = path26.resolve(path26.dirname(linkPath), fs30.readlinkSync(linkPath));
59184
60506
  } catch {
59185
60507
  existingTarget = null;
59186
60508
  }
@@ -59194,23 +60516,23 @@ function stepSymlinkSkill(opts = {}) {
59194
60516
  `${linkPath} already links to ${existingTarget ?? "(unreadable)"}; pass --force to replace it, or run \`switchbot uninstall\` first.`
59195
60517
  );
59196
60518
  }
59197
- fs27.unlinkSync(linkPath);
60519
+ fs30.unlinkSync(linkPath);
59198
60520
  } else {
59199
60521
  throw new Error(
59200
60522
  `${linkPath} exists and is not a symlink; refusing to clobber (move it aside and re-run)`
59201
60523
  );
59202
60524
  }
59203
60525
  }
59204
- fs27.mkdirSync(path24.dirname(linkPath), { recursive: true });
60526
+ fs30.mkdirSync(path26.dirname(linkPath), { recursive: true });
59205
60527
  const linkType = process.platform === "win32" ? "junction" : "dir";
59206
- fs27.symlinkSync(target, linkPath, linkType);
60528
+ fs30.symlinkSync(target, linkPath, linkType);
59207
60529
  ctx.skillLinkPath = linkPath;
59208
60530
  ctx.skillLinkCreated = true;
59209
60531
  },
59210
60532
  undo(ctx) {
59211
60533
  if (!ctx.skillLinkCreated || !ctx.skillLinkPath) return;
59212
60534
  try {
59213
- fs27.unlinkSync(ctx.skillLinkPath);
60535
+ fs30.unlinkSync(ctx.skillLinkPath);
59214
60536
  } catch {
59215
60537
  }
59216
60538
  }
@@ -59312,18 +60634,18 @@ function printDryRun(steps, ctx) {
59312
60634
  });
59313
60635
  return;
59314
60636
  }
59315
- console.log(source_default.bold("switchbot install \u2014 dry run"));
59316
- console.log(` profile: ${ctx.profile}`);
59317
- console.log(` agent: ${ctx.agent}`);
59318
- console.log(` skill: ${ctx.skillPath ?? "(none \u2014 recipe will be printed)"}`);
59319
- console.log(` policy: ${ctx.policyPath}`);
59320
- console.log("");
59321
- console.log(source_default.bold("Steps that would run (in order):"));
60637
+ console.error(source_default.bold("switchbot install \u2014 dry run"));
60638
+ console.error(` profile: ${ctx.profile}`);
60639
+ console.error(` agent: ${ctx.agent}`);
60640
+ console.error(` skill: ${ctx.skillPath ?? "(none \u2014 recipe will be printed)"}`);
60641
+ console.error(` policy: ${ctx.policyPath}`);
60642
+ console.error("");
60643
+ console.error(source_default.bold("Steps that would run (in order):"));
59322
60644
  for (const s2 of steps) {
59323
- console.log(` \u2022 ${s2.name}${s2.description ? ` \u2014 ${s2.description}` : ""}`);
60645
+ console.error(` \u2022 ${s2.name}${s2.description ? ` \u2014 ${s2.description}` : ""}`);
59324
60646
  }
59325
- console.log("");
59326
- console.log(source_default.dim("No changes made. Re-run without --dry-run to apply."));
60647
+ console.error("");
60648
+ console.error(source_default.dim("No changes made. Re-run without --dry-run to apply."));
59327
60649
  }
59328
60650
  function registerInstallCommand(program3) {
59329
60651
  program3.command("install").description("One-command bootstrap: credentials + policy + skill link (rolls back on failure)").option("--agent <name>", `target agent: ${AGENT_VALUES.join(" | ")} (default: claude-code)`).option("--skill-path <dir>", "local clone of openclaw-switchbot-skill (enables auto-link)").option("--token-file <path>", "two-line credential file (token, secret); read once and deleted on success").option("--skip <names>", 'comma-separated list of step names to skip (e.g. "scaffold-policy,symlink-skill")').option("--force", "replace an existing skill symlink pointing at a different path; allow link even without SKILL.md").option("--verify", "after a successful install, run `switchbot doctor --json` as a warn-only post-check").addHelpText(
@@ -59353,8 +60675,8 @@ Examples:
59353
60675
  const agent = parseAgent(opts.agent);
59354
60676
  const profile = getActiveProfile() ?? "default";
59355
60677
  const skip = parseSkipList(opts.skip);
59356
- const skillPath = opts.skillPath ? path25.resolve(opts.skillPath) : void 0;
59357
- const tokenFile = opts.tokenFile ? path25.resolve(opts.tokenFile) : void 0;
60678
+ const skillPath = opts.skillPath ? path27.resolve(opts.skillPath) : void 0;
60679
+ const tokenFile = opts.tokenFile ? path27.resolve(opts.tokenFile) : void 0;
59358
60680
  const force = Boolean(opts.force);
59359
60681
  const verify = Boolean(opts.verify);
59360
60682
  const globalOpts = command.parent?.opts() ?? {};
@@ -59398,7 +60720,7 @@ Examples:
59398
60720
  const report = await runInstall(steps, { context: ctx });
59399
60721
  if (report.ok && tokenFile) {
59400
60722
  try {
59401
- fs28.unlinkSync(tokenFile);
60723
+ fs31.unlinkSync(tokenFile);
59402
60724
  } catch {
59403
60725
  }
59404
60726
  }
@@ -59457,8 +60779,8 @@ Examples:
59457
60779
  init_cjs_shim();
59458
60780
  init_esm();
59459
60781
  init_load();
59460
- import fs29 from "node:fs";
59461
- import readline6 from "node:readline";
60782
+ import fs32 from "node:fs";
60783
+ import readline7 from "node:readline";
59462
60784
  init_keychain();
59463
60785
  init_output();
59464
60786
  init_request_context();
@@ -59473,7 +60795,7 @@ function parseAgent2(value) {
59473
60795
  }
59474
60796
  async function prompt(question, defaultYes) {
59475
60797
  if (!process.stdin.isTTY) return defaultYes;
59476
- const rl = readline6.createInterface({ input: process.stdin, output: process.stderr });
60798
+ const rl = readline7.createInterface({ input: process.stdin, output: process.stderr });
59477
60799
  return new Promise((resolve2) => {
59478
60800
  const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
59479
60801
  rl.question(question + suffix, (ans) => {
@@ -59522,10 +60844,10 @@ Examples:
59522
60844
  action: "remove-skill-link",
59523
60845
  detail: skillLink,
59524
60846
  run: async () => {
59525
- if (!fs29.existsSync(skillLink)) {
60847
+ if (!fs32.existsSync(skillLink)) {
59526
60848
  return { action: "remove-skill-link", status: "absent", detail: skillLink };
59527
60849
  }
59528
- const stat = fs29.lstatSync(skillLink);
60850
+ const stat = fs32.lstatSync(skillLink);
59529
60851
  if (!stat.isSymbolicLink()) {
59530
60852
  return {
59531
60853
  action: "remove-skill-link",
@@ -59536,7 +60858,7 @@ Examples:
59536
60858
  const ok = yes ? true : await prompt(`Remove skill link ${skillLink}?`, true);
59537
60859
  if (!ok) return { action: "remove-skill-link", status: "skipped", detail: skillLink };
59538
60860
  try {
59539
- fs29.unlinkSync(skillLink);
60861
+ fs32.unlinkSync(skillLink);
59540
60862
  return { action: "remove-skill-link", status: "removed", detail: skillLink };
59541
60863
  } catch (err) {
59542
60864
  return {
@@ -59591,13 +60913,13 @@ Examples:
59591
60913
  detail: "pass --remove-policy to delete policy.yaml"
59592
60914
  };
59593
60915
  }
59594
- if (!fs29.existsSync(policyPath)) {
60916
+ if (!fs32.existsSync(policyPath)) {
59595
60917
  return { action: "remove-policy", status: "absent", detail: policyPath };
59596
60918
  }
59597
60919
  const ok = yes ? true : await prompt(`Delete policy file ${policyPath}?`, false);
59598
60920
  if (!ok) return { action: "remove-policy", status: "skipped", detail: policyPath };
59599
60921
  try {
59600
- fs29.unlinkSync(policyPath);
60922
+ fs32.unlinkSync(policyPath);
59601
60923
  return { action: "remove-policy", status: "removed", detail: policyPath };
59602
60924
  } catch (err) {
59603
60925
  return {
@@ -59618,14 +60940,14 @@ Examples:
59618
60940
  plan: plan.map(({ action, detail }) => ({ action, detail }))
59619
60941
  });
59620
60942
  } else {
59621
- console.log(source_default.bold("switchbot uninstall \u2014 dry run"));
59622
- console.log(` profile: ${profile}`);
59623
- console.log(` agent: ${agent}`);
59624
- console.log("");
59625
- console.log(source_default.bold("Would run:"));
59626
- for (const p2 of plan) console.log(` \u2022 ${p2.action} \u2014 ${p2.detail}`);
59627
- console.log("");
59628
- console.log(source_default.dim("No changes made. Re-run without --dry-run (add --yes to skip prompts)."));
60943
+ console.error(source_default.bold("switchbot uninstall \u2014 dry run"));
60944
+ console.error(` profile: ${profile}`);
60945
+ console.error(` agent: ${agent}`);
60946
+ console.error("");
60947
+ console.error(source_default.bold("Would run:"));
60948
+ for (const p2 of plan) console.error(` \u2022 ${p2.action} \u2014 ${p2.detail}`);
60949
+ console.error("");
60950
+ console.error(source_default.dim("No changes made. Re-run without --dry-run (add --yes to skip prompts)."));
59629
60951
  }
59630
60952
  return;
59631
60953
  }
@@ -59660,9 +60982,9 @@ init_request_context();
59660
60982
  init_output();
59661
60983
  init_flags();
59662
60984
  import { spawn as spawn4, spawnSync as spawnSync2 } from "node:child_process";
59663
- import fs30 from "node:fs";
59664
- import os24 from "node:os";
59665
- import path26 from "node:path";
60985
+ import fs33 from "node:fs";
60986
+ import os25 from "node:os";
60987
+ import path28 from "node:path";
59666
60988
  var DEFAULT_OPENCLAW_URL = "http://localhost:18789";
59667
60989
  function resolveStatusSyncRuntime(options) {
59668
60990
  if (!tryLoadConfig()) {
@@ -59796,14 +61118,14 @@ async function probeStatusSyncStart(options = {}) {
59796
61118
  };
59797
61119
  }
59798
61120
  function resolveStatusSyncPaths(explicitStateDir) {
59799
- const stateDir = path26.resolve(
59800
- explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ?? path26.join(os24.homedir(), ".switchbot", "status-sync")
61121
+ const stateDir = path28.resolve(
61122
+ explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ?? path28.join(os25.homedir(), ".switchbot", "status-sync")
59801
61123
  );
59802
61124
  return {
59803
61125
  stateDir,
59804
- stateFile: path26.join(stateDir, "state.json"),
59805
- stdoutLog: path26.join(stateDir, "stdout.log"),
59806
- stderrLog: path26.join(stateDir, "stderr.log")
61126
+ stateFile: path28.join(stateDir, "state.json"),
61127
+ stdoutLog: path28.join(stateDir, "stdout.log"),
61128
+ stderrLog: path28.join(stateDir, "stderr.log")
59807
61129
  };
59808
61130
  }
59809
61131
  function buildStatusSyncChildArgs(options) {
@@ -59811,11 +61133,11 @@ function buildStatusSyncChildArgs(options) {
59811
61133
  if (!scriptPath) {
59812
61134
  throw new Error("Cannot determine the current CLI entrypoint path.");
59813
61135
  }
59814
- const args = [path26.resolve(scriptPath)];
61136
+ const args = [path28.resolve(scriptPath)];
59815
61137
  const configPath = getConfigPath();
59816
61138
  const profile = getActiveProfile();
59817
61139
  if (configPath) {
59818
- args.push("--config", path26.resolve(configPath));
61140
+ args.push("--config", path28.resolve(configPath));
59819
61141
  } else if (profile) {
59820
61142
  args.push("--profile", profile);
59821
61143
  }
@@ -59836,7 +61158,7 @@ function buildStatusSyncChildArgs(options) {
59836
61158
  }
59837
61159
  function safeUnlink(filePath) {
59838
61160
  try {
59839
- fs30.unlinkSync(filePath);
61161
+ fs33.unlinkSync(filePath);
59840
61162
  } catch {
59841
61163
  }
59842
61164
  }
@@ -59851,9 +61173,9 @@ function isProcessRunning(pid) {
59851
61173
  }
59852
61174
  }
59853
61175
  function readStateFile(paths) {
59854
- if (!fs30.existsSync(paths.stateFile)) return null;
61176
+ if (!fs33.existsSync(paths.stateFile)) return null;
59855
61177
  try {
59856
- const raw = JSON.parse(fs30.readFileSync(paths.stateFile, "utf-8"));
61178
+ const raw = JSON.parse(fs33.readFileSync(paths.stateFile, "utf-8"));
59857
61179
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
59858
61180
  safeUnlink(paths.stateFile);
59859
61181
  return null;
@@ -59972,14 +61294,14 @@ function startStatusSync(options = {}) {
59972
61294
  }
59973
61295
  stopStatusSync({ stateDir: paths.stateDir });
59974
61296
  }
59975
- fs30.mkdirSync(paths.stateDir, { recursive: true });
61297
+ fs33.mkdirSync(paths.stateDir, { recursive: true });
59976
61298
  const configPath = getConfigPath();
59977
61299
  const command = buildStatusSyncChildArgs(runtime);
59978
61300
  let stdoutFd = null;
59979
61301
  let stderrFd = null;
59980
61302
  try {
59981
- stdoutFd = fs30.openSync(paths.stdoutLog, "a");
59982
- stderrFd = fs30.openSync(paths.stderrLog, "a");
61303
+ stdoutFd = fs33.openSync(paths.stdoutLog, "a");
61304
+ stderrFd = fs33.openSync(paths.stderrLog, "a");
59983
61305
  const child = spawn4(process.execPath, command, {
59984
61306
  detached: true,
59985
61307
  stdio: ["ignore", stdoutFd, stderrFd],
@@ -59997,16 +61319,16 @@ function startStatusSync(options = {}) {
59997
61319
  openclawUrl: runtime.openclawUrl,
59998
61320
  openclawModel: runtime.openclawModel,
59999
61321
  topic: runtime.topic ?? null,
60000
- configPath: configPath ? path26.resolve(configPath) : null,
61322
+ configPath: configPath ? path28.resolve(configPath) : null,
60001
61323
  profile: configPath ? null : getActiveProfile() ?? null,
60002
61324
  stdoutLog: paths.stdoutLog,
60003
61325
  stderrLog: paths.stderrLog
60004
61326
  };
60005
- fs30.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
61327
+ fs33.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
60006
61328
  return toStatus(paths, state, true);
60007
61329
  } finally {
60008
- if (stdoutFd !== null) fs30.closeSync(stdoutFd);
60009
- if (stderrFd !== null) fs30.closeSync(stderrFd);
61330
+ if (stdoutFd !== null) fs33.closeSync(stdoutFd);
61331
+ if (stderrFd !== null) fs33.closeSync(stderrFd);
60010
61332
  }
60011
61333
  }
60012
61334
  async function runStatusSyncForeground(options = {}) {
@@ -60146,17 +61468,17 @@ Examples:
60146
61468
  // src/commands/health.ts
60147
61469
  init_cjs_shim();
60148
61470
  init_output();
60149
- import http5 from "node:http";
61471
+ import http7 from "node:http";
60150
61472
 
60151
61473
  // src/utils/health.ts
60152
61474
  init_cjs_shim();
60153
61475
  init_quota();
60154
61476
  init_audit();
60155
61477
  init_client();
60156
- import fs31 from "node:fs";
60157
- import os25 from "node:os";
60158
- import path27 from "node:path";
60159
- var DEFAULT_AUDIT_PATH3 = path27.join(os25.homedir(), ".switchbot", "audit.log");
61478
+ import fs34 from "node:fs";
61479
+ import os26 from "node:os";
61480
+ import path29 from "node:path";
61481
+ var DEFAULT_AUDIT_PATH3 = path29.join(os26.homedir(), ".switchbot", "audit.log");
60160
61482
  var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
60161
61483
  var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set([161, 171, 190]);
60162
61484
  function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
@@ -60178,7 +61500,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
60178
61500
  status: pct >= 90 ? "critical" : pct >= 70 ? "warn" : "ok"
60179
61501
  };
60180
61502
  let auditHealth;
60181
- if (!fs31.existsSync(auditPath)) {
61503
+ if (!fs34.existsSync(auditPath)) {
60182
61504
  auditHealth = {
60183
61505
  present: false,
60184
61506
  recentErrors: 0,
@@ -60201,7 +61523,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
60201
61523
  const breakdown = {};
60202
61524
  let expectedErrors = 0;
60203
61525
  for (const e of errorEntries) {
60204
- const code = e.statusCode !== void 0 ? String(e.statusCode) : "unknown";
61526
+ const code = e.statusCode !== void 0 ? String(e.statusCode) : "client";
60205
61527
  breakdown[code] = (breakdown[code] ?? 0) + 1;
60206
61528
  if (e.statusCode !== void 0 && EXPECTED_ERROR_CODES.has(e.statusCode)) {
60207
61529
  expectedErrors++;
@@ -60268,38 +61590,46 @@ function runHealthCheck(opts) {
60268
61590
  process.stdout.write(toPrometheusText(report));
60269
61591
  return;
60270
61592
  }
60271
- if (isJsonMode()) {
61593
+ const fmt = resolveFormat();
61594
+ if (fmt === "json" || isJsonMode()) {
60272
61595
  printJson(report);
60273
61596
  return;
60274
61597
  }
61598
+ const headers = ["Component", "Status", "Detail"];
61599
+ const rows = [
61600
+ [
61601
+ "quota",
61602
+ report.quota.status,
61603
+ `${report.quota.used}/${report.quota.limit} (${report.quota.percentUsed}% used, ${report.quota.remaining} remaining)`
61604
+ ],
61605
+ [
61606
+ "audit",
61607
+ report.audit.status,
61608
+ report.audit.present ? `${report.audit.recentErrors}/${report.audit.recentTotal} errors in 24h (${report.audit.errorRatePercent}%)` : "log not present"
61609
+ ],
61610
+ [
61611
+ "circuit",
61612
+ report.circuit.status,
61613
+ `${report.circuit.name}: ${report.circuit.state} (failures: ${report.circuit.failures})`
61614
+ ],
61615
+ [
61616
+ "process",
61617
+ "ok",
61618
+ `pid ${report.process.pid} \xB7 uptime ${report.process.uptimeSeconds}s \xB7 mem ${report.process.memoryMb}MB`
61619
+ ]
61620
+ ];
61621
+ if (fmt !== "table") {
61622
+ if (fmt === "id") {
61623
+ handleError(new UsageError("--format=id is not supported for health check (no deviceId column). Use --format json, yaml, tsv, jsonl, or markdown."));
61624
+ }
61625
+ renderRows(headers, rows, fmt);
61626
+ if (report.overall !== "ok") process.exit(1);
61627
+ return;
61628
+ }
60275
61629
  const statusEmoji = report.overall === "ok" ? "\u2713" : report.overall === "degraded" ? "\u26A0" : "\u2717";
60276
61630
  console.log(`${statusEmoji} overall: ${report.overall} (${report.generatedAt})`);
60277
61631
  console.log("");
60278
- printTable(
60279
- ["Component", "Status", "Detail"],
60280
- [
60281
- [
60282
- "quota",
60283
- report.quota.status,
60284
- `${report.quota.used}/${report.quota.limit} (${report.quota.percentUsed}% used, ${report.quota.remaining} remaining)`
60285
- ],
60286
- [
60287
- "audit",
60288
- report.audit.status,
60289
- report.audit.present ? `${report.audit.recentErrors}/${report.audit.recentTotal} errors in 24h (${report.audit.errorRatePercent}%)` : "log not present"
60290
- ],
60291
- [
60292
- "circuit",
60293
- report.circuit.status,
60294
- `${report.circuit.name}: ${report.circuit.state} (failures: ${report.circuit.failures})`
60295
- ],
60296
- [
60297
- "process",
60298
- "ok",
60299
- `pid ${report.process.pid} \xB7 uptime ${report.process.uptimeSeconds}s \xB7 mem ${report.process.memoryMb}MB`
60300
- ]
60301
- ]
60302
- );
61632
+ printTable(headers, rows);
60303
61633
  if (report.overall !== "ok") process.exit(1);
60304
61634
  }
60305
61635
  function createHealthHandler(auditLogPath) {
@@ -60340,7 +61670,7 @@ Example:
60340
61670
  const opts = cmd.optsWithGlobals();
60341
61671
  const port = parseInt(opts.port, 10);
60342
61672
  const handler = createHealthHandler(opts.auditLog);
60343
- const server = http5.createServer(handler);
61673
+ const server = http7.createServer(handler);
60344
61674
  server.on("error", (err) => {
60345
61675
  if (err.code === "EADDRINUSE") {
60346
61676
  handleError(Object.assign(new Error(`Port ${port} is already in use. Choose a different port with --port.`), { code: err.code }));
@@ -60371,13 +61701,13 @@ Example:
60371
61701
  init_cjs_shim();
60372
61702
  init_output();
60373
61703
  init_source();
60374
- import https4 from "node:https";
61704
+ import https6 from "node:https";
60375
61705
  var pkgName = "@switchbot/openapi-cli";
60376
61706
  function fetchLatestVersion(packageName, timeoutMs = 8e3) {
60377
61707
  const encoded = packageName.replace("/", "%2F");
60378
61708
  const url2 = `https://registry.npmjs.org/${encoded}/latest`;
60379
61709
  return new Promise((resolve2, reject) => {
60380
- const req = https4.get(url2, { timeout: timeoutMs }, (res) => {
61710
+ const req = https6.get(url2, { timeout: timeoutMs }, (res) => {
60381
61711
  const chunks = [];
60382
61712
  res.on("data", (c) => chunks.push(c));
60383
61713
  res.on("end", () => {
@@ -60474,8 +61804,8 @@ function registerUpgradeCheckCommand(program3) {
60474
61804
  init_cjs_shim();
60475
61805
  init_output();
60476
61806
  import { spawn as spawn5 } from "node:child_process";
60477
- import fs32 from "node:fs";
60478
- import path28 from "node:path";
61807
+ import fs35 from "node:fs";
61808
+ import path30 from "node:path";
60479
61809
  import { fileURLToPath as fileURLToPath3 } from "node:url";
60480
61810
  init_arg_parsers();
60481
61811
  init_source();
@@ -60537,7 +61867,7 @@ function persistState(partial2) {
60537
61867
  }
60538
61868
  function readLastLines(filePath, n = 20) {
60539
61869
  try {
60540
- const content = fs32.readFileSync(filePath, "utf-8");
61870
+ const content = fs35.readFileSync(filePath, "utf-8");
60541
61871
  const lines = content.split("\n");
60542
61872
  return lines.slice(Math.max(0, lines.length - n)).join("\n").trim();
60543
61873
  } catch {
@@ -60627,10 +61957,10 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60627
61957
  }
60628
61958
  }
60629
61959
  const thisFile = fileURLToPath3(import.meta.url);
60630
- const cliEntry = path28.basename(thisFile) === "index.js" ? thisFile : path28.resolve(path28.dirname(thisFile), "..", "index.js");
61960
+ const cliEntry = path30.basename(thisFile) === "index.js" ? thisFile : path30.resolve(path30.dirname(thisFile), "..", "index.js");
60631
61961
  const args = ["rules", "run"];
60632
61962
  if (opts.policy) args.push(opts.policy);
60633
- fs32.mkdirSync(path28.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
61963
+ fs35.mkdirSync(path30.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
60634
61964
  persistState({
60635
61965
  status: "starting",
60636
61966
  pid: null,
@@ -60642,14 +61972,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60642
61972
  healthzPid: null,
60643
61973
  healthzPidFile: HEALTHZ_PID_FILE
60644
61974
  });
60645
- const logFd = fs32.openSync(DAEMON_LOG_FILE, "a");
61975
+ const logFd = fs35.openSync(DAEMON_LOG_FILE, "a");
60646
61976
  const child = spawn5(process.execPath, [cliEntry, ...args], {
60647
61977
  detached: true,
60648
61978
  stdio: ["ignore", logFd, logFd],
60649
61979
  env: { ...process.env }
60650
61980
  });
60651
61981
  child.unref();
60652
- fs32.closeSync(logFd);
61982
+ fs35.closeSync(logFd);
60653
61983
  await probeLiveness({
60654
61984
  child,
60655
61985
  delayMs: 300,
@@ -60672,14 +62002,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60672
62002
  let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
60673
62003
  if (healthzPort !== null) {
60674
62004
  const healthArgs = ["health", "serve", "--port", String(healthzPort)];
60675
- const healthLogFd = fs32.openSync(DAEMON_LOG_FILE, "a");
62005
+ const healthLogFd = fs35.openSync(DAEMON_LOG_FILE, "a");
60676
62006
  const healthChild = spawn5(process.execPath, [cliEntry, ...healthArgs], {
60677
62007
  detached: true,
60678
62008
  stdio: ["ignore", healthLogFd, healthLogFd],
60679
62009
  env: { ...process.env }
60680
62010
  });
60681
62011
  healthChild.unref();
60682
- fs32.closeSync(healthLogFd);
62012
+ fs35.closeSync(healthLogFd);
60683
62013
  if (healthChild.pid) {
60684
62014
  const healthAlive = await probeLiveness({ child: healthChild, delayMs: 200, fatal: false });
60685
62015
  if (healthAlive) {
@@ -60742,11 +62072,11 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60742
62072
  });
60743
62073
  }
60744
62074
  try {
60745
- fs32.unlinkSync(DAEMON_PID_FILE);
62075
+ fs35.unlinkSync(DAEMON_PID_FILE);
60746
62076
  } catch {
60747
62077
  }
60748
62078
  try {
60749
- fs32.unlinkSync(HEALTHZ_PID_FILE);
62079
+ fs35.unlinkSync(HEALTHZ_PID_FILE);
60750
62080
  } catch {
60751
62081
  }
60752
62082
  persistState({
@@ -60853,6 +62183,7 @@ if (process.argv.includes("--no-color") || Boolean(process.env.NO_COLOR)) {
60853
62183
  source_default.level = 0;
60854
62184
  }
60855
62185
  var program2 = new Command();
62186
+ program2.allowExcessArguments(false);
60856
62187
  if (isJsonMode()) {
60857
62188
  program2.configureOutput({ writeErr: () => {
60858
62189
  } });