@switchbot/openapi-cli 3.3.3 → 3.4.1

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
  }
@@ -7221,7 +7221,7 @@ function emitJsonError(errorPayload) {
7221
7221
  function emitStreamHeader(opts) {
7222
7222
  console.log(
7223
7223
  JSON.stringify({
7224
- schemaVersion: SCHEMA_VERSION,
7224
+ schemaVersion: opts.schemaVersion ?? SCHEMA_VERSION,
7225
7225
  stream: true,
7226
7226
  eventKind: opts.eventKind,
7227
7227
  cadence: opts.cadence
@@ -7531,7 +7531,7 @@ var init_output = __esm({
7531
7531
  init_source();
7532
7532
  init_client();
7533
7533
  init_flags();
7534
- SCHEMA_VERSION = "1.1";
7534
+ SCHEMA_VERSION = "1.2";
7535
7535
  ASCII_BORDER_CHARS = {
7536
7536
  top: "-",
7537
7537
  "top-mid": "+",
@@ -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,
@@ -27958,8 +28296,21 @@ function commandToJson(cmd, opts = {}) {
27958
28296
  }
27959
28297
  function resolveTargetCommand(root, argv) {
27960
28298
  let cmd = root;
28299
+ const rootOptions = root.options;
28300
+ let consumeNext = false;
27961
28301
  for (const token of argv) {
27962
- if (token.startsWith("-")) continue;
28302
+ if (consumeNext) {
28303
+ consumeNext = false;
28304
+ continue;
28305
+ }
28306
+ if (token.startsWith("-")) {
28307
+ if (!token.includes("=")) {
28308
+ const localOpts = cmd.options;
28309
+ const opt = localOpts.find((o) => o.short === token || o.long === token) || rootOptions.find((o) => o.short === token || o.long === token);
28310
+ if (opt && (opt.required || opt.optional)) consumeNext = true;
28311
+ }
28312
+ continue;
28313
+ }
27963
28314
  const sub = cmd.commands.find(
27964
28315
  (c) => c.name() === token || c.aliases().includes(token)
27965
28316
  );
@@ -31425,6 +31776,26 @@ function buildRelaySetMode(opts) {
31425
31776
  }
31426
31777
  return `${ch};${modeInt}`;
31427
31778
  }
31779
+ function buildBrightnessSet(opts) {
31780
+ if (!opts.brightness) throw new UsageError("--brightness is required (1-100)");
31781
+ const b2 = parseInt(opts.brightness, 10);
31782
+ if (!Number.isFinite(b2) || b2 < 1 || b2 > 100) {
31783
+ throw new UsageError(`--brightness must be an integer between 1 and 100 (got "${opts.brightness}")`);
31784
+ }
31785
+ return String(b2);
31786
+ }
31787
+ function buildColorSet(opts) {
31788
+ if (!opts.color) throw new UsageError('--color is required (e.g. "255:0:0", "#FF0000", "red")');
31789
+ const result = validateSetColor(opts.color);
31790
+ if (!result.ok) throw new UsageError(result.error);
31791
+ return result.normalized ?? opts.color;
31792
+ }
31793
+ function buildColorTemperatureSet(opts) {
31794
+ if (!opts.colorTemp) throw new UsageError("--color-temp is required (2700-6500)");
31795
+ const result = validateSetColorTemperature(opts.colorTemp);
31796
+ if (!result.ok) throw new UsageError(result.error);
31797
+ return result.normalized ?? opts.colorTemp;
31798
+ }
31428
31799
  function validateParameter(deviceType, command, raw) {
31429
31800
  if (!deviceType) return { ok: true };
31430
31801
  if (deviceType === "Air Conditioner" && command === "setAll") {
@@ -31445,7 +31816,7 @@ function validateParameter(deviceType, command, raw) {
31445
31816
  if (command === "setColor" && isColorDevice(deviceType)) {
31446
31817
  return validateSetColor(raw);
31447
31818
  }
31448
- if (command === "setColorTemperature" && isColorDevice(deviceType)) {
31819
+ if (command === "setColorTemperature" && isBrightnessDevice(deviceType)) {
31449
31820
  return validateSetColorTemperature(raw);
31450
31821
  }
31451
31822
  return { ok: true };
@@ -31454,7 +31825,12 @@ function isBrightnessDevice(deviceType) {
31454
31825
  return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Ceiling Light" || deviceType === "Ceiling Light Pro" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Dimmer" || deviceType === "Fill Light";
31455
31826
  }
31456
31827
  function isColorDevice(deviceType) {
31457
- return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Ceiling Light" || deviceType === "Ceiling Light Pro" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Fill Light";
31828
+ return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Fill Light";
31829
+ }
31830
+ function isLightingCommandSupported(deviceType, command) {
31831
+ if (command === "setBrightness" || command === "setColorTemperature") return isBrightnessDevice(deviceType);
31832
+ if (command === "setColor") return isColorDevice(deviceType);
31833
+ return false;
31458
31834
  }
31459
31835
  function validateSetBrightness(raw) {
31460
31836
  if (raw === void 0 || raw === "" || raw === "default") {
@@ -32585,10 +32961,11 @@ init_arg_parsers();
32585
32961
  init_output();
32586
32962
  init_cache();
32587
32963
  init_devices();
32964
+ init_catalog();
32588
32965
  init_flags();
32589
32966
  init_client();
32590
32967
  function registerExpandCommand(devices) {
32591
- devices.command("expand").description("Send a command with semantic flags instead of raw positional parameters").argument("[deviceId]", 'Target device ID from "devices list" (or use --name)').argument("[command]", "Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2)").option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: require-unique)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Curtain", "Air Conditioner")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--temp <celsius>", "AC setAll: temperature in Celsius (16-30)", intArg("--temp", { min: 16, max: 30 })).option("--mode <mode>", "AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary", stringArg("--mode")).option("--fan <speed>", "AC setAll: fan speed auto|low|mid|high", stringArg("--fan")).option("--power <state>", "AC setAll: on|off", stringArg("--power")).option("--position <percent>", "Curtain setPosition: 0-100 (0=open, 100=closed)", intArg("--position", { min: 0, max: 100 })).option("--direction <dir>", "Blind Tilt setPosition: up|down", stringArg("--direction")).option("--angle <percent>", "Blind Tilt setPosition: 0-100 (0=closed, 100=open)", intArg("--angle", { min: 0, max: 100 })).option("--channel <n>", "Relay Switch 2 setMode: channel 1 or 2", intArg("--channel", { min: 1, max: 2 })).option("--yes", "Confirm destructive commands").addHelpText("after", `
32968
+ devices.command("expand").description("Send a command with semantic flags instead of raw positional parameters").argument("[deviceId]", 'Target device ID from "devices list" (or use --name)').argument("[command]", "Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2), setBrightness/setColor/setColorTemperature (lighting)").option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: require-unique)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Curtain", "Air Conditioner")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--temp <celsius>", "AC setAll: temperature in Celsius (16-30)", intArg("--temp", { min: 16, max: 30 })).option("--mode <mode>", "AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary", stringArg("--mode")).option("--fan <speed>", "AC setAll: fan speed auto|low|mid|high", stringArg("--fan")).option("--power <state>", "AC setAll: on|off", stringArg("--power")).option("--position <percent>", "Curtain setPosition: 0-100 (0=open, 100=closed)", intArg("--position", { min: 0, max: 100 })).option("--direction <dir>", "Blind Tilt setPosition: up|down", stringArg("--direction")).option("--angle <percent>", "Blind Tilt setPosition: 0-100 (0=closed, 100=open)", intArg("--angle", { min: 0, max: 100 })).option("--channel <n>", "Relay Switch 2 setMode: channel 1 or 2", intArg("--channel", { min: 1, max: 2 })).option("--brightness <percent>", "setBrightness: 1-100 percent", intArg("--brightness", { min: 1, max: 100 })).option("--color <value>", "setColor: R:G:B, #RRGGBB, or named color (red, blue, etc.)", stringArg("--color")).option("--color-temp <kelvin>", "setColorTemperature: 2700-6500 Kelvin", intArg("--color-temp", { min: 2700, max: 6500 })).option("--yes", "Confirm destructive commands").addHelpText("after", `
32592
32969
  Translates semantic flags into the wire parameter format, then sends the command.
32593
32970
 
32594
32971
  Supported expansions:
@@ -32609,12 +32986,25 @@ Supported expansions:
32609
32986
  --channel 1 --mode edge \u2192 "1;1"
32610
32987
  --mode values: toggle (0) | edge (1) | detached (2) | momentary (3)
32611
32988
 
32989
+ Color Bulb / Strip Light / Ceiling Light \u2014 setBrightness
32990
+ --brightness 80 \u2192 "80"
32991
+
32992
+ Color Bulb / Strip Light / Floor Lamp \u2014 setColor
32993
+ --color "255:0:0" \u2192 "255:0:0"
32994
+ --color "#FF0000" \u2192 "255:0:0"
32995
+ --color red \u2192 "255:0:0"
32996
+
32997
+ Color Bulb / Strip Light / Ceiling Light \u2014 setColorTemperature
32998
+ --color-temp 4000 \u2192 "4000"
32999
+
32612
33000
  Examples:
32613
- $ switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
32614
- $ switchbot devices expand <curtainId> setPosition --position 50 --mode silent
32615
- $ switchbot devices expand <blindId> setPosition --direction up --angle 50
32616
- $ switchbot devices expand <relayId> setMode --channel 1 --mode edge
32617
- $ switchbot devices expand <acId> setAll --temp 22 --mode heat --fan auto --power on --dry-run
33001
+ $ switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
33002
+ $ switchbot devices expand <curtainId> setPosition --position 50 --mode silent
33003
+ $ switchbot devices expand <blindId> setPosition --direction up --angle 50
33004
+ $ switchbot devices expand <relayId> setMode --channel 1 --mode edge
33005
+ $ switchbot devices expand <stripId> setBrightness --brightness 80
33006
+ $ switchbot devices expand <bulbId> setColor --color "#FF0000"
33007
+ $ switchbot devices expand <bulbId> setColorTemperature --color-temp 4000
32618
33008
  $ switchbot devices expand --name "Living Room AC" setAll --temp 26 --mode cool --fan low --power on
32619
33009
  `).action(async (deviceIdArg, commandArg, options) => {
32620
33010
  let deviceId = "";
@@ -32632,12 +33022,22 @@ Examples:
32632
33022
  category: options.nameCategory,
32633
33023
  room: options.nameRoom
32634
33024
  });
32635
- if (!effectiveCommand) throw new UsageError("A command argument is required (setAll, setPosition, setMode).");
33025
+ if (!effectiveCommand) throw new UsageError("A command argument is required (setAll, setPosition, setMode, setBrightness, setColor, setColorTemperature).");
32636
33026
  command = effectiveCommand;
32637
33027
  const cached2 = getCachedDevice(deviceId);
32638
33028
  const deviceType = cached2?.type ?? "";
32639
33029
  let parameter;
32640
33030
  if (command === "setAll") {
33031
+ if (!cached2) {
33032
+ throw new UsageError(
33033
+ `Device ${deviceId} is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' can verify this is an Air Conditioner.`
33034
+ );
33035
+ }
33036
+ if (deviceType !== "Air Conditioner") {
33037
+ throw new UsageError(
33038
+ `"setAll" is only supported on Air Conditioner devices, but "${cached2.type}" was found.`
33039
+ );
33040
+ }
32641
33041
  parameter = buildAcSetAll(options);
32642
33042
  } else if (command === "setPosition") {
32643
33043
  if (!cached2) {
@@ -32645,13 +33045,56 @@ Examples:
32645
33045
  `Device ${deviceId} is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' knows whether this is a Curtain or a Blind Tilt.`
32646
33046
  );
32647
33047
  }
33048
+ const positionTypes = ["Curtain", "Curtain 3", "Roller Shade", "Blind Tilt"];
33049
+ if (!positionTypes.some((t) => deviceType.startsWith(t))) {
33050
+ throw new UsageError(
33051
+ `"setPosition" is only supported on Curtain, Roller Shade, and Blind Tilt devices, but "${cached2.type}" was found.`
33052
+ );
33053
+ }
32648
33054
  const isBlind = deviceType.startsWith("Blind Tilt");
32649
- parameter = isBlind ? buildBlindTiltSetPosition(options) : buildCurtainSetPosition(options);
33055
+ const isRollerShade = deviceType.startsWith("Roller Shade");
33056
+ if (isBlind) {
33057
+ parameter = buildBlindTiltSetPosition(options);
33058
+ } else if (isRollerShade) {
33059
+ if (!options.position) throw new UsageError("--position is required (0-100)");
33060
+ parameter = options.position;
33061
+ } else {
33062
+ parameter = buildCurtainSetPosition(options);
33063
+ }
32650
33064
  } else if (command === "setMode" && deviceType.startsWith("Relay Switch")) {
32651
33065
  parameter = buildRelaySetMode(options);
33066
+ } else if (command === "setBrightness" || command === "setColor" || command === "setColorTemperature") {
33067
+ if (!cached2) {
33068
+ throw new UsageError(
33069
+ `Device "${deviceId}" is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' can verify this device supports ${command}.`
33070
+ );
33071
+ }
33072
+ const catalogResult = findCatalogEntry(cached2.type);
33073
+ const catalogEntry = Array.isArray(catalogResult) ? catalogResult[0] : catalogResult;
33074
+ const supportedHint = command === "setColor" ? "Color Bulb, Strip Light, Floor Lamp, and similar RGB lighting devices" : "Color Bulb, Strip Light, Ceiling Light, Floor Lamp, and similar lighting devices";
33075
+ if (catalogEntry !== null) {
33076
+ if (!catalogEntry.commands.some((c) => c.command === command)) {
33077
+ throw new UsageError(
33078
+ `Device type "${cached2.type}" does not support ${command}. Supported on: ${supportedHint}.`
33079
+ );
33080
+ }
33081
+ } else {
33082
+ if (!isLightingCommandSupported(cached2.type, command)) {
33083
+ throw new UsageError(
33084
+ `Device type "${cached2.type}" does not support ${command}. Supported on: ${supportedHint}.`
33085
+ );
33086
+ }
33087
+ }
33088
+ if (command === "setBrightness") {
33089
+ parameter = buildBrightnessSet(options);
33090
+ } else if (command === "setColor") {
33091
+ parameter = buildColorSet(options);
33092
+ } else {
33093
+ parameter = buildColorTemperatureSet(options);
33094
+ }
32652
33095
  } else {
32653
33096
  throw new UsageError(
32654
- `'expand' does not support "${command}" for device type "${deviceType || "unknown"}". Use 'switchbot devices command' to send raw parameters instead.`
33097
+ `'expand' does not support "${command}" for device type "${deviceType || "unknown"}". Supported: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch), setBrightness/setColor/setColorTemperature (lighting). Use 'switchbot devices command' to send raw parameters instead.`
32655
33098
  );
32656
33099
  }
32657
33100
  if (!options.yes && !isDryRun() && isDestructiveCommand(deviceType, command, "command")) {
@@ -33064,7 +33507,7 @@ Examples:
33064
33507
  const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
33065
33508
  const fetchedAt2 = (/* @__PURE__ */ new Date()).toISOString();
33066
33509
  const batch = results.map(
33067
- (r, i) => r.status === "fulfilled" ? { deviceId: ids[i], ok: true, _fetchedAt: fetchedAt2, ...annotateStatusPayload(ids[i], r.value) } : { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) }
33510
+ (r, i) => r.status === "fulfilled" ? { deviceId: ids[i], ok: true, fetchedAt: fetchedAt2, ...annotateStatusPayload(ids[i], r.value) } : { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) }
33068
33511
  );
33069
33512
  const batchFmt = resolveFormat();
33070
33513
  if (isJsonMode() || batchFmt === "json") {
@@ -33076,7 +33519,7 @@ Examples:
33076
33519
  } else {
33077
33520
  const rawFields = resolveFields();
33078
33521
  for (const entry of batch) {
33079
- const { deviceId: deviceId2, ok, error: error48, _fetchedAt: ts, ...status } = entry;
33522
+ const { deviceId: deviceId2, ok, error: error48, fetchedAt: ts, ...status } = entry;
33080
33523
  console.log(`
33081
33524
  \u2500\u2500\u2500 ${String(deviceId2)} \u2500\u2500\u2500`);
33082
33525
  if (!ok) {
@@ -33102,11 +33545,11 @@ Examples:
33102
33545
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
33103
33546
  const fmt = resolveFormat();
33104
33547
  if (fmt === "json" && process.argv.includes("--json")) {
33105
- printJson({ ...body, _fetchedAt: fetchedAt });
33548
+ printJson({ ...body, fetchedAt });
33106
33549
  return;
33107
33550
  }
33108
33551
  if (fmt !== "table") {
33109
- const statusWithTs = { ...body, _fetchedAt: fetchedAt };
33552
+ const statusWithTs = { ...body, fetchedAt };
33110
33553
  const allHeaders = Object.keys(statusWithTs);
33111
33554
  const allRows = [Object.values(statusWithTs)];
33112
33555
  const rawFields = resolveFields();
@@ -33435,7 +33878,7 @@ Examples:
33435
33878
  const joinedMatch = findCatalogEntry(joined);
33436
33879
  if (joinedMatch && !Array.isArray(joinedMatch)) {
33437
33880
  if (isJsonMode()) {
33438
- printJson(normalizeCatalogForJson(joinedMatch));
33881
+ printJson([normalizeCatalogForJson(joinedMatch)]);
33439
33882
  } else {
33440
33883
  renderCatalogEntry(joinedMatch);
33441
33884
  }
@@ -35224,10 +35667,10 @@ function mergeDefs(...defs) {
35224
35667
  function cloneDef(schema2) {
35225
35668
  return mergeDefs(schema2._zod.def);
35226
35669
  }
35227
- function getElementAtPath(obj, path28) {
35228
- if (!path28)
35670
+ function getElementAtPath(obj, path29) {
35671
+ if (!path29)
35229
35672
  return obj;
35230
- return path28.reduce((acc, key) => acc?.[key], obj);
35673
+ return path29.reduce((acc, key) => acc?.[key], obj);
35231
35674
  }
35232
35675
  function promiseAllObject(promisesObj) {
35233
35676
  const keys = Object.keys(promisesObj);
@@ -35610,11 +36053,11 @@ function aborted(x2, startIndex = 0) {
35610
36053
  }
35611
36054
  return false;
35612
36055
  }
35613
- function prefixIssues(path28, issues) {
36056
+ function prefixIssues(path29, issues) {
35614
36057
  return issues.map((iss) => {
35615
36058
  var _a2;
35616
36059
  (_a2 = iss).path ?? (_a2.path = []);
35617
- iss.path.unshift(path28);
36060
+ iss.path.unshift(path29);
35618
36061
  return iss;
35619
36062
  });
35620
36063
  }
@@ -35797,7 +36240,7 @@ function formatError2(error48, mapper = (issue2) => issue2.message) {
35797
36240
  }
35798
36241
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
35799
36242
  const result = { errors: [] };
35800
- const processError = (error49, path28 = []) => {
36243
+ const processError = (error49, path29 = []) => {
35801
36244
  var _a2, _b;
35802
36245
  for (const issue2 of error49.issues) {
35803
36246
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -35807,7 +36250,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
35807
36250
  } else if (issue2.code === "invalid_element") {
35808
36251
  processError({ issues: issue2.issues }, issue2.path);
35809
36252
  } else {
35810
- const fullpath = [...path28, ...issue2.path];
36253
+ const fullpath = [...path29, ...issue2.path];
35811
36254
  if (fullpath.length === 0) {
35812
36255
  result.errors.push(mapper(issue2));
35813
36256
  continue;
@@ -35839,8 +36282,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
35839
36282
  }
35840
36283
  function toDotPath(_path) {
35841
36284
  const segs = [];
35842
- const path28 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
35843
- for (const seg of path28) {
36285
+ const path29 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
36286
+ for (const seg of path29) {
35844
36287
  if (typeof seg === "number")
35845
36288
  segs.push(`[${seg}]`);
35846
36289
  else if (typeof seg === "symbol")
@@ -47895,13 +48338,13 @@ function resolveRef(ref, ctx) {
47895
48338
  if (!ref.startsWith("#")) {
47896
48339
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
47897
48340
  }
47898
- const path28 = ref.slice(1).split("/").filter(Boolean);
47899
- if (path28.length === 0) {
48341
+ const path29 = ref.slice(1).split("/").filter(Boolean);
48342
+ if (path29.length === 0) {
47900
48343
  return ctx.rootSchema;
47901
48344
  }
47902
48345
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
47903
- if (path28[0] === defsKey) {
47904
- const key = path28[1];
48346
+ if (path29[0] === defsKey) {
48347
+ const key = path29[1];
47905
48348
  if (!key || !ctx.defs[key]) {
47906
48349
  throw new Error(`Reference not found: ${ref}`);
47907
48350
  }
@@ -48334,9 +48777,9 @@ var logLevel = process.env.LOG_LEVEL || "warn";
48334
48777
  var logFormat = process.env.LOG_FORMAT || "json";
48335
48778
  var pinoConfig = {
48336
48779
  level: logLevel,
48337
- transport: logFormat === "pretty" ? { target: "pino-pretty" } : void 0
48780
+ transport: logFormat === "pretty" ? { target: "pino-pretty", options: { destination: 2 } } : void 0
48338
48781
  };
48339
- var log = pino(pinoConfig);
48782
+ var log = logFormat === "pretty" ? pino(pinoConfig) : pino(pinoConfig, pino.destination(2));
48340
48783
 
48341
48784
  // src/mcp/device-history.ts
48342
48785
  init_cjs_shim();
@@ -50027,6 +50470,282 @@ function addRuleToPolicyFile(opts) {
50027
50470
  return { ...result, written: false };
50028
50471
  }
50029
50472
 
50473
+ // src/rules/explain.ts
50474
+ init_cjs_shim();
50475
+ init_trace();
50476
+ import fs16 from "node:fs";
50477
+ function loadTraceRecords(auditFile, opts = {}) {
50478
+ if (!fs16.existsSync(auditFile)) return [];
50479
+ const lines = fs16.readFileSync(auditFile, "utf-8").split(/\r?\n/);
50480
+ return filterTraceRecords(lines, opts);
50481
+ }
50482
+ function loadRelatedAudit(auditFile, fireId) {
50483
+ if (!fs16.existsSync(auditFile)) return [];
50484
+ const raw = fs16.readFileSync(auditFile, "utf-8");
50485
+ const out = [];
50486
+ for (const line of raw.split(/\r?\n/)) {
50487
+ const trimmed = line.trim();
50488
+ if (!trimmed) continue;
50489
+ try {
50490
+ const entry = JSON.parse(trimmed);
50491
+ const entryFireId = entry.rule?.fireId ?? entry["fireId"];
50492
+ if (entryFireId === fireId) out.push(entry);
50493
+ } catch {
50494
+ }
50495
+ }
50496
+ return out;
50497
+ }
50498
+ function conditionSymbol(passed) {
50499
+ if (passed === true) return "\u2713";
50500
+ if (passed === false) return "\u2717";
50501
+ return "\xB7";
50502
+ }
50503
+ function conditionSummary(c) {
50504
+ if (c.passed === null) {
50505
+ return `\xB7 ${c.kind} \u2192 not evaluated (short-circuited)`;
50506
+ }
50507
+ const sym = conditionSymbol(c.passed);
50508
+ let detail = c.kind;
50509
+ if (c.config !== void 0) {
50510
+ if (Array.isArray(c.config)) {
50511
+ detail += ` ${c.config.join("\u2013")}`;
50512
+ } else if (c.config && typeof c.config === "object") {
50513
+ const cfg = c.config;
50514
+ if ("device" in cfg) {
50515
+ detail += ` ${cfg["device"]}.${cfg["field"]} ${cfg["op"]} ${JSON.stringify(cfg["value"])}`;
50516
+ }
50517
+ }
50518
+ }
50519
+ const status = c.passed ? "passed" : "failed";
50520
+ return ` ${sym} ${detail.padEnd(36)} \u2192 ${status}`;
50521
+ }
50522
+ function formatTimestamp(iso) {
50523
+ const d = new Date(iso);
50524
+ return `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 19)}`;
50525
+ }
50526
+ function formatExplainText(record2, relatedAudit) {
50527
+ const lines = [];
50528
+ lines.push(`Rule: ${record2.rule.name} (version ${record2.rule.version})`);
50529
+ lines.push(`Evaluated: ${formatTimestamp(record2.t)} (${record2.evaluationMs}ms)`);
50530
+ const triggerDevice = record2.trigger.deviceId ? ` on ${record2.trigger.deviceId}` : "";
50531
+ lines.push(`Trigger: ${record2.trigger.source} ${record2.trigger.event}${triggerDevice}`);
50532
+ lines.push("");
50533
+ if (record2.conditions.length > 0) {
50534
+ lines.push("Conditions (evaluated in order):");
50535
+ for (const c of record2.conditions) {
50536
+ lines.push(conditionSummary(c));
50537
+ }
50538
+ lines.push("");
50539
+ }
50540
+ lines.push(`Decision: ${record2.decision}`);
50541
+ lines.push("");
50542
+ lines.push(`Related fireId: ${record2.fireId}`);
50543
+ const nonEval = relatedAudit.filter(
50544
+ (e) => e["kind"] !== "rule-evaluate"
50545
+ );
50546
+ if (nonEval.length > 0) {
50547
+ lines.push(`Audit trail (${nonEval.length} record${nonEval.length === 1 ? "" : "s"}):`);
50548
+ for (const e of nonEval) {
50549
+ const ts = formatTimestamp(e.t);
50550
+ lines.push(` ${e.kind.padEnd(20)} ${ts}`);
50551
+ }
50552
+ } else {
50553
+ lines.push(`Audit trail: (no related records${record2.decision === "blocked-by-condition" ? " \u2014 rule did not fire" : ""})`);
50554
+ }
50555
+ return lines.join("\n");
50556
+ }
50557
+ function formatExplainJson(record2, relatedAudit) {
50558
+ return JSON.stringify({ trace: record2, relatedAudit }, null, 2);
50559
+ }
50560
+
50561
+ // src/rules/simulate.ts
50562
+ init_cjs_shim();
50563
+ init_matcher();
50564
+ init_throttle();
50565
+ init_trace();
50566
+ init_trace();
50567
+ init_matcher();
50568
+ import fs17 from "node:fs";
50569
+ import path14 from "node:path";
50570
+ import os13 from "node:os";
50571
+ import { randomUUID as randomUUID5 } from "node:crypto";
50572
+ var HOUR_MS = 60 * 60 * 1e3;
50573
+ var DEVICE_HISTORY_DIR = path14.join(os13.homedir(), ".switchbot", "device-history");
50574
+ async function simulateRule(opts) {
50575
+ const { rule, aliases = {}, liveLlm = false } = opts;
50576
+ const rv = ruleVersion(rule);
50577
+ const events = loadSourceEvents(opts);
50578
+ const windowStart = events.length > 0 ? new Date(Math.min(...events.map((e) => e.t.getTime()))) : new Date(Date.now() - 24 * HOUR_MS);
50579
+ const windowEnd = events.length > 0 ? new Date(Math.max(...events.map((e) => e.t.getTime()))) : /* @__PURE__ */ new Date();
50580
+ const counts = { wouldFire: 0, blocked: 0, throttled: 0, errored: 0, skippedLlm: 0 };
50581
+ const blockReasons = /* @__PURE__ */ new Map();
50582
+ const sampleFires = [];
50583
+ const traces = [];
50584
+ const throttle = new ThrottleGate();
50585
+ const cooldownMs = rule.cooldown ? parseMaxPerMs(rule.cooldown) : null;
50586
+ const throttleMs = rule.throttle ? parseMaxPerMs(rule.throttle.max_per) : null;
50587
+ const effectiveWindowMs = cooldownMs ?? throttleMs;
50588
+ for (const event of events) {
50589
+ const fireId = randomUUID5();
50590
+ const nowMs = event.t.getTime();
50591
+ if (rule.when.source === "mqtt") {
50592
+ const resolvedDevice = rule.when.device ? aliases[rule.when.device] ?? rule.when.device : void 0;
50593
+ if (!matchesMqttTrigger(rule.when, event, resolvedDevice)) continue;
50594
+ }
50595
+ const hasLlm = (rule.conditions ?? []).some((c) => c["llm"] !== void 0);
50596
+ if (hasLlm && !liveLlm) {
50597
+ counts.skippedLlm++;
50598
+ const fireEvent = {
50599
+ t: event.t.toISOString(),
50600
+ fireId,
50601
+ deviceId: event.deviceId,
50602
+ decision: "skipped-llm"
50603
+ };
50604
+ sampleFires.push(fireEvent);
50605
+ continue;
50606
+ }
50607
+ if (effectiveWindowMs !== null) {
50608
+ const check2 = throttle.check(rule.name, effectiveWindowMs, nowMs, event.deviceId);
50609
+ if (!check2.allowed) {
50610
+ counts.throttled++;
50611
+ const fireEvent = {
50612
+ t: event.t.toISOString(),
50613
+ fireId,
50614
+ deviceId: event.deviceId,
50615
+ decision: "throttled"
50616
+ };
50617
+ sampleFires.push(fireEvent);
50618
+ continue;
50619
+ }
50620
+ }
50621
+ const statusFetcher = buildStatusFetcher(event.t);
50622
+ let condResult;
50623
+ try {
50624
+ condResult = await evaluateConditions(rule.conditions, event.t, {
50625
+ aliases,
50626
+ fetchStatus: statusFetcher,
50627
+ event,
50628
+ ruleVersion: rv
50629
+ });
50630
+ } catch (err) {
50631
+ counts.errored++;
50632
+ sampleFires.push({ t: event.t.toISOString(), fireId, deviceId: event.deviceId, decision: "error", reason: String(err) });
50633
+ continue;
50634
+ }
50635
+ if (!condResult.matched) {
50636
+ counts.blocked++;
50637
+ const reason = condResult.failures[0] ?? "unknown";
50638
+ blockReasons.set(reason, (blockReasons.get(reason) ?? 0) + 1);
50639
+ sampleFires.push({ t: event.t.toISOString(), fireId, deviceId: event.deviceId, decision: "blocked-by-condition", reason });
50640
+ } else {
50641
+ counts.wouldFire++;
50642
+ throttle.record(rule.name, nowMs, event.deviceId);
50643
+ sampleFires.push({ t: event.t.toISOString(), fireId, deviceId: event.deviceId, decision: "would-fire" });
50644
+ }
50645
+ }
50646
+ let topBlockReason;
50647
+ let topBlockCount;
50648
+ if (blockReasons.size > 0) {
50649
+ let max = 0;
50650
+ for (const [reason, count] of blockReasons) {
50651
+ if (count > max) {
50652
+ max = count;
50653
+ topBlockReason = reason;
50654
+ topBlockCount = count;
50655
+ }
50656
+ }
50657
+ }
50658
+ return {
50659
+ ruleName: rule.name,
50660
+ ruleVersion: rv,
50661
+ windowStart,
50662
+ windowEnd,
50663
+ sourceEventCount: events.length,
50664
+ wouldFire: counts.wouldFire,
50665
+ blockedByCondition: counts.blocked,
50666
+ throttled: counts.throttled,
50667
+ errored: counts.errored,
50668
+ skippedLlm: counts.skippedLlm,
50669
+ topBlockReason,
50670
+ topBlockCount,
50671
+ sampleFires: sampleFires.slice(0, 20),
50672
+ traces
50673
+ };
50674
+ }
50675
+ function loadSourceEvents(opts) {
50676
+ if (opts.against) {
50677
+ if (!fs17.existsSync(opts.against)) return [];
50678
+ const lines2 = fs17.readFileSync(opts.against, "utf-8").split(/\r?\n/);
50679
+ const events = [];
50680
+ for (const line of lines2) {
50681
+ const trimmed = line.trim();
50682
+ if (!trimmed) continue;
50683
+ try {
50684
+ const raw = JSON.parse(trimmed);
50685
+ events.push({
50686
+ source: raw["source"] ?? "mqtt",
50687
+ event: String(raw["event"] ?? "device.shadow"),
50688
+ t: new Date(String(raw["t"] ?? (/* @__PURE__ */ new Date()).toISOString())),
50689
+ deviceId: raw["deviceId"],
50690
+ payload: raw["payload"]
50691
+ });
50692
+ } catch {
50693
+ }
50694
+ }
50695
+ return events;
50696
+ }
50697
+ const auditLog = opts.auditLog;
50698
+ if (!auditLog || !fs17.existsSync(auditLog)) return [];
50699
+ const sinceMs = opts.since ? parseSince(opts.since) : Date.now() - 24 * HOUR_MS;
50700
+ const sinceIso = new Date(sinceMs).toISOString();
50701
+ const lines = fs17.readFileSync(auditLog, "utf-8").split(/\r?\n/);
50702
+ const traceRecords = filterTraceRecords(lines, {
50703
+ ruleName: opts.rule.name,
50704
+ since: sinceIso
50705
+ });
50706
+ return traceRecords.map((r) => ({
50707
+ source: r.trigger.source,
50708
+ event: r.trigger.event,
50709
+ t: new Date(r.t),
50710
+ deviceId: r.trigger.deviceId
50711
+ }));
50712
+ }
50713
+ function parseSince(since) {
50714
+ if (since.includes("T") || since.includes("-")) {
50715
+ const d = new Date(since);
50716
+ if (!isNaN(d.getTime())) return d.getTime();
50717
+ }
50718
+ const m2 = /^(\d+)([smhd])$/.exec(since.trim());
50719
+ if (m2) {
50720
+ const n = parseInt(m2[1], 10);
50721
+ const unit = m2[2];
50722
+ const unitMs = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
50723
+ return Date.now() - n * unitMs;
50724
+ }
50725
+ return Date.now() - 24 * HOUR_MS;
50726
+ }
50727
+ function buildStatusFetcher(asOf) {
50728
+ return async (deviceId) => {
50729
+ const histFile = path14.join(DEVICE_HISTORY_DIR, `${deviceId}.jsonl`);
50730
+ if (!fs17.existsSync(histFile)) return {};
50731
+ const lines = fs17.readFileSync(histFile, "utf-8").split(/\r?\n/);
50732
+ const asOfMs = asOf.getTime();
50733
+ let best;
50734
+ for (const line of lines) {
50735
+ const trimmed = line.trim();
50736
+ if (!trimmed) continue;
50737
+ try {
50738
+ const entry = JSON.parse(trimmed);
50739
+ const entryT = new Date(String(entry["t"] ?? 0)).getTime();
50740
+ if (entryT <= asOfMs) best = entry;
50741
+ else break;
50742
+ } catch {
50743
+ }
50744
+ }
50745
+ return best ?? {};
50746
+ };
50747
+ }
50748
+
50030
50749
  // src/commands/mcp.ts
50031
50750
  init_audit();
50032
50751
  init_flags();
@@ -50045,13 +50764,13 @@ function collectPolicyDiff(left, right, at, out, limit) {
50045
50764
  const maxLen = Math.max(left.length, right.length);
50046
50765
  for (let i = 0; i < maxLen; i++) {
50047
50766
  if (out.length >= limit) return;
50048
- const path28 = `${at}[${i}]`;
50767
+ const path29 = `${at}[${i}]`;
50049
50768
  if (i >= left.length) {
50050
- out.push({ path: path28, kind: "added", after: right[i] });
50769
+ out.push({ path: path29, kind: "added", after: right[i] });
50051
50770
  } else if (i >= right.length) {
50052
- out.push({ path: path28, kind: "removed", before: left[i] });
50771
+ out.push({ path: path29, kind: "removed", before: left[i] });
50053
50772
  } else {
50054
- collectPolicyDiff(left[i], right[i], path28, out, limit);
50773
+ collectPolicyDiff(left[i], right[i], path29, out, limit);
50055
50774
  }
50056
50775
  }
50057
50776
  return;
@@ -50060,15 +50779,15 @@ function collectPolicyDiff(left, right, at, out, limit) {
50060
50779
  const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
50061
50780
  for (const key of [...keys].sort()) {
50062
50781
  if (out.length >= limit) return;
50063
- const path28 = at === "$" ? `$.${key}` : `${at}.${key}`;
50782
+ const path29 = at === "$" ? `$.${key}` : `${at}.${key}`;
50064
50783
  const leftHas = Object.prototype.hasOwnProperty.call(left, key);
50065
50784
  const rightHas = Object.prototype.hasOwnProperty.call(right, key);
50066
50785
  if (!leftHas && rightHas) {
50067
- out.push({ path: path28, kind: "added", after: right[key] });
50786
+ out.push({ path: path29, kind: "added", after: right[key] });
50068
50787
  } else if (leftHas && !rightHas) {
50069
- out.push({ path: path28, kind: "removed", before: left[key] });
50788
+ out.push({ path: path29, kind: "removed", before: left[key] });
50070
50789
  } else {
50071
- collectPolicyDiff(left[key], right[key], path28, out, limit);
50790
+ collectPolicyDiff(left[key], right[key], path29, out, limit);
50072
50791
  }
50073
50792
  }
50074
50793
  return;
@@ -50121,8 +50840,8 @@ function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges
50121
50840
  // src/commands/mcp.ts
50122
50841
  init_embedded_assets();
50123
50842
  import { dirname as pathDirname, join as pathJoin } from "node:path";
50124
- import os13 from "node:os";
50125
- import fs16 from "node:fs";
50843
+ import os14 from "node:os";
50844
+ import fs18 from "node:fs";
50126
50845
  var LATEST_SUPPORTED_VERSION = SUPPORTED_POLICY_SCHEMA_VERSIONS[SUPPORTED_POLICY_SCHEMA_VERSIONS.length - 1];
50127
50846
  function mcpError(kind, code, message, options) {
50128
50847
  const obj = { code, kind, message };
@@ -50151,7 +50870,7 @@ function apiErrorToMcpError(err) {
50151
50870
  retryAfterMs: payload.retryAfterMs
50152
50871
  });
50153
50872
  }
50154
- var DEFAULT_AUDIT_LOG_FILE = pathJoin(os13.homedir(), ".switchbot", "audit.log");
50873
+ var DEFAULT_AUDIT_LOG_FILE = pathJoin(os14.homedir(), ".switchbot", "audit.log");
50155
50874
  function resolveAuditRange(opts) {
50156
50875
  if (opts.since && (opts.from || opts.to)) {
50157
50876
  throw new Error("--since is mutually exclusive with --from/--to.");
@@ -51080,15 +51799,15 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51080
51799
  async ({ path: pathArg, force }) => {
51081
51800
  const policyPath = resolvePolicyPath({ flag: pathArg });
51082
51801
  const doForce = force === true;
51083
- if (fs16.existsSync(policyPath) && !doForce) {
51802
+ if (fs18.existsSync(policyPath) && !doForce) {
51084
51803
  return mcpError("guard", 5, `refusing to overwrite existing policy at ${policyPath}`, {
51085
51804
  hint: "pass force=true to overwrite, or choose a different path",
51086
51805
  context: { policyPath }
51087
51806
  });
51088
51807
  }
51089
51808
  const template = readPolicyExampleYaml();
51090
- fs16.mkdirSync(pathDirname(policyPath), { recursive: true });
51091
- fs16.writeFileSync(policyPath, template, { encoding: "utf-8" });
51809
+ fs18.mkdirSync(pathDirname(policyPath), { recursive: true });
51810
+ fs18.writeFileSync(policyPath, template, { encoding: "utf-8" });
51092
51811
  const structured = {
51093
51812
  policyPath,
51094
51813
  schemaVersion: CURRENT_POLICY_SCHEMA_VERSION,
@@ -51275,7 +51994,7 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51275
51994
  let leftSource = "";
51276
51995
  let rightSource = "";
51277
51996
  try {
51278
- leftSource = fs16.readFileSync(left_path, "utf-8");
51997
+ leftSource = fs18.readFileSync(left_path, "utf-8");
51279
51998
  } catch (err) {
51280
51999
  if (err?.code === "ENOENT") {
51281
52000
  return mcpError("usage", 2, `policy file not found: ${left_path}`, {
@@ -51285,7 +52004,7 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51285
52004
  return mcpError("runtime", 1, `failed to read ${left_path}: ${String(err)}`);
51286
52005
  }
51287
52006
  try {
51288
- rightSource = fs16.readFileSync(right_path, "utf-8");
52007
+ rightSource = fs18.readFileSync(right_path, "utf-8");
51289
52008
  } catch (err) {
51290
52009
  if (err?.code === "ENOENT") {
51291
52010
  return mcpError("usage", 2, `policy file not found: ${right_path}`, {
@@ -51725,6 +52444,130 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
51725
52444
  }
51726
52445
  }
51727
52446
  );
52447
+ server.registerTool(
52448
+ "rules_explain",
52449
+ {
52450
+ title: "Show why a rule evaluation fired or was blocked",
52451
+ 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".',
52452
+ _meta: { agentSafetyTier: "read" },
52453
+ inputSchema: external_exports.object({
52454
+ fire_id: external_exports.string().optional().describe("Specific fireId to explain."),
52455
+ rule_name: external_exports.string().optional().describe("Filter to this rule name."),
52456
+ since: external_exports.string().optional().describe("Duration string (e.g. 1h, 7d) \u2014 show evaluations in this window."),
52457
+ last: external_exports.boolean().optional().describe("Return only the most recent evaluation (requires rule_name)."),
52458
+ audit_log: external_exports.string().optional().describe(`Audit log path (default: ${pathJoin(os14.homedir(), ".switchbot", "audit.log")}).`)
52459
+ }).strict(),
52460
+ outputSchema: {
52461
+ records: external_exports.array(external_exports.unknown()).describe("Array of trace + relatedAudit objects."),
52462
+ count: external_exports.number().describe("Number of trace records returned.")
52463
+ }
52464
+ },
52465
+ async ({ fire_id, rule_name, since, last, audit_log }) => {
52466
+ const DEFAULT_AUDIT_PATH4 = pathJoin(os14.homedir(), ".switchbot", "audit.log");
52467
+ const auditFile = audit_log ?? DEFAULT_AUDIT_PATH4;
52468
+ const sinceIso = since ? new Date(Date.now() - (parseDurationToMs(since) ?? 0)).toISOString() : void 0;
52469
+ let records = loadTraceRecords(auditFile, {
52470
+ fireId: fire_id,
52471
+ ruleName: rule_name,
52472
+ since: sinceIso
52473
+ });
52474
+ if (records.length === 0) {
52475
+ return {
52476
+ content: [{ type: "text", text: 'No rule-evaluate trace records found. Check that automation.audit.evaluate_trace is "sampled" or "full".' }],
52477
+ structuredContent: { records: [], count: 0 }
52478
+ };
52479
+ }
52480
+ if (last) {
52481
+ records = [records[records.length - 1]];
52482
+ }
52483
+ const output = records.map((record2) => {
52484
+ const related = loadRelatedAudit(auditFile, record2.fireId);
52485
+ return JSON.parse(formatExplainJson(record2, related));
52486
+ });
52487
+ return {
52488
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
52489
+ structuredContent: { records: output, count: output.length }
52490
+ };
52491
+ }
52492
+ );
52493
+ server.registerTool(
52494
+ "rules_simulate",
52495
+ {
52496
+ title: "Simulate a rule against historical events",
52497
+ 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.",
52498
+ _meta: { agentSafetyTier: "read" },
52499
+ inputSchema: external_exports.object({
52500
+ rule_yaml: external_exports.string().optional().describe("Standalone rule YAML (takes precedence over policy_path + rule_name)."),
52501
+ policy_path: external_exports.string().optional().describe("Path to policy.yaml (defaults to ~/.switchbot/policy.yaml)."),
52502
+ rule_name: external_exports.string().optional().describe("Name of the rule in policy.yaml to simulate."),
52503
+ since: external_exports.string().optional().describe("Replay events from this window (e.g. 7d, 24h)."),
52504
+ against: external_exports.string().optional().describe("JSONL file path of EngineEvent objects to replay."),
52505
+ live_llm: external_exports.boolean().optional().describe("Allow live LLM calls for llm conditions (default: skip and report as would-call)."),
52506
+ audit_log: external_exports.string().optional().describe(`Audit log path (default: ${pathJoin(os14.homedir(), ".switchbot", "audit.log")}).`)
52507
+ }).strict(),
52508
+ outputSchema: {
52509
+ report: external_exports.unknown().describe("SimulateReport object.")
52510
+ }
52511
+ },
52512
+ async ({ rule_yaml, policy_path, rule_name, since, against, live_llm, audit_log }) => {
52513
+ const DEFAULT_AUDIT_PATH4 = pathJoin(os14.homedir(), ".switchbot", "audit.log");
52514
+ const auditFile = audit_log ?? DEFAULT_AUDIT_PATH4;
52515
+ let rule;
52516
+ if (rule_yaml) {
52517
+ try {
52518
+ rule = (0, import_yaml7.parse)(rule_yaml);
52519
+ } catch (err) {
52520
+ return {
52521
+ content: [{ type: "text", text: `Failed to parse rule_yaml: ${String(err)}` }],
52522
+ structuredContent: { report: null }
52523
+ };
52524
+ }
52525
+ } else if (policy_path || rule_name) {
52526
+ const { loadPolicyFile: loadPolicyFile2 } = await Promise.resolve().then(() => (init_load(), load_exports));
52527
+ const policyFile = policy_path ?? pathJoin(os14.homedir(), ".switchbot", "policy.yaml");
52528
+ try {
52529
+ const policy = loadPolicyFile2(policyFile);
52530
+ const data = policy.data ?? {};
52531
+ const found = data.automation?.rules?.find((r) => r.name === rule_name);
52532
+ if (!found) {
52533
+ return {
52534
+ content: [{ type: "text", text: `Rule "${rule_name}" not found in ${policyFile}.` }],
52535
+ structuredContent: { report: null }
52536
+ };
52537
+ }
52538
+ rule = found;
52539
+ } catch (err) {
52540
+ return {
52541
+ content: [{ type: "text", text: `Failed to load policy: ${String(err)}` }],
52542
+ structuredContent: { report: null }
52543
+ };
52544
+ }
52545
+ } else {
52546
+ return {
52547
+ content: [{ type: "text", text: "Provide rule_yaml or (policy_path + rule_name) to specify the rule to simulate." }],
52548
+ structuredContent: { report: null }
52549
+ };
52550
+ }
52551
+ try {
52552
+ const report = await simulateRule({
52553
+ rule,
52554
+ since,
52555
+ against,
52556
+ auditLog: auditFile,
52557
+ liveLlm: live_llm ?? false
52558
+ });
52559
+ return {
52560
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }],
52561
+ structuredContent: { report }
52562
+ };
52563
+ } catch (err) {
52564
+ return {
52565
+ content: [{ type: "text", text: `Simulate error: ${String(err)}` }],
52566
+ structuredContent: { report: null }
52567
+ };
52568
+ }
52569
+ }
52570
+ );
51728
52571
  server.registerTool(
51729
52572
  "policy_add_rule",
51730
52573
  {
@@ -51775,12 +52618,27 @@ function listRegisteredTools(server) {
51775
52618
  if (!internal._registeredTools) return [];
51776
52619
  return Object.keys(internal._registeredTools).sort();
51777
52620
  }
52621
+ function listRegisteredToolsWithMeta(server) {
52622
+ const internal = server;
52623
+ if (!internal._registeredTools) return [];
52624
+ return Object.entries(internal._registeredTools).sort(([a], [b2]) => a.localeCompare(b2)).map(([name, reg]) => {
52625
+ const entry = { name };
52626
+ if (reg.description) entry.description = reg.description;
52627
+ if (reg.inputSchema) {
52628
+ try {
52629
+ entry.inputSchema = external_exports.toJSONSchema(reg.inputSchema);
52630
+ } catch {
52631
+ }
52632
+ }
52633
+ return entry;
52634
+ });
52635
+ }
51778
52636
  function listRegisteredResources() {
51779
52637
  return ["switchbot://events"];
51780
52638
  }
51781
52639
  function printMcpToolDirectory() {
51782
52640
  const server = createSwitchBotMcpServer();
51783
- const tools = listRegisteredTools(server).map((name) => ({ name }));
52641
+ const tools = listRegisteredToolsWithMeta(server);
51784
52642
  const resources = listRegisteredResources().map((uri) => ({ uri }));
51785
52643
  if (isJsonMode()) {
51786
52644
  printJson({ tools, resources });
@@ -51788,7 +52646,8 @@ function printMcpToolDirectory() {
51788
52646
  }
51789
52647
  console.log("Tools:");
51790
52648
  for (const tool of tools) {
51791
- console.log(` ${tool.name}`);
52649
+ const desc = tool.description ? ` \u2014 ${tool.description.slice(0, 80)}` : "";
52650
+ console.log(` ${tool.name}${desc}`);
51792
52651
  }
51793
52652
  console.log("");
51794
52653
  console.log("Resources:");
@@ -51800,7 +52659,7 @@ Total: ${tools.length} tool(s), ${resources.length} resource(s)`);
51800
52659
  }
51801
52660
  function registerMcpCommand(program3) {
51802
52661
  const mcp = program3.command("mcp").description("Run as a Model Context Protocol server so AI agents can call SwitchBot tools").addHelpText("after", `
51803
- The MCP server exposes twenty-one tools:
52662
+ The MCP server exposes twenty-four tools:
51804
52663
  - list_devices fetch all physical + IR devices
51805
52664
  - get_device_status live status for a physical device
51806
52665
  - send_command control a device (destructive commands need confirm:true)
@@ -51823,6 +52682,8 @@ function registerMcpCommand(program3) {
51823
52682
  - audit_stats aggregate audit counts by kind/result/device/rule
51824
52683
  - rule_notifications query rule notify action delivery history
51825
52684
  - rules_suggest draft an automation rule YAML from intent (heuristic, no LLM)
52685
+ - rules_explain show why a rule evaluation fired or was blocked
52686
+ - rules_simulate simulate a rule against historical events
51826
52687
  - policy_add_rule append a rule into automation.rules[] in policy.yaml
51827
52688
 
51828
52689
  Resource (read-only):
@@ -51995,7 +52856,7 @@ process_uptime_seconds ${Math.floor(process.uptime())}
51995
52856
  }
51996
52857
  if (profile) {
51997
52858
  const envCredsPresent = !!(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
51998
- if (!envCredsPresent && !fs16.existsSync(profileFilePath(profile))) {
52859
+ if (!envCredsPresent && !fs18.existsSync(profileFilePath(profile))) {
51999
52860
  res.writeHead(401, { "Content-Type": "application/json" });
52000
52861
  res.end(JSON.stringify({
52001
52862
  jsonrpc: "2.0",
@@ -52265,7 +53126,7 @@ Examples:
52265
53126
  throw new UsageError(`"${match.type}" exists in the effective catalog but not in source "${source}".`);
52266
53127
  }
52267
53128
  if (isJsonMode()) {
52268
- printJson(picked);
53129
+ printJson([picked]);
52269
53130
  return;
52270
53131
  }
52271
53132
  renderEntry(picked);
@@ -52631,18 +53492,18 @@ var StdoutSink = class {
52631
53492
 
52632
53493
  // src/sinks/file.ts
52633
53494
  init_cjs_shim();
52634
- import fs17 from "node:fs";
52635
- import path14 from "node:path";
53495
+ import fs19 from "node:fs";
53496
+ import path15 from "node:path";
52636
53497
  var FileSink = class {
52637
53498
  filePath;
52638
53499
  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 });
53500
+ this.filePath = path15.resolve(filePath);
53501
+ const dir = path15.dirname(this.filePath);
53502
+ if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
52642
53503
  }
52643
53504
  async write(event) {
52644
53505
  try {
52645
- fs17.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
53506
+ fs19.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
52646
53507
  } catch {
52647
53508
  }
52648
53509
  }
@@ -53007,7 +53868,7 @@ Examples:
53007
53868
  let matchedCount = 0;
53008
53869
  const ac = new AbortController();
53009
53870
  const forTimer = forMs !== null && forMs > 0 ? setTimeout(() => ac.abort(), forMs) : null;
53010
- if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push" });
53871
+ if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push", schemaVersion: EVENTS_SCHEMA_VERSION });
53011
53872
  await new Promise((resolve2, reject) => {
53012
53873
  let server = null;
53013
53874
  try {
@@ -53159,7 +54020,7 @@ Examples:
53159
54020
  if (!isJsonMode()) {
53160
54021
  console.error("Fetching MQTT credentials from SwitchBot service\u2026");
53161
54022
  }
53162
- if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push" });
54023
+ if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push", schemaVersion: EVENTS_SCHEMA_VERSION });
53163
54024
  if (isJsonMode()) {
53164
54025
  const sessionStartAt = (/* @__PURE__ */ new Date()).toISOString();
53165
54026
  emitJsonStreamRecord({
@@ -53311,9 +54172,9 @@ init_catalog();
53311
54172
  init_config();
53312
54173
  init_cache();
53313
54174
  init_quota();
53314
- import fs20 from "node:fs";
53315
- import os16 from "node:os";
53316
- import path17 from "node:path";
54175
+ import fs22 from "node:fs";
54176
+ import os17 from "node:os";
54177
+ import path18 from "node:path";
53317
54178
  import { execSync } from "node:child_process";
53318
54179
 
53319
54180
  // src/commands/agent-bootstrap.ts
@@ -53549,38 +54410,38 @@ init_request_context();
53549
54410
 
53550
54411
  // src/lib/daemon-state.ts
53551
54412
  init_cjs_shim();
53552
- import fs18 from "node:fs";
53553
- import os14 from "node:os";
53554
- import path15 from "node:path";
54413
+ import fs20 from "node:fs";
54414
+ import os15 from "node:os";
54415
+ import path16 from "node:path";
53555
54416
  function getStateDir() {
53556
- return path15.join(os14.homedir(), ".switchbot");
54417
+ return path16.join(os15.homedir(), ".switchbot");
53557
54418
  }
53558
54419
  function getDaemonPidFile() {
53559
- return path15.join(getStateDir(), "daemon.pid");
54420
+ return path16.join(getStateDir(), "daemon.pid");
53560
54421
  }
53561
54422
  function getDaemonLogFile() {
53562
- return path15.join(getStateDir(), "daemon.log");
54423
+ return path16.join(getStateDir(), "daemon.log");
53563
54424
  }
53564
54425
  function getDaemonStateFile() {
53565
- return path15.join(getStateDir(), "daemon.state.json");
54426
+ return path16.join(getStateDir(), "daemon.state.json");
53566
54427
  }
53567
54428
  function getHealthzPidFile() {
53568
- return path15.join(getStateDir(), "healthz.pid");
54429
+ return path16.join(getStateDir(), "healthz.pid");
53569
54430
  }
53570
54431
  var DAEMON_PID_FILE = getDaemonPidFile();
53571
54432
  var DAEMON_LOG_FILE = getDaemonLogFile();
53572
54433
  var DAEMON_STATE_FILE = getDaemonStateFile();
53573
54434
  var HEALTHZ_PID_FILE = getHealthzPidFile();
53574
54435
  function ensureStateDir() {
53575
- fs18.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
54436
+ fs20.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
53576
54437
  }
53577
54438
  function writeDaemonState(state) {
53578
54439
  ensureStateDir();
53579
- fs18.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
54440
+ fs20.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
53580
54441
  }
53581
54442
  function readDaemonState() {
53582
54443
  try {
53583
- const raw = fs18.readFileSync(getDaemonStateFile(), "utf-8");
54444
+ const raw = fs20.readFileSync(getDaemonStateFile(), "utf-8");
53584
54445
  return JSON.parse(raw);
53585
54446
  } catch {
53586
54447
  return null;
@@ -53589,26 +54450,26 @@ function readDaemonState() {
53589
54450
 
53590
54451
  // src/rules/pid-file.ts
53591
54452
  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");
54453
+ import fs21 from "node:fs";
54454
+ import os16 from "node:os";
54455
+ import path17 from "node:path";
54456
+ var DEFAULT_DIR = path17.join(os16.homedir(), ".switchbot");
53596
54457
  function getDefaultPidFilePaths() {
53597
54458
  return {
53598
54459
  dir: DEFAULT_DIR,
53599
- pidFile: path16.join(DEFAULT_DIR, "rules.pid"),
53600
- reloadFile: path16.join(DEFAULT_DIR, "rules.reload")
54460
+ pidFile: path17.join(DEFAULT_DIR, "rules.pid"),
54461
+ reloadFile: path17.join(DEFAULT_DIR, "rules.reload")
53601
54462
  };
53602
54463
  }
53603
54464
  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}
54465
+ const dir = path17.dirname(pidFile);
54466
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
54467
+ fs21.writeFileSync(pidFile, `${pid}
53607
54468
  `, { mode: 384 });
53608
54469
  }
53609
54470
  function readPidFile(pidFile) {
53610
54471
  try {
53611
- const raw = fs19.readFileSync(pidFile, "utf-8").trim();
54472
+ const raw = fs21.readFileSync(pidFile, "utf-8").trim();
53612
54473
  const n = Number(raw);
53613
54474
  return Number.isInteger(n) && n > 0 ? n : null;
53614
54475
  } catch {
@@ -53618,20 +54479,20 @@ function readPidFile(pidFile) {
53618
54479
  function clearPidFile(pidFile, pid = process.pid) {
53619
54480
  try {
53620
54481
  const existing = readPidFile(pidFile);
53621
- if (existing === pid) fs19.unlinkSync(pidFile);
54482
+ if (existing === pid) fs21.unlinkSync(pidFile);
53622
54483
  } catch {
53623
54484
  }
53624
54485
  }
53625
54486
  function writeReloadSentinel(reloadFile) {
53626
- const dir = path16.dirname(reloadFile);
53627
- fs19.mkdirSync(dir, { recursive: true, mode: 448 });
53628
- fs19.writeFileSync(reloadFile, `${Date.now()}
54487
+ const dir = path17.dirname(reloadFile);
54488
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
54489
+ fs21.writeFileSync(reloadFile, `${Date.now()}
53629
54490
  `, { mode: 384 });
53630
54491
  }
53631
54492
  function consumeReloadSentinel(reloadFile) {
53632
54493
  try {
53633
- if (!fs19.existsSync(reloadFile)) return false;
53634
- fs19.unlinkSync(reloadFile);
54494
+ if (!fs21.existsSync(reloadFile)) return false;
54495
+ fs21.unlinkSync(reloadFile);
53635
54496
  return true;
53636
54497
  } catch {
53637
54498
  return false;
@@ -53702,7 +54563,7 @@ async function checkCredentials() {
53702
54563
  };
53703
54564
  }
53704
54565
  const file2 = configFilePath();
53705
- if (!fs20.existsSync(file2)) {
54566
+ if (!fs22.existsSync(file2)) {
53706
54567
  return {
53707
54568
  name: "credentials",
53708
54569
  status: "fail",
@@ -53717,7 +54578,7 @@ async function checkCredentials() {
53717
54578
  };
53718
54579
  }
53719
54580
  try {
53720
- const raw = fs20.readFileSync(file2, "utf-8");
54581
+ const raw = fs22.readFileSync(file2, "utf-8");
53721
54582
  const cfg = JSON.parse(raw);
53722
54583
  if (!cfg.token || !cfg.secret) {
53723
54584
  return {
@@ -53764,8 +54625,8 @@ async function checkCredentials() {
53764
54625
  }
53765
54626
  }
53766
54627
  function checkProfiles() {
53767
- const dir = path17.join(os16.homedir(), ".switchbot", "profiles");
53768
- if (!fs20.existsSync(dir)) {
54628
+ const dir = path18.join(os17.homedir(), ".switchbot", "profiles");
54629
+ if (!fs22.existsSync(dir)) {
53769
54630
  return { name: "profiles", status: "ok", detail: "no profile dir (default profile only)" };
53770
54631
  }
53771
54632
  const profiles = listProfiles();
@@ -53872,8 +54733,8 @@ function checkCache() {
53872
54733
  }
53873
54734
  }
53874
54735
  function checkQuotaFile() {
53875
- const p2 = path17.join(os16.homedir(), ".switchbot", "quota.json");
53876
- if (!fs20.existsSync(p2)) {
54736
+ const p2 = path18.join(os17.homedir(), ".switchbot", "quota.json");
54737
+ if (!fs22.existsSync(p2)) {
53877
54738
  return {
53878
54739
  name: "quota",
53879
54740
  status: "ok",
@@ -53886,7 +54747,7 @@ function checkQuotaFile() {
53886
54747
  };
53887
54748
  }
53888
54749
  try {
53889
- const raw = fs20.readFileSync(p2, "utf-8");
54750
+ const raw = fs22.readFileSync(p2, "utf-8");
53890
54751
  JSON.parse(raw);
53891
54752
  } catch {
53892
54753
  return {
@@ -53966,8 +54827,8 @@ function checkInventoryConsistency() {
53966
54827
  };
53967
54828
  }
53968
54829
  function checkAudit() {
53969
- const p2 = path17.join(os16.homedir(), ".switchbot", "audit.log");
53970
- if (!fs20.existsSync(p2)) {
54830
+ const p2 = path18.join(os17.homedir(), ".switchbot", "audit.log");
54831
+ if (!fs22.existsSync(p2)) {
53971
54832
  return {
53972
54833
  name: "audit",
53973
54834
  status: "ok",
@@ -53979,7 +54840,7 @@ function checkAudit() {
53979
54840
  };
53980
54841
  }
53981
54842
  try {
53982
- const raw = fs20.readFileSync(p2, "utf-8");
54843
+ const raw = fs22.readFileSync(p2, "utf-8");
53983
54844
  const since = Date.now() - 24 * 60 * 60 * 1e3;
53984
54845
  const recent = [];
53985
54846
  let total = 0;
@@ -54179,7 +55040,7 @@ function checkPathDiscoverability() {
54179
55040
  let npmBinDir = null;
54180
55041
  try {
54181
55042
  const prefix = execSync("npm prefix -g", { timeout: 4e3, encoding: "utf-8" }).trim();
54182
- npmBinDir = isWindows ? prefix : path17.join(prefix, "bin");
55043
+ npmBinDir = isWindows ? prefix : path18.join(prefix, "bin");
54183
55044
  } catch {
54184
55045
  }
54185
55046
  let binaryOnPath = false;
@@ -54209,7 +55070,7 @@ function checkPathDiscoverability() {
54209
55070
  };
54210
55071
  }
54211
55072
  const currentPath = process.env.PATH ?? "";
54212
- const missingSegment = npmBinDir && !currentPath.split(path17.delimiter).includes(npmBinDir) ? npmBinDir : null;
55073
+ const missingSegment = npmBinDir && !currentPath.split(path18.delimiter).includes(npmBinDir) ? npmBinDir : null;
54213
55074
  const currentShell = detectShellFlavor();
54214
55075
  const shellFix = buildPathFix(currentShell, missingSegment, npmBinDir);
54215
55076
  return {
@@ -54310,9 +55171,9 @@ function checkMqtt() {
54310
55171
  };
54311
55172
  }
54312
55173
  const file2 = configFilePath();
54313
- if (fs20.existsSync(file2)) {
55174
+ if (fs22.existsSync(file2)) {
54314
55175
  try {
54315
- const cfg = JSON.parse(fs20.readFileSync(file2, "utf-8"));
55176
+ const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
54316
55177
  if (cfg.token && cfg.secret) {
54317
55178
  return {
54318
55179
  name: "mqtt",
@@ -54339,9 +55200,9 @@ async function checkMqttProbe() {
54339
55200
  creds = { token, secret };
54340
55201
  } else {
54341
55202
  const file2 = configFilePath();
54342
- if (fs20.existsSync(file2)) {
55203
+ if (fs22.existsSync(file2)) {
54343
55204
  try {
54344
- const cfg = JSON.parse(fs20.readFileSync(file2, "utf-8"));
55205
+ const cfg = JSON.parse(fs22.readFileSync(file2, "utf-8"));
54345
55206
  if (cfg.token && cfg.secret) {
54346
55207
  creds = { token: cfg.token, secret: cfg.secret };
54347
55208
  }
@@ -54435,10 +55296,12 @@ function checkNotifyConnectivity() {
54435
55296
  return { name: "notify-connectivity", status: "ok", detail: { present: false, message: "policy file could not be loaded" } };
54436
55297
  }
54437
55298
  const policy = loaded.data;
54438
- const rules = policy?.automation?.rules ?? [];
55299
+ const rawRules = policy?.automation?.rules;
55300
+ const rules = Array.isArray(rawRules) ? rawRules : [];
54439
55301
  const webhookUrls = [];
54440
55302
  for (const rule of rules) {
54441
- for (const action of rule.then ?? []) {
55303
+ const then = rule.then;
55304
+ for (const action of Array.isArray(then) ? then : []) {
54442
55305
  if (action.type === "notify" && (action.channel === "webhook" || action.channel === "openclaw") && action.to) {
54443
55306
  webhookUrls.push(action.to);
54444
55307
  }
@@ -54727,7 +55590,7 @@ function runSchemaExport(options) {
54727
55590
  payload.resources = RESOURCE_CATALOG;
54728
55591
  payload.cliAddedFields = [
54729
55592
  {
54730
- field: "_fetchedAt",
55593
+ field: "fetchedAt",
54731
55594
  appliesTo: ["devices status", "devices describe"],
54732
55595
  type: "string (ISO-8601)",
54733
55596
  description: "CLI-synthesized timestamp indicating when this status response was fetched or served from the cache. Not part of the upstream SwitchBot API."
@@ -54790,7 +55653,7 @@ Common top-level fields:
54790
55653
  schemaVersion CLI schema version (stable for agent contracts)
54791
55654
  data.version Catalog schema version
54792
55655
  data.types Array of SchemaEntry (or CompactSchemaEntry with --compact)
54793
- data._fetchedAt CLI-added; present on live-query responses ('devices status'),
55656
+ data.fetchedAt CLI-added; present on live-query responses ('devices status'),
54794
55657
  not on this offline export.
54795
55658
 
54796
55659
  Examples:
@@ -54812,9 +55675,9 @@ init_arg_parsers();
54812
55675
  init_output();
54813
55676
  init_audit();
54814
55677
  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");
55678
+ import path19 from "node:path";
55679
+ import os18 from "node:os";
55680
+ var DEFAULT_AUDIT = path19.join(os18.homedir(), ".switchbot", "audit.log");
54818
55681
  function registerHistoryCommand(program3) {
54819
55682
  const history = program3.command("history").description("View and replay SwitchBot commands recorded via --audit-log").addHelpText("after", `
54820
55683
  Every 'devices command' run with --audit-log is appended as JSONL to the
@@ -55672,9 +56535,9 @@ init_load();
55672
56535
  init_validate();
55673
56536
  init_types();
55674
56537
  init_engine();
55675
- import fs22 from "node:fs";
55676
- import os19 from "node:os";
55677
- import path20 from "node:path";
56538
+ import fs24 from "node:fs";
56539
+ import os20 from "node:os";
56540
+ import path21 from "node:path";
55678
56541
 
55679
56542
  // src/rules/conflict-analyzer.ts
55680
56543
  init_cjs_shim();
@@ -55693,9 +56556,9 @@ var OPPOSING_PAIRS = [
55693
56556
  ["volumeUp", "volumeDown"],
55694
56557
  ["fanSpeedUp", "fanSpeedDown"]
55695
56558
  ];
55696
- var HIGH_FREQ_EVENTS = ["device.shadow", "*"];
56559
+ var HIGH_FREQ_EVENTS2 = ["device.shadow", "*"];
55697
56560
  function isHighFreqEvent(event) {
55698
- return HIGH_FREQ_EVENTS.includes(event);
56561
+ return HIGH_FREQ_EVENTS2.includes(event);
55699
56562
  }
55700
56563
  function commandsAreOpposing(a, b2) {
55701
56564
  for (const [x2, y] of OPPOSING_PAIRS) {
@@ -55837,9 +56700,9 @@ init_client2();
55837
56700
 
55838
56701
  // src/rules/webhook-token.ts
55839
56702
  init_cjs_shim();
55840
- import fs21 from "node:fs";
55841
- import os18 from "node:os";
55842
- import path19 from "node:path";
56703
+ import fs23 from "node:fs";
56704
+ import os19 from "node:os";
56705
+ import path20 from "node:path";
55843
56706
  import { randomBytes } from "node:crypto";
55844
56707
  var ENV_TOKEN = "SWITCHBOT_WEBHOOK_TOKEN";
55845
56708
  var DEFAULT_FILE = ".switchbot/webhook-token";
@@ -55847,7 +56710,7 @@ var WebhookTokenStore = class {
55847
56710
  filePath;
55848
56711
  envLookup;
55849
56712
  constructor(opts = {}) {
55850
- this.filePath = opts.filePath ?? path19.join(os18.homedir(), DEFAULT_FILE);
56713
+ this.filePath = opts.filePath ?? path20.join(os19.homedir(), DEFAULT_FILE);
55851
56714
  this.envLookup = opts.envLookup ?? (() => process.env[ENV_TOKEN]);
55852
56715
  }
55853
56716
  /**
@@ -55871,7 +56734,7 @@ var WebhookTokenStore = class {
55871
56734
  */
55872
56735
  readFromDisk() {
55873
56736
  try {
55874
- const raw = fs21.readFileSync(this.filePath, "utf-8").trim();
56737
+ const raw = fs23.readFileSync(this.filePath, "utf-8").trim();
55875
56738
  return raw.length > 0 ? raw : null;
55876
56739
  } catch (err) {
55877
56740
  if (err.code === "ENOENT") return null;
@@ -55888,12 +56751,12 @@ var WebhookTokenStore = class {
55888
56751
  return this.filePath;
55889
56752
  }
55890
56753
  writeToDisk(token) {
55891
- const dir = path19.dirname(this.filePath);
55892
- fs21.mkdirSync(dir, { recursive: true });
55893
- fs21.writeFileSync(this.filePath, `${token}
56754
+ const dir = path20.dirname(this.filePath);
56755
+ fs23.mkdirSync(dir, { recursive: true });
56756
+ fs23.writeFileSync(this.filePath, `${token}
55894
56757
  `, { mode: 384 });
55895
56758
  try {
55896
- fs21.chmodSync(this.filePath, 384);
56759
+ fs23.chmodSync(this.filePath, 384);
55897
56760
  } catch {
55898
56761
  }
55899
56762
  }
@@ -55978,18 +56841,18 @@ function aggregateRuleAudits(entries) {
55978
56841
  }
55979
56842
 
55980
56843
  // src/commands/rules.ts
55981
- var DEFAULT_AUDIT_PATH2 = path20.join(os19.homedir(), ".switchbot", "audit.log");
56844
+ var DEFAULT_AUDIT_PATH2 = path21.join(os20.homedir(), ".switchbot", "audit.log");
55982
56845
  function loadAutomation(policyPathFlag) {
55983
- const path28 = resolvePolicyPath({ flag: policyPathFlag });
56846
+ const path29 = resolvePolicyPath({ flag: policyPathFlag });
55984
56847
  let loaded;
55985
56848
  try {
55986
- loaded = loadPolicyFile(path28);
56849
+ loaded = loadPolicyFile(path29);
55987
56850
  } catch (err) {
55988
56851
  if (err instanceof PolicyFileNotFoundError) {
55989
56852
  exitWithError({
55990
56853
  code: 2,
55991
56854
  kind: "usage",
55992
- message: `policy file not found: ${path28}`,
56855
+ message: `policy file not found: ${path29}`,
55993
56856
  extra: { subKind: "file-not-found" }
55994
56857
  });
55995
56858
  }
@@ -55997,7 +56860,7 @@ function loadAutomation(policyPathFlag) {
55997
56860
  exitWithError({
55998
56861
  code: 3,
55999
56862
  kind: "runtime",
56000
- message: `YAML parse error in ${path28}: ${err.message}`,
56863
+ message: `YAML parse error in ${path29}: ${err.message}`,
56001
56864
  extra: { subKind: "yaml-parse", errors: err.yamlErrors }
56002
56865
  });
56003
56866
  }
@@ -56009,7 +56872,7 @@ function loadAutomation(policyPathFlag) {
56009
56872
  code: 4,
56010
56873
  kind: "runtime",
56011
56874
  message: "policy file failed schema validation. Run `switchbot policy validate` for details.",
56012
- extra: { subKind: "invalid-policy", path: path28 }
56875
+ extra: { subKind: "invalid-policy", path: path29 }
56013
56876
  });
56014
56877
  }
56015
56878
  const data = loaded.data ?? {};
@@ -56023,7 +56886,7 @@ function loadAutomation(policyPathFlag) {
56023
56886
  }
56024
56887
  const rawQH = data.quiet_hours;
56025
56888
  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 };
56889
+ return { path: path29, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
56027
56890
  }
56028
56891
  function describeTrigger(rule) {
56029
56892
  const t = rule.when;
@@ -56101,13 +56964,12 @@ function registerRun(rules) {
56101
56964
  const loaded = loadAutomation(pathArg);
56102
56965
  if (!loaded) return;
56103
56966
  if (loaded.automation?.enabled !== true) {
56104
- const msg = "automation.enabled is not true \u2014 nothing to run.";
56105
- if (isJsonMode()) {
56106
- printJson({ kind: "control", controlKind: "disabled", message: msg });
56107
- } else {
56108
- console.error(msg);
56109
- }
56110
- process.exit(0);
56967
+ exitWithError({
56968
+ code: 1,
56969
+ kind: "runtime",
56970
+ message: "automation.enabled is not true \u2014 set it to true in your policy file to start the daemon.",
56971
+ hint: "Set automation.enabled: true in your policy file, then re-run."
56972
+ });
56111
56973
  }
56112
56974
  const lint = lintRules(loaded.automation);
56113
56975
  if (!lint.valid) {
@@ -56287,7 +57149,7 @@ function registerTail(rules) {
56287
57149
  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
57150
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56289
57151
  const sinceMs = resolveSinceMs(opts.since);
56290
- const existing = fs22.existsSync(file2) ? readAudit(file2) : [];
57152
+ const existing = fs24.existsSync(file2) ? readAudit(file2) : [];
56291
57153
  const filtered = filterRuleAudits(existing, { sinceMs, ruleName: opts.rule });
56292
57154
  if (isJsonMode()) {
56293
57155
  for (const e of filtered) console.log(JSON.stringify(e));
@@ -56299,7 +57161,7 @@ function registerTail(rules) {
56299
57161
  for (const e of filtered) console.log(formatAuditLine(e));
56300
57162
  }
56301
57163
  if (!opts.follow) return;
56302
- let offset = fs22.existsSync(file2) ? fs22.statSync(file2).size : 0;
57164
+ let offset = fs24.existsSync(file2) ? fs24.statSync(file2).size : 0;
56303
57165
  let buffer = "";
56304
57166
  const emit = (line) => {
56305
57167
  const trimmed = line.trim();
@@ -56316,21 +57178,21 @@ function registerTail(rules) {
56316
57178
  else console.log(formatAuditLine(entry));
56317
57179
  };
56318
57180
  const poll = setInterval(() => {
56319
- if (!fs22.existsSync(file2)) return;
56320
- const size = fs22.statSync(file2).size;
57181
+ if (!fs24.existsSync(file2)) return;
57182
+ const size = fs24.statSync(file2).size;
56321
57183
  if (size < offset) {
56322
57184
  offset = 0;
56323
57185
  buffer = "";
56324
57186
  }
56325
57187
  if (size === offset) return;
56326
- const fd = fs22.openSync(file2, "r");
57188
+ const fd = fs24.openSync(file2, "r");
56327
57189
  try {
56328
57190
  const chunk = Buffer.alloc(size - offset);
56329
- fs22.readSync(fd, chunk, 0, chunk.length, offset);
57191
+ fs24.readSync(fd, chunk, 0, chunk.length, offset);
56330
57192
  offset = size;
56331
57193
  buffer += chunk.toString("utf-8");
56332
57194
  } finally {
56333
- fs22.closeSync(fd);
57195
+ fs24.closeSync(fd);
56334
57196
  }
56335
57197
  let newline = buffer.indexOf("\n");
56336
57198
  while (newline !== -1) {
@@ -56370,7 +57232,7 @@ function formatReplayTable(report) {
56370
57232
  function registerReplay(rules) {
56371
57233
  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
57234
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56373
- const entries = fs22.existsSync(file2) ? readAudit(file2) : [];
57235
+ const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
56374
57236
  const sinceMs = resolveSinceMs(opts.since);
56375
57237
  const filtered = filterRuleAudits(entries, {
56376
57238
  sinceMs,
@@ -56490,7 +57352,7 @@ function registerSuggest(rules) {
56490
57352
  for (const w2 of warnings) process.stderr.write(`warning: ${w2}
56491
57353
  `);
56492
57354
  if (opts.out) {
56493
- fs22.writeFileSync(opts.out, ruleYaml, "utf8");
57355
+ fs24.writeFileSync(opts.out, ruleYaml, "utf8");
56494
57356
  if (!isJsonMode()) console.log(`\u2713 rule YAML written to ${opts.out}`);
56495
57357
  } else if (isJsonMode()) {
56496
57358
  printJson({ rule, rule_yaml: ruleYaml, warnings });
@@ -56565,7 +57427,7 @@ overall: ${overall ? "ok" : "issues found"}`);
56565
57427
  function registerSummary(rules) {
56566
57428
  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
57429
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56568
- const entries = fs22.existsSync(file2) ? readAudit(file2) : [];
57430
+ const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
56569
57431
  const sinceMs = resolveSinceMs(opts.since ?? "24h");
56570
57432
  const filtered = filterRuleAudits(entries, { sinceMs, ruleName: opts.rule });
56571
57433
  const report = aggregateRuleAudits(filtered);
@@ -56596,7 +57458,7 @@ function registerLastFired(rules) {
56596
57458
  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
57459
  const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
56598
57460
  const n = opts.n ?? 10;
56599
- const entries = fs22.existsSync(file2) ? readAudit(file2) : [];
57461
+ const entries = fs24.existsSync(file2) ? readAudit(file2) : [];
56600
57462
  const fires = filterRuleAudits(entries, {
56601
57463
  ruleName: opts.rule,
56602
57464
  kinds: ["rule-fire", "rule-fire-dry"]
@@ -56638,7 +57500,7 @@ function registerExplain(rules) {
56638
57500
  return;
56639
57501
  }
56640
57502
  const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
56641
- const entries = fs22.existsSync(auditFile) ? readAudit(auditFile) : [];
57503
+ const entries = fs24.existsSync(auditFile) ? readAudit(auditFile) : [];
56642
57504
  const fires = filterRuleAudits(entries, { ruleName: name, kinds: ["rule-fire", "rule-fire-dry"] });
56643
57505
  const lastFired = fires.length > 0 ? fires[fires.length - 1].t : null;
56644
57506
  const detail = {
@@ -56675,6 +57537,115 @@ function registerExplain(rules) {
56675
57537
  console.log(`last fired: ${detail.lastFired ?? "(never)"}`);
56676
57538
  });
56677
57539
  }
57540
+ function registerTraceExplain(rules) {
57541
+ 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(
57542
+ (fireIdArg, opts) => {
57543
+ const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
57544
+ if (!fs24.existsSync(auditFile)) {
57545
+ exitWithError({ code: 1, kind: "usage", message: `Audit log not found: ${auditFile}. Make sure trace recording is enabled (automation.audit.evaluate_trace: sampled or full).` });
57546
+ return;
57547
+ }
57548
+ const sinceIso = opts.since ? new Date(Date.now() - (parseDurationToMs2(opts.since) ?? 0)).toISOString() : void 0;
57549
+ let records = loadTraceRecords(auditFile, {
57550
+ fireId: fireIdArg,
57551
+ ruleName: opts.rule,
57552
+ since: sinceIso
57553
+ });
57554
+ if (records.length === 0) {
57555
+ const hint = 'Check that automation.audit.evaluate_trace is set to "sampled" or "full".';
57556
+ exitWithError({ code: 1, kind: "usage", message: `No rule-evaluate trace records found. ${hint}` });
57557
+ return;
57558
+ }
57559
+ if (opts.last) {
57560
+ records = [records[records.length - 1]];
57561
+ }
57562
+ for (const record2 of records) {
57563
+ const related = loadRelatedAudit(auditFile, record2.fireId);
57564
+ if (isJsonMode()) {
57565
+ console.log(formatExplainJson(record2, related));
57566
+ } else {
57567
+ console.log(formatExplainText(record2, related));
57568
+ if (records.length > 1) console.log("---");
57569
+ }
57570
+ }
57571
+ }
57572
+ );
57573
+ }
57574
+ function registerSimulate(rules) {
57575
+ 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(
57576
+ async (ruleOrPolicy, opts) => {
57577
+ let rule;
57578
+ const auditLog = opts.auditLog ?? DEFAULT_AUDIT_PATH2;
57579
+ if (!fs24.existsSync(ruleOrPolicy)) {
57580
+ exitWithError({ code: 2, kind: "usage", message: `File not found: ${ruleOrPolicy}` });
57581
+ return;
57582
+ }
57583
+ let parsed;
57584
+ try {
57585
+ const { parse: yamlParse5 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
57586
+ parsed = yamlParse5(fs24.readFileSync(ruleOrPolicy, "utf-8"));
57587
+ } catch {
57588
+ exitWithError({ code: 2, kind: "usage", message: `Could not parse YAML file: ${ruleOrPolicy}` });
57589
+ return;
57590
+ }
57591
+ const asRule = parsed;
57592
+ if (asRule["name"] && asRule["when"] && asRule["then"]) {
57593
+ rule = asRule;
57594
+ } else {
57595
+ const automation = loadAutomation(ruleOrPolicy);
57596
+ if (!automation) return;
57597
+ const ruleName = opts.rule;
57598
+ if (!ruleName) {
57599
+ exitWithError({ code: 1, kind: "usage", message: "Use --rule <name> to specify which rule to simulate from the policy file." });
57600
+ return;
57601
+ }
57602
+ rule = automation.automation?.rules?.find((r) => r.name === ruleName);
57603
+ if (!rule) {
57604
+ exitWithError({ code: 1, kind: "usage", message: `Rule "${ruleName}" not found in policy file.` });
57605
+ return;
57606
+ }
57607
+ }
57608
+ try {
57609
+ const report = await simulateRule({
57610
+ rule,
57611
+ since: opts.since,
57612
+ against: opts.against,
57613
+ auditLog,
57614
+ liveLlm: opts.liveLlm ?? false
57615
+ });
57616
+ if (opts.reportOut) {
57617
+ fs24.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
57618
+ console.log(`Report written to ${opts.reportOut}`);
57619
+ }
57620
+ if (isJsonMode()) {
57621
+ printJson(report);
57622
+ } else {
57623
+ console.log(`Rule: ${report.ruleName} (version ${report.ruleVersion})`);
57624
+ console.log(`Window: ${report.windowStart.toISOString()} \u2192 ${report.windowEnd.toISOString()}`);
57625
+ console.log(`Source events: ${report.sourceEventCount}`);
57626
+ console.log("");
57627
+ console.log(` Would fire: ${report.wouldFire}`);
57628
+ console.log(` Blocked by condition:${report.blockedByCondition}`);
57629
+ console.log(` Throttled: ${report.throttled}`);
57630
+ console.log(` Errored: ${report.errored}`);
57631
+ if (report.skippedLlm > 0) {
57632
+ console.log(` Skipped (llm): ${report.skippedLlm} (use --live-llm to evaluate)`);
57633
+ }
57634
+ if (report.topBlockReason && report.topBlockCount !== void 0) {
57635
+ const total = report.blockedByCondition;
57636
+ const pct = total > 0 ? Math.round(report.topBlockCount / total * 100) : 0;
57637
+ if (pct >= 80) {
57638
+ console.log("");
57639
+ console.log(`Top block reason (${pct}%): ${report.topBlockReason}`);
57640
+ }
57641
+ }
57642
+ }
57643
+ } catch (err) {
57644
+ handleError(err);
57645
+ }
57646
+ }
57647
+ );
57648
+ }
56678
57649
  function registerRulesCommand(program3) {
56679
57650
  const rules = program3.command("rules").description("Run, list, and lint automation rules declared in policy.yaml (v0.2, preview).").addHelpText(
56680
57651
  "after",
@@ -56722,6 +57693,8 @@ Exit codes (lint):
56722
57693
  registerDoctor(rules);
56723
57694
  registerSummary(rules);
56724
57695
  registerLastFired(rules);
57696
+ registerTraceExplain(rules);
57697
+ registerSimulate(rules);
56725
57698
  registerWebhookRotateToken(rules);
56726
57699
  registerWebhookShowToken(rules);
56727
57700
  }
@@ -56732,9 +57705,9 @@ init_output();
56732
57705
  init_arg_parsers();
56733
57706
  init_request_context();
56734
57707
  init_keychain();
56735
- import fs23 from "node:fs";
56736
- import path21 from "node:path";
56737
- import os20 from "node:os";
57708
+ import fs25 from "node:fs";
57709
+ import path22 from "node:path";
57710
+ import os21 from "node:os";
56738
57711
  import readline5 from "node:readline";
56739
57712
  function activeProfile() {
56740
57713
  return getActiveProfile() ?? "default";
@@ -56780,7 +57753,7 @@ async function promptSecret2(question) {
56780
57753
  });
56781
57754
  }
56782
57755
  function readStdinFile(filePath) {
56783
- if (!fs23.existsSync(filePath)) {
57756
+ if (!fs25.existsSync(filePath)) {
56784
57757
  exitWithError({
56785
57758
  code: 2,
56786
57759
  kind: "usage",
@@ -56789,7 +57762,7 @@ function readStdinFile(filePath) {
56789
57762
  }
56790
57763
  let parsed;
56791
57764
  try {
56792
- parsed = JSON.parse(fs23.readFileSync(filePath, "utf-8"));
57765
+ parsed = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
56793
57766
  } catch (err) {
56794
57767
  exitWithError({
56795
57768
  code: 2,
@@ -56819,10 +57792,10 @@ function cleanupMigratedSourceFile(sourceFile, parsed) {
56819
57792
  delete next.token;
56820
57793
  delete next.secret;
56821
57794
  if (Object.keys(next).length === 0) {
56822
- fs23.unlinkSync(sourceFile);
57795
+ fs25.unlinkSync(sourceFile);
56823
57796
  return "deleted";
56824
57797
  }
56825
- fs23.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
57798
+ fs25.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
56826
57799
  return "scrubbed";
56827
57800
  }
56828
57801
  function registerAuthCommand(program3) {
@@ -56953,8 +57926,8 @@ function registerAuthCommand(program3) {
56953
57926
  message: `backend "${store.name}" is not writable on this machine`
56954
57927
  });
56955
57928
  }
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)) {
57929
+ const sourceFile = profile === "default" ? path22.join(os21.homedir(), ".switchbot", "config.json") : path22.join(os21.homedir(), ".switchbot", "profiles", `${profile}.json`);
57930
+ if (!fs25.existsSync(sourceFile)) {
56958
57931
  exitWithError({
56959
57932
  code: 2,
56960
57933
  kind: "usage",
@@ -56964,7 +57937,7 @@ function registerAuthCommand(program3) {
56964
57937
  }
56965
57938
  let parsed;
56966
57939
  try {
56967
- const raw = JSON.parse(fs23.readFileSync(sourceFile, "utf-8"));
57940
+ const raw = JSON.parse(fs25.readFileSync(sourceFile, "utf-8"));
56968
57941
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
56969
57942
  throw new Error("expected a JSON object");
56970
57943
  }
@@ -57026,8 +57999,8 @@ function registerAuthCommand(program3) {
57026
57999
  init_cjs_shim();
57027
58000
  init_esm();
57028
58001
  init_load();
57029
- import fs26 from "node:fs";
57030
- import path24 from "node:path";
58002
+ import fs28 from "node:fs";
58003
+ import path25 from "node:path";
57031
58004
 
57032
58005
  // src/install/steps.ts
57033
58006
  init_cjs_shim();
@@ -57079,9 +58052,9 @@ init_cjs_shim();
57079
58052
  init_load();
57080
58053
  init_validate();
57081
58054
  init_keychain();
57082
- import fs24 from "node:fs";
57083
- import path22 from "node:path";
57084
- import os21 from "node:os";
58055
+ import fs26 from "node:fs";
58056
+ import path23 from "node:path";
58057
+ import os22 from "node:os";
57085
58058
  function parseMajor(version2) {
57086
58059
  const m2 = /^v?(\d+)\./.exec(version2);
57087
58060
  if (!m2) return null;
@@ -57171,10 +58144,10 @@ async function checkKeychain2() {
57171
58144
  }
57172
58145
  }
57173
58146
  function checkHomeDirWritable() {
57174
- const home = os21.homedir();
57175
- const switchbotDir = path22.join(home, ".switchbot");
58147
+ const home = os22.homedir();
58148
+ const switchbotDir = path23.join(home, ".switchbot");
57176
58149
  try {
57177
- const homeStat = fs24.statSync(home);
58150
+ const homeStat = fs26.statSync(home);
57178
58151
  if (!homeStat.isDirectory()) {
57179
58152
  return {
57180
58153
  name: "home",
@@ -57183,8 +58156,8 @@ function checkHomeDirWritable() {
57183
58156
  hint: "check your HOME/USERPROFILE environment configuration"
57184
58157
  };
57185
58158
  }
57186
- if (fs24.existsSync(switchbotDir)) {
57187
- const sbStat = fs24.statSync(switchbotDir);
58159
+ if (fs26.existsSync(switchbotDir)) {
58160
+ const sbStat = fs26.statSync(switchbotDir);
57188
58161
  if (!sbStat.isDirectory()) {
57189
58162
  return {
57190
58163
  name: "home",
@@ -57193,10 +58166,10 @@ function checkHomeDirWritable() {
57193
58166
  hint: "move the file aside and re-run install"
57194
58167
  };
57195
58168
  }
57196
- fs24.accessSync(switchbotDir, fs24.constants.W_OK);
58169
+ fs26.accessSync(switchbotDir, fs26.constants.W_OK);
57197
58170
  return { name: "home", status: "ok", message: `writable: ${switchbotDir}` };
57198
58171
  }
57199
- fs24.accessSync(home, fs24.constants.W_OK);
58172
+ fs26.accessSync(home, fs26.constants.W_OK);
57200
58173
  return { name: "home", status: "ok", message: `writable: ${home}` };
57201
58174
  } catch (err) {
57202
58175
  return {
@@ -57210,8 +58183,8 @@ function checkHomeDirWritable() {
57210
58183
  function nearestExistingPath(target) {
57211
58184
  let cur = target;
57212
58185
  while (true) {
57213
- if (fs24.existsSync(cur)) return cur;
57214
- const parent = path22.dirname(cur);
58186
+ if (fs26.existsSync(cur)) return cur;
58187
+ const parent = path23.dirname(cur);
57215
58188
  if (parent === cur) return null;
57216
58189
  cur = parent;
57217
58190
  }
@@ -57219,8 +58192,8 @@ function nearestExistingPath(target) {
57219
58192
  function checkAgentSkillDirWritable(opts) {
57220
58193
  const shouldCheck = opts.agent === "claude-code" && (opts.expectSkillLink ?? true);
57221
58194
  if (!shouldCheck) return null;
57222
- const home = os21.homedir();
57223
- const target = path22.join(home, ".claude", "skills");
58195
+ const home = os22.homedir();
58196
+ const target = path23.join(home, ".claude", "skills");
57224
58197
  try {
57225
58198
  const existing = nearestExistingPath(target);
57226
58199
  if (!existing) {
@@ -57231,7 +58204,7 @@ function checkAgentSkillDirWritable(opts) {
57231
58204
  hint: "check your home directory path and permissions"
57232
58205
  };
57233
58206
  }
57234
- const stat = fs24.statSync(existing);
58207
+ const stat = fs26.statSync(existing);
57235
58208
  if (!stat.isDirectory()) {
57236
58209
  return {
57237
58210
  name: "agent-skills-dir",
@@ -57240,7 +58213,7 @@ function checkAgentSkillDirWritable(opts) {
57240
58213
  hint: "move the blocking file aside and re-run install"
57241
58214
  };
57242
58215
  }
57243
- fs24.accessSync(existing, fs24.constants.W_OK);
58216
+ fs26.accessSync(existing, fs26.constants.W_OK);
57244
58217
  return { name: "agent-skills-dir", status: "ok", message: `writable: ${target}` };
57245
58218
  } catch (err) {
57246
58219
  return {
@@ -57265,9 +58238,9 @@ async function runPreflight(options = {}) {
57265
58238
 
57266
58239
  // src/install/default-steps.ts
57267
58240
  init_cjs_shim();
57268
- import fs25 from "node:fs";
57269
- import path23 from "node:path";
57270
- import os22 from "node:os";
58241
+ import fs27 from "node:fs";
58242
+ import path24 from "node:path";
58243
+ import os23 from "node:os";
57271
58244
  import { spawnSync } from "node:child_process";
57272
58245
  init_keychain();
57273
58246
  function stepPromptCredentials() {
@@ -57342,15 +58315,15 @@ function stepScaffoldPolicy() {
57342
58315
  const r = ctx.policyScaffoldResult;
57343
58316
  if (!r || r.skipped) return;
57344
58317
  try {
57345
- fs25.unlinkSync(r.policyPath);
58318
+ fs27.unlinkSync(r.policyPath);
57346
58319
  } catch {
57347
58320
  }
57348
58321
  }
57349
58322
  };
57350
58323
  }
57351
- function skillLinkPathFor(agent, home = os22.homedir()) {
58324
+ function skillLinkPathFor(agent, home = os23.homedir()) {
57352
58325
  if (agent === "claude-code") {
57353
- return path23.join(home, ".claude", "skills", "switchbot");
58326
+ return path24.join(home, ".claude", "skills", "switchbot");
57354
58327
  }
57355
58328
  return null;
57356
58329
  }
@@ -57364,11 +58337,11 @@ function stepSymlinkSkill(opts = {}) {
57364
58337
  ctx.skillRecipePrinted = true;
57365
58338
  return;
57366
58339
  }
57367
- const target = path23.resolve(ctx.skillPath);
57368
- if (!fs25.existsSync(target)) {
58340
+ const target = path24.resolve(ctx.skillPath);
58341
+ if (!fs27.existsSync(target)) {
57369
58342
  throw new Error(`--skill-path does not exist: ${target}`);
57370
58343
  }
57371
- const stat = fs25.statSync(target);
58344
+ const stat = fs27.statSync(target);
57372
58345
  if (!stat.isDirectory()) {
57373
58346
  throw new Error(`--skill-path is not a directory: ${target}`);
57374
58347
  }
@@ -57377,17 +58350,17 @@ function stepSymlinkSkill(opts = {}) {
57377
58350
  ctx.skillRecipePrinted = true;
57378
58351
  return;
57379
58352
  }
57380
- if (!opts.force && !fs25.existsSync(path23.join(target, "SKILL.md"))) {
58353
+ if (!opts.force && !fs27.existsSync(path24.join(target, "SKILL.md"))) {
57381
58354
  throw new Error(
57382
58355
  `${target} does not look like a skill (no SKILL.md at the root). Pass --force if you really mean to link this directory.`
57383
58356
  );
57384
58357
  }
57385
- if (fs25.existsSync(linkPath)) {
57386
- const st = fs25.lstatSync(linkPath);
58358
+ if (fs27.existsSync(linkPath)) {
58359
+ const st = fs27.lstatSync(linkPath);
57387
58360
  if (st.isSymbolicLink()) {
57388
58361
  let existingTarget = null;
57389
58362
  try {
57390
- existingTarget = path23.resolve(path23.dirname(linkPath), fs25.readlinkSync(linkPath));
58363
+ existingTarget = path24.resolve(path24.dirname(linkPath), fs27.readlinkSync(linkPath));
57391
58364
  } catch {
57392
58365
  existingTarget = null;
57393
58366
  }
@@ -57401,23 +58374,23 @@ function stepSymlinkSkill(opts = {}) {
57401
58374
  `${linkPath} already links to ${existingTarget ?? "(unreadable)"}; pass --force to replace it, or run \`switchbot uninstall\` first.`
57402
58375
  );
57403
58376
  }
57404
- fs25.unlinkSync(linkPath);
58377
+ fs27.unlinkSync(linkPath);
57405
58378
  } else {
57406
58379
  throw new Error(
57407
58380
  `${linkPath} exists and is not a symlink; refusing to clobber (move it aside and re-run)`
57408
58381
  );
57409
58382
  }
57410
58383
  }
57411
- fs25.mkdirSync(path23.dirname(linkPath), { recursive: true });
58384
+ fs27.mkdirSync(path24.dirname(linkPath), { recursive: true });
57412
58385
  const linkType = process.platform === "win32" ? "junction" : "dir";
57413
- fs25.symlinkSync(target, linkPath, linkType);
58386
+ fs27.symlinkSync(target, linkPath, linkType);
57414
58387
  ctx.skillLinkPath = linkPath;
57415
58388
  ctx.skillLinkCreated = true;
57416
58389
  },
57417
58390
  undo(ctx) {
57418
58391
  if (!ctx.skillLinkCreated || !ctx.skillLinkPath) return;
57419
58392
  try {
57420
- fs25.unlinkSync(ctx.skillLinkPath);
58393
+ fs27.unlinkSync(ctx.skillLinkPath);
57421
58394
  } catch {
57422
58395
  }
57423
58396
  }
@@ -57560,8 +58533,8 @@ Examples:
57560
58533
  const agent = parseAgent(opts.agent);
57561
58534
  const profile = getActiveProfile() ?? "default";
57562
58535
  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;
58536
+ const skillPath = opts.skillPath ? path25.resolve(opts.skillPath) : void 0;
58537
+ const tokenFile = opts.tokenFile ? path25.resolve(opts.tokenFile) : void 0;
57565
58538
  const force = Boolean(opts.force);
57566
58539
  const verify = Boolean(opts.verify);
57567
58540
  const globalOpts = command.parent?.opts() ?? {};
@@ -57605,7 +58578,7 @@ Examples:
57605
58578
  const report = await runInstall(steps, { context: ctx });
57606
58579
  if (report.ok && tokenFile) {
57607
58580
  try {
57608
- fs26.unlinkSync(tokenFile);
58581
+ fs28.unlinkSync(tokenFile);
57609
58582
  } catch {
57610
58583
  }
57611
58584
  }
@@ -57664,7 +58637,7 @@ Examples:
57664
58637
  init_cjs_shim();
57665
58638
  init_esm();
57666
58639
  init_load();
57667
- import fs27 from "node:fs";
58640
+ import fs29 from "node:fs";
57668
58641
  import readline6 from "node:readline";
57669
58642
  init_keychain();
57670
58643
  init_output();
@@ -57729,10 +58702,10 @@ Examples:
57729
58702
  action: "remove-skill-link",
57730
58703
  detail: skillLink,
57731
58704
  run: async () => {
57732
- if (!fs27.existsSync(skillLink)) {
58705
+ if (!fs29.existsSync(skillLink)) {
57733
58706
  return { action: "remove-skill-link", status: "absent", detail: skillLink };
57734
58707
  }
57735
- const stat = fs27.lstatSync(skillLink);
58708
+ const stat = fs29.lstatSync(skillLink);
57736
58709
  if (!stat.isSymbolicLink()) {
57737
58710
  return {
57738
58711
  action: "remove-skill-link",
@@ -57743,7 +58716,7 @@ Examples:
57743
58716
  const ok = yes ? true : await prompt(`Remove skill link ${skillLink}?`, true);
57744
58717
  if (!ok) return { action: "remove-skill-link", status: "skipped", detail: skillLink };
57745
58718
  try {
57746
- fs27.unlinkSync(skillLink);
58719
+ fs29.unlinkSync(skillLink);
57747
58720
  return { action: "remove-skill-link", status: "removed", detail: skillLink };
57748
58721
  } catch (err) {
57749
58722
  return {
@@ -57798,13 +58771,13 @@ Examples:
57798
58771
  detail: "pass --remove-policy to delete policy.yaml"
57799
58772
  };
57800
58773
  }
57801
- if (!fs27.existsSync(policyPath)) {
58774
+ if (!fs29.existsSync(policyPath)) {
57802
58775
  return { action: "remove-policy", status: "absent", detail: policyPath };
57803
58776
  }
57804
58777
  const ok = yes ? true : await prompt(`Delete policy file ${policyPath}?`, false);
57805
58778
  if (!ok) return { action: "remove-policy", status: "skipped", detail: policyPath };
57806
58779
  try {
57807
- fs27.unlinkSync(policyPath);
58780
+ fs29.unlinkSync(policyPath);
57808
58781
  return { action: "remove-policy", status: "removed", detail: policyPath };
57809
58782
  } catch (err) {
57810
58783
  return {
@@ -57867,9 +58840,9 @@ init_request_context();
57867
58840
  init_output();
57868
58841
  init_flags();
57869
58842
  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";
58843
+ import fs30 from "node:fs";
58844
+ import os24 from "node:os";
58845
+ import path26 from "node:path";
57873
58846
  var DEFAULT_OPENCLAW_URL = "http://localhost:18789";
57874
58847
  function resolveStatusSyncRuntime(options) {
57875
58848
  if (!tryLoadConfig()) {
@@ -58003,14 +58976,14 @@ async function probeStatusSyncStart(options = {}) {
58003
58976
  };
58004
58977
  }
58005
58978
  function resolveStatusSyncPaths(explicitStateDir) {
58006
- const stateDir = path25.resolve(
58007
- explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ?? path25.join(os23.homedir(), ".switchbot", "status-sync")
58979
+ const stateDir = path26.resolve(
58980
+ explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ?? path26.join(os24.homedir(), ".switchbot", "status-sync")
58008
58981
  );
58009
58982
  return {
58010
58983
  stateDir,
58011
- stateFile: path25.join(stateDir, "state.json"),
58012
- stdoutLog: path25.join(stateDir, "stdout.log"),
58013
- stderrLog: path25.join(stateDir, "stderr.log")
58984
+ stateFile: path26.join(stateDir, "state.json"),
58985
+ stdoutLog: path26.join(stateDir, "stdout.log"),
58986
+ stderrLog: path26.join(stateDir, "stderr.log")
58014
58987
  };
58015
58988
  }
58016
58989
  function buildStatusSyncChildArgs(options) {
@@ -58018,11 +58991,11 @@ function buildStatusSyncChildArgs(options) {
58018
58991
  if (!scriptPath) {
58019
58992
  throw new Error("Cannot determine the current CLI entrypoint path.");
58020
58993
  }
58021
- const args = [path25.resolve(scriptPath)];
58994
+ const args = [path26.resolve(scriptPath)];
58022
58995
  const configPath = getConfigPath();
58023
58996
  const profile = getActiveProfile();
58024
58997
  if (configPath) {
58025
- args.push("--config", path25.resolve(configPath));
58998
+ args.push("--config", path26.resolve(configPath));
58026
58999
  } else if (profile) {
58027
59000
  args.push("--profile", profile);
58028
59001
  }
@@ -58043,7 +59016,7 @@ function buildStatusSyncChildArgs(options) {
58043
59016
  }
58044
59017
  function safeUnlink(filePath) {
58045
59018
  try {
58046
- fs28.unlinkSync(filePath);
59019
+ fs30.unlinkSync(filePath);
58047
59020
  } catch {
58048
59021
  }
58049
59022
  }
@@ -58058,9 +59031,9 @@ function isProcessRunning(pid) {
58058
59031
  }
58059
59032
  }
58060
59033
  function readStateFile(paths) {
58061
- if (!fs28.existsSync(paths.stateFile)) return null;
59034
+ if (!fs30.existsSync(paths.stateFile)) return null;
58062
59035
  try {
58063
- const raw = JSON.parse(fs28.readFileSync(paths.stateFile, "utf-8"));
59036
+ const raw = JSON.parse(fs30.readFileSync(paths.stateFile, "utf-8"));
58064
59037
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
58065
59038
  safeUnlink(paths.stateFile);
58066
59039
  return null;
@@ -58179,14 +59152,14 @@ function startStatusSync(options = {}) {
58179
59152
  }
58180
59153
  stopStatusSync({ stateDir: paths.stateDir });
58181
59154
  }
58182
- fs28.mkdirSync(paths.stateDir, { recursive: true });
59155
+ fs30.mkdirSync(paths.stateDir, { recursive: true });
58183
59156
  const configPath = getConfigPath();
58184
59157
  const command = buildStatusSyncChildArgs(runtime);
58185
59158
  let stdoutFd = null;
58186
59159
  let stderrFd = null;
58187
59160
  try {
58188
- stdoutFd = fs28.openSync(paths.stdoutLog, "a");
58189
- stderrFd = fs28.openSync(paths.stderrLog, "a");
59161
+ stdoutFd = fs30.openSync(paths.stdoutLog, "a");
59162
+ stderrFd = fs30.openSync(paths.stderrLog, "a");
58190
59163
  const child = spawn4(process.execPath, command, {
58191
59164
  detached: true,
58192
59165
  stdio: ["ignore", stdoutFd, stderrFd],
@@ -58204,16 +59177,16 @@ function startStatusSync(options = {}) {
58204
59177
  openclawUrl: runtime.openclawUrl,
58205
59178
  openclawModel: runtime.openclawModel,
58206
59179
  topic: runtime.topic ?? null,
58207
- configPath: configPath ? path25.resolve(configPath) : null,
59180
+ configPath: configPath ? path26.resolve(configPath) : null,
58208
59181
  profile: configPath ? null : getActiveProfile() ?? null,
58209
59182
  stdoutLog: paths.stdoutLog,
58210
59183
  stderrLog: paths.stderrLog
58211
59184
  };
58212
- fs28.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
59185
+ fs30.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
58213
59186
  return toStatus(paths, state, true);
58214
59187
  } finally {
58215
- if (stdoutFd !== null) fs28.closeSync(stdoutFd);
58216
- if (stderrFd !== null) fs28.closeSync(stderrFd);
59188
+ if (stdoutFd !== null) fs30.closeSync(stdoutFd);
59189
+ if (stderrFd !== null) fs30.closeSync(stderrFd);
58217
59190
  }
58218
59191
  }
58219
59192
  async function runStatusSyncForeground(options = {}) {
@@ -58360,10 +59333,10 @@ init_cjs_shim();
58360
59333
  init_quota();
58361
59334
  init_audit();
58362
59335
  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");
59336
+ import fs31 from "node:fs";
59337
+ import os25 from "node:os";
59338
+ import path27 from "node:path";
59339
+ var DEFAULT_AUDIT_PATH3 = path27.join(os25.homedir(), ".switchbot", "audit.log");
58367
59340
  var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
58368
59341
  function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
58369
59342
  const now = /* @__PURE__ */ new Date();
@@ -58384,7 +59357,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
58384
59357
  status: pct >= 90 ? "critical" : pct >= 70 ? "warn" : "ok"
58385
59358
  };
58386
59359
  let auditHealth;
58387
- if (!fs29.existsSync(auditPath)) {
59360
+ if (!fs31.existsSync(auditPath)) {
58388
59361
  auditHealth = { present: false, recentErrors: 0, recentTotal: 0, errorRatePercent: 0, status: "ok" };
58389
59362
  } else {
58390
59363
  const entries = readAudit(auditPath);
@@ -58654,8 +59627,8 @@ function registerUpgradeCheckCommand(program3) {
58654
59627
  init_cjs_shim();
58655
59628
  init_output();
58656
59629
  import { spawn as spawn5 } from "node:child_process";
58657
- import fs30 from "node:fs";
58658
- import path27 from "node:path";
59630
+ import fs32 from "node:fs";
59631
+ import path28 from "node:path";
58659
59632
  import { fileURLToPath as fileURLToPath3 } from "node:url";
58660
59633
  init_arg_parsers();
58661
59634
  init_source();
@@ -58717,7 +59690,7 @@ function persistState(partial2) {
58717
59690
  }
58718
59691
  function readLastLines(filePath, n = 20) {
58719
59692
  try {
58720
- const content = fs30.readFileSync(filePath, "utf-8");
59693
+ const content = fs32.readFileSync(filePath, "utf-8");
58721
59694
  const lines = content.split("\n");
58722
59695
  return lines.slice(Math.max(0, lines.length - n)).join("\n").trim();
58723
59696
  } catch {
@@ -58807,10 +59780,10 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58807
59780
  }
58808
59781
  }
58809
59782
  const thisFile = fileURLToPath3(import.meta.url);
58810
- const cliEntry = path27.resolve(path27.dirname(thisFile), "..", "index.js");
59783
+ const cliEntry = path28.basename(thisFile) === "index.js" ? thisFile : path28.resolve(path28.dirname(thisFile), "..", "index.js");
58811
59784
  const args = ["rules", "run"];
58812
59785
  if (opts.policy) args.push(opts.policy);
58813
- fs30.mkdirSync(path27.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
59786
+ fs32.mkdirSync(path28.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
58814
59787
  persistState({
58815
59788
  status: "starting",
58816
59789
  pid: null,
@@ -58822,14 +59795,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58822
59795
  healthzPid: null,
58823
59796
  healthzPidFile: HEALTHZ_PID_FILE
58824
59797
  });
58825
- const logFd = fs30.openSync(DAEMON_LOG_FILE, "a");
59798
+ const logFd = fs32.openSync(DAEMON_LOG_FILE, "a");
58826
59799
  const child = spawn5(process.execPath, [cliEntry, ...args], {
58827
59800
  detached: true,
58828
59801
  stdio: ["ignore", logFd, logFd],
58829
59802
  env: { ...process.env }
58830
59803
  });
58831
59804
  child.unref();
58832
- fs30.closeSync(logFd);
59805
+ fs32.closeSync(logFd);
58833
59806
  await probeLiveness({
58834
59807
  child,
58835
59808
  delayMs: 300,
@@ -58852,14 +59825,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58852
59825
  let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
58853
59826
  if (healthzPort !== null) {
58854
59827
  const healthArgs = ["health", "serve", "--port", String(healthzPort)];
58855
- const healthLogFd = fs30.openSync(DAEMON_LOG_FILE, "a");
59828
+ const healthLogFd = fs32.openSync(DAEMON_LOG_FILE, "a");
58856
59829
  const healthChild = spawn5(process.execPath, [cliEntry, ...healthArgs], {
58857
59830
  detached: true,
58858
59831
  stdio: ["ignore", healthLogFd, healthLogFd],
58859
59832
  env: { ...process.env }
58860
59833
  });
58861
59834
  healthChild.unref();
58862
- fs30.closeSync(healthLogFd);
59835
+ fs32.closeSync(healthLogFd);
58863
59836
  if (healthChild.pid) {
58864
59837
  const healthAlive = await probeLiveness({ child: healthChild, delayMs: 200, fatal: false });
58865
59838
  if (healthAlive) {
@@ -58922,11 +59895,11 @@ The daemon reads the same policy file as \`switchbot rules run\`.
58922
59895
  });
58923
59896
  }
58924
59897
  try {
58925
- fs30.unlinkSync(DAEMON_PID_FILE);
59898
+ fs32.unlinkSync(DAEMON_PID_FILE);
58926
59899
  } catch {
58927
59900
  }
58928
59901
  try {
58929
- fs30.unlinkSync(HEALTHZ_PID_FILE);
59902
+ fs32.unlinkSync(HEALTHZ_PID_FILE);
58930
59903
  } catch {
58931
59904
  }
58932
59905
  persistState({
@@ -59164,17 +60137,32 @@ try {
59164
60137
  } catch (err) {
59165
60138
  if (err instanceof CommanderError) {
59166
60139
  if (err.code === "commander.helpDisplayed") {
60140
+ const helpRequested = process.argv.includes("--help") || process.argv.includes("-h") || process.argv.includes("help");
60141
+ if (helpRequested) {
60142
+ if (isJsonMode()) {
60143
+ const target = resolveTargetCommand(program2, process.argv.slice(2));
60144
+ printJson(commandToJson(target, { includeIdentity: target === program2 }));
60145
+ }
60146
+ process.exit(0);
60147
+ }
59167
60148
  if (isJsonMode()) {
59168
60149
  const target = resolveTargetCommand(program2, process.argv.slice(2));
59169
- printJson(commandToJson(target, { includeIdentity: target === program2 }));
60150
+ const subNames = target.commands.map((c) => c.name()).join(", ");
60151
+ const usefulMessage = subNames ? `${target.name()}: a subcommand is required. Available: ${subNames}` : err.message;
60152
+ emitJsonError({ code: 2, kind: "usage", message: usefulMessage });
59170
60153
  }
59171
- process.exit(0);
60154
+ process.exit(2);
59172
60155
  }
59173
60156
  if (err.code === "commander.version") {
59174
60157
  process.exit(0);
59175
60158
  }
59176
60159
  if (isJsonMode()) {
59177
- emitJsonError({ code: 2, kind: "usage", message: err.message });
60160
+ const errorMessage = err.code === "commander.help" ? (() => {
60161
+ const target = resolveTargetCommand(program2, process.argv.slice(2));
60162
+ const subNames = target.commands.map((c) => c.name()).join(", ");
60163
+ return subNames ? `${target.name()}: a subcommand is required. Available: ${subNames}` : err.message;
60164
+ })() : err.message;
60165
+ emitJsonError({ code: 2, kind: "usage", message: errorMessage });
59178
60166
  }
59179
60167
  process.exit(2);
59180
60168
  }