@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/README.md +127 -575
- package/dist/index.js +1361 -373
- package/dist/policy/schema/v0.2.json +59 -0
- package/package.json +1 -1
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
|
|
993
|
-
var
|
|
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 =
|
|
1926
|
-
if (
|
|
1927
|
-
if (sourceExt.includes(
|
|
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) =>
|
|
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 =
|
|
1941
|
+
resolvedScriptPath = fs33.realpathSync(this._scriptPath);
|
|
1942
1942
|
} catch (err) {
|
|
1943
1943
|
resolvedScriptPath = this._scriptPath;
|
|
1944
1944
|
}
|
|
1945
|
-
executableDir =
|
|
1946
|
-
|
|
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 =
|
|
1953
|
+
const legacyName = path29.basename(
|
|
1954
1954
|
this._scriptPath,
|
|
1955
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
2821
|
-
if (
|
|
2822
|
-
this._executableDir =
|
|
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
|
|
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 =
|
|
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.
|
|
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,
|
|
9432
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
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,
|
|
9435
|
-
return visit_(key, ctrl, visitor,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
9453
|
-
const ck = visit_("key", node.key, visitor,
|
|
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,
|
|
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,
|
|
9480
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
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,
|
|
9483
|
-
return visitAsync_(key, ctrl, visitor,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
9501
|
-
const ck = await visitAsync_("key", node.key, visitor,
|
|
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,
|
|
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,
|
|
9545
|
+
function callVisitor(key, node, visitor, path29) {
|
|
9534
9546
|
if (typeof visitor === "function")
|
|
9535
|
-
return visitor(key, node,
|
|
9547
|
+
return visitor(key, node, path29);
|
|
9536
9548
|
if (identity.isMap(node))
|
|
9537
|
-
return visitor.Map?.(key, node,
|
|
9549
|
+
return visitor.Map?.(key, node, path29);
|
|
9538
9550
|
if (identity.isSeq(node))
|
|
9539
|
-
return visitor.Seq?.(key, node,
|
|
9551
|
+
return visitor.Seq?.(key, node, path29);
|
|
9540
9552
|
if (identity.isPair(node))
|
|
9541
|
-
return visitor.Pair?.(key, node,
|
|
9553
|
+
return visitor.Pair?.(key, node, path29);
|
|
9542
9554
|
if (identity.isScalar(node))
|
|
9543
|
-
return visitor.Scalar?.(key, node,
|
|
9555
|
+
return visitor.Scalar?.(key, node, path29);
|
|
9544
9556
|
if (identity.isAlias(node))
|
|
9545
|
-
return visitor.Alias?.(key, node,
|
|
9557
|
+
return visitor.Alias?.(key, node, path29);
|
|
9546
9558
|
return void 0;
|
|
9547
9559
|
}
|
|
9548
|
-
function replaceNode(key,
|
|
9549
|
-
const parent =
|
|
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,
|
|
10178
|
+
function collectionFromPath(schema2, path29, value) {
|
|
10167
10179
|
let v2 = value;
|
|
10168
|
-
for (let i =
|
|
10169
|
-
const k2 =
|
|
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 = (
|
|
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(
|
|
10219
|
-
if (isEmptyPath(
|
|
10230
|
+
addIn(path29, value) {
|
|
10231
|
+
if (isEmptyPath(path29))
|
|
10220
10232
|
this.add(value);
|
|
10221
10233
|
else {
|
|
10222
|
-
const [key, ...rest] =
|
|
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(
|
|
10237
|
-
const [key, ...rest] =
|
|
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(
|
|
10252
|
-
const [key, ...rest] =
|
|
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(
|
|
10271
|
-
const [key, ...rest] =
|
|
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(
|
|
10282
|
-
const [key, ...rest] =
|
|
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(
|
|
12841
|
+
addIn(path29, value) {
|
|
12830
12842
|
if (assertCollection(this.contents))
|
|
12831
|
-
this.contents.addIn(
|
|
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(
|
|
12907
|
-
if (Collection.isEmptyPath(
|
|
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(
|
|
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(
|
|
12929
|
-
if (Collection.isEmptyPath(
|
|
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(
|
|
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(
|
|
12943
|
-
if (Collection.isEmptyPath(
|
|
12954
|
+
hasIn(path29) {
|
|
12955
|
+
if (Collection.isEmptyPath(path29))
|
|
12944
12956
|
return this.contents !== void 0;
|
|
12945
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
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(
|
|
12963
|
-
if (Collection.isEmptyPath(
|
|
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(
|
|
12978
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path29), value);
|
|
12967
12979
|
} else if (assertCollection(this.contents)) {
|
|
12968
|
-
this.contents.setIn(
|
|
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,
|
|
14957
|
+
visit.itemAtPath = (cst, path29) => {
|
|
14946
14958
|
let item = cst;
|
|
14947
|
-
for (const [field, index] of
|
|
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,
|
|
14957
|
-
const parent = visit.itemAtPath(cst,
|
|
14958
|
-
const field =
|
|
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(
|
|
14965
|
-
let ctrl = visitor(item,
|
|
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(
|
|
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,
|
|
14995
|
+
ctrl = ctrl(item, path29);
|
|
14984
14996
|
}
|
|
14985
14997
|
}
|
|
14986
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
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
|
|
16287
|
+
const fs33 = this.flowScalar(this.type);
|
|
16276
16288
|
if (atNextItem || it.value) {
|
|
16277
|
-
map3.items.push({ start, key:
|
|
16289
|
+
map3.items.push({ start, key: fs33, sep: [] });
|
|
16278
16290
|
this.onKeyLine = true;
|
|
16279
16291
|
} else if (it.sep) {
|
|
16280
|
-
this.stack.push(
|
|
16292
|
+
this.stack.push(fs33);
|
|
16281
16293
|
} else {
|
|
16282
|
-
Object.assign(it, { key:
|
|
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
|
|
16422
|
+
const fs33 = this.flowScalar(this.type);
|
|
16411
16423
|
if (!it || it.value)
|
|
16412
|
-
fc.items.push({ start: [], key:
|
|
16424
|
+
fc.items.push({ start: [], key: fs33, sep: [] });
|
|
16413
16425
|
else if (it.sep)
|
|
16414
|
-
this.stack.push(
|
|
16426
|
+
this.stack.push(fs33);
|
|
16415
16427
|
else
|
|
16416
|
-
Object.assign(it, { key:
|
|
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(
|
|
20008
|
-
let input =
|
|
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 [
|
|
20209
|
-
wsComponent.path =
|
|
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
|
|
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 =
|
|
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 `${
|
|
24331
|
+
return `${path29} does not match pattern ${err.params.pattern}`;
|
|
24309
24332
|
case "const":
|
|
24310
|
-
return `${
|
|
24333
|
+
return `${path29} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
|
|
24311
24334
|
case "enum":
|
|
24312
|
-
return `${
|
|
24335
|
+
return `${path29} must be one of ${JSON.stringify(err.params.allowedValues)}`;
|
|
24313
24336
|
case "type":
|
|
24314
|
-
return `${
|
|
24337
|
+
return `${path29} must be ${err.params.type}`;
|
|
24315
24338
|
case "not":
|
|
24316
|
-
return `${
|
|
24339
|
+
return `${path29} is not allowed here`;
|
|
24317
24340
|
default:
|
|
24318
|
-
return `${
|
|
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,
|
|
24375
|
-
return errors.some((err) => err.path ===
|
|
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,
|
|
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: `${
|
|
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], `${
|
|
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], `${
|
|
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, `${
|
|
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
|
|
24464
|
-
if (hasErrorAtPath(existingErrors,
|
|
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,
|
|
24489
|
+
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
|
|
24467
24490
|
out.push({
|
|
24468
|
-
path:
|
|
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
|
|
24603
|
-
if (hasErrorAtPath(errors,
|
|
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,
|
|
24628
|
+
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path29);
|
|
24606
24629
|
errors.push({
|
|
24607
|
-
path:
|
|
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
|
|
24672
|
-
const { line: line2, col: col2 } = locateInstancePath(loaded.doc, loaded.lineCounter,
|
|
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:
|
|
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 (
|
|
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" &&
|
|
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 === "
|
|
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
|
|
32614
|
-
$ switchbot devices expand <curtainId> setPosition
|
|
32615
|
-
$ switchbot devices expand <blindId> setPosition
|
|
32616
|
-
$ switchbot devices expand <relayId> setMode
|
|
32617
|
-
$ switchbot devices expand <
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
33548
|
+
printJson({ ...body, fetchedAt });
|
|
33106
33549
|
return;
|
|
33107
33550
|
}
|
|
33108
33551
|
if (fmt !== "table") {
|
|
33109
|
-
const statusWithTs = { ...body,
|
|
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,
|
|
35228
|
-
if (!
|
|
35670
|
+
function getElementAtPath(obj, path29) {
|
|
35671
|
+
if (!path29)
|
|
35229
35672
|
return obj;
|
|
35230
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
35843
|
-
for (const seg of
|
|
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
|
|
47899
|
-
if (
|
|
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 (
|
|
47904
|
-
const key =
|
|
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
|
|
50767
|
+
const path29 = `${at}[${i}]`;
|
|
50049
50768
|
if (i >= left.length) {
|
|
50050
|
-
out.push({ path:
|
|
50769
|
+
out.push({ path: path29, kind: "added", after: right[i] });
|
|
50051
50770
|
} else if (i >= right.length) {
|
|
50052
|
-
out.push({ path:
|
|
50771
|
+
out.push({ path: path29, kind: "removed", before: left[i] });
|
|
50053
50772
|
} else {
|
|
50054
|
-
collectPolicyDiff(left[i], right[i],
|
|
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
|
|
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:
|
|
50786
|
+
out.push({ path: path29, kind: "added", after: right[key] });
|
|
50068
50787
|
} else if (leftHas && !rightHas) {
|
|
50069
|
-
out.push({ path:
|
|
50788
|
+
out.push({ path: path29, kind: "removed", before: left[key] });
|
|
50070
50789
|
} else {
|
|
50071
|
-
collectPolicyDiff(left[key], right[key],
|
|
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
|
|
50125
|
-
import
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
51091
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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-
|
|
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 && !
|
|
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
|
|
52635
|
-
import
|
|
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 =
|
|
52640
|
-
const dir =
|
|
52641
|
-
if (!
|
|
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
|
-
|
|
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
|
|
53315
|
-
import
|
|
53316
|
-
import
|
|
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
|
|
53553
|
-
import
|
|
53554
|
-
import
|
|
54413
|
+
import fs20 from "node:fs";
|
|
54414
|
+
import os15 from "node:os";
|
|
54415
|
+
import path16 from "node:path";
|
|
53555
54416
|
function getStateDir() {
|
|
53556
|
-
return
|
|
54417
|
+
return path16.join(os15.homedir(), ".switchbot");
|
|
53557
54418
|
}
|
|
53558
54419
|
function getDaemonPidFile() {
|
|
53559
|
-
return
|
|
54420
|
+
return path16.join(getStateDir(), "daemon.pid");
|
|
53560
54421
|
}
|
|
53561
54422
|
function getDaemonLogFile() {
|
|
53562
|
-
return
|
|
54423
|
+
return path16.join(getStateDir(), "daemon.log");
|
|
53563
54424
|
}
|
|
53564
54425
|
function getDaemonStateFile() {
|
|
53565
|
-
return
|
|
54426
|
+
return path16.join(getStateDir(), "daemon.state.json");
|
|
53566
54427
|
}
|
|
53567
54428
|
function getHealthzPidFile() {
|
|
53568
|
-
return
|
|
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
|
-
|
|
54436
|
+
fs20.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
|
|
53576
54437
|
}
|
|
53577
54438
|
function writeDaemonState(state) {
|
|
53578
54439
|
ensureStateDir();
|
|
53579
|
-
|
|
54440
|
+
fs20.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
|
|
53580
54441
|
}
|
|
53581
54442
|
function readDaemonState() {
|
|
53582
54443
|
try {
|
|
53583
|
-
const raw =
|
|
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
|
|
53593
|
-
import
|
|
53594
|
-
import
|
|
53595
|
-
var DEFAULT_DIR =
|
|
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:
|
|
53600
|
-
reloadFile:
|
|
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 =
|
|
53605
|
-
|
|
53606
|
-
|
|
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 =
|
|
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)
|
|
54482
|
+
if (existing === pid) fs21.unlinkSync(pidFile);
|
|
53622
54483
|
} catch {
|
|
53623
54484
|
}
|
|
53624
54485
|
}
|
|
53625
54486
|
function writeReloadSentinel(reloadFile) {
|
|
53626
|
-
const dir =
|
|
53627
|
-
|
|
53628
|
-
|
|
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 (!
|
|
53634
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
53768
|
-
if (!
|
|
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 =
|
|
53876
|
-
if (!
|
|
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 =
|
|
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 =
|
|
53970
|
-
if (!
|
|
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 =
|
|
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 :
|
|
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(
|
|
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 (
|
|
55174
|
+
if (fs22.existsSync(file2)) {
|
|
54314
55175
|
try {
|
|
54315
|
-
const cfg = JSON.parse(
|
|
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 (
|
|
55203
|
+
if (fs22.existsSync(file2)) {
|
|
54343
55204
|
try {
|
|
54344
|
-
const cfg = JSON.parse(
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
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
|
|
54816
|
-
import
|
|
54817
|
-
var DEFAULT_AUDIT =
|
|
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
|
|
55676
|
-
import
|
|
55677
|
-
import
|
|
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
|
|
56559
|
+
var HIGH_FREQ_EVENTS2 = ["device.shadow", "*"];
|
|
55697
56560
|
function isHighFreqEvent(event) {
|
|
55698
|
-
return
|
|
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
|
|
55841
|
-
import
|
|
55842
|
-
import
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
55892
|
-
|
|
55893
|
-
|
|
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
|
-
|
|
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 =
|
|
56844
|
+
var DEFAULT_AUDIT_PATH2 = path21.join(os20.homedir(), ".switchbot", "audit.log");
|
|
55982
56845
|
function loadAutomation(policyPathFlag) {
|
|
55983
|
-
const
|
|
56846
|
+
const path29 = resolvePolicyPath({ flag: policyPathFlag });
|
|
55984
56847
|
let loaded;
|
|
55985
56848
|
try {
|
|
55986
|
-
loaded = loadPolicyFile(
|
|
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: ${
|
|
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 ${
|
|
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:
|
|
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:
|
|
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
|
-
|
|
56105
|
-
|
|
56106
|
-
|
|
56107
|
-
|
|
56108
|
-
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
56320
|
-
const 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 =
|
|
57188
|
+
const fd = fs24.openSync(file2, "r");
|
|
56327
57189
|
try {
|
|
56328
57190
|
const chunk = Buffer.alloc(size - offset);
|
|
56329
|
-
|
|
57191
|
+
fs24.readSync(fd, chunk, 0, chunk.length, offset);
|
|
56330
57192
|
offset = size;
|
|
56331
57193
|
buffer += chunk.toString("utf-8");
|
|
56332
57194
|
} finally {
|
|
56333
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
56736
|
-
import
|
|
56737
|
-
import
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
57795
|
+
fs25.unlinkSync(sourceFile);
|
|
56823
57796
|
return "deleted";
|
|
56824
57797
|
}
|
|
56825
|
-
|
|
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" ?
|
|
56957
|
-
if (!
|
|
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(
|
|
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
|
|
57030
|
-
import
|
|
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
|
|
57083
|
-
import
|
|
57084
|
-
import
|
|
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 =
|
|
57175
|
-
const switchbotDir =
|
|
58147
|
+
const home = os22.homedir();
|
|
58148
|
+
const switchbotDir = path23.join(home, ".switchbot");
|
|
57176
58149
|
try {
|
|
57177
|
-
const homeStat =
|
|
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 (
|
|
57187
|
-
const sbStat =
|
|
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
|
-
|
|
58169
|
+
fs26.accessSync(switchbotDir, fs26.constants.W_OK);
|
|
57197
58170
|
return { name: "home", status: "ok", message: `writable: ${switchbotDir}` };
|
|
57198
58171
|
}
|
|
57199
|
-
|
|
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 (
|
|
57214
|
-
const parent =
|
|
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 =
|
|
57223
|
-
const target =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
57269
|
-
import
|
|
57270
|
-
import
|
|
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
|
-
|
|
58318
|
+
fs27.unlinkSync(r.policyPath);
|
|
57346
58319
|
} catch {
|
|
57347
58320
|
}
|
|
57348
58321
|
}
|
|
57349
58322
|
};
|
|
57350
58323
|
}
|
|
57351
|
-
function skillLinkPathFor(agent, home =
|
|
58324
|
+
function skillLinkPathFor(agent, home = os23.homedir()) {
|
|
57352
58325
|
if (agent === "claude-code") {
|
|
57353
|
-
return
|
|
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 =
|
|
57368
|
-
if (!
|
|
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 =
|
|
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 && !
|
|
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 (
|
|
57386
|
-
const st =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
58384
|
+
fs27.mkdirSync(path24.dirname(linkPath), { recursive: true });
|
|
57412
58385
|
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
57413
|
-
|
|
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
|
-
|
|
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 ?
|
|
57564
|
-
const tokenFile = opts.tokenFile ?
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
58705
|
+
if (!fs29.existsSync(skillLink)) {
|
|
57733
58706
|
return { action: "remove-skill-link", status: "absent", detail: skillLink };
|
|
57734
58707
|
}
|
|
57735
|
-
const stat =
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
57871
|
-
import
|
|
57872
|
-
import
|
|
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 =
|
|
58007
|
-
explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ??
|
|
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:
|
|
58012
|
-
stdoutLog:
|
|
58013
|
-
stderrLog:
|
|
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 = [
|
|
58994
|
+
const args = [path26.resolve(scriptPath)];
|
|
58022
58995
|
const configPath = getConfigPath();
|
|
58023
58996
|
const profile = getActiveProfile();
|
|
58024
58997
|
if (configPath) {
|
|
58025
|
-
args.push("--config",
|
|
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
|
-
|
|
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 (!
|
|
59034
|
+
if (!fs30.existsSync(paths.stateFile)) return null;
|
|
58062
59035
|
try {
|
|
58063
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
58189
|
-
stderrFd =
|
|
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 ?
|
|
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
|
-
|
|
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)
|
|
58216
|
-
if (stderrFd !== null)
|
|
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
|
|
58364
|
-
import
|
|
58365
|
-
import
|
|
58366
|
-
var DEFAULT_AUDIT_PATH3 =
|
|
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 (!
|
|
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
|
|
58658
|
-
import
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
59898
|
+
fs32.unlinkSync(DAEMON_PID_FILE);
|
|
58926
59899
|
} catch {
|
|
58927
59900
|
}
|
|
58928
59901
|
try {
|
|
58929
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|