@switchbot/openapi-cli 3.3.3 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 path28 = __require("node:path");
993
- var fs31 = __require("node:fs");
992
+ var path29 = __require("node:path");
993
+ var fs33 = __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 = path28.resolve(baseDir, baseName);
1926
- if (fs31.existsSync(localBin)) return localBin;
1927
- if (sourceExt.includes(path28.extname(baseName))) return void 0;
1925
+ const localBin = path29.resolve(baseDir, baseName);
1926
+ if (fs33.existsSync(localBin)) return localBin;
1927
+ if (sourceExt.includes(path29.extname(baseName))) return void 0;
1928
1928
  const foundExt = sourceExt.find(
1929
- (ext) => fs31.existsSync(`${localBin}${ext}`)
1929
+ (ext) => fs33.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 = fs31.realpathSync(this._scriptPath);
1941
+ resolvedScriptPath = fs33.realpathSync(this._scriptPath);
1942
1942
  } catch (err) {
1943
1943
  resolvedScriptPath = this._scriptPath;
1944
1944
  }
1945
- executableDir = path28.resolve(
1946
- path28.dirname(resolvedScriptPath),
1945
+ executableDir = path29.resolve(
1946
+ path29.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 = path28.basename(
1953
+ const legacyName = path29.basename(
1954
1954
  this._scriptPath,
1955
- path28.extname(this._scriptPath)
1955
+ path29.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(path28.extname(executableFile));
1966
+ launchWithNode = sourceExt.includes(path29.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 = path28.basename(filename, path28.extname(filename));
2806
+ this._name = path29.basename(filename, path29.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(path29) {
2821
- if (path29 === void 0) return this._executableDir;
2822
- this._executableDir = path29;
2820
+ executableDir(path30) {
2821
+ if (path30 === void 0) return this._executableDir;
2822
+ this._executableDir = path30;
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 os25 = __require("os");
4343
+ var os26 = __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 = os25.release().split(".");
4381
+ var osRelease = os26.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
  }
@@ -8676,6 +8676,18 @@ function writeAudit(entry) {
8676
8676
  } catch {
8677
8677
  }
8678
8678
  }
8679
+ function writeEvaluateTrace(record2, filePath) {
8680
+ const file2 = filePath ?? resolveAuditPath();
8681
+ if (!file2) return;
8682
+ const dir = path8.dirname(file2);
8683
+ try {
8684
+ if (!fs8.existsSync(dir)) {
8685
+ fs8.mkdirSync(dir, { recursive: true });
8686
+ }
8687
+ fs8.appendFileSync(file2, JSON.stringify({ auditVersion: AUDIT_VERSION, ...record2 }) + "\n");
8688
+ } catch {
8689
+ }
8690
+ }
8679
8691
  function readAudit(file2) {
8680
8692
  if (!fs8.existsSync(file2)) return [];
8681
8693
  const raw = fs8.readFileSync(file2, "utf-8");
@@ -9428,17 +9440,17 @@ var require_visit = __commonJS({
9428
9440
  visit.BREAK = BREAK;
9429
9441
  visit.SKIP = SKIP;
9430
9442
  visit.REMOVE = REMOVE;
9431
- function visit_(key, node, visitor, path28) {
9432
- const ctrl = callVisitor(key, node, visitor, path28);
9443
+ function visit_(key, node, visitor, path29) {
9444
+ const ctrl = callVisitor(key, node, visitor, path29);
9433
9445
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
9434
- replaceNode(key, path28, ctrl);
9435
- return visit_(key, ctrl, visitor, path28);
9446
+ replaceNode(key, path29, ctrl);
9447
+ return visit_(key, ctrl, visitor, path29);
9436
9448
  }
9437
9449
  if (typeof ctrl !== "symbol") {
9438
9450
  if (identity.isCollection(node)) {
9439
- path28 = Object.freeze(path28.concat(node));
9451
+ path29 = Object.freeze(path29.concat(node));
9440
9452
  for (let i = 0; i < node.items.length; ++i) {
9441
- const ci = visit_(i, node.items[i], visitor, path28);
9453
+ const ci = visit_(i, node.items[i], visitor, path29);
9442
9454
  if (typeof ci === "number")
9443
9455
  i = ci - 1;
9444
9456
  else if (ci === BREAK)
@@ -9449,13 +9461,13 @@ var require_visit = __commonJS({
9449
9461
  }
9450
9462
  }
9451
9463
  } else if (identity.isPair(node)) {
9452
- path28 = Object.freeze(path28.concat(node));
9453
- const ck = visit_("key", node.key, visitor, path28);
9464
+ path29 = Object.freeze(path29.concat(node));
9465
+ const ck = visit_("key", node.key, visitor, path29);
9454
9466
  if (ck === BREAK)
9455
9467
  return BREAK;
9456
9468
  else if (ck === REMOVE)
9457
9469
  node.key = null;
9458
- const cv = visit_("value", node.value, visitor, path28);
9470
+ const cv = visit_("value", node.value, visitor, path29);
9459
9471
  if (cv === BREAK)
9460
9472
  return BREAK;
9461
9473
  else if (cv === REMOVE)
@@ -9476,17 +9488,17 @@ var require_visit = __commonJS({
9476
9488
  visitAsync.BREAK = BREAK;
9477
9489
  visitAsync.SKIP = SKIP;
9478
9490
  visitAsync.REMOVE = REMOVE;
9479
- async function visitAsync_(key, node, visitor, path28) {
9480
- const ctrl = await callVisitor(key, node, visitor, path28);
9491
+ async function visitAsync_(key, node, visitor, path29) {
9492
+ const ctrl = await callVisitor(key, node, visitor, path29);
9481
9493
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
9482
- replaceNode(key, path28, ctrl);
9483
- return visitAsync_(key, ctrl, visitor, path28);
9494
+ replaceNode(key, path29, ctrl);
9495
+ return visitAsync_(key, ctrl, visitor, path29);
9484
9496
  }
9485
9497
  if (typeof ctrl !== "symbol") {
9486
9498
  if (identity.isCollection(node)) {
9487
- path28 = Object.freeze(path28.concat(node));
9499
+ path29 = Object.freeze(path29.concat(node));
9488
9500
  for (let i = 0; i < node.items.length; ++i) {
9489
- const ci = await visitAsync_(i, node.items[i], visitor, path28);
9501
+ const ci = await visitAsync_(i, node.items[i], visitor, path29);
9490
9502
  if (typeof ci === "number")
9491
9503
  i = ci - 1;
9492
9504
  else if (ci === BREAK)
@@ -9497,13 +9509,13 @@ var require_visit = __commonJS({
9497
9509
  }
9498
9510
  }
9499
9511
  } else if (identity.isPair(node)) {
9500
- path28 = Object.freeze(path28.concat(node));
9501
- const ck = await visitAsync_("key", node.key, visitor, path28);
9512
+ path29 = Object.freeze(path29.concat(node));
9513
+ const ck = await visitAsync_("key", node.key, visitor, path29);
9502
9514
  if (ck === BREAK)
9503
9515
  return BREAK;
9504
9516
  else if (ck === REMOVE)
9505
9517
  node.key = null;
9506
- const cv = await visitAsync_("value", node.value, visitor, path28);
9518
+ const cv = await visitAsync_("value", node.value, visitor, path29);
9507
9519
  if (cv === BREAK)
9508
9520
  return BREAK;
9509
9521
  else if (cv === REMOVE)
@@ -9530,23 +9542,23 @@ var require_visit = __commonJS({
9530
9542
  }
9531
9543
  return visitor;
9532
9544
  }
9533
- function callVisitor(key, node, visitor, path28) {
9545
+ function callVisitor(key, node, visitor, path29) {
9534
9546
  if (typeof visitor === "function")
9535
- return visitor(key, node, path28);
9547
+ return visitor(key, node, path29);
9536
9548
  if (identity.isMap(node))
9537
- return visitor.Map?.(key, node, path28);
9549
+ return visitor.Map?.(key, node, path29);
9538
9550
  if (identity.isSeq(node))
9539
- return visitor.Seq?.(key, node, path28);
9551
+ return visitor.Seq?.(key, node, path29);
9540
9552
  if (identity.isPair(node))
9541
- return visitor.Pair?.(key, node, path28);
9553
+ return visitor.Pair?.(key, node, path29);
9542
9554
  if (identity.isScalar(node))
9543
- return visitor.Scalar?.(key, node, path28);
9555
+ return visitor.Scalar?.(key, node, path29);
9544
9556
  if (identity.isAlias(node))
9545
- return visitor.Alias?.(key, node, path28);
9557
+ return visitor.Alias?.(key, node, path29);
9546
9558
  return void 0;
9547
9559
  }
9548
- function replaceNode(key, path28, node) {
9549
- const parent = path28[path28.length - 1];
9560
+ function replaceNode(key, path29, node) {
9561
+ const parent = path29[path29.length - 1];
9550
9562
  if (identity.isCollection(parent)) {
9551
9563
  parent.items[key] = node;
9552
9564
  } else if (identity.isPair(parent)) {
@@ -10163,10 +10175,10 @@ var require_Collection = __commonJS({
10163
10175
  var createNode = require_createNode();
10164
10176
  var identity = require_identity();
10165
10177
  var Node = require_Node();
10166
- function collectionFromPath(schema2, path28, value) {
10178
+ function collectionFromPath(schema2, path29, value) {
10167
10179
  let v2 = value;
10168
- for (let i = path28.length - 1; i >= 0; --i) {
10169
- const k2 = path28[i];
10180
+ for (let i = path29.length - 1; i >= 0; --i) {
10181
+ const k2 = path29[i];
10170
10182
  if (typeof k2 === "number" && Number.isInteger(k2) && k2 >= 0) {
10171
10183
  const a = [];
10172
10184
  a[k2] = v2;
@@ -10185,7 +10197,7 @@ var require_Collection = __commonJS({
10185
10197
  sourceObjects: /* @__PURE__ */ new Map()
10186
10198
  });
10187
10199
  }
10188
- var isEmptyPath = (path28) => path28 == null || typeof path28 === "object" && !!path28[Symbol.iterator]().next().done;
10200
+ var isEmptyPath = (path29) => path29 == null || typeof path29 === "object" && !!path29[Symbol.iterator]().next().done;
10189
10201
  var Collection = class extends Node.NodeBase {
10190
10202
  constructor(type2, schema2) {
10191
10203
  super(type2);
@@ -10215,11 +10227,11 @@ var require_Collection = __commonJS({
10215
10227
  * be a Pair instance or a `{ key, value }` object, which may not have a key
10216
10228
  * that already exists in the map.
10217
10229
  */
10218
- addIn(path28, value) {
10219
- if (isEmptyPath(path28))
10230
+ addIn(path29, value) {
10231
+ if (isEmptyPath(path29))
10220
10232
  this.add(value);
10221
10233
  else {
10222
- const [key, ...rest] = path28;
10234
+ const [key, ...rest] = path29;
10223
10235
  const node = this.get(key, true);
10224
10236
  if (identity.isCollection(node))
10225
10237
  node.addIn(rest, value);
@@ -10233,8 +10245,8 @@ var require_Collection = __commonJS({
10233
10245
  * Removes a value from the collection.
10234
10246
  * @returns `true` if the item was found and removed.
10235
10247
  */
10236
- deleteIn(path28) {
10237
- const [key, ...rest] = path28;
10248
+ deleteIn(path29) {
10249
+ const [key, ...rest] = path29;
10238
10250
  if (rest.length === 0)
10239
10251
  return this.delete(key);
10240
10252
  const node = this.get(key, true);
@@ -10248,8 +10260,8 @@ var require_Collection = __commonJS({
10248
10260
  * scalar values from their surrounding node; to disable set `keepScalar` to
10249
10261
  * `true` (collections are always returned intact).
10250
10262
  */
10251
- getIn(path28, keepScalar) {
10252
- const [key, ...rest] = path28;
10263
+ getIn(path29, keepScalar) {
10264
+ const [key, ...rest] = path29;
10253
10265
  const node = this.get(key, true);
10254
10266
  if (rest.length === 0)
10255
10267
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -10267,8 +10279,8 @@ var require_Collection = __commonJS({
10267
10279
  /**
10268
10280
  * Checks if the collection includes a value with the key `key`.
10269
10281
  */
10270
- hasIn(path28) {
10271
- const [key, ...rest] = path28;
10282
+ hasIn(path29) {
10283
+ const [key, ...rest] = path29;
10272
10284
  if (rest.length === 0)
10273
10285
  return this.has(key);
10274
10286
  const node = this.get(key, true);
@@ -10278,8 +10290,8 @@ var require_Collection = __commonJS({
10278
10290
  * Sets a value in this collection. For `!!set`, `value` needs to be a
10279
10291
  * boolean to add/remove the item from the set.
10280
10292
  */
10281
- setIn(path28, value) {
10282
- const [key, ...rest] = path28;
10293
+ setIn(path29, value) {
10294
+ const [key, ...rest] = path29;
10283
10295
  if (rest.length === 0) {
10284
10296
  this.set(key, value);
10285
10297
  } else {
@@ -12826,9 +12838,9 @@ var require_Document = __commonJS({
12826
12838
  this.contents.add(value);
12827
12839
  }
12828
12840
  /** Adds a value to the document. */
12829
- addIn(path28, value) {
12841
+ addIn(path29, value) {
12830
12842
  if (assertCollection(this.contents))
12831
- this.contents.addIn(path28, value);
12843
+ this.contents.addIn(path29, value);
12832
12844
  }
12833
12845
  /**
12834
12846
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -12903,14 +12915,14 @@ var require_Document = __commonJS({
12903
12915
  * Removes a value from the document.
12904
12916
  * @returns `true` if the item was found and removed.
12905
12917
  */
12906
- deleteIn(path28) {
12907
- if (Collection.isEmptyPath(path28)) {
12918
+ deleteIn(path29) {
12919
+ if (Collection.isEmptyPath(path29)) {
12908
12920
  if (this.contents == null)
12909
12921
  return false;
12910
12922
  this.contents = null;
12911
12923
  return true;
12912
12924
  }
12913
- return assertCollection(this.contents) ? this.contents.deleteIn(path28) : false;
12925
+ return assertCollection(this.contents) ? this.contents.deleteIn(path29) : false;
12914
12926
  }
12915
12927
  /**
12916
12928
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -12925,10 +12937,10 @@ var require_Document = __commonJS({
12925
12937
  * scalar values from their surrounding node; to disable set `keepScalar` to
12926
12938
  * `true` (collections are always returned intact).
12927
12939
  */
12928
- getIn(path28, keepScalar) {
12929
- if (Collection.isEmptyPath(path28))
12940
+ getIn(path29, keepScalar) {
12941
+ if (Collection.isEmptyPath(path29))
12930
12942
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
12931
- return identity.isCollection(this.contents) ? this.contents.getIn(path28, keepScalar) : void 0;
12943
+ return identity.isCollection(this.contents) ? this.contents.getIn(path29, keepScalar) : void 0;
12932
12944
  }
12933
12945
  /**
12934
12946
  * Checks if the document includes a value with the key `key`.
@@ -12939,10 +12951,10 @@ var require_Document = __commonJS({
12939
12951
  /**
12940
12952
  * Checks if the document includes a value at `path`.
12941
12953
  */
12942
- hasIn(path28) {
12943
- if (Collection.isEmptyPath(path28))
12954
+ hasIn(path29) {
12955
+ if (Collection.isEmptyPath(path29))
12944
12956
  return this.contents !== void 0;
12945
- return identity.isCollection(this.contents) ? this.contents.hasIn(path28) : false;
12957
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path29) : false;
12946
12958
  }
12947
12959
  /**
12948
12960
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -12959,13 +12971,13 @@ var require_Document = __commonJS({
12959
12971
  * Sets a value in this document. For `!!set`, `value` needs to be a
12960
12972
  * boolean to add/remove the item from the set.
12961
12973
  */
12962
- setIn(path28, value) {
12963
- if (Collection.isEmptyPath(path28)) {
12974
+ setIn(path29, value) {
12975
+ if (Collection.isEmptyPath(path29)) {
12964
12976
  this.contents = value;
12965
12977
  } else if (this.contents == null) {
12966
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path28), value);
12978
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path29), value);
12967
12979
  } else if (assertCollection(this.contents)) {
12968
- this.contents.setIn(path28, value);
12980
+ this.contents.setIn(path29, value);
12969
12981
  }
12970
12982
  }
12971
12983
  /**
@@ -14942,9 +14954,9 @@ var require_cst_visit = __commonJS({
14942
14954
  visit.BREAK = BREAK;
14943
14955
  visit.SKIP = SKIP;
14944
14956
  visit.REMOVE = REMOVE;
14945
- visit.itemAtPath = (cst, path28) => {
14957
+ visit.itemAtPath = (cst, path29) => {
14946
14958
  let item = cst;
14947
- for (const [field, index] of path28) {
14959
+ for (const [field, index] of path29) {
14948
14960
  const tok = item?.[field];
14949
14961
  if (tok && "items" in tok) {
14950
14962
  item = tok.items[index];
@@ -14953,23 +14965,23 @@ var require_cst_visit = __commonJS({
14953
14965
  }
14954
14966
  return item;
14955
14967
  };
14956
- visit.parentCollection = (cst, path28) => {
14957
- const parent = visit.itemAtPath(cst, path28.slice(0, -1));
14958
- const field = path28[path28.length - 1][0];
14968
+ visit.parentCollection = (cst, path29) => {
14969
+ const parent = visit.itemAtPath(cst, path29.slice(0, -1));
14970
+ const field = path29[path29.length - 1][0];
14959
14971
  const coll = parent?.[field];
14960
14972
  if (coll && "items" in coll)
14961
14973
  return coll;
14962
14974
  throw new Error("Parent collection not found");
14963
14975
  };
14964
- function _visit(path28, item, visitor) {
14965
- let ctrl = visitor(item, path28);
14976
+ function _visit(path29, item, visitor) {
14977
+ let ctrl = visitor(item, path29);
14966
14978
  if (typeof ctrl === "symbol")
14967
14979
  return ctrl;
14968
14980
  for (const field of ["key", "value"]) {
14969
14981
  const token = item[field];
14970
14982
  if (token && "items" in token) {
14971
14983
  for (let i = 0; i < token.items.length; ++i) {
14972
- const ci = _visit(Object.freeze(path28.concat([[field, i]])), token.items[i], visitor);
14984
+ const ci = _visit(Object.freeze(path29.concat([[field, i]])), token.items[i], visitor);
14973
14985
  if (typeof ci === "number")
14974
14986
  i = ci - 1;
14975
14987
  else if (ci === BREAK)
@@ -14980,10 +14992,10 @@ var require_cst_visit = __commonJS({
14980
14992
  }
14981
14993
  }
14982
14994
  if (typeof ctrl === "function" && field === "key")
14983
- ctrl = ctrl(item, path28);
14995
+ ctrl = ctrl(item, path29);
14984
14996
  }
14985
14997
  }
14986
- return typeof ctrl === "function" ? ctrl(item, path28) : ctrl;
14998
+ return typeof ctrl === "function" ? ctrl(item, path29) : ctrl;
14987
14999
  }
14988
15000
  exports.visit = visit;
14989
15001
  }
@@ -16272,14 +16284,14 @@ var require_parser = __commonJS({
16272
16284
  case "scalar":
16273
16285
  case "single-quoted-scalar":
16274
16286
  case "double-quoted-scalar": {
16275
- const fs31 = this.flowScalar(this.type);
16287
+ const fs33 = this.flowScalar(this.type);
16276
16288
  if (atNextItem || it.value) {
16277
- map3.items.push({ start, key: fs31, sep: [] });
16289
+ map3.items.push({ start, key: fs33, sep: [] });
16278
16290
  this.onKeyLine = true;
16279
16291
  } else if (it.sep) {
16280
- this.stack.push(fs31);
16292
+ this.stack.push(fs33);
16281
16293
  } else {
16282
- Object.assign(it, { key: fs31, sep: [] });
16294
+ Object.assign(it, { key: fs33, sep: [] });
16283
16295
  this.onKeyLine = true;
16284
16296
  }
16285
16297
  return;
@@ -16407,13 +16419,13 @@ var require_parser = __commonJS({
16407
16419
  case "scalar":
16408
16420
  case "single-quoted-scalar":
16409
16421
  case "double-quoted-scalar": {
16410
- const fs31 = this.flowScalar(this.type);
16422
+ const fs33 = this.flowScalar(this.type);
16411
16423
  if (!it || it.value)
16412
- fc.items.push({ start: [], key: fs31, sep: [] });
16424
+ fc.items.push({ start: [], key: fs33, sep: [] });
16413
16425
  else if (it.sep)
16414
- this.stack.push(fs31);
16426
+ this.stack.push(fs33);
16415
16427
  else
16416
- Object.assign(it, { key: fs31, sep: [] });
16428
+ Object.assign(it, { key: fs33, sep: [] });
16417
16429
  return;
16418
16430
  }
16419
16431
  case "flow-map-end":
@@ -16724,6 +16736,14 @@ var require_dist = __commonJS({
16724
16736
  });
16725
16737
 
16726
16738
  // src/policy/load.ts
16739
+ var load_exports = {};
16740
+ __export(load_exports, {
16741
+ DEFAULT_POLICY_PATH: () => DEFAULT_POLICY_PATH,
16742
+ PolicyFileNotFoundError: () => PolicyFileNotFoundError,
16743
+ PolicyYamlParseError: () => PolicyYamlParseError,
16744
+ loadPolicyFile: () => loadPolicyFile,
16745
+ resolvePolicyPath: () => resolvePolicyPath
16746
+ });
16727
16747
  import { readFileSync } from "node:fs";
16728
16748
  import { homedir } from "node:os";
16729
16749
  import { join, resolve } from "node:path";
@@ -20004,8 +20024,8 @@ var require_utils2 = __commonJS({
20004
20024
  }
20005
20025
  return ind;
20006
20026
  }
20007
- function removeDotSegments(path28) {
20008
- let input = path28;
20027
+ function removeDotSegments(path29) {
20028
+ let input = path29;
20009
20029
  const output = [];
20010
20030
  let nextSlash = -1;
20011
20031
  let len = 0;
@@ -20205,8 +20225,8 @@ var require_schemes = __commonJS({
20205
20225
  wsComponent.secure = void 0;
20206
20226
  }
20207
20227
  if (wsComponent.resourceName) {
20208
- const [path28, query] = wsComponent.resourceName.split("?");
20209
- wsComponent.path = path28 && path28 !== "/" ? path28 : void 0;
20228
+ const [path29, query] = wsComponent.resourceName.split("?");
20229
+ wsComponent.path = path29 && path29 !== "/" ? path29 : void 0;
20210
20230
  wsComponent.query = query;
20211
20231
  wsComponent.resourceName = void 0;
20212
20232
  }
@@ -24010,6 +24030,9 @@ function isAnyCondition(c) {
24010
24030
  function isNotCondition(c) {
24011
24031
  return c.not !== void 0 && !Array.isArray(c.not);
24012
24032
  }
24033
+ function isLlmCondition(c) {
24034
+ return c.llm !== void 0 && typeof c.llm === "object";
24035
+ }
24013
24036
  var init_types = __esm({
24014
24037
  "src/rules/types.ts"() {
24015
24038
  "use strict";
@@ -24293,7 +24316,7 @@ function locateError(doc, lineCounter, err) {
24293
24316
  return { line: pos.line, col: pos.col };
24294
24317
  }
24295
24318
  function humanMessage(err) {
24296
- const path28 = err.instancePath || "(root)";
24319
+ const path29 = err.instancePath || "(root)";
24297
24320
  switch (err.keyword) {
24298
24321
  case "required":
24299
24322
  return `missing required property "${err.params.missingProperty}"`;
@@ -24301,21 +24324,21 @@ function humanMessage(err) {
24301
24324
  return `unknown property "${err.params.additionalProperty}"`;
24302
24325
  case "dependentRequired": {
24303
24326
  const { property, missingProperty } = err.params;
24304
- const parent = path28 === "(root)" ? "" : `${path28}: `;
24327
+ const parent = path29 === "(root)" ? "" : `${path29}: `;
24305
24328
  return `${parent}when "${property}" is set, "${missingProperty}" is also required`;
24306
24329
  }
24307
24330
  case "pattern":
24308
- return `${path28} does not match pattern ${err.params.pattern}`;
24331
+ return `${path29} does not match pattern ${err.params.pattern}`;
24309
24332
  case "const":
24310
- return `${path28} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
24333
+ return `${path29} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
24311
24334
  case "enum":
24312
- return `${path28} must be one of ${JSON.stringify(err.params.allowedValues)}`;
24335
+ return `${path29} must be one of ${JSON.stringify(err.params.allowedValues)}`;
24313
24336
  case "type":
24314
- return `${path28} must be ${err.params.type}`;
24337
+ return `${path29} must be ${err.params.type}`;
24315
24338
  case "not":
24316
- return `${path28} is not allowed here`;
24339
+ return `${path29} is not allowed here`;
24317
24340
  default:
24318
- return `${path28} ${err.message ?? "is invalid"}`;
24341
+ return `${path29} ${err.message ?? "is invalid"}`;
24319
24342
  }
24320
24343
  }
24321
24344
  function hintFor(err) {
@@ -24371,8 +24394,8 @@ function escapeJsonPointerSegment(segment) {
24371
24394
  function isPlausibleDeviceId(value) {
24372
24395
  return HEX_MAC_DEVICE_ID_RE.test(value) || HYPHENATED_DEVICE_ID_RE.test(value);
24373
24396
  }
24374
- function hasErrorAtPath(errors, path28) {
24375
- return errors.some((err) => err.path === path28);
24397
+ function hasErrorAtPath(errors, path29) {
24398
+ return errors.some((err) => err.path === path29);
24376
24399
  }
24377
24400
  function resolvePolicyDeviceRef(raw, aliases) {
24378
24401
  if (!raw) return { ok: false, reason: "missing-device" };
@@ -24395,25 +24418,25 @@ function isDeviceStateConditionLike(value) {
24395
24418
  const candidate = value;
24396
24419
  return typeof candidate.device === "string" && typeof candidate.field === "string" && typeof candidate.op === "string";
24397
24420
  }
24398
- function collectConditionDeviceRefs(condition, path28) {
24421
+ function collectConditionDeviceRefs(condition, path29) {
24399
24422
  if (!condition || typeof condition !== "object" || Array.isArray(condition)) return [];
24400
24423
  const out = [];
24401
24424
  if (isDeviceStateConditionLike(condition)) {
24402
- out.push({ path: `${path28}/device`, ref: condition.device });
24425
+ out.push({ path: `${path29}/device`, ref: condition.device });
24403
24426
  }
24404
24427
  const candidate = condition;
24405
24428
  if (Array.isArray(candidate.all)) {
24406
24429
  for (let i = 0; i < candidate.all.length; i++) {
24407
- out.push(...collectConditionDeviceRefs(candidate.all[i], `${path28}/all/${i}`));
24430
+ out.push(...collectConditionDeviceRefs(candidate.all[i], `${path29}/all/${i}`));
24408
24431
  }
24409
24432
  }
24410
24433
  if (Array.isArray(candidate.any)) {
24411
24434
  for (let i = 0; i < candidate.any.length; i++) {
24412
- out.push(...collectConditionDeviceRefs(candidate.any[i], `${path28}/any/${i}`));
24435
+ out.push(...collectConditionDeviceRefs(candidate.any[i], `${path29}/any/${i}`));
24413
24436
  }
24414
24437
  }
24415
24438
  if (candidate.not !== void 0) {
24416
- out.push(...collectConditionDeviceRefs(candidate.not, `${path28}/not`));
24439
+ out.push(...collectConditionDeviceRefs(candidate.not, `${path29}/not`));
24417
24440
  }
24418
24441
  return out;
24419
24442
  }
@@ -24460,12 +24483,12 @@ function collectOfflineSemanticErrors(loaded, existingErrors) {
24460
24483
  const out = [];
24461
24484
  const aliases = collectAliasMap(data);
24462
24485
  for (const [aliasName, deviceId] of Object.entries(aliases)) {
24463
- const path28 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24464
- if (hasErrorAtPath(existingErrors, path28)) continue;
24486
+ const path29 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24487
+ if (hasErrorAtPath(existingErrors, path29)) continue;
24465
24488
  if (isPlausibleDeviceId(deviceId)) continue;
24466
- const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path28);
24489
+ const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
24467
24490
  out.push({
24468
- path: path28,
24491
+ path: path29,
24469
24492
  line,
24470
24493
  col,
24471
24494
  keyword: "alias-device-id",
@@ -24599,12 +24622,12 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
24599
24622
  inventoryById.set(remote.deviceId, { typeName: remote.remoteType });
24600
24623
  }
24601
24624
  for (const [aliasName, deviceId] of Object.entries(aliases)) {
24602
- const path28 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24603
- if (hasErrorAtPath(errors, path28)) continue;
24625
+ const path29 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
24626
+ if (hasErrorAtPath(errors, path29)) continue;
24604
24627
  if (!inventoryById.has(deviceId)) {
24605
- const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path28);
24628
+ const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
24606
24629
  errors.push({
24607
- path: path28,
24630
+ path: path29,
24608
24631
  line,
24609
24632
  col,
24610
24633
  keyword: "alias-live-device-not-found",
@@ -24668,10 +24691,10 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
24668
24691
  if (!effectiveDeviceId) continue;
24669
24692
  const target = inventoryById.get(effectiveDeviceId);
24670
24693
  if (!target) {
24671
- const path28 = typeof action?.device === "string" ? devicePath : commandPath;
24672
- const { line: line2, col: col2 } = locateInstancePath(loaded.doc, loaded.lineCounter, path28);
24694
+ const path29 = typeof action?.device === "string" ? devicePath : commandPath;
24695
+ const { line: line2, col: col2 } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
24673
24696
  errors.push({
24674
- path: path28,
24697
+ path: path29,
24675
24698
  line: line2,
24676
24699
  col: col2,
24677
24700
  keyword: "rule-live-device-not-found",
@@ -24850,6 +24873,70 @@ var init_openai = __esm({
24850
24873
  if (!content) throw new Error("OpenAI returned empty content");
24851
24874
  return content.replace(/^```ya?ml\n?/i, "").replace(/\n?```\s*$/i, "").trim();
24852
24875
  }
24876
+ async decide(prompt2, opts = {}) {
24877
+ const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
24878
+ const body = JSON.stringify({
24879
+ model: this.model,
24880
+ max_tokens: 256,
24881
+ tools: [{
24882
+ type: "function",
24883
+ function: {
24884
+ name: "decide",
24885
+ description: "Return a boolean pass/fail decision with a brief reason.",
24886
+ parameters: {
24887
+ type: "object",
24888
+ properties: {
24889
+ pass: { type: "boolean", description: "true if the condition passes, false otherwise." },
24890
+ reason: { type: "string", description: "Brief reason (\u2264200 chars)." }
24891
+ },
24892
+ required: ["pass", "reason"]
24893
+ }
24894
+ }
24895
+ }],
24896
+ tool_choice: { type: "function", function: { name: "decide" } },
24897
+ messages: [{ role: "user", content: prompt2 }]
24898
+ });
24899
+ const parsed = new URL(`${this.baseUrl}/v1/chat/completions`);
24900
+ const isHttps = parsed.protocol === "https:";
24901
+ const responseBody = await new Promise((resolve2, reject) => {
24902
+ const req = (isHttps ? https : http).request(
24903
+ {
24904
+ hostname: parsed.hostname,
24905
+ port: parsed.port || (isHttps ? 443 : 80),
24906
+ path: parsed.pathname,
24907
+ method: "POST",
24908
+ headers: {
24909
+ "Authorization": `Bearer ${this.apiKey}`,
24910
+ "Content-Type": "application/json",
24911
+ "Content-Length": Buffer.byteLength(body)
24912
+ },
24913
+ timeout: timeoutMs
24914
+ },
24915
+ (res) => {
24916
+ const chunks = [];
24917
+ res.on("data", (c) => chunks.push(c));
24918
+ res.on("end", () => {
24919
+ const text = Buffer.concat(chunks).toString("utf-8");
24920
+ if (res.statusCode !== void 0 && res.statusCode >= 400) {
24921
+ reject(new Error(`OpenAI API error ${res.statusCode}: ${text.slice(0, 200)}`));
24922
+ } else {
24923
+ resolve2(text);
24924
+ }
24925
+ });
24926
+ }
24927
+ );
24928
+ req.on("error", reject);
24929
+ req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
24930
+ req.write(body);
24931
+ req.end();
24932
+ });
24933
+ const json3 = JSON.parse(responseBody);
24934
+ const toolCall = json3.choices?.[0]?.message?.tool_calls?.find((tc) => tc.function.name === "decide");
24935
+ if (!toolCall) throw new Error("OpenAI decide: no tool call in response");
24936
+ const args = JSON.parse(toolCall.function.arguments);
24937
+ if (typeof args.pass !== "boolean") throw new Error("OpenAI decide: malformed function-call response");
24938
+ return { pass: args.pass, reason: String(args.reason ?? "").slice(0, 200) };
24939
+ }
24853
24940
  };
24854
24941
  }
24855
24942
  });
@@ -24920,6 +25007,66 @@ var init_anthropic = __esm({
24920
25007
  if (!content) throw new Error("Anthropic returned empty content");
24921
25008
  return content.replace(/^```ya?ml\n?/i, "").replace(/\n?```\s*$/i, "").trim();
24922
25009
  }
25010
+ async decide(prompt2, opts = {}) {
25011
+ const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
25012
+ const body = JSON.stringify({
25013
+ model: this.model,
25014
+ max_tokens: 256,
25015
+ tools: [{
25016
+ name: "decide",
25017
+ description: "Return a boolean pass/fail decision with a brief reason.",
25018
+ input_schema: {
25019
+ type: "object",
25020
+ properties: {
25021
+ pass: { type: "boolean", description: "true if the condition passes, false otherwise." },
25022
+ reason: { type: "string", description: "Brief reason (\u2264200 chars)." }
25023
+ },
25024
+ required: ["pass", "reason"]
25025
+ }
25026
+ }],
25027
+ tool_choice: { type: "tool", name: "decide" },
25028
+ messages: [{ role: "user", content: prompt2 }]
25029
+ });
25030
+ const responseBody = await new Promise((resolve2, reject) => {
25031
+ const req = https2.request(
25032
+ {
25033
+ hostname: "api.anthropic.com",
25034
+ port: 443,
25035
+ path: "/v1/messages",
25036
+ method: "POST",
25037
+ headers: {
25038
+ "x-api-key": this.apiKey,
25039
+ "anthropic-version": "2023-06-01",
25040
+ "Content-Type": "application/json",
25041
+ "Content-Length": Buffer.byteLength(body)
25042
+ },
25043
+ timeout: timeoutMs
25044
+ },
25045
+ (res) => {
25046
+ const chunks = [];
25047
+ res.on("data", (c) => chunks.push(c));
25048
+ res.on("end", () => {
25049
+ const text = Buffer.concat(chunks).toString("utf-8");
25050
+ if (res.statusCode !== void 0 && res.statusCode >= 400) {
25051
+ reject(new Error(`Anthropic API error ${res.statusCode}: ${text.slice(0, 200)}`));
25052
+ } else {
25053
+ resolve2(text);
25054
+ }
25055
+ });
25056
+ }
25057
+ );
25058
+ req.on("error", reject);
25059
+ req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
25060
+ req.write(body);
25061
+ req.end();
25062
+ });
25063
+ const json3 = JSON.parse(responseBody);
25064
+ const toolUse = json3.content?.find((c) => c.type === "tool_use" && c.name === "decide");
25065
+ if (!toolUse?.input || typeof toolUse.input.pass !== "boolean") {
25066
+ throw new Error("Anthropic decide: malformed tool-use response");
25067
+ }
25068
+ return { pass: toolUse.input.pass, reason: String(toolUse.input.reason ?? "").slice(0, 200) };
25069
+ }
24923
25070
  };
24924
25071
  }
24925
25072
  });
@@ -25086,8 +25233,10 @@ function matchesMqttTrigger(trigger, event, resolvedTriggerDeviceId) {
25086
25233
  async function evaluateConditions(conditions, now, ctx = {}) {
25087
25234
  const result = { matched: true, failures: [], unsupported: [] };
25088
25235
  if (!conditions || conditions.length === 0) return result;
25236
+ const evaluated = [];
25089
25237
  for (const c of conditions) {
25090
25238
  const sub = await evaluateSingle(c, now, ctx);
25239
+ evaluated.push({ c, sub });
25091
25240
  if (!sub.matched) {
25092
25241
  result.matched = false;
25093
25242
  result.failures.push(...sub.failures);
@@ -25096,6 +25245,13 @@ async function evaluateConditions(conditions, now, ctx = {}) {
25096
25245
  if (!sub.matched && result.unsupported.length > 0) {
25097
25246
  }
25098
25247
  }
25248
+ if (ctx.trace) {
25249
+ for (const { c, sub } of evaluated) {
25250
+ if (!isLlmCondition(c)) {
25251
+ pushConditionTrace(ctx.trace, c, sub);
25252
+ }
25253
+ }
25254
+ }
25099
25255
  return result;
25100
25256
  }
25101
25257
  async function evaluateSingle(c, now, ctx) {
@@ -25157,6 +25313,29 @@ async function evaluateSingle(c, now, ctx) {
25157
25313
  return fail(`device_state ${c.device}.${c.field}: fetch failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
25158
25314
  }
25159
25315
  }
25316
+ if (isLlmCondition(c)) {
25317
+ if (!ctx.llmEvaluator || !ctx.event) {
25318
+ return {
25319
+ matched: false,
25320
+ failures: [],
25321
+ unsupported: [{ keyword: "llm", hint: "llm condition requires an LlmConditionEvaluator and event in context." }]
25322
+ };
25323
+ }
25324
+ try {
25325
+ const res = await ctx.llmEvaluator.evaluate(
25326
+ c.llm,
25327
+ { event: ctx.event },
25328
+ ctx.ruleVersion ?? "unknown",
25329
+ ctx.globalLlmMaxCallsPerHour
25330
+ );
25331
+ if (ctx.trace) {
25332
+ ctx.trace.push({ kind: "llm", config: res.traceFields, passed: res.pass });
25333
+ }
25334
+ return res.pass ? ok : fail(`llm condition returned false: ${res.traceFields.reason}`);
25335
+ } catch (err) {
25336
+ return fail(`llm condition error: ${err instanceof Error ? err.message : String(err)}`);
25337
+ }
25338
+ }
25160
25339
  return {
25161
25340
  matched: false,
25162
25341
  failures: [],
@@ -25216,6 +25395,28 @@ function formatValue(v2) {
25216
25395
  if (typeof v2 === "string") return JSON.stringify(v2);
25217
25396
  return String(v2);
25218
25397
  }
25398
+ function conditionKind(c) {
25399
+ if (isAllCondition(c)) return "all";
25400
+ if (isAnyCondition(c)) return "any";
25401
+ if (isNotCondition(c)) return "not";
25402
+ if (isTimeBetween(c)) return "time_between";
25403
+ if (isDeviceState(c)) return "device_state";
25404
+ if (isLlmCondition(c)) return "llm";
25405
+ return "unknown";
25406
+ }
25407
+ function conditionConfig(c) {
25408
+ if (isTimeBetween(c)) return c.time_between;
25409
+ if (isDeviceState(c)) return { device: c.device, field: c.field, op: c.op, value: c.value };
25410
+ if (isLlmCondition(c)) return { prompt: c.llm.prompt.slice(0, 80) };
25411
+ return void 0;
25412
+ }
25413
+ function pushConditionTrace(trace, c, sub) {
25414
+ trace.push({
25415
+ kind: conditionKind(c),
25416
+ config: conditionConfig(c),
25417
+ passed: sub.unsupported.length > 0 ? false : sub.matched
25418
+ });
25419
+ }
25219
25420
  var EVENT_CLASSIFIERS;
25220
25421
  var init_matcher = __esm({
25221
25422
  "src/rules/matcher.ts"() {
@@ -26529,6 +26730,85 @@ var init_notify = __esm({
26529
26730
  }
26530
26731
  });
26531
26732
 
26733
+ // src/rules/trace.ts
26734
+ import { createHash as createHash3 } from "node:crypto";
26735
+ function shouldWriteTrace(mode, event, decision) {
26736
+ if (mode === "off") return false;
26737
+ if (mode === "full") return true;
26738
+ if (HIGH_FREQ_EVENTS.has(event.event) && decision === "blocked-by-condition") return false;
26739
+ return true;
26740
+ }
26741
+ function deepSortedJson(value) {
26742
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
26743
+ if (Array.isArray(value)) {
26744
+ return "[" + value.map(deepSortedJson).join(",") + "]";
26745
+ }
26746
+ const obj = value;
26747
+ const keys = Object.keys(obj).sort();
26748
+ return "{" + keys.map((k2) => JSON.stringify(k2) + ":" + deepSortedJson(obj[k2])).join(",") + "}";
26749
+ }
26750
+ function canonicalizeRule(rule) {
26751
+ return deepSortedJson(rule);
26752
+ }
26753
+ function ruleVersion(rule) {
26754
+ return createHash3("sha256").update(canonicalizeRule(rule)).digest("hex").slice(0, 8);
26755
+ }
26756
+ function filterTraceRecords(lines, opts = {}) {
26757
+ const sinceMs = opts.since ? new Date(opts.since).getTime() : void 0;
26758
+ const results = [];
26759
+ for (const line of lines) {
26760
+ const trimmed = line.trim();
26761
+ if (!trimmed) continue;
26762
+ let entry;
26763
+ try {
26764
+ entry = JSON.parse(trimmed);
26765
+ } catch {
26766
+ continue;
26767
+ }
26768
+ if (entry.kind !== "rule-evaluate") continue;
26769
+ if (opts.fireId && entry.fireId !== opts.fireId) continue;
26770
+ if (opts.ruleName && entry.rule.name !== opts.ruleName) continue;
26771
+ if (sinceMs !== void 0 && new Date(entry.t).getTime() < sinceMs) continue;
26772
+ if (opts.noFireOnly && (entry.decision === "fire" || entry.decision === "dry")) continue;
26773
+ results.push(entry);
26774
+ }
26775
+ return results;
26776
+ }
26777
+ var HIGH_FREQ_EVENTS, TraceBuilder;
26778
+ var init_trace = __esm({
26779
+ "src/rules/trace.ts"() {
26780
+ "use strict";
26781
+ init_cjs_shim();
26782
+ HIGH_FREQ_EVENTS = /* @__PURE__ */ new Set([
26783
+ "device.shadow",
26784
+ "motion.detected",
26785
+ "motion.cleared"
26786
+ ]);
26787
+ TraceBuilder = class {
26788
+ conditions = [];
26789
+ startMs;
26790
+ constructor() {
26791
+ this.startMs = Date.now();
26792
+ }
26793
+ push(entry) {
26794
+ this.conditions.push(entry);
26795
+ }
26796
+ build(rule, event, fireId, decision) {
26797
+ return {
26798
+ t: (/* @__PURE__ */ new Date()).toISOString(),
26799
+ kind: "rule-evaluate",
26800
+ rule: { name: rule.name, version: ruleVersion(rule) },
26801
+ trigger: { source: event.source, event: event.event, deviceId: event.deviceId },
26802
+ fireId,
26803
+ conditions: [...this.conditions],
26804
+ decision,
26805
+ evaluationMs: Date.now() - this.startMs
26806
+ };
26807
+ }
26808
+ };
26809
+ }
26810
+ });
26811
+
26532
26812
  // src/rules/engine.ts
26533
26813
  var engine_exports = {};
26534
26814
  __export(engine_exports, {
@@ -26721,6 +27001,44 @@ function lintRules(automation) {
26721
27001
  });
26722
27002
  }
26723
27003
  }
27004
+ for (const c of r.conditions ?? []) {
27005
+ if (!isLlmCondition(c)) continue;
27006
+ const llm = c.llm;
27007
+ if (!llm.provider || llm.provider === "auto") {
27008
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.LLM_API_KEY) {
27009
+ issues.push({
27010
+ rule: r.name,
27011
+ severity: "error",
27012
+ code: "condition-llm-no-provider",
27013
+ message: 'llm condition uses provider "auto" but no LLM API key env var is set (ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY).'
27014
+ });
27015
+ }
27016
+ }
27017
+ if (isMqttTrigger(r.when) && HIGH_FREQ_EVENTS.has(r.when.event) && !llm.cache_ttl) {
27018
+ issues.push({
27019
+ rule: r.name,
27020
+ severity: "warning",
27021
+ code: "condition-llm-no-cache-ttl-high-freq",
27022
+ message: `llm condition on high-frequency event "${r.when.event}" without explicit cache_ttl \u2014 consider setting cache_ttl to reduce LLM calls.`
27023
+ });
27024
+ }
27025
+ if (llm.budget?.max_calls_per_hour === 0) {
27026
+ issues.push({
27027
+ rule: r.name,
27028
+ severity: "warning",
27029
+ code: "condition-llm-budget-zero",
27030
+ message: "llm condition budget.max_calls_per_hour is 0 \u2014 condition will always take the on_error path."
27031
+ });
27032
+ }
27033
+ if (llm.on_error === "pass") {
27034
+ issues.push({
27035
+ rule: r.name,
27036
+ severity: "warning",
27037
+ code: "condition-llm-on-error-pass",
27038
+ message: 'llm condition on_error is "pass" \u2014 the condition will silently pass when the LLM is unavailable or over-budget.'
27039
+ });
27040
+ }
27041
+ }
26724
27042
  const enabled = r.enabled !== false;
26725
27043
  const hasError = issues.some((i) => i.severity === "error");
26726
27044
  const hasUnsupported = issues.some((i) => i.code === "trigger-unsupported");
@@ -26749,6 +27067,7 @@ var init_engine = __esm({
26749
27067
  init_notify();
26750
27068
  init_croner();
26751
27069
  init_audit();
27070
+ init_trace();
26752
27071
  RulesEngine = class {
26753
27072
  opts;
26754
27073
  rules;
@@ -27079,6 +27398,13 @@ var init_engine = __esm({
27079
27398
  }
27080
27399
  async dispatchRule(rule, event) {
27081
27400
  const fireId = randomUUID4();
27401
+ const traceMode = this.opts.automation?.audit?.evaluate_trace ?? "sampled";
27402
+ const trace = new TraceBuilder();
27403
+ const emitTrace = (decision) => {
27404
+ if (shouldWriteTrace(traceMode, event, decision)) {
27405
+ writeEvaluateTrace(trace.build(rule, event, fireId, decision));
27406
+ }
27407
+ };
27082
27408
  const statusCache = /* @__PURE__ */ new Map();
27083
27409
  const baseFetcher = this.opts.statusFetcher ?? ((id) => fetchDeviceStatus(id, this.opts.httpClient));
27084
27410
  const fetchStatus = (deviceId) => {
@@ -27090,7 +27416,8 @@ var init_engine = __esm({
27090
27416
  };
27091
27417
  const cond = await evaluateConditions(rule.conditions, event.t, {
27092
27418
  aliases: this.aliases,
27093
- fetchStatus
27419
+ fetchStatus,
27420
+ trace
27094
27421
  });
27095
27422
  if (!cond.matched) {
27096
27423
  const hasHysteresis = rule.hysteresis ?? rule.requires_stable_for;
@@ -27099,6 +27426,7 @@ var init_engine = __esm({
27099
27426
  this.hysteresisFirstSeen.delete(hysteresisKey);
27100
27427
  }
27101
27428
  if (cond.unsupported.length > 0) {
27429
+ emitTrace("error");
27102
27430
  writeAudit({
27103
27431
  t: event.t.toISOString(),
27104
27432
  kind: "rule-fire",
@@ -27121,6 +27449,7 @@ var init_engine = __esm({
27121
27449
  return;
27122
27450
  }
27123
27451
  this.stats.conditionsFailed++;
27452
+ emitTrace("blocked-by-condition");
27124
27453
  this.opts.onFire?.({ ruleName: rule.name, fireId, status: "conditions-failed", deviceId: event.deviceId, reason: cond.failures.join("; ") });
27125
27454
  return;
27126
27455
  }
@@ -27130,6 +27459,7 @@ var init_engine = __esm({
27130
27459
  const check2 = this.throttle.check(rule.name, effectiveMaxPerMs, event.t.getTime(), throttleKey, dedupeWindowMs);
27131
27460
  if (!check2.allowed) {
27132
27461
  this.stats.throttled++;
27462
+ emitTrace("throttled");
27133
27463
  writeAudit({
27134
27464
  t: event.t.toISOString(),
27135
27465
  kind: "rule-throttled",
@@ -27157,6 +27487,7 @@ var init_engine = __esm({
27157
27487
  const now = event.t.getTime();
27158
27488
  if (firstSeen === void 0) {
27159
27489
  this.hysteresisFirstSeen.set(hysteresisKey, now);
27490
+ emitTrace("throttled");
27160
27491
  writeAudit({
27161
27492
  t: event.t.toISOString(),
27162
27493
  kind: "rule-throttled",
@@ -27172,6 +27503,7 @@ var init_engine = __esm({
27172
27503
  return;
27173
27504
  }
27174
27505
  if (now - firstSeen < hysteresisMs) {
27506
+ emitTrace("throttled");
27175
27507
  writeAudit({
27176
27508
  t: event.t.toISOString(),
27177
27509
  kind: "rule-throttled",
@@ -27192,6 +27524,7 @@ var init_engine = __esm({
27192
27524
  const countCheck = this.throttle.checkMaxFirings(rule.name, rule.maxFiringsPerHour, 36e5, event.t.getTime(), event.deviceId);
27193
27525
  if (!countCheck.allowed) {
27194
27526
  this.stats.throttled++;
27527
+ emitTrace("throttled");
27195
27528
  writeAudit({
27196
27529
  t: event.t.toISOString(),
27197
27530
  kind: "rule-throttled",
@@ -27218,6 +27551,7 @@ var init_engine = __esm({
27218
27551
  const deviceStatus = await fetchStatus(targetId);
27219
27552
  const powerState = deviceStatus["powerState"];
27220
27553
  if (verb === "turnOn" && powerState === "on" || verb === "turnOff" && powerState === "off") {
27554
+ emitTrace("throttled");
27221
27555
  writeAudit({
27222
27556
  t: event.t.toISOString(),
27223
27557
  kind: "rule-throttled",
@@ -27269,6 +27603,8 @@ var init_engine = __esm({
27269
27603
  if (!result.ok && (action.on_error ?? "continue") === "stop") break;
27270
27604
  }
27271
27605
  if (fired) {
27606
+ const decision = allDry ? "dry" : "fire";
27607
+ emitTrace(decision);
27272
27608
  if (allDry) this.stats.dryFires++;
27273
27609
  else this.stats.fires++;
27274
27610
  this.throttle.record(rule.name, event.t.getTime(), throttleKey);
@@ -27870,6 +28206,8 @@ var init_capabilities = __esm({
27870
28206
  "rules summary": READ_LOCAL,
27871
28207
  "rules last-fired": READ_LOCAL,
27872
28208
  "rules explain": READ_LOCAL,
28209
+ "rules trace-explain": READ_LOCAL,
28210
+ "rules simulate": READ_LOCAL,
27873
28211
  "schema export": READ_LOCAL,
27874
28212
  "scenes list": READ_REMOTE,
27875
28213
  "scenes execute": ACTION_REMOTE,
@@ -35224,10 +35562,10 @@ function mergeDefs(...defs) {
35224
35562
  function cloneDef(schema2) {
35225
35563
  return mergeDefs(schema2._zod.def);
35226
35564
  }
35227
- function getElementAtPath(obj, path28) {
35228
- if (!path28)
35565
+ function getElementAtPath(obj, path29) {
35566
+ if (!path29)
35229
35567
  return obj;
35230
- return path28.reduce((acc, key) => acc?.[key], obj);
35568
+ return path29.reduce((acc, key) => acc?.[key], obj);
35231
35569
  }
35232
35570
  function promiseAllObject(promisesObj) {
35233
35571
  const keys = Object.keys(promisesObj);
@@ -35610,11 +35948,11 @@ function aborted(x2, startIndex = 0) {
35610
35948
  }
35611
35949
  return false;
35612
35950
  }
35613
- function prefixIssues(path28, issues) {
35951
+ function prefixIssues(path29, issues) {
35614
35952
  return issues.map((iss) => {
35615
35953
  var _a2;
35616
35954
  (_a2 = iss).path ?? (_a2.path = []);
35617
- iss.path.unshift(path28);
35955
+ iss.path.unshift(path29);
35618
35956
  return iss;
35619
35957
  });
35620
35958
  }
@@ -35797,7 +36135,7 @@ function formatError2(error48, mapper = (issue2) => issue2.message) {
35797
36135
  }
35798
36136
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
35799
36137
  const result = { errors: [] };
35800
- const processError = (error49, path28 = []) => {
36138
+ const processError = (error49, path29 = []) => {
35801
36139
  var _a2, _b;
35802
36140
  for (const issue2 of error49.issues) {
35803
36141
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -35807,7 +36145,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
35807
36145
  } else if (issue2.code === "invalid_element") {
35808
36146
  processError({ issues: issue2.issues }, issue2.path);
35809
36147
  } else {
35810
- const fullpath = [...path28, ...issue2.path];
36148
+ const fullpath = [...path29, ...issue2.path];
35811
36149
  if (fullpath.length === 0) {
35812
36150
  result.errors.push(mapper(issue2));
35813
36151
  continue;
@@ -35839,8 +36177,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
35839
36177
  }
35840
36178
  function toDotPath(_path) {
35841
36179
  const segs = [];
35842
- const path28 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
35843
- for (const seg of path28) {
36180
+ const path29 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
36181
+ for (const seg of path29) {
35844
36182
  if (typeof seg === "number")
35845
36183
  segs.push(`[${seg}]`);
35846
36184
  else if (typeof seg === "symbol")
@@ -47895,13 +48233,13 @@ function resolveRef(ref, ctx) {
47895
48233
  if (!ref.startsWith("#")) {
47896
48234
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
47897
48235
  }
47898
- const path28 = ref.slice(1).split("/").filter(Boolean);
47899
- if (path28.length === 0) {
48236
+ const path29 = ref.slice(1).split("/").filter(Boolean);
48237
+ if (path29.length === 0) {
47900
48238
  return ctx.rootSchema;
47901
48239
  }
47902
48240
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
47903
- if (path28[0] === defsKey) {
47904
- const key = path28[1];
48241
+ if (path29[0] === defsKey) {
48242
+ const key = path29[1];
47905
48243
  if (!key || !ctx.defs[key]) {
47906
48244
  throw new Error(`Reference not found: ${ref}`);
47907
48245
  }
@@ -50027,6 +50365,282 @@ function addRuleToPolicyFile(opts) {
50027
50365
  return { ...result, written: false };
50028
50366
  }
50029
50367
 
50368
+ // src/rules/explain.ts
50369
+ init_cjs_shim();
50370
+ init_trace();
50371
+ import fs16 from "node:fs";
50372
+ function loadTraceRecords(auditFile, opts = {}) {
50373
+ if (!fs16.existsSync(auditFile)) return [];
50374
+ const lines = fs16.readFileSync(auditFile, "utf-8").split(/\r?\n/);
50375
+ return filterTraceRecords(lines, opts);
50376
+ }
50377
+ function loadRelatedAudit(auditFile, fireId) {
50378
+ if (!fs16.existsSync(auditFile)) return [];
50379
+ const raw = fs16.readFileSync(auditFile, "utf-8");
50380
+ const out = [];
50381
+ for (const line of raw.split(/\r?\n/)) {
50382
+ const trimmed = line.trim();
50383
+ if (!trimmed) continue;
50384
+ try {
50385
+ const entry = JSON.parse(trimmed);
50386
+ const entryFireId = entry.rule?.fireId ?? entry["fireId"];
50387
+ if (entryFireId === fireId) out.push(entry);
50388
+ } catch {
50389
+ }
50390
+ }
50391
+ return out;
50392
+ }
50393
+ function conditionSymbol(passed) {
50394
+ if (passed === true) return "\u2713";
50395
+ if (passed === false) return "\u2717";
50396
+ return "\xB7";
50397
+ }
50398
+ function conditionSummary(c) {
50399
+ if (c.passed === null) {
50400
+ return `\xB7 ${c.kind} \u2192 not evaluated (short-circuited)`;
50401
+ }
50402
+ const sym = conditionSymbol(c.passed);
50403
+ let detail = c.kind;
50404
+ if (c.config !== void 0) {
50405
+ if (Array.isArray(c.config)) {
50406
+ detail += ` ${c.config.join("\u2013")}`;
50407
+ } else if (c.config && typeof c.config === "object") {
50408
+ const cfg = c.config;
50409
+ if ("device" in cfg) {
50410
+ detail += ` ${cfg["device"]}.${cfg["field"]} ${cfg["op"]} ${JSON.stringify(cfg["value"])}`;
50411
+ }
50412
+ }
50413
+ }
50414
+ const status = c.passed ? "passed" : "failed";
50415
+ return ` ${sym} ${detail.padEnd(36)} \u2192 ${status}`;
50416
+ }
50417
+ function formatTimestamp(iso) {
50418
+ const d = new Date(iso);
50419
+ return `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 19)}`;
50420
+ }
50421
+ function formatExplainText(record2, relatedAudit) {
50422
+ const lines = [];
50423
+ lines.push(`Rule: ${record2.rule.name} (version ${record2.rule.version})`);
50424
+ lines.push(`Evaluated: ${formatTimestamp(record2.t)} (${record2.evaluationMs}ms)`);
50425
+ const triggerDevice = record2.trigger.deviceId ? ` on ${record2.trigger.deviceId}` : "";
50426
+ lines.push(`Trigger: ${record2.trigger.source} ${record2.trigger.event}${triggerDevice}`);
50427
+ lines.push("");
50428
+ if (record2.conditions.length > 0) {
50429
+ lines.push("Conditions (evaluated in order):");
50430
+ for (const c of record2.conditions) {
50431
+ lines.push(conditionSummary(c));
50432
+ }
50433
+ lines.push("");
50434
+ }
50435
+ lines.push(`Decision: ${record2.decision}`);
50436
+ lines.push("");
50437
+ lines.push(`Related fireId: ${record2.fireId}`);
50438
+ const nonEval = relatedAudit.filter(
50439
+ (e) => e["kind"] !== "rule-evaluate"
50440
+ );
50441
+ if (nonEval.length > 0) {
50442
+ lines.push(`Audit trail (${nonEval.length} record${nonEval.length === 1 ? "" : "s"}):`);
50443
+ for (const e of nonEval) {
50444
+ const ts = formatTimestamp(e.t);
50445
+ lines.push(` ${e.kind.padEnd(20)} ${ts}`);
50446
+ }
50447
+ } else {
50448
+ lines.push(`Audit trail: (no related records${record2.decision === "blocked-by-condition" ? " \u2014 rule did not fire" : ""})`);
50449
+ }
50450
+ return lines.join("\n");
50451
+ }
50452
+ function formatExplainJson(record2, relatedAudit) {
50453
+ return JSON.stringify({ trace: record2, relatedAudit }, null, 2);
50454
+ }
50455
+
50456
+ // src/rules/simulate.ts
50457
+ init_cjs_shim();
50458
+ init_matcher();
50459
+ init_throttle();
50460
+ init_trace();
50461
+ init_trace();
50462
+ init_matcher();
50463
+ import fs17 from "node:fs";
50464
+ import path14 from "node:path";
50465
+ import os13 from "node:os";
50466
+ import { randomUUID as randomUUID5 } from "node:crypto";
50467
+ var HOUR_MS = 60 * 60 * 1e3;
50468
+ var DEVICE_HISTORY_DIR = path14.join(os13.homedir(), ".switchbot", "device-history");
50469
+ async function simulateRule(opts) {
50470
+ const { rule, aliases = {}, liveLlm = false } = opts;
50471
+ const rv = ruleVersion(rule);
50472
+ const events = loadSourceEvents(opts);
50473
+ const windowStart = events.length > 0 ? new Date(Math.min(...events.map((e) => e.t.getTime()))) : new Date(Date.now() - 24 * HOUR_MS);
50474
+ const windowEnd = events.length > 0 ? new Date(Math.max(...events.map((e) => e.t.getTime()))) : /* @__PURE__ */ new Date();
50475
+ const counts = { wouldFire: 0, blocked: 0, throttled: 0, errored: 0, skippedLlm: 0 };
50476
+ const blockReasons = /* @__PURE__ */ new Map();
50477
+ const sampleFires = [];
50478
+ const traces = [];
50479
+ const throttle = new ThrottleGate();
50480
+ const cooldownMs = rule.cooldown ? parseMaxPerMs(rule.cooldown) : null;
50481
+ const throttleMs = rule.throttle ? parseMaxPerMs(rule.throttle.max_per) : null;
50482
+ const effectiveWindowMs = cooldownMs ?? throttleMs;
50483
+ for (const event of events) {
50484
+ const fireId = randomUUID5();
50485
+ const nowMs = event.t.getTime();
50486
+ if (rule.when.source === "mqtt") {
50487
+ const resolvedDevice = rule.when.device ? aliases[rule.when.device] ?? rule.when.device : void 0;
50488
+ if (!matchesMqttTrigger(rule.when, event, resolvedDevice)) continue;
50489
+ }
50490
+ const hasLlm = (rule.conditions ?? []).some((c) => c["llm"] !== void 0);
50491
+ if (hasLlm && !liveLlm) {
50492
+ counts.skippedLlm++;
50493
+ const fireEvent = {
50494
+ t: event.t.toISOString(),
50495
+ fireId,
50496
+ deviceId: event.deviceId,
50497
+ decision: "skipped-llm"
50498
+ };
50499
+ sampleFires.push(fireEvent);
50500
+ continue;
50501
+ }
50502
+ if (effectiveWindowMs !== null) {
50503
+ const check2 = throttle.check(rule.name, effectiveWindowMs, nowMs, event.deviceId);
50504
+ if (!check2.allowed) {
50505
+ counts.throttled++;
50506
+ const fireEvent = {
50507
+ t: event.t.toISOString(),
50508
+ fireId,
50509
+ deviceId: event.deviceId,
50510
+ decision: "throttled"
50511
+ };
50512
+ sampleFires.push(fireEvent);
50513
+ continue;
50514
+ }
50515
+ }
50516
+ const statusFetcher = buildStatusFetcher(event.t);
50517
+ let condResult;
50518
+ try {
50519
+ condResult = await evaluateConditions(rule.conditions, event.t, {
50520
+ aliases,
50521
+ fetchStatus: statusFetcher,
50522
+ event,
50523
+ ruleVersion: rv
50524
+ });
50525
+ } catch (err) {
50526
+ counts.errored++;
50527
+ sampleFires.push({ t: event.t.toISOString(), fireId, deviceId: event.deviceId, decision: "error", reason: String(err) });
50528
+ continue;
50529
+ }
50530
+ if (!condResult.matched) {
50531
+ counts.blocked++;
50532
+ const reason = condResult.failures[0] ?? "unknown";
50533
+ blockReasons.set(reason, (blockReasons.get(reason) ?? 0) + 1);
50534
+ sampleFires.push({ t: event.t.toISOString(), fireId, deviceId: event.deviceId, decision: "blocked-by-condition", reason });
50535
+ } else {
50536
+ counts.wouldFire++;
50537
+ throttle.record(rule.name, nowMs, event.deviceId);
50538
+ sampleFires.push({ t: event.t.toISOString(), fireId, deviceId: event.deviceId, decision: "would-fire" });
50539
+ }
50540
+ }
50541
+ let topBlockReason;
50542
+ let topBlockCount;
50543
+ if (blockReasons.size > 0) {
50544
+ let max = 0;
50545
+ for (const [reason, count] of blockReasons) {
50546
+ if (count > max) {
50547
+ max = count;
50548
+ topBlockReason = reason;
50549
+ topBlockCount = count;
50550
+ }
50551
+ }
50552
+ }
50553
+ return {
50554
+ ruleName: rule.name,
50555
+ ruleVersion: rv,
50556
+ windowStart,
50557
+ windowEnd,
50558
+ sourceEventCount: events.length,
50559
+ wouldFire: counts.wouldFire,
50560
+ blockedByCondition: counts.blocked,
50561
+ throttled: counts.throttled,
50562
+ errored: counts.errored,
50563
+ skippedLlm: counts.skippedLlm,
50564
+ topBlockReason,
50565
+ topBlockCount,
50566
+ sampleFires: sampleFires.slice(0, 20),
50567
+ traces
50568
+ };
50569
+ }
50570
+ function loadSourceEvents(opts) {
50571
+ if (opts.against) {
50572
+ if (!fs17.existsSync(opts.against)) return [];
50573
+ const lines2 = fs17.readFileSync(opts.against, "utf-8").split(/\r?\n/);
50574
+ const events = [];
50575
+ for (const line of lines2) {
50576
+ const trimmed = line.trim();
50577
+ if (!trimmed) continue;
50578
+ try {
50579
+ const raw = JSON.parse(trimmed);
50580
+ events.push({
50581
+ source: raw["source"] ?? "mqtt",
50582
+ event: String(raw["event"] ?? "device.shadow"),
50583
+ t: new Date(String(raw["t"] ?? (/* @__PURE__ */ new Date()).toISOString())),
50584
+ deviceId: raw["deviceId"],
50585
+ payload: raw["payload"]
50586
+ });
50587
+ } catch {
50588
+ }
50589
+ }
50590
+ return events;
50591
+ }
50592
+ const auditLog = opts.auditLog;
50593
+ if (!auditLog || !fs17.existsSync(auditLog)) return [];
50594
+ const sinceMs = opts.since ? parseSince(opts.since) : Date.now() - 24 * HOUR_MS;
50595
+ const sinceIso = new Date(sinceMs).toISOString();
50596
+ const lines = fs17.readFileSync(auditLog, "utf-8").split(/\r?\n/);
50597
+ const traceRecords = filterTraceRecords(lines, {
50598
+ ruleName: opts.rule.name,
50599
+ since: sinceIso
50600
+ });
50601
+ return traceRecords.map((r) => ({
50602
+ source: r.trigger.source,
50603
+ event: r.trigger.event,
50604
+ t: new Date(r.t),
50605
+ deviceId: r.trigger.deviceId
50606
+ }));
50607
+ }
50608
+ function parseSince(since) {
50609
+ if (since.includes("T") || since.includes("-")) {
50610
+ const d = new Date(since);
50611
+ if (!isNaN(d.getTime())) return d.getTime();
50612
+ }
50613
+ const m2 = /^(\d+)([smhd])$/.exec(since.trim());
50614
+ if (m2) {
50615
+ const n = parseInt(m2[1], 10);
50616
+ const unit = m2[2];
50617
+ const unitMs = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
50618
+ return Date.now() - n * unitMs;
50619
+ }
50620
+ return Date.now() - 24 * HOUR_MS;
50621
+ }
50622
+ function buildStatusFetcher(asOf) {
50623
+ return async (deviceId) => {
50624
+ const histFile = path14.join(DEVICE_HISTORY_DIR, `${deviceId}.jsonl`);
50625
+ if (!fs17.existsSync(histFile)) return {};
50626
+ const lines = fs17.readFileSync(histFile, "utf-8").split(/\r?\n/);
50627
+ const asOfMs = asOf.getTime();
50628
+ let best;
50629
+ for (const line of lines) {
50630
+ const trimmed = line.trim();
50631
+ if (!trimmed) continue;
50632
+ try {
50633
+ const entry = JSON.parse(trimmed);
50634
+ const entryT = new Date(String(entry["t"] ?? 0)).getTime();
50635
+ if (entryT <= asOfMs) best = entry;
50636
+ else break;
50637
+ } catch {
50638
+ }
50639
+ }
50640
+ return best ?? {};
50641
+ };
50642
+ }
50643
+
50030
50644
  // src/commands/mcp.ts
50031
50645
  init_audit();
50032
50646
  init_flags();
@@ -50045,13 +50659,13 @@ function collectPolicyDiff(left, right, at, out, limit) {
50045
50659
  const maxLen = Math.max(left.length, right.length);
50046
50660
  for (let i = 0; i < maxLen; i++) {
50047
50661
  if (out.length >= limit) return;
50048
- const path28 = `${at}[${i}]`;
50662
+ const path29 = `${at}[${i}]`;
50049
50663
  if (i >= left.length) {
50050
- out.push({ path: path28, kind: "added", after: right[i] });
50664
+ out.push({ path: path29, kind: "added", after: right[i] });
50051
50665
  } else if (i >= right.length) {
50052
- out.push({ path: path28, kind: "removed", before: left[i] });
50666
+ out.push({ path: path29, kind: "removed", before: left[i] });
50053
50667
  } else {
50054
- collectPolicyDiff(left[i], right[i], path28, out, limit);
50668
+ collectPolicyDiff(left[i], right[i], path29, out, limit);
50055
50669
  }
50056
50670
  }
50057
50671
  return;
@@ -50060,15 +50674,15 @@ function collectPolicyDiff(left, right, at, out, limit) {
50060
50674
  const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
50061
50675
  for (const key of [...keys].sort()) {
50062
50676
  if (out.length >= limit) return;
50063
- const path28 = at === "$" ? `$.${key}` : `${at}.${key}`;
50677
+ const path29 = at === "$" ? `$.${key}` : `${at}.${key}`;
50064
50678
  const leftHas = Object.prototype.hasOwnProperty.call(left, key);
50065
50679
  const rightHas = Object.prototype.hasOwnProperty.call(right, key);
50066
50680
  if (!leftHas && rightHas) {
50067
- out.push({ path: path28, kind: "added", after: right[key] });
50681
+ out.push({ path: path29, kind: "added", after: right[key] });
50068
50682
  } else if (leftHas && !rightHas) {
50069
- out.push({ path: path28, kind: "removed", before: left[key] });
50683
+ out.push({ path: path29, kind: "removed", before: left[key] });
50070
50684
  } else {
50071
- collectPolicyDiff(left[key], right[key], path28, out, limit);
50685
+ collectPolicyDiff(left[key], right[key], path29, out, limit);
50072
50686
  }
50073
50687
  }
50074
50688
  return;
@@ -50121,8 +50735,8 @@ function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges
50121
50735
  // src/commands/mcp.ts
50122
50736
  init_embedded_assets();
50123
50737
  import { dirname as pathDirname, join as pathJoin } from "node:path";
50124
- import os13 from "node:os";
50125
- import fs16 from "node:fs";
50738
+ import os14 from "node:os";
50739
+ import fs18 from "node:fs";
50126
50740
  var LATEST_SUPPORTED_VERSION = SUPPORTED_POLICY_SCHEMA_VERSIONS[SUPPORTED_POLICY_SCHEMA_VERSIONS.length - 1];
50127
50741
  function mcpError(kind, code, message, options) {
50128
50742
  const obj = { code, kind, message };
@@ -50151,7 +50765,7 @@ function apiErrorToMcpError(err) {
50151
50765
  retryAfterMs: payload.retryAfterMs
50152
50766
  });
50153
50767
  }
50154
- var DEFAULT_AUDIT_LOG_FILE = pathJoin(os13.homedir(), ".switchbot", "audit.log");
50768
+ var DEFAULT_AUDIT_LOG_FILE = pathJoin(os14.homedir(), ".switchbot", "audit.log");
50155
50769
  function resolveAuditRange(opts) {
50156
50770
  if (opts.since && (opts.from || opts.to)) {
50157
50771
  throw new Error("--since is mutually exclusive with --from/--to.");
@@ -51080,15 +51694,15 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51080
51694
  async ({ path: pathArg, force }) => {
51081
51695
  const policyPath = resolvePolicyPath({ flag: pathArg });
51082
51696
  const doForce = force === true;
51083
- if (fs16.existsSync(policyPath) && !doForce) {
51697
+ if (fs18.existsSync(policyPath) && !doForce) {
51084
51698
  return mcpError("guard", 5, `refusing to overwrite existing policy at ${policyPath}`, {
51085
51699
  hint: "pass force=true to overwrite, or choose a different path",
51086
51700
  context: { policyPath }
51087
51701
  });
51088
51702
  }
51089
51703
  const template = readPolicyExampleYaml();
51090
- fs16.mkdirSync(pathDirname(policyPath), { recursive: true });
51091
- fs16.writeFileSync(policyPath, template, { encoding: "utf-8" });
51704
+ fs18.mkdirSync(pathDirname(policyPath), { recursive: true });
51705
+ fs18.writeFileSync(policyPath, template, { encoding: "utf-8" });
51092
51706
  const structured = {
51093
51707
  policyPath,
51094
51708
  schemaVersion: CURRENT_POLICY_SCHEMA_VERSION,
@@ -51275,7 +51889,7 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51275
51889
  let leftSource = "";
51276
51890
  let rightSource = "";
51277
51891
  try {
51278
- leftSource = fs16.readFileSync(left_path, "utf-8");
51892
+ leftSource = fs18.readFileSync(left_path, "utf-8");
51279
51893
  } catch (err) {
51280
51894
  if (err?.code === "ENOENT") {
51281
51895
  return mcpError("usage", 2, `policy file not found: ${left_path}`, {
@@ -51285,7 +51899,7 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51285
51899
  return mcpError("runtime", 1, `failed to read ${left_path}: ${String(err)}`);
51286
51900
  }
51287
51901
  try {
51288
- rightSource = fs16.readFileSync(right_path, "utf-8");
51902
+ rightSource = fs18.readFileSync(right_path, "utf-8");
51289
51903
  } catch (err) {
51290
51904
  if (err?.code === "ENOENT") {
51291
51905
  return mcpError("usage", 2, `policy file not found: ${right_path}`, {
@@ -51725,6 +52339,130 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51725
52339
  }
51726
52340
  }
51727
52341
  );
52342
+ server.registerTool(
52343
+ "rules_explain",
52344
+ {
52345
+ title: "Show why a rule evaluation fired or was blocked",
52346
+ description: 'Read rule-evaluate trace records from the audit log and format them for inspection. Pass fire_id to explain a specific evaluation; or pass rule_name with last:true for the most recent evaluation; or pass rule_name + since for a window. Returns trace records only when automation.audit.evaluate_trace is "sampled" or "full".',
52347
+ _meta: { agentSafetyTier: "read" },
52348
+ inputSchema: external_exports.object({
52349
+ fire_id: external_exports.string().optional().describe("Specific fireId to explain."),
52350
+ rule_name: external_exports.string().optional().describe("Filter to this rule name."),
52351
+ since: external_exports.string().optional().describe("Duration string (e.g. 1h, 7d) \u2014 show evaluations in this window."),
52352
+ last: external_exports.boolean().optional().describe("Return only the most recent evaluation (requires rule_name)."),
52353
+ audit_log: external_exports.string().optional().describe(`Audit log path (default: ${pathJoin(os14.homedir(), ".switchbot", "audit.log")}).`)
52354
+ }).strict(),
52355
+ outputSchema: {
52356
+ records: external_exports.array(external_exports.unknown()).describe("Array of trace + relatedAudit objects."),
52357
+ count: external_exports.number().describe("Number of trace records returned.")
52358
+ }
52359
+ },
52360
+ async ({ fire_id, rule_name, since, last, audit_log }) => {
52361
+ const DEFAULT_AUDIT_PATH4 = pathJoin(os14.homedir(), ".switchbot", "audit.log");
52362
+ const auditFile = audit_log ?? DEFAULT_AUDIT_PATH4;
52363
+ const sinceIso = since ? new Date(Date.now() - (parseDurationToMs(since) ?? 0)).toISOString() : void 0;
52364
+ let records = loadTraceRecords(auditFile, {
52365
+ fireId: fire_id,
52366
+ ruleName: rule_name,
52367
+ since: sinceIso
52368
+ });
52369
+ if (records.length === 0) {
52370
+ return {
52371
+ content: [{ type: "text", text: 'No rule-evaluate trace records found. Check that automation.audit.evaluate_trace is "sampled" or "full".' }],
52372
+ structuredContent: { records: [], count: 0 }
52373
+ };
52374
+ }
52375
+ if (last) {
52376
+ records = [records[records.length - 1]];
52377
+ }
52378
+ const output = records.map((record2) => {
52379
+ const related = loadRelatedAudit(auditFile, record2.fireId);
52380
+ return JSON.parse(formatExplainJson(record2, related));
52381
+ });
52382
+ return {
52383
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
52384
+ structuredContent: { records: output, count: output.length }
52385
+ };
52386
+ }
52387
+ );
52388
+ server.registerTool(
52389
+ "rules_simulate",
52390
+ {
52391
+ title: "Simulate a rule against historical events",
52392
+ description: "Replay historical events from the audit log or a JSONL file against a rule definition and report would-fire / blocked-by-condition / throttled outcomes. Useful for validating a new or modified rule before deployment. Pass rule_yaml to test an unpublished rule, or rule_name + policy_path to test a deployed rule.",
52393
+ _meta: { agentSafetyTier: "read" },
52394
+ inputSchema: external_exports.object({
52395
+ rule_yaml: external_exports.string().optional().describe("Standalone rule YAML (takes precedence over policy_path + rule_name)."),
52396
+ policy_path: external_exports.string().optional().describe("Path to policy.yaml (defaults to ~/.switchbot/policy.yaml)."),
52397
+ rule_name: external_exports.string().optional().describe("Name of the rule in policy.yaml to simulate."),
52398
+ since: external_exports.string().optional().describe("Replay events from this window (e.g. 7d, 24h)."),
52399
+ against: external_exports.string().optional().describe("JSONL file path of EngineEvent objects to replay."),
52400
+ live_llm: external_exports.boolean().optional().describe("Allow live LLM calls for llm conditions (default: skip and report as would-call)."),
52401
+ audit_log: external_exports.string().optional().describe(`Audit log path (default: ${pathJoin(os14.homedir(), ".switchbot", "audit.log")}).`)
52402
+ }).strict(),
52403
+ outputSchema: {
52404
+ report: external_exports.unknown().describe("SimulateReport object.")
52405
+ }
52406
+ },
52407
+ async ({ rule_yaml, policy_path, rule_name, since, against, live_llm, audit_log }) => {
52408
+ const DEFAULT_AUDIT_PATH4 = pathJoin(os14.homedir(), ".switchbot", "audit.log");
52409
+ const auditFile = audit_log ?? DEFAULT_AUDIT_PATH4;
52410
+ let rule;
52411
+ if (rule_yaml) {
52412
+ try {
52413
+ rule = (0, import_yaml7.parse)(rule_yaml);
52414
+ } catch (err) {
52415
+ return {
52416
+ content: [{ type: "text", text: `Failed to parse rule_yaml: ${String(err)}` }],
52417
+ structuredContent: { report: null }
52418
+ };
52419
+ }
52420
+ } else if (policy_path || rule_name) {
52421
+ const { loadPolicyFile: loadPolicyFile2 } = await Promise.resolve().then(() => (init_load(), load_exports));
52422
+ const policyFile = policy_path ?? pathJoin(os14.homedir(), ".switchbot", "policy.yaml");
52423
+ try {
52424
+ const policy = loadPolicyFile2(policyFile);
52425
+ const data = policy.data ?? {};
52426
+ const found = data.automation?.rules?.find((r) => r.name === rule_name);
52427
+ if (!found) {
52428
+ return {
52429
+ content: [{ type: "text", text: `Rule "${rule_name}" not found in ${policyFile}.` }],
52430
+ structuredContent: { report: null }
52431
+ };
52432
+ }
52433
+ rule = found;
52434
+ } catch (err) {
52435
+ return {
52436
+ content: [{ type: "text", text: `Failed to load policy: ${String(err)}` }],
52437
+ structuredContent: { report: null }
52438
+ };
52439
+ }
52440
+ } else {
52441
+ return {
52442
+ content: [{ type: "text", text: "Provide rule_yaml or (policy_path + rule_name) to specify the rule to simulate." }],
52443
+ structuredContent: { report: null }
52444
+ };
52445
+ }
52446
+ try {
52447
+ const report = await simulateRule({
52448
+ rule,
52449
+ since,
52450
+ against,
52451
+ auditLog: auditFile,
52452
+ liveLlm: live_llm ?? false
52453
+ });
52454
+ return {
52455
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }],
52456
+ structuredContent: { report }
52457
+ };
52458
+ } catch (err) {
52459
+ return {
52460
+ content: [{ type: "text", text: `Simulate error: ${String(err)}` }],
52461
+ structuredContent: { report: null }
52462
+ };
52463
+ }
52464
+ }
52465
+ );
51728
52466
  server.registerTool(
51729
52467
  "policy_add_rule",
51730
52468
  {
@@ -51995,7 +52733,7 @@ process_uptime_seconds ${Math.floor(process.uptime())}
51995
52733
  }
51996
52734
  if (profile) {
51997
52735
  const envCredsPresent = !!(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
51998
- if (!envCredsPresent && !fs16.existsSync(profileFilePath(profile))) {
52736
+ if (!envCredsPresent && !fs18.existsSync(profileFilePath(profile))) {
51999
52737
  res.writeHead(401, { "Content-Type": "application/json" });
52000
52738
  res.end(JSON.stringify({
52001
52739
  jsonrpc: "2.0",
@@ -52631,18 +53369,18 @@ var StdoutSink = class {
52631
53369
 
52632
53370
  // src/sinks/file.ts
52633
53371
  init_cjs_shim();
52634
- import fs17 from "node:fs";
52635
- import path14 from "node:path";
53372
+ import fs19 from "node:fs";
53373
+ import path15 from "node:path";
52636
53374
  var FileSink = class {
52637
53375
  filePath;
52638
53376
  constructor(filePath) {
52639
- this.filePath = path14.resolve(filePath);
52640
- const dir = path14.dirname(this.filePath);
52641
- if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
53377
+ this.filePath = path15.resolve(filePath);
53378
+ const dir = path15.dirname(this.filePath);
53379
+ if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
52642
53380
  }
52643
53381
  async write(event) {
52644
53382
  try {
52645
- fs17.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
53383
+ fs19.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
52646
53384
  } catch {
52647
53385
  }
52648
53386
  }
@@ -53311,9 +54049,9 @@ init_catalog();
53311
54049
  init_config();
53312
54050
  init_cache();
53313
54051
  init_quota();
53314
- import fs20 from "node:fs";
53315
- import os16 from "node:os";
53316
- import path17 from "node:path";
54052
+ import fs22 from "node:fs";
54053
+ import os17 from "node:os";
54054
+ import path18 from "node:path";
53317
54055
  import { execSync } from "node:child_process";
53318
54056
 
53319
54057
  // src/commands/agent-bootstrap.ts
@@ -53549,38 +54287,38 @@ init_request_context();
53549
54287
 
53550
54288
  // src/lib/daemon-state.ts
53551
54289
  init_cjs_shim();
53552
- import fs18 from "node:fs";
53553
- import os14 from "node:os";
53554
- import path15 from "node:path";
54290
+ import fs20 from "node:fs";
54291
+ import os15 from "node:os";
54292
+ import path16 from "node:path";
53555
54293
  function getStateDir() {
53556
- return path15.join(os14.homedir(), ".switchbot");
54294
+ return path16.join(os15.homedir(), ".switchbot");
53557
54295
  }
53558
54296
  function getDaemonPidFile() {
53559
- return path15.join(getStateDir(), "daemon.pid");
54297
+ return path16.join(getStateDir(), "daemon.pid");
53560
54298
  }
53561
54299
  function getDaemonLogFile() {
53562
- return path15.join(getStateDir(), "daemon.log");
54300
+ return path16.join(getStateDir(), "daemon.log");
53563
54301
  }
53564
54302
  function getDaemonStateFile() {
53565
- return path15.join(getStateDir(), "daemon.state.json");
54303
+ return path16.join(getStateDir(), "daemon.state.json");
53566
54304
  }
53567
54305
  function getHealthzPidFile() {
53568
- return path15.join(getStateDir(), "healthz.pid");
54306
+ return path16.join(getStateDir(), "healthz.pid");
53569
54307
  }
53570
54308
  var DAEMON_PID_FILE = getDaemonPidFile();
53571
54309
  var DAEMON_LOG_FILE = getDaemonLogFile();
53572
54310
  var DAEMON_STATE_FILE = getDaemonStateFile();
53573
54311
  var HEALTHZ_PID_FILE = getHealthzPidFile();
53574
54312
  function ensureStateDir() {
53575
- fs18.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
54313
+ fs20.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
53576
54314
  }
53577
54315
  function writeDaemonState(state) {
53578
54316
  ensureStateDir();
53579
- fs18.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
54317
+ fs20.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
53580
54318
  }
53581
54319
  function readDaemonState() {
53582
54320
  try {
53583
- const raw = fs18.readFileSync(getDaemonStateFile(), "utf-8");
54321
+ const raw = fs20.readFileSync(getDaemonStateFile(), "utf-8");
53584
54322
  return JSON.parse(raw);
53585
54323
  } catch {
53586
54324
  return null;
@@ -53589,26 +54327,26 @@ function readDaemonState() {
53589
54327
 
53590
54328
  // src/rules/pid-file.ts
53591
54329
  init_cjs_shim();
53592
- import fs19 from "node:fs";
53593
- import os15 from "node:os";
53594
- import path16 from "node:path";
53595
- var DEFAULT_DIR = path16.join(os15.homedir(), ".switchbot");
54330
+ import fs21 from "node:fs";
54331
+ import os16 from "node:os";
54332
+ import path17 from "node:path";
54333
+ var DEFAULT_DIR = path17.join(os16.homedir(), ".switchbot");
53596
54334
  function getDefaultPidFilePaths() {
53597
54335
  return {
53598
54336
  dir: DEFAULT_DIR,
53599
- pidFile: path16.join(DEFAULT_DIR, "rules.pid"),
53600
- reloadFile: path16.join(DEFAULT_DIR, "rules.reload")
54337
+ pidFile: path17.join(DEFAULT_DIR, "rules.pid"),
54338
+ reloadFile: path17.join(DEFAULT_DIR, "rules.reload")
53601
54339
  };
53602
54340
  }
53603
54341
  function writePidFile(pidFile, pid = process.pid) {
53604
- const dir = path16.dirname(pidFile);
53605
- fs19.mkdirSync(dir, { recursive: true, mode: 448 });
53606
- fs19.writeFileSync(pidFile, `${pid}
54342
+ const dir = path17.dirname(pidFile);
54343
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
54344
+ fs21.writeFileSync(pidFile, `${pid}
53607
54345
  `, { mode: 384 });
53608
54346
  }
53609
54347
  function readPidFile(pidFile) {
53610
54348
  try {
53611
- const raw = fs19.readFileSync(pidFile, "utf-8").trim();
54349
+ const raw = fs21.readFileSync(pidFile, "utf-8").trim();
53612
54350
  const n = Number(raw);
53613
54351
  return Number.isInteger(n) && n > 0 ? n : null;
53614
54352
  } catch {
@@ -53618,20 +54356,20 @@ function readPidFile(pidFile) {
53618
54356
  function clearPidFile(pidFile, pid = process.pid) {
53619
54357
  try {
53620
54358
  const existing = readPidFile(pidFile);
53621
- if (existing === pid) fs19.unlinkSync(pidFile);
54359
+ if (existing === pid) fs21.unlinkSync(pidFile);
53622
54360
  } catch {
53623
54361
  }
53624
54362
  }
53625
54363
  function writeReloadSentinel(reloadFile) {
53626
- const dir = path16.dirname(reloadFile);
53627
- fs19.mkdirSync(dir, { recursive: true, mode: 448 });
53628
- fs19.writeFileSync(reloadFile, `${Date.now()}
54364
+ const dir = path17.dirname(reloadFile);
54365
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
54366
+ fs21.writeFileSync(reloadFile, `${Date.now()}
53629
54367
  `, { mode: 384 });
53630
54368
  }
53631
54369
  function consumeReloadSentinel(reloadFile) {
53632
54370
  try {
53633
- if (!fs19.existsSync(reloadFile)) return false;
53634
- fs19.unlinkSync(reloadFile);
54371
+ if (!fs21.existsSync(reloadFile)) return false;
54372
+ fs21.unlinkSync(reloadFile);
53635
54373
  return true;
53636
54374
  } catch {
53637
54375
  return false;
@@ -53702,7 +54440,7 @@ async function checkCredentials() {
53702
54440
  };
53703
54441
  }
53704
54442
  const file2 = configFilePath();
53705
- if (!fs20.existsSync(file2)) {
54443
+ if (!fs22.existsSync(file2)) {
53706
54444
  return {
53707
54445
  name: "credentials",
53708
54446
  status: "fail",
@@ -53717,7 +54455,7 @@ async function checkCredentials() {
53717
54455
  };
53718
54456
  }
53719
54457
  try {
53720
- const raw = fs20.readFileSync(file2, "utf-8");
54458
+ const raw = fs22.readFileSync(file2, "utf-8");
53721
54459
  const cfg = JSON.parse(raw);
53722
54460
  if (!cfg.token || !cfg.secret) {
53723
54461
  return {
@@ -53764,8 +54502,8 @@ async function checkCredentials() {
53764
54502
  }
53765
54503
  }
53766
54504
  function checkProfiles() {
53767
- const dir = path17.join(os16.homedir(), ".switchbot", "profiles");
53768
- if (!fs20.existsSync(dir)) {
54505
+ const dir = path18.join(os17.homedir(), ".switchbot", "profiles");
54506
+ if (!fs22.existsSync(dir)) {
53769
54507
  return { name: "profiles", status: "ok", detail: "no profile dir (default profile only)" };
53770
54508
  }
53771
54509
  const profiles = listProfiles();
@@ -53872,8 +54610,8 @@ function checkCache() {
53872
54610
  }
53873
54611
  }
53874
54612
  function checkQuotaFile() {
53875
- const p2 = path17.join(os16.homedir(), ".switchbot", "quota.json");
53876
- if (!fs20.existsSync(p2)) {
54613
+ const p2 = path18.join(os17.homedir(), ".switchbot", "quota.json");
54614
+ if (!fs22.existsSync(p2)) {
53877
54615
  return {
53878
54616
  name: "quota",
53879
54617
  status: "ok",
@@ -53886,7 +54624,7 @@ function checkQuotaFile() {
53886
54624
  };
53887
54625
  }
53888
54626
  try {
53889
- const raw = fs20.readFileSync(p2, "utf-8");
54627
+ const raw = fs22.readFileSync(p2, "utf-8");
53890
54628
  JSON.parse(raw);
53891
54629
  } catch {
53892
54630
  return {
@@ -53966,8 +54704,8 @@ function checkInventoryConsistency() {
53966
54704
  };
53967
54705
  }
53968
54706
  function checkAudit() {
53969
- const p2 = path17.join(os16.homedir(), ".switchbot", "audit.log");
53970
- if (!fs20.existsSync(p2)) {
54707
+ const p2 = path18.join(os17.homedir(), ".switchbot", "audit.log");
54708
+ if (!fs22.existsSync(p2)) {
53971
54709
  return {
53972
54710
  name: "audit",
53973
54711
  status: "ok",
@@ -53979,7 +54717,7 @@ function checkAudit() {
53979
54717
  };
53980
54718
  }
53981
54719
  try {
53982
- const raw = fs20.readFileSync(p2, "utf-8");
54720
+ const raw = fs22.readFileSync(p2, "utf-8");
53983
54721
  const since = Date.now() - 24 * 60 * 60 * 1e3;
53984
54722
  const recent = [];
53985
54723
  let total = 0;
@@ -54179,7 +54917,7 @@ function checkPathDiscoverability() {
54179
54917
  let npmBinDir = null;
54180
54918
  try {
54181
54919
  const prefix = execSync("npm prefix -g", { timeout: 4e3, encoding: "utf-8" }).trim();
54182
- npmBinDir = isWindows ? prefix : path17.join(prefix, "bin");
54920
+ npmBinDir = isWindows ? prefix : path18.join(prefix, "bin");
54183
54921
  } catch {
54184
54922
  }
54185
54923
  let binaryOnPath = false;
@@ -54209,7 +54947,7 @@ function checkPathDiscoverability() {
54209
54947
  };
54210
54948
  }
54211
54949
  const currentPath = process.env.PATH ?? "";
54212
- const missingSegment = npmBinDir && !currentPath.split(path17.delimiter).includes(npmBinDir) ? npmBinDir : null;
54950
+ const missingSegment = npmBinDir && !currentPath.split(path18.delimiter).includes(npmBinDir) ? npmBinDir : null;
54213
54951
  const currentShell = detectShellFlavor();
54214
54952
  const shellFix = buildPathFix(currentShell, missingSegment, npmBinDir);
54215
54953
  return {
@@ -54310,9 +55048,9 @@ function checkMqtt() {
54310
55048
  };
54311
55049
  }
54312
55050
  const file2 = configFilePath();
54313
- if (fs20.existsSync(file2)) {
55051
+ if (fs22.existsSync(file2)) {
54314
55052
  try {
54315
- const cfg = JSON.parse(fs20.readFileSync(file2, "utf-8"));
55053
+ const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
54316
55054
  if (cfg.token && cfg.secret) {
54317
55055
  return {
54318
55056
  name: "mqtt",
@@ -54339,9 +55077,9 @@ async function checkMqttProbe() {
54339
55077
  creds = { token, secret };
54340
55078
  } else {
54341
55079
  const file2 = configFilePath();
54342
- if (fs20.existsSync(file2)) {
55080
+ if (fs22.existsSync(file2)) {
54343
55081
  try {
54344
- const cfg = JSON.parse(fs20.readFileSync(file2, "utf-8"));
55082
+ const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
54345
55083
  if (cfg.token && cfg.secret) {
54346
55084
  creds = { token: cfg.token, secret: cfg.secret };
54347
55085
  }
@@ -54435,10 +55173,12 @@ function checkNotifyConnectivity() {
54435
55173
  return { name: "notify-connectivity", status: "ok", detail: { present: false, message: "policy file could not be loaded" } };
54436
55174
  }
54437
55175
  const policy = loaded.data;
54438
- const rules = policy?.automation?.rules ?? [];
55176
+ const rawRules = policy?.automation?.rules;
55177
+ const rules = Array.isArray(rawRules) ? rawRules : [];
54439
55178
  const webhookUrls = [];
54440
55179
  for (const rule of rules) {
54441
- for (const action of rule.then ?? []) {
55180
+ const then = rule.then;
55181
+ for (const action of Array.isArray(then) ? then : []) {
54442
55182
  if (action.type === "notify" && (action.channel === "webhook" || action.channel === "openclaw") && action.to) {
54443
55183
  webhookUrls.push(action.to);
54444
55184
  }
@@ -54812,9 +55552,9 @@ init_arg_parsers();
54812
55552
  init_output();
54813
55553
  init_audit();
54814
55554
  init_devices();
54815
- import path18 from "node:path";
54816
- import os17 from "node:os";
54817
- var DEFAULT_AUDIT = path18.join(os17.homedir(), ".switchbot", "audit.log");
55555
+ import path19 from "node:path";
55556
+ import os18 from "node:os";
55557
+ var DEFAULT_AUDIT = path19.join(os18.homedir(), ".switchbot", "audit.log");
54818
55558
  function registerHistoryCommand(program3) {
54819
55559
  const history = program3.command("history").description("View and replay SwitchBot commands recorded via --audit-log").addHelpText("after", `
54820
55560
  Every 'devices command' run with --audit-log is appended as JSONL to the
@@ -55672,9 +56412,9 @@ init_load();
55672
56412
  init_validate();
55673
56413
  init_types();
55674
56414
  init_engine();
55675
- import fs22 from "node:fs";
55676
- import os19 from "node:os";
55677
- import path20 from "node:path";
56415
+ import fs24 from "node:fs";
56416
+ import os20 from "node:os";
56417
+ import path21 from "node:path";
55678
56418
 
55679
56419
  // src/rules/conflict-analyzer.ts
55680
56420
  init_cjs_shim();
@@ -55693,9 +56433,9 @@ var OPPOSING_PAIRS = [
55693
56433
  ["volumeUp", "volumeDown"],
55694
56434
  ["fanSpeedUp", "fanSpeedDown"]
55695
56435
  ];
55696
- var HIGH_FREQ_EVENTS = ["device.shadow", "*"];
56436
+ var HIGH_FREQ_EVENTS2 = ["device.shadow", "*"];
55697
56437
  function isHighFreqEvent(event) {
55698
- return HIGH_FREQ_EVENTS.includes(event);
56438
+ return HIGH_FREQ_EVENTS2.includes(event);
55699
56439
  }
55700
56440
  function commandsAreOpposing(a, b2) {
55701
56441
  for (const [x2, y] of OPPOSING_PAIRS) {
@@ -55837,9 +56577,9 @@ init_client2();
55837
56577
 
55838
56578
  // src/rules/webhook-token.ts
55839
56579
  init_cjs_shim();
55840
- import fs21 from "node:fs";
55841
- import os18 from "node:os";
55842
- import path19 from "node:path";
56580
+ import fs23 from "node:fs";
56581
+ import os19 from "node:os";
56582
+ import path20 from "node:path";
55843
56583
  import { randomBytes } from "node:crypto";
55844
56584
  var ENV_TOKEN = "SWITCHBOT_WEBHOOK_TOKEN";
55845
56585
  var DEFAULT_FILE = ".switchbot/webhook-token";
@@ -55847,7 +56587,7 @@ var WebhookTokenStore = class {
55847
56587
  filePath;
55848
56588
  envLookup;
55849
56589
  constructor(opts = {}) {
55850
- this.filePath = opts.filePath ?? path19.join(os18.homedir(), DEFAULT_FILE);
56590
+ this.filePath = opts.filePath ?? path20.join(os19.homedir(), DEFAULT_FILE);
55851
56591
  this.envLookup = opts.envLookup ?? (() => process.env[ENV_TOKEN]);
55852
56592
  }
55853
56593
  /**
@@ -55871,7 +56611,7 @@ var WebhookTokenStore = class {
55871
56611
  */
55872
56612
  readFromDisk() {
55873
56613
  try {
55874
- const raw = fs21.readFileSync(this.filePath, "utf-8").trim();
56614
+ const raw = fs23.readFileSync(this.filePath, "utf-8").trim();
55875
56615
  return raw.length > 0 ? raw : null;
55876
56616
  } catch (err) {
55877
56617
  if (err.code === "ENOENT") return null;
@@ -55888,12 +56628,12 @@ var WebhookTokenStore = class {
55888
56628
  return this.filePath;
55889
56629
  }
55890
56630
  writeToDisk(token) {
55891
- const dir = path19.dirname(this.filePath);
55892
- fs21.mkdirSync(dir, { recursive: true });
55893
- fs21.writeFileSync(this.filePath, `${token}
56631
+ const dir = path20.dirname(this.filePath);
56632
+ fs23.mkdirSync(dir, { recursive: true });
56633
+ fs23.writeFileSync(this.filePath, `${token}
55894
56634
  `, { mode: 384 });
55895
56635
  try {
55896
- fs21.chmodSync(this.filePath, 384);
56636
+ fs23.chmodSync(this.filePath, 384);
55897
56637
  } catch {
55898
56638
  }
55899
56639
  }
@@ -55978,18 +56718,18 @@ function aggregateRuleAudits(entries) {
55978
56718
  }
55979
56719
 
55980
56720
  // src/commands/rules.ts
55981
- var DEFAULT_AUDIT_PATH2 = path20.join(os19.homedir(), ".switchbot", "audit.log");
56721
+ var DEFAULT_AUDIT_PATH2 = path21.join(os20.homedir(), ".switchbot", "audit.log");
55982
56722
  function loadAutomation(policyPathFlag) {
55983
- const path28 = resolvePolicyPath({ flag: policyPathFlag });
56723
+ const path29 = resolvePolicyPath({ flag: policyPathFlag });
55984
56724
  let loaded;
55985
56725
  try {
55986
- loaded = loadPolicyFile(path28);
56726
+ loaded = loadPolicyFile(path29);
55987
56727
  } catch (err) {
55988
56728
  if (err instanceof PolicyFileNotFoundError) {
55989
56729
  exitWithError({
55990
56730
  code: 2,
55991
56731
  kind: "usage",
55992
- message: `policy file not found: ${path28}`,
56732
+ message: `policy file not found: ${path29}`,
55993
56733
  extra: { subKind: "file-not-found" }
55994
56734
  });
55995
56735
  }
@@ -55997,7 +56737,7 @@ function loadAutomation(policyPathFlag) {
55997
56737
  exitWithError({
55998
56738
  code: 3,
55999
56739
  kind: "runtime",
56000
- message: `YAML parse error in ${path28}: ${err.message}`,
56740
+ message: `YAML parse error in ${path29}: ${err.message}`,
56001
56741
  extra: { subKind: "yaml-parse", errors: err.yamlErrors }
56002
56742
  });
56003
56743
  }
@@ -56009,7 +56749,7 @@ function loadAutomation(policyPathFlag) {
56009
56749
  code: 4,
56010
56750
  kind: "runtime",
56011
56751
  message: "policy file failed schema validation. Run `switchbot policy validate` for details.",
56012
- extra: { subKind: "invalid-policy", path: path28 }
56752
+ extra: { subKind: "invalid-policy", path: path29 }
56013
56753
  });
56014
56754
  }
56015
56755
  const data = loaded.data ?? {};
@@ -56023,7 +56763,7 @@ function loadAutomation(policyPathFlag) {
56023
56763
  }
56024
56764
  const rawQH = data.quiet_hours;
56025
56765
  const quietHours = rawQH && typeof rawQH.start === "string" && typeof rawQH.end === "string" ? { start: rawQH.start, end: rawQH.end } : null;
56026
- return { path: path28, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
56766
+ return { path: path29, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
56027
56767
  }
56028
56768
  function describeTrigger(rule) {
56029
56769
  const t = rule.when;
@@ -56287,7 +57027,7 @@ function registerTail(rules) {
56287
57027
  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) => {
56288
57028
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56289
57029
  const sinceMs = resolveSinceMs(opts.since);
56290
- const existing = fs22.existsSync(file2) ? readAudit(file2) : [];
57030
+ const existing = fs24.existsSync(file2) ? readAudit(file2) : [];
56291
57031
  const filtered = filterRuleAudits(existing, { sinceMs, ruleName: opts.rule });
56292
57032
  if (isJsonMode()) {
56293
57033
  for (const e of filtered) console.log(JSON.stringify(e));
@@ -56299,7 +57039,7 @@ function registerTail(rules) {
56299
57039
  for (const e of filtered) console.log(formatAuditLine(e));
56300
57040
  }
56301
57041
  if (!opts.follow) return;
56302
- let offset = fs22.existsSync(file2) ? fs22.statSync(file2).size : 0;
57042
+ let offset = fs24.existsSync(file2) ? fs24.statSync(file2).size : 0;
56303
57043
  let buffer = "";
56304
57044
  const emit = (line) => {
56305
57045
  const trimmed = line.trim();
@@ -56316,21 +57056,21 @@ function registerTail(rules) {
56316
57056
  else console.log(formatAuditLine(entry));
56317
57057
  };
56318
57058
  const poll = setInterval(() => {
56319
- if (!fs22.existsSync(file2)) return;
56320
- const size = fs22.statSync(file2).size;
57059
+ if (!fs24.existsSync(file2)) return;
57060
+ const size = fs24.statSync(file2).size;
56321
57061
  if (size < offset) {
56322
57062
  offset = 0;
56323
57063
  buffer = "";
56324
57064
  }
56325
57065
  if (size === offset) return;
56326
- const fd = fs22.openSync(file2, "r");
57066
+ const fd = fs24.openSync(file2, "r");
56327
57067
  try {
56328
57068
  const chunk = Buffer.alloc(size - offset);
56329
- fs22.readSync(fd, chunk, 0, chunk.length, offset);
57069
+ fs24.readSync(fd, chunk, 0, chunk.length, offset);
56330
57070
  offset = size;
56331
57071
  buffer += chunk.toString("utf-8");
56332
57072
  } finally {
56333
- fs22.closeSync(fd);
57073
+ fs24.closeSync(fd);
56334
57074
  }
56335
57075
  let newline = buffer.indexOf("\n");
56336
57076
  while (newline !== -1) {
@@ -56370,7 +57110,7 @@ function formatReplayTable(report) {
56370
57110
  function registerReplay(rules) {
56371
57111
  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) => {
56372
57112
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56373
- const entries = fs22.existsSync(file2) ? readAudit(file2) : [];
57113
+ const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
56374
57114
  const sinceMs = resolveSinceMs(opts.since);
56375
57115
  const filtered = filterRuleAudits(entries, {
56376
57116
  sinceMs,
@@ -56490,7 +57230,7 @@ function registerSuggest(rules) {
56490
57230
  for (const w2 of warnings) process.stderr.write(`warning: ${w2}
56491
57231
  `);
56492
57232
  if (opts.out) {
56493
- fs22.writeFileSync(opts.out, ruleYaml, "utf8");
57233
+ fs24.writeFileSync(opts.out, ruleYaml, "utf8");
56494
57234
  if (!isJsonMode()) console.log(`\u2713 rule YAML written to ${opts.out}`);
56495
57235
  } else if (isJsonMode()) {
56496
57236
  printJson({ rule, rule_yaml: ruleYaml, warnings });
@@ -56565,7 +57305,7 @@ overall: ${overall ? "ok" : "issues found"}`);
56565
57305
  function registerSummary(rules) {
56566
57306
  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) => {
56567
57307
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56568
- const entries = fs22.existsSync(file2) ? readAudit(file2) : [];
57308
+ const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
56569
57309
  const sinceMs = resolveSinceMs(opts.since ?? "24h");
56570
57310
  const filtered = filterRuleAudits(entries, { sinceMs, ruleName: opts.rule });
56571
57311
  const report = aggregateRuleAudits(filtered);
@@ -56596,7 +57336,7 @@ function registerLastFired(rules) {
56596
57336
  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) => {
56597
57337
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56598
57338
  const n = opts.n ?? 10;
56599
- const entries = fs22.existsSync(file2) ? readAudit(file2) : [];
57339
+ const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
56600
57340
  const fires = filterRuleAudits(entries, {
56601
57341
  ruleName: opts.rule,
56602
57342
  kinds: ["rule-fire", "rule-fire-dry"]
@@ -56638,7 +57378,7 @@ function registerExplain(rules) {
56638
57378
  return;
56639
57379
  }
56640
57380
  const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
56641
- const entries = fs22.existsSync(auditFile) ? readAudit(auditFile) : [];
57381
+ const entries = fs24.existsSync(auditFile) ? readAudit(auditFile) : [];
56642
57382
  const fires = filterRuleAudits(entries, { ruleName: name, kinds: ["rule-fire", "rule-fire-dry"] });
56643
57383
  const lastFired = fires.length > 0 ? fires[fires.length - 1].t : null;
56644
57384
  const detail = {
@@ -56675,6 +57415,115 @@ function registerExplain(rules) {
56675
57415
  console.log(`last fired: ${detail.lastFired ?? "(never)"}`);
56676
57416
  });
56677
57417
  }
57418
+ function registerTraceExplain(rules) {
57419
+ 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(
57420
+ (fireIdArg, opts) => {
57421
+ const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
57422
+ if (!fs24.existsSync(auditFile)) {
57423
+ exitWithError({ code: 1, kind: "usage", message: `Audit log not found: ${auditFile}. Make sure trace recording is enabled (automation.audit.evaluate_trace: sampled or full).` });
57424
+ return;
57425
+ }
57426
+ const sinceIso = opts.since ? new Date(Date.now() - (parseDurationToMs2(opts.since) ?? 0)).toISOString() : void 0;
57427
+ let records = loadTraceRecords(auditFile, {
57428
+ fireId: fireIdArg,
57429
+ ruleName: opts.rule,
57430
+ since: sinceIso
57431
+ });
57432
+ if (records.length === 0) {
57433
+ const hint = 'Check that automation.audit.evaluate_trace is set to "sampled" or "full".';
57434
+ exitWithError({ code: 1, kind: "usage", message: `No rule-evaluate trace records found. ${hint}` });
57435
+ return;
57436
+ }
57437
+ if (opts.last) {
57438
+ records = [records[records.length - 1]];
57439
+ }
57440
+ for (const record2 of records) {
57441
+ const related = loadRelatedAudit(auditFile, record2.fireId);
57442
+ if (isJsonMode()) {
57443
+ console.log(formatExplainJson(record2, related));
57444
+ } else {
57445
+ console.log(formatExplainText(record2, related));
57446
+ if (records.length > 1) console.log("---");
57447
+ }
57448
+ }
57449
+ }
57450
+ );
57451
+ }
57452
+ function registerSimulate(rules) {
57453
+ rules.command("simulate <rule-or-policy>").description("Replay historical events against a rule and report would-fire / blocked outcomes.").option("--rule <name>", "Rule name to simulate (when <rule-or-policy> is a policy file).").option("--since <duration>", "Replay events from this window (e.g. 7d, 24h).").option("--against <file>", "Replay from a JSONL file of EngineEvent objects instead of the audit log.").option("--live-llm", "Allow live LLM calls for llm conditions (default: mark as would-call).").option("--audit-log <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2}).`).option("--report-out <path>", "Write the full JSON report to this file.").action(
57454
+ async (ruleOrPolicy, opts) => {
57455
+ let rule;
57456
+ const auditLog = opts.auditLog ?? DEFAULT_AUDIT_PATH2;
57457
+ if (!fs24.existsSync(ruleOrPolicy)) {
57458
+ exitWithError({ code: 2, kind: "usage", message: `File not found: ${ruleOrPolicy}` });
57459
+ return;
57460
+ }
57461
+ let parsed;
57462
+ try {
57463
+ const { parse: yamlParse5 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
57464
+ parsed = yamlParse5(fs24.readFileSync(ruleOrPolicy, "utf-8"));
57465
+ } catch {
57466
+ exitWithError({ code: 2, kind: "usage", message: `Could not parse YAML file: ${ruleOrPolicy}` });
57467
+ return;
57468
+ }
57469
+ const asRule = parsed;
57470
+ if (asRule["name"] && asRule["when"] && asRule["then"]) {
57471
+ rule = asRule;
57472
+ } else {
57473
+ const automation = loadAutomation(ruleOrPolicy);
57474
+ if (!automation) return;
57475
+ const ruleName = opts.rule;
57476
+ if (!ruleName) {
57477
+ exitWithError({ code: 1, kind: "usage", message: "Use --rule <name> to specify which rule to simulate from the policy file." });
57478
+ return;
57479
+ }
57480
+ rule = automation.automation?.rules?.find((r) => r.name === ruleName);
57481
+ if (!rule) {
57482
+ exitWithError({ code: 1, kind: "usage", message: `Rule "${ruleName}" not found in policy file.` });
57483
+ return;
57484
+ }
57485
+ }
57486
+ try {
57487
+ const report = await simulateRule({
57488
+ rule,
57489
+ since: opts.since,
57490
+ against: opts.against,
57491
+ auditLog,
57492
+ liveLlm: opts.liveLlm ?? false
57493
+ });
57494
+ if (opts.reportOut) {
57495
+ fs24.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
57496
+ console.log(`Report written to ${opts.reportOut}`);
57497
+ }
57498
+ if (isJsonMode()) {
57499
+ printJson(report);
57500
+ } else {
57501
+ console.log(`Rule: ${report.ruleName} (version ${report.ruleVersion})`);
57502
+ console.log(`Window: ${report.windowStart.toISOString()} \u2192 ${report.windowEnd.toISOString()}`);
57503
+ console.log(`Source events: ${report.sourceEventCount}`);
57504
+ console.log("");
57505
+ console.log(` Would fire: ${report.wouldFire}`);
57506
+ console.log(` Blocked by condition:${report.blockedByCondition}`);
57507
+ console.log(` Throttled: ${report.throttled}`);
57508
+ console.log(` Errored: ${report.errored}`);
57509
+ if (report.skippedLlm > 0) {
57510
+ console.log(` Skipped (llm): ${report.skippedLlm} (use --live-llm to evaluate)`);
57511
+ }
57512
+ if (report.topBlockReason && report.topBlockCount !== void 0) {
57513
+ const total = report.blockedByCondition;
57514
+ const pct = total > 0 ? Math.round(report.topBlockCount / total * 100) : 0;
57515
+ if (pct >= 80) {
57516
+ console.log("");
57517
+ console.log(`Top block reason (${pct}%): ${report.topBlockReason}`);
57518
+ }
57519
+ }
57520
+ }
57521
+ } catch (err) {
57522
+ handleError(err);
57523
+ }
57524
+ }
57525
+ );
57526
+ }
56678
57527
  function registerRulesCommand(program3) {
56679
57528
  const rules = program3.command("rules").description("Run, list, and lint automation rules declared in policy.yaml (v0.2, preview).").addHelpText(
56680
57529
  "after",
@@ -56722,6 +57571,8 @@ Exit codes (lint):
56722
57571
  registerDoctor(rules);
56723
57572
  registerSummary(rules);
56724
57573
  registerLastFired(rules);
57574
+ registerTraceExplain(rules);
57575
+ registerSimulate(rules);
56725
57576
  registerWebhookRotateToken(rules);
56726
57577
  registerWebhookShowToken(rules);
56727
57578
  }
@@ -56732,9 +57583,9 @@ init_output();
56732
57583
  init_arg_parsers();
56733
57584
  init_request_context();
56734
57585
  init_keychain();
56735
- import fs23 from "node:fs";
56736
- import path21 from "node:path";
56737
- import os20 from "node:os";
57586
+ import fs25 from "node:fs";
57587
+ import path22 from "node:path";
57588
+ import os21 from "node:os";
56738
57589
  import readline5 from "node:readline";
56739
57590
  function activeProfile() {
56740
57591
  return getActiveProfile() ?? "default";
@@ -56780,7 +57631,7 @@ async function promptSecret2(question) {
56780
57631
  });
56781
57632
  }
56782
57633
  function readStdinFile(filePath) {
56783
- if (!fs23.existsSync(filePath)) {
57634
+ if (!fs25.existsSync(filePath)) {
56784
57635
  exitWithError({
56785
57636
  code: 2,
56786
57637
  kind: "usage",
@@ -56789,7 +57640,7 @@ function readStdinFile(filePath) {
56789
57640
  }
56790
57641
  let parsed;
56791
57642
  try {
56792
- parsed = JSON.parse(fs23.readFileSync(filePath, "utf-8"));
57643
+ parsed = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
56793
57644
  } catch (err) {
56794
57645
  exitWithError({
56795
57646
  code: 2,
@@ -56819,10 +57670,10 @@ function cleanupMigratedSourceFile(sourceFile, parsed) {
56819
57670
  delete next.token;
56820
57671
  delete next.secret;
56821
57672
  if (Object.keys(next).length === 0) {
56822
- fs23.unlinkSync(sourceFile);
57673
+ fs25.unlinkSync(sourceFile);
56823
57674
  return "deleted";
56824
57675
  }
56825
- fs23.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
57676
+ fs25.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
56826
57677
  return "scrubbed";
56827
57678
  }
56828
57679
  function registerAuthCommand(program3) {
@@ -56953,8 +57804,8 @@ function registerAuthCommand(program3) {
56953
57804
  message: `backend "${store.name}" is not writable on this machine`
56954
57805
  });
56955
57806
  }
56956
- const sourceFile = profile === "default" ? path21.join(os20.homedir(), ".switchbot", "config.json") : path21.join(os20.homedir(), ".switchbot", "profiles", `${profile}.json`);
56957
- if (!fs23.existsSync(sourceFile)) {
57807
+ const sourceFile = profile === "default" ? path22.join(os21.homedir(), ".switchbot", "config.json") : path22.join(os21.homedir(), ".switchbot", "profiles", `${profile}.json`);
57808
+ if (!fs25.existsSync(sourceFile)) {
56958
57809
  exitWithError({
56959
57810
  code: 2,
56960
57811
  kind: "usage",
@@ -56964,7 +57815,7 @@ function registerAuthCommand(program3) {
56964
57815
  }
56965
57816
  let parsed;
56966
57817
  try {
56967
- const raw = JSON.parse(fs23.readFileSync(sourceFile, "utf-8"));
57818
+ const raw = JSON.parse(fs25.readFileSync(sourceFile, "utf-8"));
56968
57819
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
56969
57820
  throw new Error("expected a JSON object");
56970
57821
  }
@@ -57026,8 +57877,8 @@ function registerAuthCommand(program3) {
57026
57877
  init_cjs_shim();
57027
57878
  init_esm();
57028
57879
  init_load();
57029
- import fs26 from "node:fs";
57030
- import path24 from "node:path";
57880
+ import fs28 from "node:fs";
57881
+ import path25 from "node:path";
57031
57882
 
57032
57883
  // src/install/steps.ts
57033
57884
  init_cjs_shim();
@@ -57079,9 +57930,9 @@ init_cjs_shim();
57079
57930
  init_load();
57080
57931
  init_validate();
57081
57932
  init_keychain();
57082
- import fs24 from "node:fs";
57083
- import path22 from "node:path";
57084
- import os21 from "node:os";
57933
+ import fs26 from "node:fs";
57934
+ import path23 from "node:path";
57935
+ import os22 from "node:os";
57085
57936
  function parseMajor(version2) {
57086
57937
  const m2 = /^v?(\d+)\./.exec(version2);
57087
57938
  if (!m2) return null;
@@ -57171,10 +58022,10 @@ async function checkKeychain2() {
57171
58022
  }
57172
58023
  }
57173
58024
  function checkHomeDirWritable() {
57174
- const home = os21.homedir();
57175
- const switchbotDir = path22.join(home, ".switchbot");
58025
+ const home = os22.homedir();
58026
+ const switchbotDir = path23.join(home, ".switchbot");
57176
58027
  try {
57177
- const homeStat = fs24.statSync(home);
58028
+ const homeStat = fs26.statSync(home);
57178
58029
  if (!homeStat.isDirectory()) {
57179
58030
  return {
57180
58031
  name: "home",
@@ -57183,8 +58034,8 @@ function checkHomeDirWritable() {
57183
58034
  hint: "check your HOME/USERPROFILE environment configuration"
57184
58035
  };
57185
58036
  }
57186
- if (fs24.existsSync(switchbotDir)) {
57187
- const sbStat = fs24.statSync(switchbotDir);
58037
+ if (fs26.existsSync(switchbotDir)) {
58038
+ const sbStat = fs26.statSync(switchbotDir);
57188
58039
  if (!sbStat.isDirectory()) {
57189
58040
  return {
57190
58041
  name: "home",
@@ -57193,10 +58044,10 @@ function checkHomeDirWritable() {
57193
58044
  hint: "move the file aside and re-run install"
57194
58045
  };
57195
58046
  }
57196
- fs24.accessSync(switchbotDir, fs24.constants.W_OK);
58047
+ fs26.accessSync(switchbotDir, fs26.constants.W_OK);
57197
58048
  return { name: "home", status: "ok", message: `writable: ${switchbotDir}` };
57198
58049
  }
57199
- fs24.accessSync(home, fs24.constants.W_OK);
58050
+ fs26.accessSync(home, fs26.constants.W_OK);
57200
58051
  return { name: "home", status: "ok", message: `writable: ${home}` };
57201
58052
  } catch (err) {
57202
58053
  return {
@@ -57210,8 +58061,8 @@ function checkHomeDirWritable() {
57210
58061
  function nearestExistingPath(target) {
57211
58062
  let cur = target;
57212
58063
  while (true) {
57213
- if (fs24.existsSync(cur)) return cur;
57214
- const parent = path22.dirname(cur);
58064
+ if (fs26.existsSync(cur)) return cur;
58065
+ const parent = path23.dirname(cur);
57215
58066
  if (parent === cur) return null;
57216
58067
  cur = parent;
57217
58068
  }
@@ -57219,8 +58070,8 @@ function nearestExistingPath(target) {
57219
58070
  function checkAgentSkillDirWritable(opts) {
57220
58071
  const shouldCheck = opts.agent === "claude-code" && (opts.expectSkillLink ?? true);
57221
58072
  if (!shouldCheck) return null;
57222
- const home = os21.homedir();
57223
- const target = path22.join(home, ".claude", "skills");
58073
+ const home = os22.homedir();
58074
+ const target = path23.join(home, ".claude", "skills");
57224
58075
  try {
57225
58076
  const existing = nearestExistingPath(target);
57226
58077
  if (!existing) {
@@ -57231,7 +58082,7 @@ function checkAgentSkillDirWritable(opts) {
57231
58082
  hint: "check your home directory path and permissions"
57232
58083
  };
57233
58084
  }
57234
- const stat = fs24.statSync(existing);
58085
+ const stat = fs26.statSync(existing);
57235
58086
  if (!stat.isDirectory()) {
57236
58087
  return {
57237
58088
  name: "agent-skills-dir",
@@ -57240,7 +58091,7 @@ function checkAgentSkillDirWritable(opts) {
57240
58091
  hint: "move the blocking file aside and re-run install"
57241
58092
  };
57242
58093
  }
57243
- fs24.accessSync(existing, fs24.constants.W_OK);
58094
+ fs26.accessSync(existing, fs26.constants.W_OK);
57244
58095
  return { name: "agent-skills-dir", status: "ok", message: `writable: ${target}` };
57245
58096
  } catch (err) {
57246
58097
  return {
@@ -57265,9 +58116,9 @@ async function runPreflight(options = {}) {
57265
58116
 
57266
58117
  // src/install/default-steps.ts
57267
58118
  init_cjs_shim();
57268
- import fs25 from "node:fs";
57269
- import path23 from "node:path";
57270
- import os22 from "node:os";
58119
+ import fs27 from "node:fs";
58120
+ import path24 from "node:path";
58121
+ import os23 from "node:os";
57271
58122
  import { spawnSync } from "node:child_process";
57272
58123
  init_keychain();
57273
58124
  function stepPromptCredentials() {
@@ -57342,15 +58193,15 @@ function stepScaffoldPolicy() {
57342
58193
  const r = ctx.policyScaffoldResult;
57343
58194
  if (!r || r.skipped) return;
57344
58195
  try {
57345
- fs25.unlinkSync(r.policyPath);
58196
+ fs27.unlinkSync(r.policyPath);
57346
58197
  } catch {
57347
58198
  }
57348
58199
  }
57349
58200
  };
57350
58201
  }
57351
- function skillLinkPathFor(agent, home = os22.homedir()) {
58202
+ function skillLinkPathFor(agent, home = os23.homedir()) {
57352
58203
  if (agent === "claude-code") {
57353
- return path23.join(home, ".claude", "skills", "switchbot");
58204
+ return path24.join(home, ".claude", "skills", "switchbot");
57354
58205
  }
57355
58206
  return null;
57356
58207
  }
@@ -57364,11 +58215,11 @@ function stepSymlinkSkill(opts = {}) {
57364
58215
  ctx.skillRecipePrinted = true;
57365
58216
  return;
57366
58217
  }
57367
- const target = path23.resolve(ctx.skillPath);
57368
- if (!fs25.existsSync(target)) {
58218
+ const target = path24.resolve(ctx.skillPath);
58219
+ if (!fs27.existsSync(target)) {
57369
58220
  throw new Error(`--skill-path does not exist: ${target}`);
57370
58221
  }
57371
- const stat = fs25.statSync(target);
58222
+ const stat = fs27.statSync(target);
57372
58223
  if (!stat.isDirectory()) {
57373
58224
  throw new Error(`--skill-path is not a directory: ${target}`);
57374
58225
  }
@@ -57377,17 +58228,17 @@ function stepSymlinkSkill(opts = {}) {
57377
58228
  ctx.skillRecipePrinted = true;
57378
58229
  return;
57379
58230
  }
57380
- if (!opts.force && !fs25.existsSync(path23.join(target, "SKILL.md"))) {
58231
+ if (!opts.force && !fs27.existsSync(path24.join(target, "SKILL.md"))) {
57381
58232
  throw new Error(
57382
58233
  `${target} does not look like a skill (no SKILL.md at the root). Pass --force if you really mean to link this directory.`
57383
58234
  );
57384
58235
  }
57385
- if (fs25.existsSync(linkPath)) {
57386
- const st = fs25.lstatSync(linkPath);
58236
+ if (fs27.existsSync(linkPath)) {
58237
+ const st = fs27.lstatSync(linkPath);
57387
58238
  if (st.isSymbolicLink()) {
57388
58239
  let existingTarget = null;
57389
58240
  try {
57390
- existingTarget = path23.resolve(path23.dirname(linkPath), fs25.readlinkSync(linkPath));
58241
+ existingTarget = path24.resolve(path24.dirname(linkPath), fs27.readlinkSync(linkPath));
57391
58242
  } catch {
57392
58243
  existingTarget = null;
57393
58244
  }
@@ -57401,23 +58252,23 @@ function stepSymlinkSkill(opts = {}) {
57401
58252
  `${linkPath} already links to ${existingTarget ?? "(unreadable)"}; pass --force to replace it, or run \`switchbot uninstall\` first.`
57402
58253
  );
57403
58254
  }
57404
- fs25.unlinkSync(linkPath);
58255
+ fs27.unlinkSync(linkPath);
57405
58256
  } else {
57406
58257
  throw new Error(
57407
58258
  `${linkPath} exists and is not a symlink; refusing to clobber (move it aside and re-run)`
57408
58259
  );
57409
58260
  }
57410
58261
  }
57411
- fs25.mkdirSync(path23.dirname(linkPath), { recursive: true });
58262
+ fs27.mkdirSync(path24.dirname(linkPath), { recursive: true });
57412
58263
  const linkType = process.platform === "win32" ? "junction" : "dir";
57413
- fs25.symlinkSync(target, linkPath, linkType);
58264
+ fs27.symlinkSync(target, linkPath, linkType);
57414
58265
  ctx.skillLinkPath = linkPath;
57415
58266
  ctx.skillLinkCreated = true;
57416
58267
  },
57417
58268
  undo(ctx) {
57418
58269
  if (!ctx.skillLinkCreated || !ctx.skillLinkPath) return;
57419
58270
  try {
57420
- fs25.unlinkSync(ctx.skillLinkPath);
58271
+ fs27.unlinkSync(ctx.skillLinkPath);
57421
58272
  } catch {
57422
58273
  }
57423
58274
  }
@@ -57560,8 +58411,8 @@ Examples:
57560
58411
  const agent = parseAgent(opts.agent);
57561
58412
  const profile = getActiveProfile() ?? "default";
57562
58413
  const skip = parseSkipList(opts.skip);
57563
- const skillPath = opts.skillPath ? path24.resolve(opts.skillPath) : void 0;
57564
- const tokenFile = opts.tokenFile ? path24.resolve(opts.tokenFile) : void 0;
58414
+ const skillPath = opts.skillPath ? path25.resolve(opts.skillPath) : void 0;
58415
+ const tokenFile = opts.tokenFile ? path25.resolve(opts.tokenFile) : void 0;
57565
58416
  const force = Boolean(opts.force);
57566
58417
  const verify = Boolean(opts.verify);
57567
58418
  const globalOpts = command.parent?.opts() ?? {};
@@ -57605,7 +58456,7 @@ Examples:
57605
58456
  const report = await runInstall(steps, { context: ctx });
57606
58457
  if (report.ok && tokenFile) {
57607
58458
  try {
57608
- fs26.unlinkSync(tokenFile);
58459
+ fs28.unlinkSync(tokenFile);
57609
58460
  } catch {
57610
58461
  }
57611
58462
  }
@@ -57664,7 +58515,7 @@ Examples:
57664
58515
  init_cjs_shim();
57665
58516
  init_esm();
57666
58517
  init_load();
57667
- import fs27 from "node:fs";
58518
+ import fs29 from "node:fs";
57668
58519
  import readline6 from "node:readline";
57669
58520
  init_keychain();
57670
58521
  init_output();
@@ -57729,10 +58580,10 @@ Examples:
57729
58580
  action: "remove-skill-link",
57730
58581
  detail: skillLink,
57731
58582
  run: async () => {
57732
- if (!fs27.existsSync(skillLink)) {
58583
+ if (!fs29.existsSync(skillLink)) {
57733
58584
  return { action: "remove-skill-link", status: "absent", detail: skillLink };
57734
58585
  }
57735
- const stat = fs27.lstatSync(skillLink);
58586
+ const stat = fs29.lstatSync(skillLink);
57736
58587
  if (!stat.isSymbolicLink()) {
57737
58588
  return {
57738
58589
  action: "remove-skill-link",
@@ -57743,7 +58594,7 @@ Examples:
57743
58594
  const ok = yes ? true : await prompt(`Remove skill link ${skillLink}?`, true);
57744
58595
  if (!ok) return { action: "remove-skill-link", status: "skipped", detail: skillLink };
57745
58596
  try {
57746
- fs27.unlinkSync(skillLink);
58597
+ fs29.unlinkSync(skillLink);
57747
58598
  return { action: "remove-skill-link", status: "removed", detail: skillLink };
57748
58599
  } catch (err) {
57749
58600
  return {
@@ -57798,13 +58649,13 @@ Examples:
57798
58649
  detail: "pass --remove-policy to delete policy.yaml"
57799
58650
  };
57800
58651
  }
57801
- if (!fs27.existsSync(policyPath)) {
58652
+ if (!fs29.existsSync(policyPath)) {
57802
58653
  return { action: "remove-policy", status: "absent", detail: policyPath };
57803
58654
  }
57804
58655
  const ok = yes ? true : await prompt(`Delete policy file ${policyPath}?`, false);
57805
58656
  if (!ok) return { action: "remove-policy", status: "skipped", detail: policyPath };
57806
58657
  try {
57807
- fs27.unlinkSync(policyPath);
58658
+ fs29.unlinkSync(policyPath);
57808
58659
  return { action: "remove-policy", status: "removed", detail: policyPath };
57809
58660
  } catch (err) {
57810
58661
  return {
@@ -57867,9 +58718,9 @@ init_request_context();
57867
58718
  init_output();
57868
58719
  init_flags();
57869
58720
  import { spawn as spawn4, spawnSync as spawnSync2 } from "node:child_process";
57870
- import fs28 from "node:fs";
57871
- import os23 from "node:os";
57872
- import path25 from "node:path";
58721
+ import fs30 from "node:fs";
58722
+ import os24 from "node:os";
58723
+ import path26 from "node:path";
57873
58724
  var DEFAULT_OPENCLAW_URL = "http://localhost:18789";
57874
58725
  function resolveStatusSyncRuntime(options) {
57875
58726
  if (!tryLoadConfig()) {
@@ -58003,14 +58854,14 @@ async function probeStatusSyncStart(options = {}) {
58003
58854
  };
58004
58855
  }
58005
58856
  function resolveStatusSyncPaths(explicitStateDir) {
58006
- const stateDir = path25.resolve(
58007
- explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ?? path25.join(os23.homedir(), ".switchbot", "status-sync")
58857
+ const stateDir = path26.resolve(
58858
+ explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ?? path26.join(os24.homedir(), ".switchbot", "status-sync")
58008
58859
  );
58009
58860
  return {
58010
58861
  stateDir,
58011
- stateFile: path25.join(stateDir, "state.json"),
58012
- stdoutLog: path25.join(stateDir, "stdout.log"),
58013
- stderrLog: path25.join(stateDir, "stderr.log")
58862
+ stateFile: path26.join(stateDir, "state.json"),
58863
+ stdoutLog: path26.join(stateDir, "stdout.log"),
58864
+ stderrLog: path26.join(stateDir, "stderr.log")
58014
58865
  };
58015
58866
  }
58016
58867
  function buildStatusSyncChildArgs(options) {
@@ -58018,11 +58869,11 @@ function buildStatusSyncChildArgs(options) {
58018
58869
  if (!scriptPath) {
58019
58870
  throw new Error("Cannot determine the current CLI entrypoint path.");
58020
58871
  }
58021
- const args = [path25.resolve(scriptPath)];
58872
+ const args = [path26.resolve(scriptPath)];
58022
58873
  const configPath = getConfigPath();
58023
58874
  const profile = getActiveProfile();
58024
58875
  if (configPath) {
58025
- args.push("--config", path25.resolve(configPath));
58876
+ args.push("--config", path26.resolve(configPath));
58026
58877
  } else if (profile) {
58027
58878
  args.push("--profile", profile);
58028
58879
  }
@@ -58043,7 +58894,7 @@ function buildStatusSyncChildArgs(options) {
58043
58894
  }
58044
58895
  function safeUnlink(filePath) {
58045
58896
  try {
58046
- fs28.unlinkSync(filePath);
58897
+ fs30.unlinkSync(filePath);
58047
58898
  } catch {
58048
58899
  }
58049
58900
  }
@@ -58058,9 +58909,9 @@ function isProcessRunning(pid) {
58058
58909
  }
58059
58910
  }
58060
58911
  function readStateFile(paths) {
58061
- if (!fs28.existsSync(paths.stateFile)) return null;
58912
+ if (!fs30.existsSync(paths.stateFile)) return null;
58062
58913
  try {
58063
- const raw = JSON.parse(fs28.readFileSync(paths.stateFile, "utf-8"));
58914
+ const raw = JSON.parse(fs30.readFileSync(paths.stateFile, "utf-8"));
58064
58915
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
58065
58916
  safeUnlink(paths.stateFile);
58066
58917
  return null;
@@ -58179,14 +59030,14 @@ function startStatusSync(options = {}) {
58179
59030
  }
58180
59031
  stopStatusSync({ stateDir: paths.stateDir });
58181
59032
  }
58182
- fs28.mkdirSync(paths.stateDir, { recursive: true });
59033
+ fs30.mkdirSync(paths.stateDir, { recursive: true });
58183
59034
  const configPath = getConfigPath();
58184
59035
  const command = buildStatusSyncChildArgs(runtime);
58185
59036
  let stdoutFd = null;
58186
59037
  let stderrFd = null;
58187
59038
  try {
58188
- stdoutFd = fs28.openSync(paths.stdoutLog, "a");
58189
- stderrFd = fs28.openSync(paths.stderrLog, "a");
59039
+ stdoutFd = fs30.openSync(paths.stdoutLog, "a");
59040
+ stderrFd = fs30.openSync(paths.stderrLog, "a");
58190
59041
  const child = spawn4(process.execPath, command, {
58191
59042
  detached: true,
58192
59043
  stdio: ["ignore", stdoutFd, stderrFd],
@@ -58204,16 +59055,16 @@ function startStatusSync(options = {}) {
58204
59055
  openclawUrl: runtime.openclawUrl,
58205
59056
  openclawModel: runtime.openclawModel,
58206
59057
  topic: runtime.topic ?? null,
58207
- configPath: configPath ? path25.resolve(configPath) : null,
59058
+ configPath: configPath ? path26.resolve(configPath) : null,
58208
59059
  profile: configPath ? null : getActiveProfile() ?? null,
58209
59060
  stdoutLog: paths.stdoutLog,
58210
59061
  stderrLog: paths.stderrLog
58211
59062
  };
58212
- fs28.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
59063
+ fs30.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
58213
59064
  return toStatus(paths, state, true);
58214
59065
  } finally {
58215
- if (stdoutFd !== null) fs28.closeSync(stdoutFd);
58216
- if (stderrFd !== null) fs28.closeSync(stderrFd);
59066
+ if (stdoutFd !== null) fs30.closeSync(stdoutFd);
59067
+ if (stderrFd !== null) fs30.closeSync(stderrFd);
58217
59068
  }
58218
59069
  }
58219
59070
  async function runStatusSyncForeground(options = {}) {
@@ -58360,10 +59211,10 @@ init_cjs_shim();
58360
59211
  init_quota();
58361
59212
  init_audit();
58362
59213
  init_client();
58363
- import fs29 from "node:fs";
58364
- import os24 from "node:os";
58365
- import path26 from "node:path";
58366
- var DEFAULT_AUDIT_PATH3 = path26.join(os24.homedir(), ".switchbot", "audit.log");
59214
+ import fs31 from "node:fs";
59215
+ import os25 from "node:os";
59216
+ import path27 from "node:path";
59217
+ var DEFAULT_AUDIT_PATH3 = path27.join(os25.homedir(), ".switchbot", "audit.log");
58367
59218
  var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
58368
59219
  function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
58369
59220
  const now = /* @__PURE__ */ new Date();
@@ -58384,7 +59235,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
58384
59235
  status: pct >= 90 ? "critical" : pct >= 70 ? "warn" : "ok"
58385
59236
  };
58386
59237
  let auditHealth;
58387
- if (!fs29.existsSync(auditPath)) {
59238
+ if (!fs31.existsSync(auditPath)) {
58388
59239
  auditHealth = { present: false, recentErrors: 0, recentTotal: 0, errorRatePercent: 0, status: "ok" };
58389
59240
  } else {
58390
59241
  const entries = readAudit(auditPath);
@@ -58654,8 +59505,8 @@ function registerUpgradeCheckCommand(program3) {
58654
59505
  init_cjs_shim();
58655
59506
  init_output();
58656
59507
  import { spawn as spawn5 } from "node:child_process";
58657
- import fs30 from "node:fs";
58658
- import path27 from "node:path";
59508
+ import fs32 from "node:fs";
59509
+ import path28 from "node:path";
58659
59510
  import { fileURLToPath as fileURLToPath3 } from "node:url";
58660
59511
  init_arg_parsers();
58661
59512
  init_source();
@@ -58717,7 +59568,7 @@ function persistState(partial2) {
58717
59568
  }
58718
59569
  function readLastLines(filePath, n = 20) {
58719
59570
  try {
58720
- const content = fs30.readFileSync(filePath, "utf-8");
59571
+ const content = fs32.readFileSync(filePath, "utf-8");
58721
59572
  const lines = content.split("\n");
58722
59573
  return lines.slice(Math.max(0, lines.length - n)).join("\n").trim();
58723
59574
  } catch {
@@ -58807,10 +59658,10 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58807
59658
  }
58808
59659
  }
58809
59660
  const thisFile = fileURLToPath3(import.meta.url);
58810
- const cliEntry = path27.resolve(path27.dirname(thisFile), "..", "index.js");
59661
+ const cliEntry = path28.resolve(path28.dirname(thisFile), "..", "index.js");
58811
59662
  const args = ["rules", "run"];
58812
59663
  if (opts.policy) args.push(opts.policy);
58813
- fs30.mkdirSync(path27.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
59664
+ fs32.mkdirSync(path28.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
58814
59665
  persistState({
58815
59666
  status: "starting",
58816
59667
  pid: null,
@@ -58822,14 +59673,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58822
59673
  healthzPid: null,
58823
59674
  healthzPidFile: HEALTHZ_PID_FILE
58824
59675
  });
58825
- const logFd = fs30.openSync(DAEMON_LOG_FILE, "a");
59676
+ const logFd = fs32.openSync(DAEMON_LOG_FILE, "a");
58826
59677
  const child = spawn5(process.execPath, [cliEntry, ...args], {
58827
59678
  detached: true,
58828
59679
  stdio: ["ignore", logFd, logFd],
58829
59680
  env: { ...process.env }
58830
59681
  });
58831
59682
  child.unref();
58832
- fs30.closeSync(logFd);
59683
+ fs32.closeSync(logFd);
58833
59684
  await probeLiveness({
58834
59685
  child,
58835
59686
  delayMs: 300,
@@ -58852,14 +59703,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58852
59703
  let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
58853
59704
  if (healthzPort !== null) {
58854
59705
  const healthArgs = ["health", "serve", "--port", String(healthzPort)];
58855
- const healthLogFd = fs30.openSync(DAEMON_LOG_FILE, "a");
59706
+ const healthLogFd = fs32.openSync(DAEMON_LOG_FILE, "a");
58856
59707
  const healthChild = spawn5(process.execPath, [cliEntry, ...healthArgs], {
58857
59708
  detached: true,
58858
59709
  stdio: ["ignore", healthLogFd, healthLogFd],
58859
59710
  env: { ...process.env }
58860
59711
  });
58861
59712
  healthChild.unref();
58862
- fs30.closeSync(healthLogFd);
59713
+ fs32.closeSync(healthLogFd);
58863
59714
  if (healthChild.pid) {
58864
59715
  const healthAlive = await probeLiveness({ child: healthChild, delayMs: 200, fatal: false });
58865
59716
  if (healthAlive) {
@@ -58922,11 +59773,11 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58922
59773
  });
58923
59774
  }
58924
59775
  try {
58925
- fs30.unlinkSync(DAEMON_PID_FILE);
59776
+ fs32.unlinkSync(DAEMON_PID_FILE);
58926
59777
  } catch {
58927
59778
  }
58928
59779
  try {
58929
- fs30.unlinkSync(HEALTHZ_PID_FILE);
59780
+ fs32.unlinkSync(HEALTHZ_PID_FILE);
58930
59781
  } catch {
58931
59782
  }
58932
59783
  persistState({