@switchbot/openapi-cli 3.6.1 → 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
  }
@@ -7853,9 +7853,9 @@ var init_catalog = __esm({
7853
7853
  {
7854
7854
  type: "Smart Lock",
7855
7855
  category: "physical",
7856
- 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").',
7857
7857
  role: "security",
7858
- aliases: ["Lock", "Smart Lock Pro", "Lock Pro"],
7858
+ aliases: ["Lock", "Smart Lock Pro", "Lock Pro", "Smart Lock Pro Wifi"],
7859
7859
  commands: [
7860
7860
  { command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
7861
7861
  { command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." },
@@ -7888,6 +7888,29 @@ var init_catalog = __esm({
7888
7888
  ],
7889
7889
  statusFields: ["battery", "version", "lockState", "doorState", "calibrate"]
7890
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
+ },
7891
7914
  {
7892
7915
  type: "Plug",
7893
7916
  category: "physical",
@@ -8195,11 +8218,12 @@ var init_catalog = __esm({
8195
8218
  {
8196
8219
  type: "AI Art Frame",
8197
8220
  category: "physical",
8198
- 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.",
8199
8222
  role: "other",
8200
8223
  commands: [
8201
8224
  { command: "next", parameter: "\u2014", description: "Switch to the next image", idempotent: false },
8202
- { 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"] }
8203
8227
  ],
8204
8228
  statusFields: ["version"]
8205
8229
  },
@@ -8225,6 +8249,16 @@ var init_catalog = __esm({
8225
8249
  commands: [],
8226
8250
  statusFields: ["temperature", "humidity", "battery", "version"]
8227
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
+ },
8228
8262
  {
8229
8263
  type: "Motion Sensor",
8230
8264
  category: "physical",
@@ -9521,6 +9555,161 @@ var init_credential = __esm({
9521
9555
  }
9522
9556
  });
9523
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
+
9524
9713
  // node_modules/yaml/dist/nodes/identity.js
9525
9714
  var require_identity = __commonJS({
9526
9715
  "node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -9600,17 +9789,17 @@ var require_visit = __commonJS({
9600
9789
  visit.BREAK = BREAK;
9601
9790
  visit.SKIP = SKIP;
9602
9791
  visit.REMOVE = REMOVE;
9603
- function visit_(key, node, visitor, path29) {
9604
- const ctrl = callVisitor(key, node, visitor, path29);
9792
+ function visit_(key, node, visitor, path31) {
9793
+ const ctrl = callVisitor(key, node, visitor, path31);
9605
9794
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
9606
- replaceNode(key, path29, ctrl);
9607
- return visit_(key, ctrl, visitor, path29);
9795
+ replaceNode(key, path31, ctrl);
9796
+ return visit_(key, ctrl, visitor, path31);
9608
9797
  }
9609
9798
  if (typeof ctrl !== "symbol") {
9610
9799
  if (identity.isCollection(node)) {
9611
- path29 = Object.freeze(path29.concat(node));
9800
+ path31 = Object.freeze(path31.concat(node));
9612
9801
  for (let i = 0; i < node.items.length; ++i) {
9613
- const ci = visit_(i, node.items[i], visitor, path29);
9802
+ const ci = visit_(i, node.items[i], visitor, path31);
9614
9803
  if (typeof ci === "number")
9615
9804
  i = ci - 1;
9616
9805
  else if (ci === BREAK)
@@ -9621,13 +9810,13 @@ var require_visit = __commonJS({
9621
9810
  }
9622
9811
  }
9623
9812
  } else if (identity.isPair(node)) {
9624
- path29 = Object.freeze(path29.concat(node));
9625
- const ck = visit_("key", node.key, visitor, path29);
9813
+ path31 = Object.freeze(path31.concat(node));
9814
+ const ck = visit_("key", node.key, visitor, path31);
9626
9815
  if (ck === BREAK)
9627
9816
  return BREAK;
9628
9817
  else if (ck === REMOVE)
9629
9818
  node.key = null;
9630
- const cv = visit_("value", node.value, visitor, path29);
9819
+ const cv = visit_("value", node.value, visitor, path31);
9631
9820
  if (cv === BREAK)
9632
9821
  return BREAK;
9633
9822
  else if (cv === REMOVE)
@@ -9648,17 +9837,17 @@ var require_visit = __commonJS({
9648
9837
  visitAsync.BREAK = BREAK;
9649
9838
  visitAsync.SKIP = SKIP;
9650
9839
  visitAsync.REMOVE = REMOVE;
9651
- async function visitAsync_(key, node, visitor, path29) {
9652
- 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);
9653
9842
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
9654
- replaceNode(key, path29, ctrl);
9655
- return visitAsync_(key, ctrl, visitor, path29);
9843
+ replaceNode(key, path31, ctrl);
9844
+ return visitAsync_(key, ctrl, visitor, path31);
9656
9845
  }
9657
9846
  if (typeof ctrl !== "symbol") {
9658
9847
  if (identity.isCollection(node)) {
9659
- path29 = Object.freeze(path29.concat(node));
9848
+ path31 = Object.freeze(path31.concat(node));
9660
9849
  for (let i = 0; i < node.items.length; ++i) {
9661
- const ci = await visitAsync_(i, node.items[i], visitor, path29);
9850
+ const ci = await visitAsync_(i, node.items[i], visitor, path31);
9662
9851
  if (typeof ci === "number")
9663
9852
  i = ci - 1;
9664
9853
  else if (ci === BREAK)
@@ -9669,13 +9858,13 @@ var require_visit = __commonJS({
9669
9858
  }
9670
9859
  }
9671
9860
  } else if (identity.isPair(node)) {
9672
- path29 = Object.freeze(path29.concat(node));
9673
- 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);
9674
9863
  if (ck === BREAK)
9675
9864
  return BREAK;
9676
9865
  else if (ck === REMOVE)
9677
9866
  node.key = null;
9678
- const cv = await visitAsync_("value", node.value, visitor, path29);
9867
+ const cv = await visitAsync_("value", node.value, visitor, path31);
9679
9868
  if (cv === BREAK)
9680
9869
  return BREAK;
9681
9870
  else if (cv === REMOVE)
@@ -9702,23 +9891,23 @@ var require_visit = __commonJS({
9702
9891
  }
9703
9892
  return visitor;
9704
9893
  }
9705
- function callVisitor(key, node, visitor, path29) {
9894
+ function callVisitor(key, node, visitor, path31) {
9706
9895
  if (typeof visitor === "function")
9707
- return visitor(key, node, path29);
9896
+ return visitor(key, node, path31);
9708
9897
  if (identity.isMap(node))
9709
- return visitor.Map?.(key, node, path29);
9898
+ return visitor.Map?.(key, node, path31);
9710
9899
  if (identity.isSeq(node))
9711
- return visitor.Seq?.(key, node, path29);
9900
+ return visitor.Seq?.(key, node, path31);
9712
9901
  if (identity.isPair(node))
9713
- return visitor.Pair?.(key, node, path29);
9902
+ return visitor.Pair?.(key, node, path31);
9714
9903
  if (identity.isScalar(node))
9715
- return visitor.Scalar?.(key, node, path29);
9904
+ return visitor.Scalar?.(key, node, path31);
9716
9905
  if (identity.isAlias(node))
9717
- return visitor.Alias?.(key, node, path29);
9906
+ return visitor.Alias?.(key, node, path31);
9718
9907
  return void 0;
9719
9908
  }
9720
- function replaceNode(key, path29, node) {
9721
- const parent = path29[path29.length - 1];
9909
+ function replaceNode(key, path31, node) {
9910
+ const parent = path31[path31.length - 1];
9722
9911
  if (identity.isCollection(parent)) {
9723
9912
  parent.items[key] = node;
9724
9913
  } else if (identity.isPair(parent)) {
@@ -10335,10 +10524,10 @@ var require_Collection = __commonJS({
10335
10524
  var createNode = require_createNode();
10336
10525
  var identity = require_identity();
10337
10526
  var Node = require_Node();
10338
- function collectionFromPath(schema2, path29, value) {
10527
+ function collectionFromPath(schema2, path31, value) {
10339
10528
  let v2 = value;
10340
- for (let i = path29.length - 1; i >= 0; --i) {
10341
- const k2 = path29[i];
10529
+ for (let i = path31.length - 1; i >= 0; --i) {
10530
+ const k2 = path31[i];
10342
10531
  if (typeof k2 === "number" && Number.isInteger(k2) && k2 >= 0) {
10343
10532
  const a = [];
10344
10533
  a[k2] = v2;
@@ -10357,7 +10546,7 @@ var require_Collection = __commonJS({
10357
10546
  sourceObjects: /* @__PURE__ */ new Map()
10358
10547
  });
10359
10548
  }
10360
- 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;
10361
10550
  var Collection = class extends Node.NodeBase {
10362
10551
  constructor(type2, schema2) {
10363
10552
  super(type2);
@@ -10387,11 +10576,11 @@ var require_Collection = __commonJS({
10387
10576
  * be a Pair instance or a `{ key, value }` object, which may not have a key
10388
10577
  * that already exists in the map.
10389
10578
  */
10390
- addIn(path29, value) {
10391
- if (isEmptyPath(path29))
10579
+ addIn(path31, value) {
10580
+ if (isEmptyPath(path31))
10392
10581
  this.add(value);
10393
10582
  else {
10394
- const [key, ...rest] = path29;
10583
+ const [key, ...rest] = path31;
10395
10584
  const node = this.get(key, true);
10396
10585
  if (identity.isCollection(node))
10397
10586
  node.addIn(rest, value);
@@ -10405,8 +10594,8 @@ var require_Collection = __commonJS({
10405
10594
  * Removes a value from the collection.
10406
10595
  * @returns `true` if the item was found and removed.
10407
10596
  */
10408
- deleteIn(path29) {
10409
- const [key, ...rest] = path29;
10597
+ deleteIn(path31) {
10598
+ const [key, ...rest] = path31;
10410
10599
  if (rest.length === 0)
10411
10600
  return this.delete(key);
10412
10601
  const node = this.get(key, true);
@@ -10420,8 +10609,8 @@ var require_Collection = __commonJS({
10420
10609
  * scalar values from their surrounding node; to disable set `keepScalar` to
10421
10610
  * `true` (collections are always returned intact).
10422
10611
  */
10423
- getIn(path29, keepScalar) {
10424
- const [key, ...rest] = path29;
10612
+ getIn(path31, keepScalar) {
10613
+ const [key, ...rest] = path31;
10425
10614
  const node = this.get(key, true);
10426
10615
  if (rest.length === 0)
10427
10616
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -10439,8 +10628,8 @@ var require_Collection = __commonJS({
10439
10628
  /**
10440
10629
  * Checks if the collection includes a value with the key `key`.
10441
10630
  */
10442
- hasIn(path29) {
10443
- const [key, ...rest] = path29;
10631
+ hasIn(path31) {
10632
+ const [key, ...rest] = path31;
10444
10633
  if (rest.length === 0)
10445
10634
  return this.has(key);
10446
10635
  const node = this.get(key, true);
@@ -10450,8 +10639,8 @@ var require_Collection = __commonJS({
10450
10639
  * Sets a value in this collection. For `!!set`, `value` needs to be a
10451
10640
  * boolean to add/remove the item from the set.
10452
10641
  */
10453
- setIn(path29, value) {
10454
- const [key, ...rest] = path29;
10642
+ setIn(path31, value) {
10643
+ const [key, ...rest] = path31;
10455
10644
  if (rest.length === 0) {
10456
10645
  this.set(key, value);
10457
10646
  } else {
@@ -12998,9 +13187,9 @@ var require_Document = __commonJS({
12998
13187
  this.contents.add(value);
12999
13188
  }
13000
13189
  /** Adds a value to the document. */
13001
- addIn(path29, value) {
13190
+ addIn(path31, value) {
13002
13191
  if (assertCollection(this.contents))
13003
- this.contents.addIn(path29, value);
13192
+ this.contents.addIn(path31, value);
13004
13193
  }
13005
13194
  /**
13006
13195
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -13075,14 +13264,14 @@ var require_Document = __commonJS({
13075
13264
  * Removes a value from the document.
13076
13265
  * @returns `true` if the item was found and removed.
13077
13266
  */
13078
- deleteIn(path29) {
13079
- if (Collection.isEmptyPath(path29)) {
13267
+ deleteIn(path31) {
13268
+ if (Collection.isEmptyPath(path31)) {
13080
13269
  if (this.contents == null)
13081
13270
  return false;
13082
13271
  this.contents = null;
13083
13272
  return true;
13084
13273
  }
13085
- return assertCollection(this.contents) ? this.contents.deleteIn(path29) : false;
13274
+ return assertCollection(this.contents) ? this.contents.deleteIn(path31) : false;
13086
13275
  }
13087
13276
  /**
13088
13277
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -13097,10 +13286,10 @@ var require_Document = __commonJS({
13097
13286
  * scalar values from their surrounding node; to disable set `keepScalar` to
13098
13287
  * `true` (collections are always returned intact).
13099
13288
  */
13100
- getIn(path29, keepScalar) {
13101
- if (Collection.isEmptyPath(path29))
13289
+ getIn(path31, keepScalar) {
13290
+ if (Collection.isEmptyPath(path31))
13102
13291
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
13103
- 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;
13104
13293
  }
13105
13294
  /**
13106
13295
  * Checks if the document includes a value with the key `key`.
@@ -13111,10 +13300,10 @@ var require_Document = __commonJS({
13111
13300
  /**
13112
13301
  * Checks if the document includes a value at `path`.
13113
13302
  */
13114
- hasIn(path29) {
13115
- if (Collection.isEmptyPath(path29))
13303
+ hasIn(path31) {
13304
+ if (Collection.isEmptyPath(path31))
13116
13305
  return this.contents !== void 0;
13117
- return identity.isCollection(this.contents) ? this.contents.hasIn(path29) : false;
13306
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path31) : false;
13118
13307
  }
13119
13308
  /**
13120
13309
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -13131,13 +13320,13 @@ var require_Document = __commonJS({
13131
13320
  * Sets a value in this document. For `!!set`, `value` needs to be a
13132
13321
  * boolean to add/remove the item from the set.
13133
13322
  */
13134
- setIn(path29, value) {
13135
- if (Collection.isEmptyPath(path29)) {
13323
+ setIn(path31, value) {
13324
+ if (Collection.isEmptyPath(path31)) {
13136
13325
  this.contents = value;
13137
13326
  } else if (this.contents == null) {
13138
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path29), value);
13327
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path31), value);
13139
13328
  } else if (assertCollection(this.contents)) {
13140
- this.contents.setIn(path29, value);
13329
+ this.contents.setIn(path31, value);
13141
13330
  }
13142
13331
  }
13143
13332
  /**
@@ -15114,9 +15303,9 @@ var require_cst_visit = __commonJS({
15114
15303
  visit.BREAK = BREAK;
15115
15304
  visit.SKIP = SKIP;
15116
15305
  visit.REMOVE = REMOVE;
15117
- visit.itemAtPath = (cst, path29) => {
15306
+ visit.itemAtPath = (cst, path31) => {
15118
15307
  let item = cst;
15119
- for (const [field, index] of path29) {
15308
+ for (const [field, index] of path31) {
15120
15309
  const tok = item?.[field];
15121
15310
  if (tok && "items" in tok) {
15122
15311
  item = tok.items[index];
@@ -15125,23 +15314,23 @@ var require_cst_visit = __commonJS({
15125
15314
  }
15126
15315
  return item;
15127
15316
  };
15128
- visit.parentCollection = (cst, path29) => {
15129
- const parent = visit.itemAtPath(cst, path29.slice(0, -1));
15130
- 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];
15131
15320
  const coll = parent?.[field];
15132
15321
  if (coll && "items" in coll)
15133
15322
  return coll;
15134
15323
  throw new Error("Parent collection not found");
15135
15324
  };
15136
- function _visit(path29, item, visitor) {
15137
- let ctrl = visitor(item, path29);
15325
+ function _visit(path31, item, visitor) {
15326
+ let ctrl = visitor(item, path31);
15138
15327
  if (typeof ctrl === "symbol")
15139
15328
  return ctrl;
15140
15329
  for (const field of ["key", "value"]) {
15141
15330
  const token = item[field];
15142
15331
  if (token && "items" in token) {
15143
15332
  for (let i = 0; i < token.items.length; ++i) {
15144
- 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);
15145
15334
  if (typeof ci === "number")
15146
15335
  i = ci - 1;
15147
15336
  else if (ci === BREAK)
@@ -15152,10 +15341,10 @@ var require_cst_visit = __commonJS({
15152
15341
  }
15153
15342
  }
15154
15343
  if (typeof ctrl === "function" && field === "key")
15155
- ctrl = ctrl(item, path29);
15344
+ ctrl = ctrl(item, path31);
15156
15345
  }
15157
15346
  }
15158
- return typeof ctrl === "function" ? ctrl(item, path29) : ctrl;
15347
+ return typeof ctrl === "function" ? ctrl(item, path31) : ctrl;
15159
15348
  }
15160
15349
  exports.visit = visit;
15161
15350
  }
@@ -16444,14 +16633,14 @@ var require_parser = __commonJS({
16444
16633
  case "scalar":
16445
16634
  case "single-quoted-scalar":
16446
16635
  case "double-quoted-scalar": {
16447
- const fs33 = this.flowScalar(this.type);
16636
+ const fs36 = this.flowScalar(this.type);
16448
16637
  if (atNextItem || it.value) {
16449
- map3.items.push({ start, key: fs33, sep: [] });
16638
+ map3.items.push({ start, key: fs36, sep: [] });
16450
16639
  this.onKeyLine = true;
16451
16640
  } else if (it.sep) {
16452
- this.stack.push(fs33);
16641
+ this.stack.push(fs36);
16453
16642
  } else {
16454
- Object.assign(it, { key: fs33, sep: [] });
16643
+ Object.assign(it, { key: fs36, sep: [] });
16455
16644
  this.onKeyLine = true;
16456
16645
  }
16457
16646
  return;
@@ -16579,13 +16768,13 @@ var require_parser = __commonJS({
16579
16768
  case "scalar":
16580
16769
  case "single-quoted-scalar":
16581
16770
  case "double-quoted-scalar": {
16582
- const fs33 = this.flowScalar(this.type);
16771
+ const fs36 = this.flowScalar(this.type);
16583
16772
  if (!it || it.value)
16584
- fc.items.push({ start: [], key: fs33, sep: [] });
16773
+ fc.items.push({ start: [], key: fs36, sep: [] });
16585
16774
  else if (it.sep)
16586
- this.stack.push(fs33);
16775
+ this.stack.push(fs36);
16587
16776
  else
16588
- Object.assign(it, { key: fs33, sep: [] });
16777
+ Object.assign(it, { key: fs36, sep: [] });
16589
16778
  return;
16590
16779
  }
16591
16780
  case "flow-map-end":
@@ -20184,8 +20373,8 @@ var require_utils2 = __commonJS({
20184
20373
  }
20185
20374
  return ind;
20186
20375
  }
20187
- function removeDotSegments(path29) {
20188
- let input = path29;
20376
+ function removeDotSegments(path31) {
20377
+ let input = path31;
20189
20378
  const output = [];
20190
20379
  let nextSlash = -1;
20191
20380
  let len = 0;
@@ -20385,8 +20574,8 @@ var require_schemes = __commonJS({
20385
20574
  wsComponent.secure = void 0;
20386
20575
  }
20387
20576
  if (wsComponent.resourceName) {
20388
- const [path29, query] = wsComponent.resourceName.split("?");
20389
- wsComponent.path = path29 && path29 !== "/" ? path29 : void 0;
20577
+ const [path31, query] = wsComponent.resourceName.split("?");
20578
+ wsComponent.path = path31 && path31 !== "/" ? path31 : void 0;
20390
20579
  wsComponent.query = query;
20391
20580
  wsComponent.resourceName = void 0;
20392
20581
  }
@@ -20445,7 +20634,7 @@ var require_schemes = __commonJS({
20445
20634
  urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
20446
20635
  return urnComponent;
20447
20636
  }
20448
- var http6 = (
20637
+ var http8 = (
20449
20638
  /** @type {SchemeHandler} */
20450
20639
  {
20451
20640
  scheme: "http",
@@ -20454,11 +20643,11 @@ var require_schemes = __commonJS({
20454
20643
  serialize: httpSerialize
20455
20644
  }
20456
20645
  );
20457
- var https5 = (
20646
+ var https7 = (
20458
20647
  /** @type {SchemeHandler} */
20459
20648
  {
20460
20649
  scheme: "https",
20461
- domainHost: http6.domainHost,
20650
+ domainHost: http8.domainHost,
20462
20651
  parse: httpParse,
20463
20652
  serialize: httpSerialize
20464
20653
  }
@@ -20502,8 +20691,8 @@ var require_schemes = __commonJS({
20502
20691
  var SCHEMES = (
20503
20692
  /** @type {Record<SchemeName, SchemeHandler>} */
20504
20693
  {
20505
- http: http6,
20506
- https: https5,
20694
+ http: http8,
20695
+ https: https7,
20507
20696
  ws,
20508
20697
  wss,
20509
20698
  urn,
@@ -24193,6 +24382,10 @@ function isNotCondition(c) {
24193
24382
  function isLlmCondition(c) {
24194
24383
  return c.llm !== void 0 && typeof c.llm === "object";
24195
24384
  }
24385
+ function isEventCountCondition(c) {
24386
+ const ec = c.event_count;
24387
+ return ec !== void 0 && typeof ec === "object" && typeof ec.device === "string";
24388
+ }
24196
24389
  var init_types = __esm({
24197
24390
  "src/rules/types.ts"() {
24198
24391
  "use strict";
@@ -24476,7 +24669,7 @@ function locateError(doc, lineCounter, err) {
24476
24669
  return { line: pos.line, col: pos.col };
24477
24670
  }
24478
24671
  function humanMessage(err) {
24479
- const path29 = err.instancePath || "(root)";
24672
+ const path31 = err.instancePath || "(root)";
24480
24673
  switch (err.keyword) {
24481
24674
  case "required":
24482
24675
  return `missing required property "${err.params.missingProperty}"`;
@@ -24484,21 +24677,21 @@ function humanMessage(err) {
24484
24677
  return `unknown property "${err.params.additionalProperty}"`;
24485
24678
  case "dependentRequired": {
24486
24679
  const { property, missingProperty } = err.params;
24487
- const parent = path29 === "(root)" ? "" : `${path29}: `;
24680
+ const parent = path31 === "(root)" ? "" : `${path31}: `;
24488
24681
  return `${parent}when "${property}" is set, "${missingProperty}" is also required`;
24489
24682
  }
24490
24683
  case "pattern":
24491
- return `${path29} does not match pattern ${err.params.pattern}`;
24684
+ return `${path31} does not match pattern ${err.params.pattern}`;
24492
24685
  case "const":
24493
- return `${path29} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
24686
+ return `${path31} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
24494
24687
  case "enum":
24495
- return `${path29} must be one of ${JSON.stringify(err.params.allowedValues)}`;
24688
+ return `${path31} must be one of ${JSON.stringify(err.params.allowedValues)}`;
24496
24689
  case "type":
24497
- return `${path29} must be ${err.params.type}`;
24690
+ return `${path31} must be ${err.params.type}`;
24498
24691
  case "not":
24499
- return `${path29} is not allowed here`;
24692
+ return `${path31} is not allowed here`;
24500
24693
  default:
24501
- return `${path29} ${err.message ?? "is invalid"}`;
24694
+ return `${path31} ${err.message ?? "is invalid"}`;
24502
24695
  }
24503
24696
  }
24504
24697
  function hintFor(err) {
@@ -24554,8 +24747,8 @@ function escapeJsonPointerSegment(segment) {
24554
24747
  function isPlausibleDeviceId(value) {
24555
24748
  return HEX_MAC_DEVICE_ID_RE.test(value) || HYPHENATED_DEVICE_ID_RE.test(value);
24556
24749
  }
24557
- function hasErrorAtPath(errors, path29) {
24558
- return errors.some((err) => err.path === path29);
24750
+ function hasErrorAtPath(errors, path31) {
24751
+ return errors.some((err) => err.path === path31);
24559
24752
  }
24560
24753
  function resolvePolicyDeviceRef(raw, aliases) {
24561
24754
  if (!raw) return { ok: false, reason: "missing-device" };
@@ -24578,25 +24771,25 @@ function isDeviceStateConditionLike(value) {
24578
24771
  const candidate = value;
24579
24772
  return typeof candidate.device === "string" && typeof candidate.field === "string" && typeof candidate.op === "string";
24580
24773
  }
24581
- function collectConditionDeviceRefs(condition, path29) {
24774
+ function collectConditionDeviceRefs(condition, path31) {
24582
24775
  if (!condition || typeof condition !== "object" || Array.isArray(condition)) return [];
24583
24776
  const out = [];
24584
24777
  if (isDeviceStateConditionLike(condition)) {
24585
- out.push({ path: `${path29}/device`, ref: condition.device });
24778
+ out.push({ path: `${path31}/device`, ref: condition.device });
24586
24779
  }
24587
24780
  const candidate = condition;
24588
24781
  if (Array.isArray(candidate.all)) {
24589
24782
  for (let i = 0; i < candidate.all.length; i++) {
24590
- out.push(...collectConditionDeviceRefs(candidate.all[i], `${path29}/all/${i}`));
24783
+ out.push(...collectConditionDeviceRefs(candidate.all[i], `${path31}/all/${i}`));
24591
24784
  }
24592
24785
  }
24593
24786
  if (Array.isArray(candidate.any)) {
24594
24787
  for (let i = 0; i < candidate.any.length; i++) {
24595
- out.push(...collectConditionDeviceRefs(candidate.any[i], `${path29}/any/${i}`));
24788
+ out.push(...collectConditionDeviceRefs(candidate.any[i], `${path31}/any/${i}`));
24596
24789
  }
24597
24790
  }
24598
24791
  if (candidate.not !== void 0) {
24599
- out.push(...collectConditionDeviceRefs(candidate.not, `${path29}/not`));
24792
+ out.push(...collectConditionDeviceRefs(candidate.not, `${path31}/not`));
24600
24793
  }
24601
24794
  return out;
24602
24795
  }
@@ -24643,12 +24836,12 @@ function collectOfflineSemanticErrors(loaded, existingErrors) {
24643
24836
  const out = [];
24644
24837
  const aliases = collectAliasMap(data);
24645
24838
  for (const [aliasName, deviceId] of Object.entries(aliases)) {
24646
- const path29 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24647
- if (hasErrorAtPath(existingErrors, path29)) continue;
24839
+ const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24840
+ if (hasErrorAtPath(existingErrors, path31)) continue;
24648
24841
  if (isPlausibleDeviceId(deviceId)) continue;
24649
- const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
24842
+ const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
24650
24843
  out.push({
24651
- path: path29,
24844
+ path: path31,
24652
24845
  line,
24653
24846
  col,
24654
24847
  keyword: "alias-device-id",
@@ -24782,12 +24975,12 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
24782
24975
  inventoryById.set(remote.deviceId, { typeName: remote.remoteType });
24783
24976
  }
24784
24977
  for (const [aliasName, deviceId] of Object.entries(aliases)) {
24785
- const path29 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24786
- if (hasErrorAtPath(errors, path29)) continue;
24978
+ const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24979
+ if (hasErrorAtPath(errors, path31)) continue;
24787
24980
  if (!inventoryById.has(deviceId)) {
24788
- const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
24981
+ const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
24789
24982
  errors.push({
24790
- path: path29,
24983
+ path: path31,
24791
24984
  line,
24792
24985
  col,
24793
24986
  keyword: "alias-live-device-not-found",
@@ -24851,10 +25044,10 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
24851
25044
  if (!effectiveDeviceId) continue;
24852
25045
  const target = inventoryById.get(effectiveDeviceId);
24853
25046
  if (!target) {
24854
- const path29 = typeof action?.device === "string" ? devicePath : commandPath;
24855
- 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);
24856
25049
  errors.push({
24857
- path: path29,
25050
+ path: path31,
24858
25051
  line: line2,
24859
25052
  col: col2,
24860
25053
  keyword: "rule-live-device-not-found",
@@ -24960,6 +25153,32 @@ var init_validate = __esm({
24960
25153
  }
24961
25154
  });
24962
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
+
24963
25182
  // src/llm/providers/openai.ts
24964
25183
  import https from "node:https";
24965
25184
  import http from "node:http";
@@ -24968,9 +25187,11 @@ var init_openai = __esm({
24968
25187
  "src/llm/providers/openai.ts"() {
24969
25188
  "use strict";
24970
25189
  init_cjs_shim();
25190
+ init_pricing();
24971
25191
  OpenAIProvider = class {
24972
25192
  name = "openai";
24973
25193
  model;
25194
+ capabilities = { toolUse: true };
24974
25195
  apiKey;
24975
25196
  baseUrl;
24976
25197
  timeoutMs;
@@ -25095,7 +25316,10 @@ var init_openai = __esm({
25095
25316
  if (!toolCall) throw new Error("OpenAI decide: no tool call in response");
25096
25317
  const args = JSON.parse(toolCall.function.arguments);
25097
25318
  if (typeof args.pass !== "boolean") throw new Error("OpenAI decide: malformed function-call response");
25098
- 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 };
25099
25323
  }
25100
25324
  };
25101
25325
  }
@@ -25108,9 +25332,11 @@ var init_anthropic = __esm({
25108
25332
  "src/llm/providers/anthropic.ts"() {
25109
25333
  "use strict";
25110
25334
  init_cjs_shim();
25335
+ init_pricing();
25111
25336
  AnthropicProvider = class {
25112
25337
  name = "anthropic";
25113
25338
  model;
25339
+ capabilities = { toolUse: true };
25114
25340
  apiKey;
25115
25341
  timeoutMs;
25116
25342
  maxTokens;
@@ -25225,7 +25451,352 @@ var init_anthropic = __esm({
25225
25451
  if (!toolUse?.input || typeof toolUse.input.pass !== "boolean") {
25226
25452
  throw new Error("Anthropic decide: malformed tool-use response");
25227
25453
  }
25228
- 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;
25229
25800
  }
25230
25801
  };
25231
25802
  }
@@ -25267,8 +25838,9 @@ __export(llm_exports, {
25267
25838
  scoreIntentComplexity: () => scoreIntentComplexity
25268
25839
  });
25269
25840
  function createLLMProvider(backend, opts = {}) {
25270
- if (backend === "openai" || backend === "local") return new OpenAIProvider(opts);
25841
+ if (backend === "openai") return new OpenAIProvider(opts);
25271
25842
  if (backend === "anthropic") return new AnthropicProvider(opts);
25843
+ if (backend === "local") return new LocalProvider(opts);
25272
25844
  throw new Error(`Unknown LLM backend: ${backend}`);
25273
25845
  }
25274
25846
  var LLM_AUTO_THRESHOLD;
@@ -25278,6 +25850,7 @@ var init_llm = __esm({
25278
25850
  init_cjs_shim();
25279
25851
  init_openai();
25280
25852
  init_anthropic();
25853
+ init_local();
25281
25854
  init_complexity();
25282
25855
  LLM_AUTO_THRESHOLD = 4;
25283
25856
  }
@@ -25482,9 +26055,10 @@ async function evaluateSingle(c, now, ctx) {
25482
26055
  };
25483
26056
  }
25484
26057
  try {
26058
+ const recent = await fetchRecentEvents(c.llm.recent_events, ctx);
25485
26059
  const res = await ctx.llmEvaluator.evaluate(
25486
26060
  c.llm,
25487
- { event: ctx.event },
26061
+ { event: ctx.event, recentEvents: recent },
25488
26062
  ctx.ruleVersion ?? "unknown",
25489
26063
  ctx.globalLlmMaxCallsPerHour
25490
26064
  );
@@ -25496,6 +26070,44 @@ async function evaluateSingle(c, now, ctx) {
25496
26070
  return fail(`llm condition error: ${err instanceof Error ? err.message : String(err)}`);
25497
26071
  }
25498
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
+ }
25499
26111
  return {
25500
26112
  matched: false,
25501
26113
  failures: [],
@@ -25562,12 +26174,14 @@ function conditionKind(c) {
25562
26174
  if (isTimeBetween(c)) return "time_between";
25563
26175
  if (isDeviceState(c)) return "device_state";
25564
26176
  if (isLlmCondition(c)) return "llm";
26177
+ if (isEventCountCondition(c)) return "event_count";
25565
26178
  return "unknown";
25566
26179
  }
25567
26180
  function conditionConfig(c) {
25568
26181
  if (isTimeBetween(c)) return c.time_between;
25569
26182
  if (isDeviceState(c)) return { device: c.device, field: c.field, op: c.op, value: c.value };
25570
26183
  if (isLlmCondition(c)) return { prompt: c.llm.prompt.slice(0, 80) };
26184
+ if (isEventCountCondition(c)) return c.event_count;
25571
26185
  return void 0;
25572
26186
  }
25573
26187
  function pushConditionTrace(trace, c, sub) {
@@ -25577,6 +26191,31 @@ function pushConditionTrace(trace, c, sub) {
25577
26191
  passed: sub.unsupported.length > 0 ? false : sub.matched
25578
26192
  });
25579
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
+ }
25580
26219
  var EVENT_CLASSIFIERS;
25581
26220
  var init_matcher = __esm({
25582
26221
  "src/rules/matcher.ts"() {
@@ -26502,7 +27141,7 @@ var init_cron_scheduler = __esm({
26502
27141
  });
26503
27142
 
26504
27143
  // src/rules/webhook-listener.ts
26505
- import http2 from "node:http";
27144
+ import http4 from "node:http";
26506
27145
  import { timingSafeEqual } from "node:crypto";
26507
27146
  function normalisePath(p2) {
26508
27147
  if (!p2) return "/";
@@ -26558,7 +27197,7 @@ var init_webhook_listener = __esm({
26558
27197
  /** Start listening. Resolves once the server has bound a port. */
26559
27198
  async start() {
26560
27199
  if (this.server) return;
26561
- const server = http2.createServer((req, res) => {
27200
+ const server = http4.createServer((req, res) => {
26562
27201
  this.handle(req, res).catch((err) => {
26563
27202
  if (!res.headersSent) {
26564
27203
  res.writeHead(500);
@@ -26698,8 +27337,8 @@ var init_webhook_listener = __esm({
26698
27337
  // src/rules/notify.ts
26699
27338
  import fs14 from "node:fs";
26700
27339
  import path12 from "node:path";
26701
- import http3 from "node:http";
26702
- import https3 from "node:https";
27340
+ import http5 from "node:http";
27341
+ import https5 from "node:https";
26703
27342
  function renderNotifyTemplate(template, vars) {
26704
27343
  return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_2, key) => {
26705
27344
  const val = vars[key];
@@ -26782,7 +27421,7 @@ async function sendWebhook(url2, body) {
26782
27421
  "User-Agent": "switchbot-cli/notify"
26783
27422
  }
26784
27423
  };
26785
- const req = (isHttps ? https3 : http3).request(options, (res) => {
27424
+ const req = (isHttps ? https5 : http5).request(options, (res) => {
26786
27425
  res.resume();
26787
27426
  const status = res.statusCode ?? 0;
26788
27427
  if (status >= 200 && status < 300) {
@@ -26969,10 +27608,250 @@ var init_trace = __esm({
26969
27608
  }
26970
27609
  });
26971
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
+
26972
27850
  // src/rules/engine.ts
26973
27851
  var engine_exports = {};
26974
27852
  __export(engine_exports, {
26975
27853
  RulesEngine: () => RulesEngine,
27854
+ defaultEventWindowFetcher: () => defaultEventWindowFetcher,
26976
27855
  lintRules: () => lintRules
26977
27856
  });
26978
27857
  import { randomUUID as randomUUID4 } from "node:crypto";
@@ -27190,6 +28069,25 @@ function lintRules(automation) {
27190
28069
  message: "llm condition budget.max_calls_per_hour is 0 \u2014 condition will always take the on_error path."
27191
28070
  });
27192
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
+ }
27193
28091
  if (llm.on_error === "pass") {
27194
28092
  issues.push({
27195
28093
  rule: r.name,
@@ -27199,6 +28097,26 @@ function lintRules(automation) {
27199
28097
  });
27200
28098
  }
27201
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
+ }
27202
28120
  const enabled = r.enabled !== false;
27203
28121
  const hasError = issues.some((i) => i.severity === "error");
27204
28122
  const hasUnsupported = issues.some((i) => i.code === "trigger-unsupported");
@@ -27211,6 +28129,21 @@ function lintRules(automation) {
27211
28129
  unsupportedCount
27212
28130
  };
27213
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
+ }
27214
28147
  var RulesEngine;
27215
28148
  var init_engine = __esm({
27216
28149
  "src/rules/engine.ts"() {
@@ -27228,6 +28161,8 @@ var init_engine = __esm({
27228
28161
  init_croner();
27229
28162
  init_audit();
27230
28163
  init_trace();
28164
+ init_llm_condition();
28165
+ init_history_window();
27231
28166
  RulesEngine = class {
27232
28167
  opts;
27233
28168
  rules;
@@ -27248,6 +28183,8 @@ var init_engine = __esm({
27248
28183
  * keeps the semantics of `max_per` honest.
27249
28184
  */
27250
28185
  pendingChain = Promise.resolve();
28186
+ llmEvaluator = new LlmConditionEvaluator();
28187
+ eventWindowFetcher = defaultEventWindowFetcher;
27251
28188
  stats = {
27252
28189
  started: false,
27253
28190
  rulesLoaded: 0,
@@ -27577,7 +28514,12 @@ var init_engine = __esm({
27577
28514
  const cond = await evaluateConditions(rule.conditions, event.t, {
27578
28515
  aliases: this.aliases,
27579
28516
  fetchStatus,
27580
- 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
27581
28523
  });
27582
28524
  if (!cond.matched) {
27583
28525
  const hasHysteresis = rule.hysteresis ?? rule.requires_stable_for;
@@ -27778,6 +28720,162 @@ var init_engine = __esm({
27778
28720
  }
27779
28721
  });
27780
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
+
27781
28879
  // src/devices/resources.ts
27782
28880
  var COMMON_WEBHOOK_FIELDS, RESOURCE_CATALOG;
27783
28881
  var init_resources = __esm({
@@ -28411,6 +29509,128 @@ var init_capabilities = __esm({
28411
29509
  }
28412
29510
  });
28413
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
+
28414
29634
  // src/index.ts
28415
29635
  init_cjs_shim();
28416
29636
  init_esm();
@@ -36466,10 +37686,10 @@ function mergeDefs(...defs) {
36466
37686
  function cloneDef(schema2) {
36467
37687
  return mergeDefs(schema2._zod.def);
36468
37688
  }
36469
- function getElementAtPath(obj, path29) {
36470
- if (!path29)
37689
+ function getElementAtPath(obj, path31) {
37690
+ if (!path31)
36471
37691
  return obj;
36472
- return path29.reduce((acc, key) => acc?.[key], obj);
37692
+ return path31.reduce((acc, key) => acc?.[key], obj);
36473
37693
  }
36474
37694
  function promiseAllObject(promisesObj) {
36475
37695
  const keys = Object.keys(promisesObj);
@@ -36852,11 +38072,11 @@ function aborted(x2, startIndex = 0) {
36852
38072
  }
36853
38073
  return false;
36854
38074
  }
36855
- function prefixIssues(path29, issues) {
38075
+ function prefixIssues(path31, issues) {
36856
38076
  return issues.map((iss) => {
36857
38077
  var _a2;
36858
38078
  (_a2 = iss).path ?? (_a2.path = []);
36859
- iss.path.unshift(path29);
38079
+ iss.path.unshift(path31);
36860
38080
  return iss;
36861
38081
  });
36862
38082
  }
@@ -37039,7 +38259,7 @@ function formatError2(error48, mapper = (issue2) => issue2.message) {
37039
38259
  }
37040
38260
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
37041
38261
  const result = { errors: [] };
37042
- const processError = (error49, path29 = []) => {
38262
+ const processError = (error49, path31 = []) => {
37043
38263
  var _a2, _b;
37044
38264
  for (const issue2 of error49.issues) {
37045
38265
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -37049,7 +38269,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
37049
38269
  } else if (issue2.code === "invalid_element") {
37050
38270
  processError({ issues: issue2.issues }, issue2.path);
37051
38271
  } else {
37052
- const fullpath = [...path29, ...issue2.path];
38272
+ const fullpath = [...path31, ...issue2.path];
37053
38273
  if (fullpath.length === 0) {
37054
38274
  result.errors.push(mapper(issue2));
37055
38275
  continue;
@@ -37081,8 +38301,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
37081
38301
  }
37082
38302
  function toDotPath(_path) {
37083
38303
  const segs = [];
37084
- const path29 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
37085
- for (const seg of path29) {
38304
+ const path31 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
38305
+ for (const seg of path31) {
37086
38306
  if (typeof seg === "number")
37087
38307
  segs.push(`[${seg}]`);
37088
38308
  else if (typeof seg === "symbol")
@@ -49137,13 +50357,13 @@ function resolveRef(ref, ctx) {
49137
50357
  if (!ref.startsWith("#")) {
49138
50358
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
49139
50359
  }
49140
- const path29 = ref.slice(1).split("/").filter(Boolean);
49141
- if (path29.length === 0) {
50360
+ const path31 = ref.slice(1).split("/").filter(Boolean);
50361
+ if (path31.length === 0) {
49142
50362
  return ctx.rootSchema;
49143
50363
  }
49144
50364
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
49145
- if (path29[0] === defsKey) {
49146
- const key = path29[1];
50365
+ if (path31[0] === defsKey) {
50366
+ const key = path31[1];
49147
50367
  if (!key || !ctx.defs[key]) {
49148
50368
  throw new Error(`Reference not found: ${ref}`);
49149
50369
  }
@@ -49926,157 +51146,12 @@ function resolveToolProfile(name) {
49926
51146
  throw new Error(`Unknown tool profile "${name}". Valid profiles: ${valid}`);
49927
51147
  }
49928
51148
 
49929
- // src/devices/history-query.ts
49930
- init_cjs_shim();
49931
- import fs10 from "node:fs";
49932
- import path10 from "node:path";
49933
- import os11 from "node:os";
49934
- import readline2 from "node:readline";
49935
- var DEFAULT_LIMIT = 1e3;
49936
- function historyDir2() {
49937
- return path10.join(os11.homedir(), ".switchbot", "device-history");
49938
- }
49939
- function parseDurationToMs2(spec) {
49940
- const m2 = spec.trim().match(/^(\d+)(ms|s|m|h|d)$/i);
49941
- if (!m2) return null;
49942
- const n = Number(m2[1]);
49943
- const unit = m2[2].toLowerCase();
49944
- const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
49945
- return n * factor;
49946
- }
49947
- function parseInstantToMs(spec) {
49948
- const ms = Date.parse(spec);
49949
- return Number.isFinite(ms) ? ms : null;
49950
- }
49951
- function resolveRange(opts) {
49952
- let fromMs = Number.NEGATIVE_INFINITY;
49953
- let toMs = Number.POSITIVE_INFINITY;
49954
- if (opts.since && (opts.from || opts.to)) {
49955
- throw new Error("--since is mutually exclusive with --from/--to.");
49956
- }
49957
- if (opts.since) {
49958
- const durMs = parseDurationToMs2(opts.since);
49959
- if (durMs === null) {
49960
- throw new Error(`Invalid --since value "${opts.since}". Expected e.g. "30s", "15m", "1h", "7d".`);
49961
- }
49962
- fromMs = Date.now() - durMs;
49963
- } else {
49964
- if (opts.from) {
49965
- const parsed = parseInstantToMs(opts.from);
49966
- if (parsed === null) throw new Error(`Invalid --from value "${opts.from}". Expected ISO-8601 timestamp.`);
49967
- fromMs = parsed;
49968
- }
49969
- if (opts.to) {
49970
- const parsed = parseInstantToMs(opts.to);
49971
- if (parsed === null) throw new Error(`Invalid --to value "${opts.to}". Expected ISO-8601 timestamp.`);
49972
- toMs = parsed;
49973
- }
49974
- if (fromMs > toMs) throw new Error("--from must be <= --to.");
49975
- }
49976
- return { fromMs, toMs };
49977
- }
49978
- function jsonlFilesForDevice(deviceId, baseDir = historyDir2()) {
49979
- const out = [];
49980
- if (!fs10.existsSync(baseDir)) return out;
49981
- for (let i = 3; i >= 1; i--) {
49982
- const p2 = path10.join(baseDir, `${deviceId}.jsonl.${i}`);
49983
- if (fs10.existsSync(p2)) out.push(p2);
49984
- }
49985
- const current = path10.join(baseDir, `${deviceId}.jsonl`);
49986
- if (fs10.existsSync(current)) out.push(current);
49987
- return out;
49988
- }
49989
- function projectFields(record2, fields) {
49990
- if (fields.length === 0) return record2;
49991
- const projected = {};
49992
- const payload = record2.payload ?? {};
49993
- for (const f2 of fields) {
49994
- if (f2 in payload) projected[f2] = payload[f2];
49995
- }
49996
- return { t: record2.t, topic: record2.topic, deviceType: record2.deviceType, payload: projected };
49997
- }
49998
- async function queryDeviceHistory(deviceId, opts = {}) {
49999
- const { fromMs, toMs } = resolveRange(opts);
50000
- const limit = Math.max(0, opts.limit ?? DEFAULT_LIMIT);
50001
- const fields = opts.fields ?? [];
50002
- const files = jsonlFilesForDevice(deviceId);
50003
- const out = [];
50004
- for (const file2 of files) {
50005
- try {
50006
- const stat = fs10.statSync(file2);
50007
- if (stat.mtimeMs < fromMs) continue;
50008
- } catch {
50009
- continue;
50010
- }
50011
- const stream = fs10.createReadStream(file2, { encoding: "utf-8" });
50012
- const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
50013
- for await (const line of rl) {
50014
- if (!line) continue;
50015
- let rec;
50016
- try {
50017
- rec = JSON.parse(line);
50018
- } catch {
50019
- continue;
50020
- }
50021
- const tMs = Date.parse(rec.t);
50022
- if (!Number.isFinite(tMs)) continue;
50023
- if (tMs < fromMs || tMs > toMs) continue;
50024
- out.push(projectFields(rec, fields));
50025
- if (out.length >= limit) {
50026
- rl.close();
50027
- stream.destroy();
50028
- return out;
50029
- }
50030
- }
50031
- }
50032
- return out;
50033
- }
50034
- function queryDeviceHistoryStats(deviceId) {
50035
- const dir = historyDir2();
50036
- const files = jsonlFilesForDevice(deviceId);
50037
- let totalBytes = 0;
50038
- let oldest = null;
50039
- let newest = null;
50040
- let count = 0;
50041
- for (const file2 of files) {
50042
- try {
50043
- totalBytes += fs10.statSync(file2).size;
50044
- } catch {
50045
- }
50046
- }
50047
- for (const file2 of files) {
50048
- try {
50049
- const lines = fs10.readFileSync(file2, "utf-8").split("\n");
50050
- for (const line of lines) {
50051
- if (!line) continue;
50052
- count += 1;
50053
- try {
50054
- const rec = JSON.parse(line);
50055
- const tMs = Date.parse(rec.t);
50056
- if (Number.isFinite(tMs)) {
50057
- if (oldest === null || tMs < oldest) oldest = tMs;
50058
- if (newest === null || tMs > newest) newest = tMs;
50059
- }
50060
- } catch {
50061
- }
50062
- }
50063
- } catch {
50064
- }
50065
- }
50066
- return {
50067
- deviceId,
50068
- fileCount: files.length,
50069
- totalBytes,
50070
- recordCount: count,
50071
- oldest: oldest !== null ? new Date(oldest).toISOString() : void 0,
50072
- newest: newest !== null ? new Date(newest).toISOString() : void 0,
50073
- jsonlFiles: files.map((f2) => path10.basename(f2)),
50074
- historyDir: dir
50075
- };
50076
- }
51149
+ // src/commands/mcp.ts
51150
+ init_history_query();
50077
51151
 
50078
51152
  // src/devices/history-agg.ts
50079
51153
  init_cjs_shim();
51154
+ init_history_query();
50080
51155
  import fs11 from "node:fs";
50081
51156
  import readline3 from "node:readline";
50082
51157
  var ALL_AGG_FNS = ["count", "min", "max", "avg", "sum", "p50", "p95"];
@@ -51200,7 +52275,7 @@ var import_yaml5 = __toESM(require_dist(), 1);
51200
52275
  var import_yaml6 = __toESM(require_dist(), 1);
51201
52276
  init_load();
51202
52277
  init_validate();
51203
- import fs15 from "node:fs";
52278
+ import fs16 from "node:fs";
51204
52279
  var AddRuleError = class extends Error {
51205
52280
  constructor(message, code) {
51206
52281
  super(message);
@@ -51316,7 +52391,7 @@ ${msgs}`,
51316
52391
  function addRuleToPolicyFile(opts) {
51317
52392
  const result = addRuleToPolicySource(opts);
51318
52393
  if (!opts.dryRun) {
51319
- fs15.writeFileSync(opts.policyPath, result.nextSource, "utf8");
52394
+ fs16.writeFileSync(opts.policyPath, result.nextSource, "utf8");
51320
52395
  return { ...result, written: true };
51321
52396
  }
51322
52397
  return { ...result, written: false };
@@ -51325,15 +52400,15 @@ function addRuleToPolicyFile(opts) {
51325
52400
  // src/rules/explain.ts
51326
52401
  init_cjs_shim();
51327
52402
  init_trace();
51328
- import fs16 from "node:fs";
52403
+ import fs17 from "node:fs";
51329
52404
  function loadTraceRecords(auditFile, opts = {}) {
51330
- if (!fs16.existsSync(auditFile)) return [];
51331
- 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/);
51332
52407
  return filterTraceRecords(lines, opts);
51333
52408
  }
51334
52409
  function loadRelatedAudit(auditFile, fireId) {
51335
- if (!fs16.existsSync(auditFile)) return [];
51336
- const raw = fs16.readFileSync(auditFile, "utf-8");
52410
+ if (!fs17.existsSync(auditFile)) return [];
52411
+ const raw = fs17.readFileSync(auditFile, "utf-8");
51337
52412
  const out = [];
51338
52413
  for (const line of raw.split(/\r?\n/)) {
51339
52414
  const trimmed = line.trim();
@@ -51417,17 +52492,18 @@ init_throttle();
51417
52492
  init_trace();
51418
52493
  init_trace();
51419
52494
  init_matcher();
51420
- import fs17 from "node:fs";
52495
+ init_engine();
52496
+ import fs18 from "node:fs";
51421
52497
  import path14 from "node:path";
51422
52498
  import os13 from "node:os";
51423
52499
  import { randomUUID as randomUUID5 } from "node:crypto";
51424
- var HOUR_MS = 60 * 60 * 1e3;
52500
+ var HOUR_MS2 = 60 * 60 * 1e3;
51425
52501
  var DEVICE_HISTORY_DIR = path14.join(os13.homedir(), ".switchbot", "device-history");
51426
52502
  async function simulateRule(opts) {
51427
52503
  const { rule, aliases = {}, liveLlm = false } = opts;
51428
52504
  const rv = ruleVersion(rule);
51429
52505
  const events = loadSourceEvents(opts);
51430
- 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);
51431
52507
  const windowEnd = events.length > 0 ? new Date(Math.max(...events.map((e) => e.t.getTime()))) : /* @__PURE__ */ new Date();
51432
52508
  const counts = { wouldFire: 0, blocked: 0, throttled: 0, errored: 0, skippedLlm: 0 };
51433
52509
  const blockReasons = /* @__PURE__ */ new Map();
@@ -51477,7 +52553,8 @@ async function simulateRule(opts) {
51477
52553
  aliases,
51478
52554
  fetchStatus: statusFetcher,
51479
52555
  event,
51480
- ruleVersion: rv
52556
+ ruleVersion: rv,
52557
+ eventWindowFetcher: defaultEventWindowFetcher
51481
52558
  });
51482
52559
  } catch (err) {
51483
52560
  counts.errored++;
@@ -51526,8 +52603,8 @@ async function simulateRule(opts) {
51526
52603
  }
51527
52604
  function loadSourceEvents(opts) {
51528
52605
  if (opts.against) {
51529
- if (!fs17.existsSync(opts.against)) return [];
51530
- 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/);
51531
52608
  const events = [];
51532
52609
  for (const line of lines2) {
51533
52610
  const trimmed = line.trim();
@@ -51547,10 +52624,10 @@ function loadSourceEvents(opts) {
51547
52624
  return events;
51548
52625
  }
51549
52626
  const auditLog = opts.auditLog;
51550
- if (!auditLog || !fs17.existsSync(auditLog)) return [];
51551
- 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;
51552
52629
  const sinceIso = new Date(sinceMs).toISOString();
51553
- const lines = fs17.readFileSync(auditLog, "utf-8").split(/\r?\n/);
52630
+ const lines = fs18.readFileSync(auditLog, "utf-8").split(/\r?\n/);
51554
52631
  const traceRecords = filterTraceRecords(lines, {
51555
52632
  ruleName: opts.rule.name,
51556
52633
  since: sinceIso
@@ -51574,13 +52651,13 @@ function parseSince(since) {
51574
52651
  const unitMs = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
51575
52652
  return Date.now() - n * unitMs;
51576
52653
  }
51577
- return Date.now() - 24 * HOUR_MS;
52654
+ return Date.now() - 24 * HOUR_MS2;
51578
52655
  }
51579
52656
  function buildStatusFetcher(asOf) {
51580
52657
  return async (deviceId) => {
51581
52658
  const histFile = path14.join(DEVICE_HISTORY_DIR, `${deviceId}.jsonl`);
51582
- if (!fs17.existsSync(histFile)) return {};
51583
- 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/);
51584
52661
  const asOfMs = asOf.getTime();
51585
52662
  let best;
51586
52663
  for (const line of lines) {
@@ -51616,13 +52693,13 @@ function collectPolicyDiff(left, right, at, out, limit) {
51616
52693
  const maxLen = Math.max(left.length, right.length);
51617
52694
  for (let i = 0; i < maxLen; i++) {
51618
52695
  if (out.length >= limit) return;
51619
- const path29 = `${at}[${i}]`;
52696
+ const path31 = `${at}[${i}]`;
51620
52697
  if (i >= left.length) {
51621
- out.push({ path: path29, kind: "added", after: right[i] });
52698
+ out.push({ path: path31, kind: "added", after: right[i] });
51622
52699
  } else if (i >= right.length) {
51623
- out.push({ path: path29, kind: "removed", before: left[i] });
52700
+ out.push({ path: path31, kind: "removed", before: left[i] });
51624
52701
  } else {
51625
- collectPolicyDiff(left[i], right[i], path29, out, limit);
52702
+ collectPolicyDiff(left[i], right[i], path31, out, limit);
51626
52703
  }
51627
52704
  }
51628
52705
  return;
@@ -51631,15 +52708,15 @@ function collectPolicyDiff(left, right, at, out, limit) {
51631
52708
  const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
51632
52709
  for (const key of [...keys].sort()) {
51633
52710
  if (out.length >= limit) return;
51634
- const path29 = at === "$" ? `$.${key}` : `${at}.${key}`;
52711
+ const path31 = at === "$" ? `$.${key}` : `${at}.${key}`;
51635
52712
  const leftHas = Object.prototype.hasOwnProperty.call(left, key);
51636
52713
  const rightHas = Object.prototype.hasOwnProperty.call(right, key);
51637
52714
  if (!leftHas && rightHas) {
51638
- out.push({ path: path29, kind: "added", after: right[key] });
52715
+ out.push({ path: path31, kind: "added", after: right[key] });
51639
52716
  } else if (leftHas && !rightHas) {
51640
- out.push({ path: path29, kind: "removed", before: left[key] });
52717
+ out.push({ path: path31, kind: "removed", before: left[key] });
51641
52718
  } else {
51642
- collectPolicyDiff(left[key], right[key], path29, out, limit);
52719
+ collectPolicyDiff(left[key], right[key], path31, out, limit);
51643
52720
  }
51644
52721
  }
51645
52722
  return;
@@ -51693,7 +52770,7 @@ function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges
51693
52770
  init_embedded_assets();
51694
52771
  import { dirname as pathDirname, join as pathJoin } from "node:path";
51695
52772
  import os14 from "node:os";
51696
- import fs18 from "node:fs";
52773
+ import fs19 from "node:fs";
51697
52774
  var LATEST_SUPPORTED_VERSION = SUPPORTED_POLICY_SCHEMA_VERSIONS[SUPPORTED_POLICY_SCHEMA_VERSIONS.length - 1];
51698
52775
  function mcpError(kind, code, message, options) {
51699
52776
  const obj = { code, kind, message };
@@ -52669,15 +53746,15 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
52669
53746
  async ({ path: pathArg, force }) => {
52670
53747
  const policyPath = resolvePolicyPath({ flag: pathArg });
52671
53748
  const doForce = force === true;
52672
- if (fs18.existsSync(policyPath) && !doForce) {
53749
+ if (fs19.existsSync(policyPath) && !doForce) {
52673
53750
  return mcpError("guard", 5, `refusing to overwrite existing policy at ${policyPath}`, {
52674
53751
  hint: "pass force=true to overwrite, or choose a different path",
52675
53752
  context: { policyPath }
52676
53753
  });
52677
53754
  }
52678
53755
  const template = readPolicyExampleYaml();
52679
- fs18.mkdirSync(pathDirname(policyPath), { recursive: true });
52680
- fs18.writeFileSync(policyPath, template, { encoding: "utf-8" });
53756
+ fs19.mkdirSync(pathDirname(policyPath), { recursive: true });
53757
+ fs19.writeFileSync(policyPath, template, { encoding: "utf-8" });
52681
53758
  const structured = {
52682
53759
  policyPath,
52683
53760
  schemaVersion: CURRENT_POLICY_SCHEMA_VERSION,
@@ -52866,7 +53943,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
52866
53943
  let leftSource = "";
52867
53944
  let rightSource = "";
52868
53945
  try {
52869
- leftSource = fs18.readFileSync(left_path, "utf-8");
53946
+ leftSource = fs19.readFileSync(left_path, "utf-8");
52870
53947
  } catch (err) {
52871
53948
  if (err?.code === "ENOENT") {
52872
53949
  return mcpError("usage", 2, `policy file not found: ${left_path}`, {
@@ -52876,7 +53953,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
52876
53953
  return mcpError("runtime", 1, `failed to read ${left_path}: ${String(err)}`);
52877
53954
  }
52878
53955
  try {
52879
- rightSource = fs18.readFileSync(right_path, "utf-8");
53956
+ rightSource = fs19.readFileSync(right_path, "utf-8");
52880
53957
  } catch (err) {
52881
53958
  if (err?.code === "ENOENT") {
52882
53959
  return mcpError("usage", 2, `policy file not found: ${right_path}`, {
@@ -53751,7 +54828,7 @@ process_uptime_seconds ${Math.floor(process.uptime())}
53751
54828
  }
53752
54829
  if (profile) {
53753
54830
  const envCredsPresent = !!(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
53754
- if (!envCredsPresent && !fs18.existsSync(profileFilePath(profile))) {
54831
+ if (!envCredsPresent && !fs19.existsSync(profileFilePath(profile))) {
53755
54832
  res.writeHead(401, { "Content-Type": "application/json" });
53756
54833
  res.end(JSON.stringify({
53757
54834
  jsonrpc: "2.0",
@@ -54357,7 +55434,7 @@ init_cjs_shim();
54357
55434
  init_output();
54358
55435
  init_arg_parsers();
54359
55436
  init_flags();
54360
- import http4 from "node:http";
55437
+ import http6 from "node:http";
54361
55438
  import crypto4 from "node:crypto";
54362
55439
  init_client2();
54363
55440
  init_credential();
@@ -54388,18 +55465,18 @@ var StdoutSink = class {
54388
55465
 
54389
55466
  // src/sinks/file.ts
54390
55467
  init_cjs_shim();
54391
- import fs19 from "node:fs";
55468
+ import fs20 from "node:fs";
54392
55469
  import path15 from "node:path";
54393
55470
  var FileSink = class {
54394
55471
  filePath;
54395
55472
  constructor(filePath) {
54396
55473
  this.filePath = path15.resolve(filePath);
54397
55474
  const dir = path15.dirname(this.filePath);
54398
- if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
55475
+ if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
54399
55476
  }
54400
55477
  async write(event) {
54401
55478
  try {
54402
- fs19.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
55479
+ fs20.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
54403
55480
  } catch {
54404
55481
  }
54405
55482
  }
@@ -54653,7 +55730,7 @@ function parseFilter2(flag) {
54653
55730
  }
54654
55731
  }
54655
55732
  function startReceiver(port, pathMatch, filter, onEvent) {
54656
- const server = http4.createServer((req, res) => {
55733
+ const server = http6.createServer((req, res) => {
54657
55734
  if (req.method !== "POST") {
54658
55735
  res.statusCode = 405;
54659
55736
  res.end("method not allowed");
@@ -55068,10 +56145,10 @@ init_catalog();
55068
56145
  init_config();
55069
56146
  init_cache();
55070
56147
  init_quota();
55071
- import fs22 from "node:fs";
55072
- import os17 from "node:os";
55073
- import path18 from "node:path";
55074
- 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";
55075
56152
 
55076
56153
  // src/commands/agent-bootstrap.ts
55077
56154
  init_cjs_shim();
@@ -55306,7 +56383,7 @@ init_request_context();
55306
56383
 
55307
56384
  // src/lib/daemon-state.ts
55308
56385
  init_cjs_shim();
55309
- import fs20 from "node:fs";
56386
+ import fs21 from "node:fs";
55310
56387
  import os15 from "node:os";
55311
56388
  import path16 from "node:path";
55312
56389
  function getStateDir() {
@@ -55329,15 +56406,15 @@ var DAEMON_LOG_FILE = getDaemonLogFile();
55329
56406
  var DAEMON_STATE_FILE = getDaemonStateFile();
55330
56407
  var HEALTHZ_PID_FILE = getHealthzPidFile();
55331
56408
  function ensureStateDir() {
55332
- fs20.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
56409
+ fs21.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
55333
56410
  }
55334
56411
  function writeDaemonState(state) {
55335
56412
  ensureStateDir();
55336
- fs20.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
56413
+ fs21.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
55337
56414
  }
55338
56415
  function readDaemonState() {
55339
56416
  try {
55340
- const raw = fs20.readFileSync(getDaemonStateFile(), "utf-8");
56417
+ const raw = fs21.readFileSync(getDaemonStateFile(), "utf-8");
55341
56418
  return JSON.parse(raw);
55342
56419
  } catch {
55343
56420
  return null;
@@ -55346,7 +56423,7 @@ function readDaemonState() {
55346
56423
 
55347
56424
  // src/rules/pid-file.ts
55348
56425
  init_cjs_shim();
55349
- import fs21 from "node:fs";
56426
+ import fs22 from "node:fs";
55350
56427
  import os16 from "node:os";
55351
56428
  import path17 from "node:path";
55352
56429
  var DEFAULT_DIR = path17.join(os16.homedir(), ".switchbot");
@@ -55359,13 +56436,13 @@ function getDefaultPidFilePaths() {
55359
56436
  }
55360
56437
  function writePidFile(pidFile, pid = process.pid) {
55361
56438
  const dir = path17.dirname(pidFile);
55362
- fs21.mkdirSync(dir, { recursive: true, mode: 448 });
55363
- fs21.writeFileSync(pidFile, `${pid}
56439
+ fs22.mkdirSync(dir, { recursive: true, mode: 448 });
56440
+ fs22.writeFileSync(pidFile, `${pid}
55364
56441
  `, { mode: 384 });
55365
56442
  }
55366
56443
  function readPidFile(pidFile) {
55367
56444
  try {
55368
- const raw = fs21.readFileSync(pidFile, "utf-8").trim();
56445
+ const raw = fs22.readFileSync(pidFile, "utf-8").trim();
55369
56446
  const n = Number(raw);
55370
56447
  return Number.isInteger(n) && n > 0 ? n : null;
55371
56448
  } catch {
@@ -55375,20 +56452,20 @@ function readPidFile(pidFile) {
55375
56452
  function clearPidFile(pidFile, pid = process.pid) {
55376
56453
  try {
55377
56454
  const existing = readPidFile(pidFile);
55378
- if (existing === pid) fs21.unlinkSync(pidFile);
56455
+ if (existing === pid) fs22.unlinkSync(pidFile);
55379
56456
  } catch {
55380
56457
  }
55381
56458
  }
55382
56459
  function writeReloadSentinel(reloadFile) {
55383
56460
  const dir = path17.dirname(reloadFile);
55384
- fs21.mkdirSync(dir, { recursive: true, mode: 448 });
55385
- fs21.writeFileSync(reloadFile, `${Date.now()}
56461
+ fs22.mkdirSync(dir, { recursive: true, mode: 448 });
56462
+ fs22.writeFileSync(reloadFile, `${Date.now()}
55386
56463
  `, { mode: 384 });
55387
56464
  }
55388
56465
  function consumeReloadSentinel(reloadFile) {
55389
56466
  try {
55390
- if (!fs21.existsSync(reloadFile)) return false;
55391
- fs21.unlinkSync(reloadFile);
56467
+ if (!fs22.existsSync(reloadFile)) return false;
56468
+ fs22.unlinkSync(reloadFile);
55392
56469
  return true;
55393
56470
  } catch {
55394
56471
  return false;
@@ -55459,7 +56536,7 @@ async function checkCredentials() {
55459
56536
  };
55460
56537
  }
55461
56538
  const file2 = configFilePath();
55462
- if (!fs22.existsSync(file2)) {
56539
+ if (!fs24.existsSync(file2)) {
55463
56540
  return {
55464
56541
  name: "credentials",
55465
56542
  status: "fail",
@@ -55474,7 +56551,7 @@ async function checkCredentials() {
55474
56551
  };
55475
56552
  }
55476
56553
  try {
55477
- const raw = fs22.readFileSync(file2, "utf-8");
56554
+ const raw = fs24.readFileSync(file2, "utf-8");
55478
56555
  const cfg = JSON.parse(raw);
55479
56556
  if (!cfg.token || !cfg.secret) {
55480
56557
  return {
@@ -55521,8 +56598,8 @@ async function checkCredentials() {
55521
56598
  }
55522
56599
  }
55523
56600
  function checkProfiles() {
55524
- const dir = path18.join(os17.homedir(), ".switchbot", "profiles");
55525
- if (!fs22.existsSync(dir)) {
56601
+ const dir = path19.join(os18.homedir(), ".switchbot", "profiles");
56602
+ if (!fs24.existsSync(dir)) {
55526
56603
  return { name: "profiles", status: "ok", detail: "no profile dir (default profile only)" };
55527
56604
  }
55528
56605
  const profiles = listProfiles();
@@ -55648,8 +56725,8 @@ function checkCache() {
55648
56725
  }
55649
56726
  }
55650
56727
  function checkQuotaFile() {
55651
- const p2 = path18.join(os17.homedir(), ".switchbot", "quota.json");
55652
- if (!fs22.existsSync(p2)) {
56728
+ const p2 = path19.join(os18.homedir(), ".switchbot", "quota.json");
56729
+ if (!fs24.existsSync(p2)) {
55653
56730
  return {
55654
56731
  name: "quota",
55655
56732
  status: "ok",
@@ -55662,7 +56739,7 @@ function checkQuotaFile() {
55662
56739
  };
55663
56740
  }
55664
56741
  try {
55665
- const raw = fs22.readFileSync(p2, "utf-8");
56742
+ const raw = fs24.readFileSync(p2, "utf-8");
55666
56743
  JSON.parse(raw);
55667
56744
  } catch {
55668
56745
  return {
@@ -55743,8 +56820,8 @@ function checkInventoryConsistency() {
55743
56820
  };
55744
56821
  }
55745
56822
  function checkAudit() {
55746
- const p2 = path18.join(os17.homedir(), ".switchbot", "audit.log");
55747
- if (!fs22.existsSync(p2)) {
56823
+ const p2 = path19.join(os18.homedir(), ".switchbot", "audit.log");
56824
+ if (!fs24.existsSync(p2)) {
55748
56825
  return {
55749
56826
  name: "audit",
55750
56827
  status: "ok",
@@ -55756,7 +56833,7 @@ function checkAudit() {
55756
56833
  };
55757
56834
  }
55758
56835
  try {
55759
- const raw = fs22.readFileSync(p2, "utf-8");
56836
+ const raw = fs24.readFileSync(p2, "utf-8");
55760
56837
  const since = Date.now() - 24 * 60 * 60 * 1e3;
55761
56838
  const recent = [];
55762
56839
  let total = 0;
@@ -55955,14 +57032,14 @@ function checkPathDiscoverability() {
55955
57032
  const binaryName = isWindows ? "switchbot.cmd" : "switchbot";
55956
57033
  let npmBinDir = null;
55957
57034
  try {
55958
- const prefix = execSync("npm prefix -g", { timeout: 4e3, encoding: "utf-8" }).trim();
55959
- 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");
55960
57037
  } catch {
55961
57038
  }
55962
57039
  let binaryOnPath = false;
55963
57040
  let resolvedPath = null;
55964
57041
  try {
55965
- const which = execSync(
57042
+ const which = execSync2(
55966
57043
  isWindows ? `where ${binaryName}` : `which ${binaryName}`,
55967
57044
  { timeout: 3e3, encoding: "utf-8" }
55968
57045
  ).trim().split(/\r?\n/)[0];
@@ -55986,7 +57063,7 @@ function checkPathDiscoverability() {
55986
57063
  };
55987
57064
  }
55988
57065
  const currentPath = process.env.PATH ?? "";
55989
- const missingSegment = npmBinDir && !currentPath.split(path18.delimiter).includes(npmBinDir) ? npmBinDir : null;
57066
+ const missingSegment = npmBinDir && !currentPath.split(path19.delimiter).includes(npmBinDir) ? npmBinDir : null;
55990
57067
  const currentShell = detectShellFlavor();
55991
57068
  const shellFix = buildPathFix(currentShell, missingSegment, npmBinDir);
55992
57069
  return {
@@ -56087,9 +57164,9 @@ function checkMqtt() {
56087
57164
  };
56088
57165
  }
56089
57166
  const file2 = configFilePath();
56090
- if (fs22.existsSync(file2)) {
57167
+ if (fs24.existsSync(file2)) {
56091
57168
  try {
56092
- const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
57169
+ const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
56093
57170
  if (cfg.token && cfg.secret) {
56094
57171
  return {
56095
57172
  name: "mqtt",
@@ -56116,9 +57193,9 @@ async function checkMqttProbe() {
56116
57193
  creds = { token, secret };
56117
57194
  } else {
56118
57195
  const file2 = configFilePath();
56119
- if (fs22.existsSync(file2)) {
57196
+ if (fs24.existsSync(file2)) {
56120
57197
  try {
56121
- const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
57198
+ const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
56122
57199
  if (cfg.token && cfg.secret) {
56123
57200
  creds = { token: cfg.token, secret: cfg.secret };
56124
57201
  }
@@ -56232,6 +57309,112 @@ function checkNotifyConnectivity() {
56232
57309
  detail: { webhookCount: webhookUrls.length, message: `${webhookUrls.length} webhook URL(s) configured (live probe not run \u2014 use --probe to test connectivity)` }
56233
57310
  };
56234
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
+ }
56235
57418
  var CHECK_REGISTRY = [
56236
57419
  { name: "node", description: "Node.js version compatibility", run: () => checkNodeVersion() },
56237
57420
  { name: "path", description: "switchbot binary reachable on PATH", run: () => checkPathDiscoverability() },
@@ -56256,7 +57439,9 @@ var CHECK_REGISTRY = [
56256
57439
  { name: "audit", description: "recent command errors (last 24h)", run: () => checkAudit() },
56257
57440
  { name: "daemon", description: "daemon state file + runtime status", run: () => checkDaemon() },
56258
57441
  { name: "health", description: "health endpoint availability (daemon --healthz-port)", run: () => checkHealthEndpoint() },
56259
- { 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() }
56260
57445
  ];
56261
57446
  function applyFixes(checks, writeOk) {
56262
57447
  const results = [];
@@ -56594,9 +57779,10 @@ init_arg_parsers();
56594
57779
  init_output();
56595
57780
  init_audit();
56596
57781
  init_devices();
56597
- import path19 from "node:path";
56598
- import os18 from "node:os";
56599
- 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");
56600
57786
  function registerHistoryCommand(program3) {
56601
57787
  const history = program3.command("history").description("View and replay SwitchBot commands recorded via --audit-log").addHelpText("after", `
56602
57788
  Every 'devices command' run with --audit-log is appended as JSONL to the
@@ -57454,9 +58640,9 @@ init_load();
57454
58640
  init_validate();
57455
58641
  init_types();
57456
58642
  init_engine();
57457
- import fs24 from "node:fs";
57458
- import os20 from "node:os";
57459
- import path21 from "node:path";
58643
+ import fs27 from "node:fs";
58644
+ import os21 from "node:os";
58645
+ import path23 from "node:path";
57460
58646
 
57461
58647
  // src/rules/conflict-analyzer.ts
57462
58648
  init_cjs_shim();
@@ -57619,9 +58805,9 @@ init_client2();
57619
58805
 
57620
58806
  // src/rules/webhook-token.ts
57621
58807
  init_cjs_shim();
57622
- import fs23 from "node:fs";
57623
- import os19 from "node:os";
57624
- import path20 from "node:path";
58808
+ import fs25 from "node:fs";
58809
+ import os20 from "node:os";
58810
+ import path21 from "node:path";
57625
58811
  import { randomBytes } from "node:crypto";
57626
58812
  var ENV_TOKEN = "SWITCHBOT_WEBHOOK_TOKEN";
57627
58813
  var DEFAULT_FILE = ".switchbot/webhook-token";
@@ -57629,7 +58815,7 @@ var WebhookTokenStore = class {
57629
58815
  filePath;
57630
58816
  envLookup;
57631
58817
  constructor(opts = {}) {
57632
- this.filePath = opts.filePath ?? path20.join(os19.homedir(), DEFAULT_FILE);
58818
+ this.filePath = opts.filePath ?? path21.join(os20.homedir(), DEFAULT_FILE);
57633
58819
  this.envLookup = opts.envLookup ?? (() => process.env[ENV_TOKEN]);
57634
58820
  }
57635
58821
  /**
@@ -57653,7 +58839,7 @@ var WebhookTokenStore = class {
57653
58839
  */
57654
58840
  readFromDisk() {
57655
58841
  try {
57656
- const raw = fs23.readFileSync(this.filePath, "utf-8").trim();
58842
+ const raw = fs25.readFileSync(this.filePath, "utf-8").trim();
57657
58843
  return raw.length > 0 ? raw : null;
57658
58844
  } catch (err) {
57659
58845
  if (err.code === "ENOENT") return null;
@@ -57670,12 +58856,12 @@ var WebhookTokenStore = class {
57670
58856
  return this.filePath;
57671
58857
  }
57672
58858
  writeToDisk(token) {
57673
- const dir = path20.dirname(this.filePath);
57674
- fs23.mkdirSync(dir, { recursive: true });
57675
- fs23.writeFileSync(this.filePath, `${token}
58859
+ const dir = path21.dirname(this.filePath);
58860
+ fs25.mkdirSync(dir, { recursive: true });
58861
+ fs25.writeFileSync(this.filePath, `${token}
57676
58862
  `, { mode: 384 });
57677
58863
  try {
57678
- fs23.chmodSync(this.filePath, 384);
58864
+ fs25.chmodSync(this.filePath, 384);
57679
58865
  } catch {
57680
58866
  }
57681
58867
  }
@@ -57760,18 +58946,19 @@ function aggregateRuleAudits(entries) {
57760
58946
  }
57761
58947
 
57762
58948
  // src/commands/rules.ts
57763
- 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");
57764
58951
  function loadAutomation(policyPathFlag) {
57765
- const path29 = resolvePolicyPath({ flag: policyPathFlag });
58952
+ const path31 = resolvePolicyPath({ flag: policyPathFlag });
57766
58953
  let loaded;
57767
58954
  try {
57768
- loaded = loadPolicyFile(path29);
58955
+ loaded = loadPolicyFile(path31);
57769
58956
  } catch (err) {
57770
58957
  if (err instanceof PolicyFileNotFoundError) {
57771
58958
  exitWithError({
57772
58959
  code: 2,
57773
58960
  kind: "usage",
57774
- message: `policy file not found: ${path29}`,
58961
+ message: `policy file not found: ${path31}`,
57775
58962
  extra: { subKind: "file-not-found" }
57776
58963
  });
57777
58964
  }
@@ -57779,7 +58966,7 @@ function loadAutomation(policyPathFlag) {
57779
58966
  exitWithError({
57780
58967
  code: 3,
57781
58968
  kind: "runtime",
57782
- message: `YAML parse error in ${path29}: ${err.message}`,
58969
+ message: `YAML parse error in ${path31}: ${err.message}`,
57783
58970
  extra: { subKind: "yaml-parse", errors: err.yamlErrors }
57784
58971
  });
57785
58972
  }
@@ -57791,7 +58978,7 @@ function loadAutomation(policyPathFlag) {
57791
58978
  code: 4,
57792
58979
  kind: "runtime",
57793
58980
  message: "policy file failed schema validation. Run `switchbot policy validate` for details.",
57794
- extra: { subKind: "invalid-policy", path: path29 }
58981
+ extra: { subKind: "invalid-policy", path: path31 }
57795
58982
  });
57796
58983
  }
57797
58984
  const data = loaded.data ?? {};
@@ -57805,7 +58992,7 @@ function loadAutomation(policyPathFlag) {
57805
58992
  }
57806
58993
  const rawQH = data.quiet_hours;
57807
58994
  const quietHours = rawQH && typeof rawQH.start === "string" && typeof rawQH.end === "string" ? { start: rawQH.start, end: rawQH.end } : null;
57808
- return { path: path29, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
58995
+ return { path: path31, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
57809
58996
  }
57810
58997
  function describeTrigger(rule) {
57811
58998
  const t = rule.when;
@@ -57940,6 +59127,8 @@ function registerRun(rules) {
57940
59127
  let stopping = false;
57941
59128
  const pidPaths = getDefaultPidFilePaths();
57942
59129
  writePidFile(pidPaths.pidFile);
59130
+ const ipcStartedAt = /* @__PURE__ */ new Date();
59131
+ let ipcServerHandle = null;
57943
59132
  const cleanup = () => {
57944
59133
  clearPidFile(pidPaths.pidFile);
57945
59134
  consumeReloadSentinel(pidPaths.reloadFile);
@@ -57948,6 +59137,12 @@ function registerRun(rules) {
57948
59137
  if (stopping) return;
57949
59138
  stopping = true;
57950
59139
  try {
59140
+ if (ipcServerHandle) {
59141
+ try {
59142
+ await ipcServerHandle.close();
59143
+ } catch {
59144
+ }
59145
+ }
57951
59146
  await engine.stop();
57952
59147
  await client.disconnect();
57953
59148
  } finally {
@@ -58006,6 +59201,34 @@ function registerRun(rules) {
58006
59201
  }
58007
59202
  }, 2e3);
58008
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
+ }
58009
59232
  if (!isJsonMode()) {
58010
59233
  console.error(
58011
59234
  `Rules engine started \u2014 ${engine.getStats().rulesActive} active rule(s), ${opts.dryRun ? "global dry-run" : "live"}.`
@@ -58068,7 +59291,7 @@ function registerTail(rules) {
58068
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) => {
58069
59292
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
58070
59293
  const sinceMs = resolveSinceMs(opts.since);
58071
- const existing = fs24.existsSync(file2) ? readAudit(file2) : [];
59294
+ const existing = fs27.existsSync(file2) ? readAudit(file2) : [];
58072
59295
  const filtered = filterRuleAudits(existing, { sinceMs, ruleName: opts.rule });
58073
59296
  if (isJsonMode()) {
58074
59297
  for (const e of filtered) console.log(JSON.stringify(e));
@@ -58080,7 +59303,7 @@ function registerTail(rules) {
58080
59303
  for (const e of filtered) console.log(formatAuditLine(e));
58081
59304
  }
58082
59305
  if (!opts.follow) return;
58083
- let offset = fs24.existsSync(file2) ? fs24.statSync(file2).size : 0;
59306
+ let offset = fs27.existsSync(file2) ? fs27.statSync(file2).size : 0;
58084
59307
  let buffer = "";
58085
59308
  const emit = (line) => {
58086
59309
  const trimmed = line.trim();
@@ -58097,21 +59320,21 @@ function registerTail(rules) {
58097
59320
  else console.log(formatAuditLine(entry));
58098
59321
  };
58099
59322
  const poll = setInterval(() => {
58100
- if (!fs24.existsSync(file2)) return;
58101
- const size = fs24.statSync(file2).size;
59323
+ if (!fs27.existsSync(file2)) return;
59324
+ const size = fs27.statSync(file2).size;
58102
59325
  if (size < offset) {
58103
59326
  offset = 0;
58104
59327
  buffer = "";
58105
59328
  }
58106
59329
  if (size === offset) return;
58107
- const fd = fs24.openSync(file2, "r");
59330
+ const fd = fs27.openSync(file2, "r");
58108
59331
  try {
58109
59332
  const chunk = Buffer.alloc(size - offset);
58110
- fs24.readSync(fd, chunk, 0, chunk.length, offset);
59333
+ fs27.readSync(fd, chunk, 0, chunk.length, offset);
58111
59334
  offset = size;
58112
59335
  buffer += chunk.toString("utf-8");
58113
59336
  } finally {
58114
- fs24.closeSync(fd);
59337
+ fs27.closeSync(fd);
58115
59338
  }
58116
59339
  let newline = buffer.indexOf("\n");
58117
59340
  while (newline !== -1) {
@@ -58151,7 +59374,7 @@ function formatReplayTable(report) {
58151
59374
  function registerReplay(rules) {
58152
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) => {
58153
59376
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
58154
- const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
59377
+ const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
58155
59378
  const sinceMs = resolveSinceMs(opts.since);
58156
59379
  const filtered = filterRuleAudits(entries, {
58157
59380
  sinceMs,
@@ -58271,7 +59494,7 @@ function registerSuggest(rules) {
58271
59494
  for (const w2 of warnings) process.stderr.write(`warning: ${w2}
58272
59495
  `);
58273
59496
  if (opts.out) {
58274
- fs24.writeFileSync(opts.out, ruleYaml, "utf8");
59497
+ fs27.writeFileSync(opts.out, ruleYaml, "utf8");
58275
59498
  if (!isJsonMode()) console.log(`\u2713 rule YAML written to ${opts.out}`);
58276
59499
  } else if (isJsonMode()) {
58277
59500
  printJson({ rule, rule_yaml: ruleYaml, warnings });
@@ -58346,7 +59569,7 @@ overall: ${overall ? "ok" : "issues found"}`);
58346
59569
  function registerSummary(rules) {
58347
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) => {
58348
59571
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
58349
- const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
59572
+ const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
58350
59573
  const sinceMs = resolveSinceMs(opts.since ?? "24h");
58351
59574
  const filtered = filterRuleAudits(entries, { sinceMs, ruleName: opts.rule });
58352
59575
  const report = aggregateRuleAudits(filtered);
@@ -58377,7 +59600,7 @@ function registerLastFired(rules) {
58377
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) => {
58378
59601
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
58379
59602
  const n = opts.n ?? 10;
58380
- const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
59603
+ const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
58381
59604
  const fires = filterRuleAudits(entries, {
58382
59605
  ruleName: opts.rule,
58383
59606
  kinds: ["rule-fire", "rule-fire-dry"]
@@ -58419,7 +59642,7 @@ function registerExplain(rules) {
58419
59642
  return;
58420
59643
  }
58421
59644
  const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
58422
- const entries = fs24.existsSync(auditFile) ? readAudit(auditFile) : [];
59645
+ const entries = fs27.existsSync(auditFile) ? readAudit(auditFile) : [];
58423
59646
  const fires = filterRuleAudits(entries, { ruleName: name, kinds: ["rule-fire", "rule-fire-dry"] });
58424
59647
  const lastFired = fires.length > 0 ? fires[fires.length - 1].t : null;
58425
59648
  const detail = {
@@ -58460,7 +59683,7 @@ function registerTraceExplain(rules) {
58460
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(
58461
59684
  (fireIdArg, opts) => {
58462
59685
  const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
58463
- if (!fs24.existsSync(auditFile)) {
59686
+ if (!fs27.existsSync(auditFile)) {
58464
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).` });
58465
59688
  return;
58466
59689
  }
@@ -58495,14 +59718,14 @@ function registerSimulate(rules) {
58495
59718
  async (ruleOrPolicy, opts) => {
58496
59719
  let rule;
58497
59720
  const auditLog = opts.auditLog ?? DEFAULT_AUDIT_PATH2;
58498
- if (!fs24.existsSync(ruleOrPolicy)) {
59721
+ if (!fs27.existsSync(ruleOrPolicy)) {
58499
59722
  exitWithError({ code: 2, kind: "usage", message: `File not found: ${ruleOrPolicy}` });
58500
59723
  return;
58501
59724
  }
58502
59725
  let parsed;
58503
59726
  try {
58504
59727
  const { parse: yamlParse5 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
58505
- parsed = yamlParse5(fs24.readFileSync(ruleOrPolicy, "utf-8"));
59728
+ parsed = yamlParse5(fs27.readFileSync(ruleOrPolicy, "utf-8"));
58506
59729
  } catch {
58507
59730
  exitWithError({ code: 2, kind: "usage", message: `Could not parse YAML file: ${ruleOrPolicy}` });
58508
59731
  return;
@@ -58533,7 +59756,7 @@ function registerSimulate(rules) {
58533
59756
  liveLlm: opts.liveLlm ?? false
58534
59757
  });
58535
59758
  if (opts.reportOut) {
58536
- fs24.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
59759
+ fs27.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
58537
59760
  console.log(`Report written to ${opts.reportOut}`);
58538
59761
  }
58539
59762
  if (isJsonMode()) {
@@ -58624,10 +59847,10 @@ init_output();
58624
59847
  init_arg_parsers();
58625
59848
  init_request_context();
58626
59849
  init_keychain();
58627
- import fs25 from "node:fs";
58628
- import path22 from "node:path";
58629
- import os21 from "node:os";
58630
- 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";
58631
59854
  function activeProfile() {
58632
59855
  return getActiveProfile() ?? "default";
58633
59856
  }
@@ -58639,7 +59862,7 @@ function maskValue2(value) {
58639
59862
  return `${head}${"*".repeat(Math.max(4, value.length - 4))}${tail}`;
58640
59863
  }
58641
59864
  async function promptSecret2(question) {
58642
- 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 });
58643
59866
  return new Promise((resolve2) => {
58644
59867
  process.stderr.write(question);
58645
59868
  const stdin = process.stdin;
@@ -58672,7 +59895,7 @@ async function promptSecret2(question) {
58672
59895
  });
58673
59896
  }
58674
59897
  function readStdinFile(filePath) {
58675
- if (!fs25.existsSync(filePath)) {
59898
+ if (!fs28.existsSync(filePath)) {
58676
59899
  exitWithError({
58677
59900
  code: 2,
58678
59901
  kind: "usage",
@@ -58681,7 +59904,7 @@ function readStdinFile(filePath) {
58681
59904
  }
58682
59905
  let parsed;
58683
59906
  try {
58684
- parsed = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
59907
+ parsed = JSON.parse(fs28.readFileSync(filePath, "utf-8"));
58685
59908
  } catch (err) {
58686
59909
  exitWithError({
58687
59910
  code: 2,
@@ -58711,10 +59934,10 @@ function cleanupMigratedSourceFile(sourceFile, parsed) {
58711
59934
  delete next.token;
58712
59935
  delete next.secret;
58713
59936
  if (Object.keys(next).length === 0) {
58714
- fs25.unlinkSync(sourceFile);
59937
+ fs28.unlinkSync(sourceFile);
58715
59938
  return "deleted";
58716
59939
  }
58717
- fs25.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
59940
+ fs28.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
58718
59941
  return "scrubbed";
58719
59942
  }
58720
59943
  function registerAuthCommand(program3) {
@@ -58845,8 +60068,8 @@ function registerAuthCommand(program3) {
58845
60068
  message: `backend "${store.name}" is not writable on this machine`
58846
60069
  });
58847
60070
  }
58848
- const sourceFile = profile === "default" ? path22.join(os21.homedir(), ".switchbot", "config.json") : path22.join(os21.homedir(), ".switchbot", "profiles", `${profile}.json`);
58849
- 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)) {
58850
60073
  exitWithError({
58851
60074
  code: 2,
58852
60075
  kind: "usage",
@@ -58856,7 +60079,7 @@ function registerAuthCommand(program3) {
58856
60079
  }
58857
60080
  let parsed;
58858
60081
  try {
58859
- const raw = JSON.parse(fs25.readFileSync(sourceFile, "utf-8"));
60082
+ const raw = JSON.parse(fs28.readFileSync(sourceFile, "utf-8"));
58860
60083
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
58861
60084
  throw new Error("expected a JSON object");
58862
60085
  }
@@ -58918,8 +60141,8 @@ function registerAuthCommand(program3) {
58918
60141
  init_cjs_shim();
58919
60142
  init_esm();
58920
60143
  init_load();
58921
- import fs28 from "node:fs";
58922
- import path25 from "node:path";
60144
+ import fs31 from "node:fs";
60145
+ import path27 from "node:path";
58923
60146
 
58924
60147
  // src/install/steps.ts
58925
60148
  init_cjs_shim();
@@ -58971,9 +60194,9 @@ init_cjs_shim();
58971
60194
  init_load();
58972
60195
  init_validate();
58973
60196
  init_keychain();
58974
- import fs26 from "node:fs";
58975
- import path23 from "node:path";
58976
- import os22 from "node:os";
60197
+ import fs29 from "node:fs";
60198
+ import path25 from "node:path";
60199
+ import os23 from "node:os";
58977
60200
  function parseMajor(version2) {
58978
60201
  const m2 = /^v?(\d+)\./.exec(version2);
58979
60202
  if (!m2) return null;
@@ -59063,10 +60286,10 @@ async function checkKeychain2() {
59063
60286
  }
59064
60287
  }
59065
60288
  function checkHomeDirWritable() {
59066
- const home = os22.homedir();
59067
- const switchbotDir = path23.join(home, ".switchbot");
60289
+ const home = os23.homedir();
60290
+ const switchbotDir = path25.join(home, ".switchbot");
59068
60291
  try {
59069
- const homeStat = fs26.statSync(home);
60292
+ const homeStat = fs29.statSync(home);
59070
60293
  if (!homeStat.isDirectory()) {
59071
60294
  return {
59072
60295
  name: "home",
@@ -59075,8 +60298,8 @@ function checkHomeDirWritable() {
59075
60298
  hint: "check your HOME/USERPROFILE environment configuration"
59076
60299
  };
59077
60300
  }
59078
- if (fs26.existsSync(switchbotDir)) {
59079
- const sbStat = fs26.statSync(switchbotDir);
60301
+ if (fs29.existsSync(switchbotDir)) {
60302
+ const sbStat = fs29.statSync(switchbotDir);
59080
60303
  if (!sbStat.isDirectory()) {
59081
60304
  return {
59082
60305
  name: "home",
@@ -59085,10 +60308,10 @@ function checkHomeDirWritable() {
59085
60308
  hint: "move the file aside and re-run install"
59086
60309
  };
59087
60310
  }
59088
- fs26.accessSync(switchbotDir, fs26.constants.W_OK);
60311
+ fs29.accessSync(switchbotDir, fs29.constants.W_OK);
59089
60312
  return { name: "home", status: "ok", message: `writable: ${switchbotDir}` };
59090
60313
  }
59091
- fs26.accessSync(home, fs26.constants.W_OK);
60314
+ fs29.accessSync(home, fs29.constants.W_OK);
59092
60315
  return { name: "home", status: "ok", message: `writable: ${home}` };
59093
60316
  } catch (err) {
59094
60317
  return {
@@ -59102,8 +60325,8 @@ function checkHomeDirWritable() {
59102
60325
  function nearestExistingPath(target) {
59103
60326
  let cur = target;
59104
60327
  while (true) {
59105
- if (fs26.existsSync(cur)) return cur;
59106
- const parent = path23.dirname(cur);
60328
+ if (fs29.existsSync(cur)) return cur;
60329
+ const parent = path25.dirname(cur);
59107
60330
  if (parent === cur) return null;
59108
60331
  cur = parent;
59109
60332
  }
@@ -59111,8 +60334,8 @@ function nearestExistingPath(target) {
59111
60334
  function checkAgentSkillDirWritable(opts) {
59112
60335
  const shouldCheck = opts.agent === "claude-code" && (opts.expectSkillLink ?? true);
59113
60336
  if (!shouldCheck) return null;
59114
- const home = os22.homedir();
59115
- const target = path23.join(home, ".claude", "skills");
60337
+ const home = os23.homedir();
60338
+ const target = path25.join(home, ".claude", "skills");
59116
60339
  try {
59117
60340
  const existing = nearestExistingPath(target);
59118
60341
  if (!existing) {
@@ -59123,7 +60346,7 @@ function checkAgentSkillDirWritable(opts) {
59123
60346
  hint: "check your home directory path and permissions"
59124
60347
  };
59125
60348
  }
59126
- const stat = fs26.statSync(existing);
60349
+ const stat = fs29.statSync(existing);
59127
60350
  if (!stat.isDirectory()) {
59128
60351
  return {
59129
60352
  name: "agent-skills-dir",
@@ -59132,7 +60355,7 @@ function checkAgentSkillDirWritable(opts) {
59132
60355
  hint: "move the blocking file aside and re-run install"
59133
60356
  };
59134
60357
  }
59135
- fs26.accessSync(existing, fs26.constants.W_OK);
60358
+ fs29.accessSync(existing, fs29.constants.W_OK);
59136
60359
  return { name: "agent-skills-dir", status: "ok", message: `writable: ${target}` };
59137
60360
  } catch (err) {
59138
60361
  return {
@@ -59157,9 +60380,9 @@ async function runPreflight(options = {}) {
59157
60380
 
59158
60381
  // src/install/default-steps.ts
59159
60382
  init_cjs_shim();
59160
- import fs27 from "node:fs";
59161
- import path24 from "node:path";
59162
- import os23 from "node:os";
60383
+ import fs30 from "node:fs";
60384
+ import path26 from "node:path";
60385
+ import os24 from "node:os";
59163
60386
  import { spawnSync } from "node:child_process";
59164
60387
  init_keychain();
59165
60388
  function stepPromptCredentials() {
@@ -59234,15 +60457,15 @@ function stepScaffoldPolicy() {
59234
60457
  const r = ctx.policyScaffoldResult;
59235
60458
  if (!r || r.skipped) return;
59236
60459
  try {
59237
- fs27.unlinkSync(r.policyPath);
60460
+ fs30.unlinkSync(r.policyPath);
59238
60461
  } catch {
59239
60462
  }
59240
60463
  }
59241
60464
  };
59242
60465
  }
59243
- function skillLinkPathFor(agent, home = os23.homedir()) {
60466
+ function skillLinkPathFor(agent, home = os24.homedir()) {
59244
60467
  if (agent === "claude-code") {
59245
- return path24.join(home, ".claude", "skills", "switchbot");
60468
+ return path26.join(home, ".claude", "skills", "switchbot");
59246
60469
  }
59247
60470
  return null;
59248
60471
  }
@@ -59256,11 +60479,11 @@ function stepSymlinkSkill(opts = {}) {
59256
60479
  ctx.skillRecipePrinted = true;
59257
60480
  return;
59258
60481
  }
59259
- const target = path24.resolve(ctx.skillPath);
59260
- if (!fs27.existsSync(target)) {
60482
+ const target = path26.resolve(ctx.skillPath);
60483
+ if (!fs30.existsSync(target)) {
59261
60484
  throw new Error(`--skill-path does not exist: ${target}`);
59262
60485
  }
59263
- const stat = fs27.statSync(target);
60486
+ const stat = fs30.statSync(target);
59264
60487
  if (!stat.isDirectory()) {
59265
60488
  throw new Error(`--skill-path is not a directory: ${target}`);
59266
60489
  }
@@ -59269,17 +60492,17 @@ function stepSymlinkSkill(opts = {}) {
59269
60492
  ctx.skillRecipePrinted = true;
59270
60493
  return;
59271
60494
  }
59272
- if (!opts.force && !fs27.existsSync(path24.join(target, "SKILL.md"))) {
60495
+ if (!opts.force && !fs30.existsSync(path26.join(target, "SKILL.md"))) {
59273
60496
  throw new Error(
59274
60497
  `${target} does not look like a skill (no SKILL.md at the root). Pass --force if you really mean to link this directory.`
59275
60498
  );
59276
60499
  }
59277
- if (fs27.existsSync(linkPath)) {
59278
- const st = fs27.lstatSync(linkPath);
60500
+ if (fs30.existsSync(linkPath)) {
60501
+ const st = fs30.lstatSync(linkPath);
59279
60502
  if (st.isSymbolicLink()) {
59280
60503
  let existingTarget = null;
59281
60504
  try {
59282
- existingTarget = path24.resolve(path24.dirname(linkPath), fs27.readlinkSync(linkPath));
60505
+ existingTarget = path26.resolve(path26.dirname(linkPath), fs30.readlinkSync(linkPath));
59283
60506
  } catch {
59284
60507
  existingTarget = null;
59285
60508
  }
@@ -59293,23 +60516,23 @@ function stepSymlinkSkill(opts = {}) {
59293
60516
  `${linkPath} already links to ${existingTarget ?? "(unreadable)"}; pass --force to replace it, or run \`switchbot uninstall\` first.`
59294
60517
  );
59295
60518
  }
59296
- fs27.unlinkSync(linkPath);
60519
+ fs30.unlinkSync(linkPath);
59297
60520
  } else {
59298
60521
  throw new Error(
59299
60522
  `${linkPath} exists and is not a symlink; refusing to clobber (move it aside and re-run)`
59300
60523
  );
59301
60524
  }
59302
60525
  }
59303
- fs27.mkdirSync(path24.dirname(linkPath), { recursive: true });
60526
+ fs30.mkdirSync(path26.dirname(linkPath), { recursive: true });
59304
60527
  const linkType = process.platform === "win32" ? "junction" : "dir";
59305
- fs27.symlinkSync(target, linkPath, linkType);
60528
+ fs30.symlinkSync(target, linkPath, linkType);
59306
60529
  ctx.skillLinkPath = linkPath;
59307
60530
  ctx.skillLinkCreated = true;
59308
60531
  },
59309
60532
  undo(ctx) {
59310
60533
  if (!ctx.skillLinkCreated || !ctx.skillLinkPath) return;
59311
60534
  try {
59312
- fs27.unlinkSync(ctx.skillLinkPath);
60535
+ fs30.unlinkSync(ctx.skillLinkPath);
59313
60536
  } catch {
59314
60537
  }
59315
60538
  }
@@ -59452,8 +60675,8 @@ Examples:
59452
60675
  const agent = parseAgent(opts.agent);
59453
60676
  const profile = getActiveProfile() ?? "default";
59454
60677
  const skip = parseSkipList(opts.skip);
59455
- const skillPath = opts.skillPath ? path25.resolve(opts.skillPath) : void 0;
59456
- 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;
59457
60680
  const force = Boolean(opts.force);
59458
60681
  const verify = Boolean(opts.verify);
59459
60682
  const globalOpts = command.parent?.opts() ?? {};
@@ -59497,7 +60720,7 @@ Examples:
59497
60720
  const report = await runInstall(steps, { context: ctx });
59498
60721
  if (report.ok && tokenFile) {
59499
60722
  try {
59500
- fs28.unlinkSync(tokenFile);
60723
+ fs31.unlinkSync(tokenFile);
59501
60724
  } catch {
59502
60725
  }
59503
60726
  }
@@ -59556,8 +60779,8 @@ Examples:
59556
60779
  init_cjs_shim();
59557
60780
  init_esm();
59558
60781
  init_load();
59559
- import fs29 from "node:fs";
59560
- import readline6 from "node:readline";
60782
+ import fs32 from "node:fs";
60783
+ import readline7 from "node:readline";
59561
60784
  init_keychain();
59562
60785
  init_output();
59563
60786
  init_request_context();
@@ -59572,7 +60795,7 @@ function parseAgent2(value) {
59572
60795
  }
59573
60796
  async function prompt(question, defaultYes) {
59574
60797
  if (!process.stdin.isTTY) return defaultYes;
59575
- const rl = readline6.createInterface({ input: process.stdin, output: process.stderr });
60798
+ const rl = readline7.createInterface({ input: process.stdin, output: process.stderr });
59576
60799
  return new Promise((resolve2) => {
59577
60800
  const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
59578
60801
  rl.question(question + suffix, (ans) => {
@@ -59621,10 +60844,10 @@ Examples:
59621
60844
  action: "remove-skill-link",
59622
60845
  detail: skillLink,
59623
60846
  run: async () => {
59624
- if (!fs29.existsSync(skillLink)) {
60847
+ if (!fs32.existsSync(skillLink)) {
59625
60848
  return { action: "remove-skill-link", status: "absent", detail: skillLink };
59626
60849
  }
59627
- const stat = fs29.lstatSync(skillLink);
60850
+ const stat = fs32.lstatSync(skillLink);
59628
60851
  if (!stat.isSymbolicLink()) {
59629
60852
  return {
59630
60853
  action: "remove-skill-link",
@@ -59635,7 +60858,7 @@ Examples:
59635
60858
  const ok = yes ? true : await prompt(`Remove skill link ${skillLink}?`, true);
59636
60859
  if (!ok) return { action: "remove-skill-link", status: "skipped", detail: skillLink };
59637
60860
  try {
59638
- fs29.unlinkSync(skillLink);
60861
+ fs32.unlinkSync(skillLink);
59639
60862
  return { action: "remove-skill-link", status: "removed", detail: skillLink };
59640
60863
  } catch (err) {
59641
60864
  return {
@@ -59690,13 +60913,13 @@ Examples:
59690
60913
  detail: "pass --remove-policy to delete policy.yaml"
59691
60914
  };
59692
60915
  }
59693
- if (!fs29.existsSync(policyPath)) {
60916
+ if (!fs32.existsSync(policyPath)) {
59694
60917
  return { action: "remove-policy", status: "absent", detail: policyPath };
59695
60918
  }
59696
60919
  const ok = yes ? true : await prompt(`Delete policy file ${policyPath}?`, false);
59697
60920
  if (!ok) return { action: "remove-policy", status: "skipped", detail: policyPath };
59698
60921
  try {
59699
- fs29.unlinkSync(policyPath);
60922
+ fs32.unlinkSync(policyPath);
59700
60923
  return { action: "remove-policy", status: "removed", detail: policyPath };
59701
60924
  } catch (err) {
59702
60925
  return {
@@ -59759,9 +60982,9 @@ init_request_context();
59759
60982
  init_output();
59760
60983
  init_flags();
59761
60984
  import { spawn as spawn4, spawnSync as spawnSync2 } from "node:child_process";
59762
- import fs30 from "node:fs";
59763
- import os24 from "node:os";
59764
- import path26 from "node:path";
60985
+ import fs33 from "node:fs";
60986
+ import os25 from "node:os";
60987
+ import path28 from "node:path";
59765
60988
  var DEFAULT_OPENCLAW_URL = "http://localhost:18789";
59766
60989
  function resolveStatusSyncRuntime(options) {
59767
60990
  if (!tryLoadConfig()) {
@@ -59895,14 +61118,14 @@ async function probeStatusSyncStart(options = {}) {
59895
61118
  };
59896
61119
  }
59897
61120
  function resolveStatusSyncPaths(explicitStateDir) {
59898
- const stateDir = path26.resolve(
59899
- 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")
59900
61123
  );
59901
61124
  return {
59902
61125
  stateDir,
59903
- stateFile: path26.join(stateDir, "state.json"),
59904
- stdoutLog: path26.join(stateDir, "stdout.log"),
59905
- 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")
59906
61129
  };
59907
61130
  }
59908
61131
  function buildStatusSyncChildArgs(options) {
@@ -59910,11 +61133,11 @@ function buildStatusSyncChildArgs(options) {
59910
61133
  if (!scriptPath) {
59911
61134
  throw new Error("Cannot determine the current CLI entrypoint path.");
59912
61135
  }
59913
- const args = [path26.resolve(scriptPath)];
61136
+ const args = [path28.resolve(scriptPath)];
59914
61137
  const configPath = getConfigPath();
59915
61138
  const profile = getActiveProfile();
59916
61139
  if (configPath) {
59917
- args.push("--config", path26.resolve(configPath));
61140
+ args.push("--config", path28.resolve(configPath));
59918
61141
  } else if (profile) {
59919
61142
  args.push("--profile", profile);
59920
61143
  }
@@ -59935,7 +61158,7 @@ function buildStatusSyncChildArgs(options) {
59935
61158
  }
59936
61159
  function safeUnlink(filePath) {
59937
61160
  try {
59938
- fs30.unlinkSync(filePath);
61161
+ fs33.unlinkSync(filePath);
59939
61162
  } catch {
59940
61163
  }
59941
61164
  }
@@ -59950,9 +61173,9 @@ function isProcessRunning(pid) {
59950
61173
  }
59951
61174
  }
59952
61175
  function readStateFile(paths) {
59953
- if (!fs30.existsSync(paths.stateFile)) return null;
61176
+ if (!fs33.existsSync(paths.stateFile)) return null;
59954
61177
  try {
59955
- const raw = JSON.parse(fs30.readFileSync(paths.stateFile, "utf-8"));
61178
+ const raw = JSON.parse(fs33.readFileSync(paths.stateFile, "utf-8"));
59956
61179
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
59957
61180
  safeUnlink(paths.stateFile);
59958
61181
  return null;
@@ -60071,14 +61294,14 @@ function startStatusSync(options = {}) {
60071
61294
  }
60072
61295
  stopStatusSync({ stateDir: paths.stateDir });
60073
61296
  }
60074
- fs30.mkdirSync(paths.stateDir, { recursive: true });
61297
+ fs33.mkdirSync(paths.stateDir, { recursive: true });
60075
61298
  const configPath = getConfigPath();
60076
61299
  const command = buildStatusSyncChildArgs(runtime);
60077
61300
  let stdoutFd = null;
60078
61301
  let stderrFd = null;
60079
61302
  try {
60080
- stdoutFd = fs30.openSync(paths.stdoutLog, "a");
60081
- stderrFd = fs30.openSync(paths.stderrLog, "a");
61303
+ stdoutFd = fs33.openSync(paths.stdoutLog, "a");
61304
+ stderrFd = fs33.openSync(paths.stderrLog, "a");
60082
61305
  const child = spawn4(process.execPath, command, {
60083
61306
  detached: true,
60084
61307
  stdio: ["ignore", stdoutFd, stderrFd],
@@ -60096,16 +61319,16 @@ function startStatusSync(options = {}) {
60096
61319
  openclawUrl: runtime.openclawUrl,
60097
61320
  openclawModel: runtime.openclawModel,
60098
61321
  topic: runtime.topic ?? null,
60099
- configPath: configPath ? path26.resolve(configPath) : null,
61322
+ configPath: configPath ? path28.resolve(configPath) : null,
60100
61323
  profile: configPath ? null : getActiveProfile() ?? null,
60101
61324
  stdoutLog: paths.stdoutLog,
60102
61325
  stderrLog: paths.stderrLog
60103
61326
  };
60104
- fs30.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
61327
+ fs33.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
60105
61328
  return toStatus(paths, state, true);
60106
61329
  } finally {
60107
- if (stdoutFd !== null) fs30.closeSync(stdoutFd);
60108
- if (stderrFd !== null) fs30.closeSync(stderrFd);
61330
+ if (stdoutFd !== null) fs33.closeSync(stdoutFd);
61331
+ if (stderrFd !== null) fs33.closeSync(stderrFd);
60109
61332
  }
60110
61333
  }
60111
61334
  async function runStatusSyncForeground(options = {}) {
@@ -60245,17 +61468,17 @@ Examples:
60245
61468
  // src/commands/health.ts
60246
61469
  init_cjs_shim();
60247
61470
  init_output();
60248
- import http5 from "node:http";
61471
+ import http7 from "node:http";
60249
61472
 
60250
61473
  // src/utils/health.ts
60251
61474
  init_cjs_shim();
60252
61475
  init_quota();
60253
61476
  init_audit();
60254
61477
  init_client();
60255
- import fs31 from "node:fs";
60256
- import os25 from "node:os";
60257
- import path27 from "node:path";
60258
- 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");
60259
61482
  var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
60260
61483
  var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set([161, 171, 190]);
60261
61484
  function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
@@ -60277,7 +61500,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
60277
61500
  status: pct >= 90 ? "critical" : pct >= 70 ? "warn" : "ok"
60278
61501
  };
60279
61502
  let auditHealth;
60280
- if (!fs31.existsSync(auditPath)) {
61503
+ if (!fs34.existsSync(auditPath)) {
60281
61504
  auditHealth = {
60282
61505
  present: false,
60283
61506
  recentErrors: 0,
@@ -60447,7 +61670,7 @@ Example:
60447
61670
  const opts = cmd.optsWithGlobals();
60448
61671
  const port = parseInt(opts.port, 10);
60449
61672
  const handler = createHealthHandler(opts.auditLog);
60450
- const server = http5.createServer(handler);
61673
+ const server = http7.createServer(handler);
60451
61674
  server.on("error", (err) => {
60452
61675
  if (err.code === "EADDRINUSE") {
60453
61676
  handleError(Object.assign(new Error(`Port ${port} is already in use. Choose a different port with --port.`), { code: err.code }));
@@ -60478,13 +61701,13 @@ Example:
60478
61701
  init_cjs_shim();
60479
61702
  init_output();
60480
61703
  init_source();
60481
- import https4 from "node:https";
61704
+ import https6 from "node:https";
60482
61705
  var pkgName = "@switchbot/openapi-cli";
60483
61706
  function fetchLatestVersion(packageName, timeoutMs = 8e3) {
60484
61707
  const encoded = packageName.replace("/", "%2F");
60485
61708
  const url2 = `https://registry.npmjs.org/${encoded}/latest`;
60486
61709
  return new Promise((resolve2, reject) => {
60487
- const req = https4.get(url2, { timeout: timeoutMs }, (res) => {
61710
+ const req = https6.get(url2, { timeout: timeoutMs }, (res) => {
60488
61711
  const chunks = [];
60489
61712
  res.on("data", (c) => chunks.push(c));
60490
61713
  res.on("end", () => {
@@ -60581,8 +61804,8 @@ function registerUpgradeCheckCommand(program3) {
60581
61804
  init_cjs_shim();
60582
61805
  init_output();
60583
61806
  import { spawn as spawn5 } from "node:child_process";
60584
- import fs32 from "node:fs";
60585
- import path28 from "node:path";
61807
+ import fs35 from "node:fs";
61808
+ import path30 from "node:path";
60586
61809
  import { fileURLToPath as fileURLToPath3 } from "node:url";
60587
61810
  init_arg_parsers();
60588
61811
  init_source();
@@ -60644,7 +61867,7 @@ function persistState(partial2) {
60644
61867
  }
60645
61868
  function readLastLines(filePath, n = 20) {
60646
61869
  try {
60647
- const content = fs32.readFileSync(filePath, "utf-8");
61870
+ const content = fs35.readFileSync(filePath, "utf-8");
60648
61871
  const lines = content.split("\n");
60649
61872
  return lines.slice(Math.max(0, lines.length - n)).join("\n").trim();
60650
61873
  } catch {
@@ -60734,10 +61957,10 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60734
61957
  }
60735
61958
  }
60736
61959
  const thisFile = fileURLToPath3(import.meta.url);
60737
- 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");
60738
61961
  const args = ["rules", "run"];
60739
61962
  if (opts.policy) args.push(opts.policy);
60740
- fs32.mkdirSync(path28.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
61963
+ fs35.mkdirSync(path30.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
60741
61964
  persistState({
60742
61965
  status: "starting",
60743
61966
  pid: null,
@@ -60749,14 +61972,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60749
61972
  healthzPid: null,
60750
61973
  healthzPidFile: HEALTHZ_PID_FILE
60751
61974
  });
60752
- const logFd = fs32.openSync(DAEMON_LOG_FILE, "a");
61975
+ const logFd = fs35.openSync(DAEMON_LOG_FILE, "a");
60753
61976
  const child = spawn5(process.execPath, [cliEntry, ...args], {
60754
61977
  detached: true,
60755
61978
  stdio: ["ignore", logFd, logFd],
60756
61979
  env: { ...process.env }
60757
61980
  });
60758
61981
  child.unref();
60759
- fs32.closeSync(logFd);
61982
+ fs35.closeSync(logFd);
60760
61983
  await probeLiveness({
60761
61984
  child,
60762
61985
  delayMs: 300,
@@ -60779,14 +62002,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60779
62002
  let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
60780
62003
  if (healthzPort !== null) {
60781
62004
  const healthArgs = ["health", "serve", "--port", String(healthzPort)];
60782
- const healthLogFd = fs32.openSync(DAEMON_LOG_FILE, "a");
62005
+ const healthLogFd = fs35.openSync(DAEMON_LOG_FILE, "a");
60783
62006
  const healthChild = spawn5(process.execPath, [cliEntry, ...healthArgs], {
60784
62007
  detached: true,
60785
62008
  stdio: ["ignore", healthLogFd, healthLogFd],
60786
62009
  env: { ...process.env }
60787
62010
  });
60788
62011
  healthChild.unref();
60789
- fs32.closeSync(healthLogFd);
62012
+ fs35.closeSync(healthLogFd);
60790
62013
  if (healthChild.pid) {
60791
62014
  const healthAlive = await probeLiveness({ child: healthChild, delayMs: 200, fatal: false });
60792
62015
  if (healthAlive) {
@@ -60849,11 +62072,11 @@ The daemon reads the same policy file as \`switchbot rules run\`.
60849
62072
  });
60850
62073
  }
60851
62074
  try {
60852
- fs32.unlinkSync(DAEMON_PID_FILE);
62075
+ fs35.unlinkSync(DAEMON_PID_FILE);
60853
62076
  } catch {
60854
62077
  }
60855
62078
  try {
60856
- fs32.unlinkSync(HEALTHZ_PID_FILE);
62079
+ fs35.unlinkSync(HEALTHZ_PID_FILE);
60857
62080
  } catch {
60858
62081
  }
60859
62082
  persistState({