@switchbot/openapi-cli 3.6.1 → 3.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/index.js +1737 -514
- package/dist/policy/schema/v0.2.json +33 -1
- 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 path31 = __require("node:path");
|
|
993
|
+
var fs36 = __require("node:fs");
|
|
994
994
|
var process4 = __require("node:process");
|
|
995
995
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
996
996
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -1922,11 +1922,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1922
1922
|
let launchWithNode = false;
|
|
1923
1923
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
1924
1924
|
function findFile(baseDir, baseName) {
|
|
1925
|
-
const localBin =
|
|
1926
|
-
if (
|
|
1927
|
-
if (sourceExt.includes(
|
|
1925
|
+
const localBin = path31.resolve(baseDir, baseName);
|
|
1926
|
+
if (fs36.existsSync(localBin)) return localBin;
|
|
1927
|
+
if (sourceExt.includes(path31.extname(baseName))) return void 0;
|
|
1928
1928
|
const foundExt = sourceExt.find(
|
|
1929
|
-
(ext) =>
|
|
1929
|
+
(ext) => fs36.existsSync(`${localBin}${ext}`)
|
|
1930
1930
|
);
|
|
1931
1931
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
1932
1932
|
return void 0;
|
|
@@ -1938,21 +1938,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1938
1938
|
if (this._scriptPath) {
|
|
1939
1939
|
let resolvedScriptPath;
|
|
1940
1940
|
try {
|
|
1941
|
-
resolvedScriptPath =
|
|
1941
|
+
resolvedScriptPath = fs36.realpathSync(this._scriptPath);
|
|
1942
1942
|
} catch (err) {
|
|
1943
1943
|
resolvedScriptPath = this._scriptPath;
|
|
1944
1944
|
}
|
|
1945
|
-
executableDir =
|
|
1946
|
-
|
|
1945
|
+
executableDir = path31.resolve(
|
|
1946
|
+
path31.dirname(resolvedScriptPath),
|
|
1947
1947
|
executableDir
|
|
1948
1948
|
);
|
|
1949
1949
|
}
|
|
1950
1950
|
if (executableDir) {
|
|
1951
1951
|
let localFile = findFile(executableDir, executableFile);
|
|
1952
1952
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
1953
|
-
const legacyName =
|
|
1953
|
+
const legacyName = path31.basename(
|
|
1954
1954
|
this._scriptPath,
|
|
1955
|
-
|
|
1955
|
+
path31.extname(this._scriptPath)
|
|
1956
1956
|
);
|
|
1957
1957
|
if (legacyName !== this._name) {
|
|
1958
1958
|
localFile = findFile(
|
|
@@ -1963,7 +1963,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1963
1963
|
}
|
|
1964
1964
|
executableFile = localFile || executableFile;
|
|
1965
1965
|
}
|
|
1966
|
-
launchWithNode = sourceExt.includes(
|
|
1966
|
+
launchWithNode = sourceExt.includes(path31.extname(executableFile));
|
|
1967
1967
|
let proc;
|
|
1968
1968
|
if (process4.platform !== "win32") {
|
|
1969
1969
|
if (launchWithNode) {
|
|
@@ -2803,7 +2803,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2803
2803
|
* @return {Command}
|
|
2804
2804
|
*/
|
|
2805
2805
|
nameFromFilename(filename) {
|
|
2806
|
-
this._name =
|
|
2806
|
+
this._name = path31.basename(filename, path31.extname(filename));
|
|
2807
2807
|
return this;
|
|
2808
2808
|
}
|
|
2809
2809
|
/**
|
|
@@ -2817,9 +2817,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2817
2817
|
* @param {string} [path]
|
|
2818
2818
|
* @return {(string|null|Command)}
|
|
2819
2819
|
*/
|
|
2820
|
-
executableDir(
|
|
2821
|
-
if (
|
|
2822
|
-
this._executableDir =
|
|
2820
|
+
executableDir(path32) {
|
|
2821
|
+
if (path32 === void 0) return this._executableDir;
|
|
2822
|
+
this._executableDir = path32;
|
|
2823
2823
|
return this;
|
|
2824
2824
|
}
|
|
2825
2825
|
/**
|
|
@@ -4340,7 +4340,7 @@ var require_supports_colors = __commonJS({
|
|
|
4340
4340
|
"node_modules/@colors/colors/lib/system/supports-colors.js"(exports, module) {
|
|
4341
4341
|
"use strict";
|
|
4342
4342
|
init_cjs_shim();
|
|
4343
|
-
var
|
|
4343
|
+
var os27 = __require("os");
|
|
4344
4344
|
var hasFlag2 = require_has_flag();
|
|
4345
4345
|
var env2 = process.env;
|
|
4346
4346
|
var forceColor = void 0;
|
|
@@ -4378,7 +4378,7 @@ var require_supports_colors = __commonJS({
|
|
|
4378
4378
|
}
|
|
4379
4379
|
var min = forceColor ? 1 : 0;
|
|
4380
4380
|
if (process.platform === "win32") {
|
|
4381
|
-
var osRelease =
|
|
4381
|
+
var osRelease = os27.release().split(".");
|
|
4382
4382
|
if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
4383
4383
|
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
4384
4384
|
}
|
|
@@ -7853,9 +7853,9 @@ var init_catalog = __esm({
|
|
|
7853
7853
|
{
|
|
7854
7854
|
type: "Smart Lock",
|
|
7855
7855
|
category: "physical",
|
|
7856
|
-
description:
|
|
7856
|
+
description: 'Bluetooth/Wi-Fi electronic deadbolt that locks and unlocks a door via cloud API. Pro models support the Matter protocol when configured via the SwitchBot app (deviceType "Smart Lock Pro Wifi").',
|
|
7857
7857
|
role: "security",
|
|
7858
|
-
aliases: ["Lock", "Smart Lock Pro", "Lock Pro"],
|
|
7858
|
+
aliases: ["Lock", "Smart Lock Pro", "Lock Pro", "Smart Lock Pro Wifi"],
|
|
7859
7859
|
commands: [
|
|
7860
7860
|
{ command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
|
|
7861
7861
|
{ command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." },
|
|
@@ -7888,6 +7888,29 @@ var init_catalog = __esm({
|
|
|
7888
7888
|
],
|
|
7889
7889
|
statusFields: ["battery", "version", "lockState", "doorState", "calibrate"]
|
|
7890
7890
|
},
|
|
7891
|
+
{
|
|
7892
|
+
type: "Lock Vision",
|
|
7893
|
+
category: "physical",
|
|
7894
|
+
description: "Video smart lock with built-in camera; supports lock and unlock control plus status reporting.",
|
|
7895
|
+
role: "security",
|
|
7896
|
+
commands: [
|
|
7897
|
+
{ command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
|
|
7898
|
+
{ command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." }
|
|
7899
|
+
],
|
|
7900
|
+
statusFields: ["lockState", "doorState", "battery", "keyList", "version"]
|
|
7901
|
+
},
|
|
7902
|
+
{
|
|
7903
|
+
type: "Lock Vision Pro",
|
|
7904
|
+
category: "physical",
|
|
7905
|
+
description: "Pro-tier video smart lock with camera; supports lock, unlock, and deadbolt control.",
|
|
7906
|
+
role: "security",
|
|
7907
|
+
commands: [
|
|
7908
|
+
{ command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
|
|
7909
|
+
{ command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." },
|
|
7910
|
+
{ command: "deadbolt", parameter: "\u2014", description: "Engage deadbolt", idempotent: true }
|
|
7911
|
+
],
|
|
7912
|
+
statusFields: ["lockState", "doorState", "battery", "keyList", "version"]
|
|
7913
|
+
},
|
|
7891
7914
|
{
|
|
7892
7915
|
type: "Plug",
|
|
7893
7916
|
category: "physical",
|
|
@@ -8195,11 +8218,12 @@ var init_catalog = __esm({
|
|
|
8195
8218
|
{
|
|
8196
8219
|
type: "AI Art Frame",
|
|
8197
8220
|
category: "physical",
|
|
8198
|
-
description: "Digital art frame that can switch
|
|
8221
|
+
description: "Digital art frame that can switch images and accept new artwork uploads.",
|
|
8199
8222
|
role: "other",
|
|
8200
8223
|
commands: [
|
|
8201
8224
|
{ command: "next", parameter: "\u2014", description: "Switch to the next image", idempotent: false },
|
|
8202
|
-
{ command: "previous", parameter: "\u2014", description: "Switch to the previous image", idempotent: false }
|
|
8225
|
+
{ command: "previous", parameter: "\u2014", description: "Switch to the previous image", idempotent: false },
|
|
8226
|
+
{ command: "uploadImage", parameter: "<imageUrl>", description: "Upload a new image from an https:// URL to display on the frame", idempotent: true, exampleParams: ["https://example.com/art.jpg"] }
|
|
8203
8227
|
],
|
|
8204
8228
|
statusFields: ["version"]
|
|
8205
8229
|
},
|
|
@@ -8225,6 +8249,16 @@ var init_catalog = __esm({
|
|
|
8225
8249
|
commands: [],
|
|
8226
8250
|
statusFields: ["temperature", "humidity", "battery", "version"]
|
|
8227
8251
|
},
|
|
8252
|
+
{
|
|
8253
|
+
type: "WeatherStation",
|
|
8254
|
+
category: "physical",
|
|
8255
|
+
description: "Outdoor weather station reporting temperature, humidity, and atmospheric pressure; read-only.",
|
|
8256
|
+
role: "sensor",
|
|
8257
|
+
readOnly: true,
|
|
8258
|
+
aliases: ["Weather Station"],
|
|
8259
|
+
commands: [],
|
|
8260
|
+
statusFields: ["temperature", "humidity", "atmosphericPressure", "battery", "version"]
|
|
8261
|
+
},
|
|
8228
8262
|
{
|
|
8229
8263
|
type: "Motion Sensor",
|
|
8230
8264
|
category: "physical",
|
|
@@ -9521,6 +9555,161 @@ var init_credential = __esm({
|
|
|
9521
9555
|
}
|
|
9522
9556
|
});
|
|
9523
9557
|
|
|
9558
|
+
// src/devices/history-query.ts
|
|
9559
|
+
import fs10 from "node:fs";
|
|
9560
|
+
import path10 from "node:path";
|
|
9561
|
+
import os11 from "node:os";
|
|
9562
|
+
import readline2 from "node:readline";
|
|
9563
|
+
function historyDir2() {
|
|
9564
|
+
return path10.join(os11.homedir(), ".switchbot", "device-history");
|
|
9565
|
+
}
|
|
9566
|
+
function parseDurationToMs2(spec) {
|
|
9567
|
+
const m2 = spec.trim().match(/^(\d+)(ms|s|m|h|d)$/i);
|
|
9568
|
+
if (!m2) return null;
|
|
9569
|
+
const n = Number(m2[1]);
|
|
9570
|
+
const unit = m2[2].toLowerCase();
|
|
9571
|
+
const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
9572
|
+
return n * factor;
|
|
9573
|
+
}
|
|
9574
|
+
function parseInstantToMs(spec) {
|
|
9575
|
+
const ms = Date.parse(spec);
|
|
9576
|
+
return Number.isFinite(ms) ? ms : null;
|
|
9577
|
+
}
|
|
9578
|
+
function resolveRange(opts) {
|
|
9579
|
+
let fromMs = Number.NEGATIVE_INFINITY;
|
|
9580
|
+
let toMs = Number.POSITIVE_INFINITY;
|
|
9581
|
+
if (opts.since && (opts.from || opts.to)) {
|
|
9582
|
+
throw new Error("--since is mutually exclusive with --from/--to.");
|
|
9583
|
+
}
|
|
9584
|
+
if (opts.since) {
|
|
9585
|
+
const durMs = parseDurationToMs2(opts.since);
|
|
9586
|
+
if (durMs === null) {
|
|
9587
|
+
throw new Error(`Invalid --since value "${opts.since}". Expected e.g. "30s", "15m", "1h", "7d".`);
|
|
9588
|
+
}
|
|
9589
|
+
fromMs = Date.now() - durMs;
|
|
9590
|
+
} else {
|
|
9591
|
+
if (opts.from) {
|
|
9592
|
+
const parsed = parseInstantToMs(opts.from);
|
|
9593
|
+
if (parsed === null) throw new Error(`Invalid --from value "${opts.from}". Expected ISO-8601 timestamp.`);
|
|
9594
|
+
fromMs = parsed;
|
|
9595
|
+
}
|
|
9596
|
+
if (opts.to) {
|
|
9597
|
+
const parsed = parseInstantToMs(opts.to);
|
|
9598
|
+
if (parsed === null) throw new Error(`Invalid --to value "${opts.to}". Expected ISO-8601 timestamp.`);
|
|
9599
|
+
toMs = parsed;
|
|
9600
|
+
}
|
|
9601
|
+
if (fromMs > toMs) throw new Error("--from must be <= --to.");
|
|
9602
|
+
}
|
|
9603
|
+
return { fromMs, toMs };
|
|
9604
|
+
}
|
|
9605
|
+
function jsonlFilesForDevice(deviceId, baseDir = historyDir2()) {
|
|
9606
|
+
const out = [];
|
|
9607
|
+
if (!fs10.existsSync(baseDir)) return out;
|
|
9608
|
+
for (let i = 3; i >= 1; i--) {
|
|
9609
|
+
const p2 = path10.join(baseDir, `${deviceId}.jsonl.${i}`);
|
|
9610
|
+
if (fs10.existsSync(p2)) out.push(p2);
|
|
9611
|
+
}
|
|
9612
|
+
const current = path10.join(baseDir, `${deviceId}.jsonl`);
|
|
9613
|
+
if (fs10.existsSync(current)) out.push(current);
|
|
9614
|
+
return out;
|
|
9615
|
+
}
|
|
9616
|
+
function projectFields(record2, fields) {
|
|
9617
|
+
if (fields.length === 0) return record2;
|
|
9618
|
+
const projected = {};
|
|
9619
|
+
const payload = record2.payload ?? {};
|
|
9620
|
+
for (const f2 of fields) {
|
|
9621
|
+
if (f2 in payload) projected[f2] = payload[f2];
|
|
9622
|
+
}
|
|
9623
|
+
return { t: record2.t, topic: record2.topic, deviceType: record2.deviceType, payload: projected };
|
|
9624
|
+
}
|
|
9625
|
+
async function queryDeviceHistory(deviceId, opts = {}) {
|
|
9626
|
+
const { fromMs, toMs } = resolveRange(opts);
|
|
9627
|
+
const limit = Math.max(0, opts.limit ?? DEFAULT_LIMIT);
|
|
9628
|
+
const fields = opts.fields ?? [];
|
|
9629
|
+
const files = jsonlFilesForDevice(deviceId);
|
|
9630
|
+
const out = [];
|
|
9631
|
+
for (const file2 of files) {
|
|
9632
|
+
try {
|
|
9633
|
+
const stat = fs10.statSync(file2);
|
|
9634
|
+
if (stat.mtimeMs < fromMs) continue;
|
|
9635
|
+
} catch {
|
|
9636
|
+
continue;
|
|
9637
|
+
}
|
|
9638
|
+
const stream = fs10.createReadStream(file2, { encoding: "utf-8" });
|
|
9639
|
+
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
9640
|
+
for await (const line of rl) {
|
|
9641
|
+
if (!line) continue;
|
|
9642
|
+
let rec;
|
|
9643
|
+
try {
|
|
9644
|
+
rec = JSON.parse(line);
|
|
9645
|
+
} catch {
|
|
9646
|
+
continue;
|
|
9647
|
+
}
|
|
9648
|
+
const tMs = Date.parse(rec.t);
|
|
9649
|
+
if (!Number.isFinite(tMs)) continue;
|
|
9650
|
+
if (tMs < fromMs || tMs > toMs) continue;
|
|
9651
|
+
out.push(projectFields(rec, fields));
|
|
9652
|
+
if (out.length >= limit) {
|
|
9653
|
+
rl.close();
|
|
9654
|
+
stream.destroy();
|
|
9655
|
+
return out;
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
}
|
|
9659
|
+
return out;
|
|
9660
|
+
}
|
|
9661
|
+
function queryDeviceHistoryStats(deviceId) {
|
|
9662
|
+
const dir = historyDir2();
|
|
9663
|
+
const files = jsonlFilesForDevice(deviceId);
|
|
9664
|
+
let totalBytes = 0;
|
|
9665
|
+
let oldest = null;
|
|
9666
|
+
let newest = null;
|
|
9667
|
+
let count = 0;
|
|
9668
|
+
for (const file2 of files) {
|
|
9669
|
+
try {
|
|
9670
|
+
totalBytes += fs10.statSync(file2).size;
|
|
9671
|
+
} catch {
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
for (const file2 of files) {
|
|
9675
|
+
try {
|
|
9676
|
+
const lines = fs10.readFileSync(file2, "utf-8").split("\n");
|
|
9677
|
+
for (const line of lines) {
|
|
9678
|
+
if (!line) continue;
|
|
9679
|
+
count += 1;
|
|
9680
|
+
try {
|
|
9681
|
+
const rec = JSON.parse(line);
|
|
9682
|
+
const tMs = Date.parse(rec.t);
|
|
9683
|
+
if (Number.isFinite(tMs)) {
|
|
9684
|
+
if (oldest === null || tMs < oldest) oldest = tMs;
|
|
9685
|
+
if (newest === null || tMs > newest) newest = tMs;
|
|
9686
|
+
}
|
|
9687
|
+
} catch {
|
|
9688
|
+
}
|
|
9689
|
+
}
|
|
9690
|
+
} catch {
|
|
9691
|
+
}
|
|
9692
|
+
}
|
|
9693
|
+
return {
|
|
9694
|
+
deviceId,
|
|
9695
|
+
fileCount: files.length,
|
|
9696
|
+
totalBytes,
|
|
9697
|
+
recordCount: count,
|
|
9698
|
+
oldest: oldest !== null ? new Date(oldest).toISOString() : void 0,
|
|
9699
|
+
newest: newest !== null ? new Date(newest).toISOString() : void 0,
|
|
9700
|
+
jsonlFiles: files.map((f2) => path10.basename(f2)),
|
|
9701
|
+
historyDir: dir
|
|
9702
|
+
};
|
|
9703
|
+
}
|
|
9704
|
+
var DEFAULT_LIMIT;
|
|
9705
|
+
var init_history_query = __esm({
|
|
9706
|
+
"src/devices/history-query.ts"() {
|
|
9707
|
+
"use strict";
|
|
9708
|
+
init_cjs_shim();
|
|
9709
|
+
DEFAULT_LIMIT = 1e3;
|
|
9710
|
+
}
|
|
9711
|
+
});
|
|
9712
|
+
|
|
9524
9713
|
// node_modules/yaml/dist/nodes/identity.js
|
|
9525
9714
|
var require_identity = __commonJS({
|
|
9526
9715
|
"node_modules/yaml/dist/nodes/identity.js"(exports) {
|
|
@@ -9600,17 +9789,17 @@ var require_visit = __commonJS({
|
|
|
9600
9789
|
visit.BREAK = BREAK;
|
|
9601
9790
|
visit.SKIP = SKIP;
|
|
9602
9791
|
visit.REMOVE = REMOVE;
|
|
9603
|
-
function visit_(key, node, visitor,
|
|
9604
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
9792
|
+
function visit_(key, node, visitor, path31) {
|
|
9793
|
+
const ctrl = callVisitor(key, node, visitor, path31);
|
|
9605
9794
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
9606
|
-
replaceNode(key,
|
|
9607
|
-
return visit_(key, ctrl, visitor,
|
|
9795
|
+
replaceNode(key, path31, ctrl);
|
|
9796
|
+
return visit_(key, ctrl, visitor, path31);
|
|
9608
9797
|
}
|
|
9609
9798
|
if (typeof ctrl !== "symbol") {
|
|
9610
9799
|
if (identity.isCollection(node)) {
|
|
9611
|
-
|
|
9800
|
+
path31 = Object.freeze(path31.concat(node));
|
|
9612
9801
|
for (let i = 0; i < node.items.length; ++i) {
|
|
9613
|
-
const ci = visit_(i, node.items[i], visitor,
|
|
9802
|
+
const ci = visit_(i, node.items[i], visitor, path31);
|
|
9614
9803
|
if (typeof ci === "number")
|
|
9615
9804
|
i = ci - 1;
|
|
9616
9805
|
else if (ci === BREAK)
|
|
@@ -9621,13 +9810,13 @@ var require_visit = __commonJS({
|
|
|
9621
9810
|
}
|
|
9622
9811
|
}
|
|
9623
9812
|
} else if (identity.isPair(node)) {
|
|
9624
|
-
|
|
9625
|
-
const ck = visit_("key", node.key, visitor,
|
|
9813
|
+
path31 = Object.freeze(path31.concat(node));
|
|
9814
|
+
const ck = visit_("key", node.key, visitor, path31);
|
|
9626
9815
|
if (ck === BREAK)
|
|
9627
9816
|
return BREAK;
|
|
9628
9817
|
else if (ck === REMOVE)
|
|
9629
9818
|
node.key = null;
|
|
9630
|
-
const cv = visit_("value", node.value, visitor,
|
|
9819
|
+
const cv = visit_("value", node.value, visitor, path31);
|
|
9631
9820
|
if (cv === BREAK)
|
|
9632
9821
|
return BREAK;
|
|
9633
9822
|
else if (cv === REMOVE)
|
|
@@ -9648,17 +9837,17 @@ var require_visit = __commonJS({
|
|
|
9648
9837
|
visitAsync.BREAK = BREAK;
|
|
9649
9838
|
visitAsync.SKIP = SKIP;
|
|
9650
9839
|
visitAsync.REMOVE = REMOVE;
|
|
9651
|
-
async function visitAsync_(key, node, visitor,
|
|
9652
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
9840
|
+
async function visitAsync_(key, node, visitor, path31) {
|
|
9841
|
+
const ctrl = await callVisitor(key, node, visitor, path31);
|
|
9653
9842
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
9654
|
-
replaceNode(key,
|
|
9655
|
-
return visitAsync_(key, ctrl, visitor,
|
|
9843
|
+
replaceNode(key, path31, ctrl);
|
|
9844
|
+
return visitAsync_(key, ctrl, visitor, path31);
|
|
9656
9845
|
}
|
|
9657
9846
|
if (typeof ctrl !== "symbol") {
|
|
9658
9847
|
if (identity.isCollection(node)) {
|
|
9659
|
-
|
|
9848
|
+
path31 = Object.freeze(path31.concat(node));
|
|
9660
9849
|
for (let i = 0; i < node.items.length; ++i) {
|
|
9661
|
-
const ci = await visitAsync_(i, node.items[i], visitor,
|
|
9850
|
+
const ci = await visitAsync_(i, node.items[i], visitor, path31);
|
|
9662
9851
|
if (typeof ci === "number")
|
|
9663
9852
|
i = ci - 1;
|
|
9664
9853
|
else if (ci === BREAK)
|
|
@@ -9669,13 +9858,13 @@ var require_visit = __commonJS({
|
|
|
9669
9858
|
}
|
|
9670
9859
|
}
|
|
9671
9860
|
} else if (identity.isPair(node)) {
|
|
9672
|
-
|
|
9673
|
-
const ck = await visitAsync_("key", node.key, visitor,
|
|
9861
|
+
path31 = Object.freeze(path31.concat(node));
|
|
9862
|
+
const ck = await visitAsync_("key", node.key, visitor, path31);
|
|
9674
9863
|
if (ck === BREAK)
|
|
9675
9864
|
return BREAK;
|
|
9676
9865
|
else if (ck === REMOVE)
|
|
9677
9866
|
node.key = null;
|
|
9678
|
-
const cv = await visitAsync_("value", node.value, visitor,
|
|
9867
|
+
const cv = await visitAsync_("value", node.value, visitor, path31);
|
|
9679
9868
|
if (cv === BREAK)
|
|
9680
9869
|
return BREAK;
|
|
9681
9870
|
else if (cv === REMOVE)
|
|
@@ -9702,23 +9891,23 @@ var require_visit = __commonJS({
|
|
|
9702
9891
|
}
|
|
9703
9892
|
return visitor;
|
|
9704
9893
|
}
|
|
9705
|
-
function callVisitor(key, node, visitor,
|
|
9894
|
+
function callVisitor(key, node, visitor, path31) {
|
|
9706
9895
|
if (typeof visitor === "function")
|
|
9707
|
-
return visitor(key, node,
|
|
9896
|
+
return visitor(key, node, path31);
|
|
9708
9897
|
if (identity.isMap(node))
|
|
9709
|
-
return visitor.Map?.(key, node,
|
|
9898
|
+
return visitor.Map?.(key, node, path31);
|
|
9710
9899
|
if (identity.isSeq(node))
|
|
9711
|
-
return visitor.Seq?.(key, node,
|
|
9900
|
+
return visitor.Seq?.(key, node, path31);
|
|
9712
9901
|
if (identity.isPair(node))
|
|
9713
|
-
return visitor.Pair?.(key, node,
|
|
9902
|
+
return visitor.Pair?.(key, node, path31);
|
|
9714
9903
|
if (identity.isScalar(node))
|
|
9715
|
-
return visitor.Scalar?.(key, node,
|
|
9904
|
+
return visitor.Scalar?.(key, node, path31);
|
|
9716
9905
|
if (identity.isAlias(node))
|
|
9717
|
-
return visitor.Alias?.(key, node,
|
|
9906
|
+
return visitor.Alias?.(key, node, path31);
|
|
9718
9907
|
return void 0;
|
|
9719
9908
|
}
|
|
9720
|
-
function replaceNode(key,
|
|
9721
|
-
const parent =
|
|
9909
|
+
function replaceNode(key, path31, node) {
|
|
9910
|
+
const parent = path31[path31.length - 1];
|
|
9722
9911
|
if (identity.isCollection(parent)) {
|
|
9723
9912
|
parent.items[key] = node;
|
|
9724
9913
|
} else if (identity.isPair(parent)) {
|
|
@@ -10335,10 +10524,10 @@ var require_Collection = __commonJS({
|
|
|
10335
10524
|
var createNode = require_createNode();
|
|
10336
10525
|
var identity = require_identity();
|
|
10337
10526
|
var Node = require_Node();
|
|
10338
|
-
function collectionFromPath(schema2,
|
|
10527
|
+
function collectionFromPath(schema2, path31, value) {
|
|
10339
10528
|
let v2 = value;
|
|
10340
|
-
for (let i =
|
|
10341
|
-
const k2 =
|
|
10529
|
+
for (let i = path31.length - 1; i >= 0; --i) {
|
|
10530
|
+
const k2 = path31[i];
|
|
10342
10531
|
if (typeof k2 === "number" && Number.isInteger(k2) && k2 >= 0) {
|
|
10343
10532
|
const a = [];
|
|
10344
10533
|
a[k2] = v2;
|
|
@@ -10357,7 +10546,7 @@ var require_Collection = __commonJS({
|
|
|
10357
10546
|
sourceObjects: /* @__PURE__ */ new Map()
|
|
10358
10547
|
});
|
|
10359
10548
|
}
|
|
10360
|
-
var isEmptyPath = (
|
|
10549
|
+
var isEmptyPath = (path31) => path31 == null || typeof path31 === "object" && !!path31[Symbol.iterator]().next().done;
|
|
10361
10550
|
var Collection = class extends Node.NodeBase {
|
|
10362
10551
|
constructor(type2, schema2) {
|
|
10363
10552
|
super(type2);
|
|
@@ -10387,11 +10576,11 @@ var require_Collection = __commonJS({
|
|
|
10387
10576
|
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
|
10388
10577
|
* that already exists in the map.
|
|
10389
10578
|
*/
|
|
10390
|
-
addIn(
|
|
10391
|
-
if (isEmptyPath(
|
|
10579
|
+
addIn(path31, value) {
|
|
10580
|
+
if (isEmptyPath(path31))
|
|
10392
10581
|
this.add(value);
|
|
10393
10582
|
else {
|
|
10394
|
-
const [key, ...rest] =
|
|
10583
|
+
const [key, ...rest] = path31;
|
|
10395
10584
|
const node = this.get(key, true);
|
|
10396
10585
|
if (identity.isCollection(node))
|
|
10397
10586
|
node.addIn(rest, value);
|
|
@@ -10405,8 +10594,8 @@ var require_Collection = __commonJS({
|
|
|
10405
10594
|
* Removes a value from the collection.
|
|
10406
10595
|
* @returns `true` if the item was found and removed.
|
|
10407
10596
|
*/
|
|
10408
|
-
deleteIn(
|
|
10409
|
-
const [key, ...rest] =
|
|
10597
|
+
deleteIn(path31) {
|
|
10598
|
+
const [key, ...rest] = path31;
|
|
10410
10599
|
if (rest.length === 0)
|
|
10411
10600
|
return this.delete(key);
|
|
10412
10601
|
const node = this.get(key, true);
|
|
@@ -10420,8 +10609,8 @@ var require_Collection = __commonJS({
|
|
|
10420
10609
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
10421
10610
|
* `true` (collections are always returned intact).
|
|
10422
10611
|
*/
|
|
10423
|
-
getIn(
|
|
10424
|
-
const [key, ...rest] =
|
|
10612
|
+
getIn(path31, keepScalar) {
|
|
10613
|
+
const [key, ...rest] = path31;
|
|
10425
10614
|
const node = this.get(key, true);
|
|
10426
10615
|
if (rest.length === 0)
|
|
10427
10616
|
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
|
@@ -10439,8 +10628,8 @@ var require_Collection = __commonJS({
|
|
|
10439
10628
|
/**
|
|
10440
10629
|
* Checks if the collection includes a value with the key `key`.
|
|
10441
10630
|
*/
|
|
10442
|
-
hasIn(
|
|
10443
|
-
const [key, ...rest] =
|
|
10631
|
+
hasIn(path31) {
|
|
10632
|
+
const [key, ...rest] = path31;
|
|
10444
10633
|
if (rest.length === 0)
|
|
10445
10634
|
return this.has(key);
|
|
10446
10635
|
const node = this.get(key, true);
|
|
@@ -10450,8 +10639,8 @@ var require_Collection = __commonJS({
|
|
|
10450
10639
|
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
|
10451
10640
|
* boolean to add/remove the item from the set.
|
|
10452
10641
|
*/
|
|
10453
|
-
setIn(
|
|
10454
|
-
const [key, ...rest] =
|
|
10642
|
+
setIn(path31, value) {
|
|
10643
|
+
const [key, ...rest] = path31;
|
|
10455
10644
|
if (rest.length === 0) {
|
|
10456
10645
|
this.set(key, value);
|
|
10457
10646
|
} else {
|
|
@@ -12998,9 +13187,9 @@ var require_Document = __commonJS({
|
|
|
12998
13187
|
this.contents.add(value);
|
|
12999
13188
|
}
|
|
13000
13189
|
/** Adds a value to the document. */
|
|
13001
|
-
addIn(
|
|
13190
|
+
addIn(path31, value) {
|
|
13002
13191
|
if (assertCollection(this.contents))
|
|
13003
|
-
this.contents.addIn(
|
|
13192
|
+
this.contents.addIn(path31, value);
|
|
13004
13193
|
}
|
|
13005
13194
|
/**
|
|
13006
13195
|
* Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
|
@@ -13075,14 +13264,14 @@ var require_Document = __commonJS({
|
|
|
13075
13264
|
* Removes a value from the document.
|
|
13076
13265
|
* @returns `true` if the item was found and removed.
|
|
13077
13266
|
*/
|
|
13078
|
-
deleteIn(
|
|
13079
|
-
if (Collection.isEmptyPath(
|
|
13267
|
+
deleteIn(path31) {
|
|
13268
|
+
if (Collection.isEmptyPath(path31)) {
|
|
13080
13269
|
if (this.contents == null)
|
|
13081
13270
|
return false;
|
|
13082
13271
|
this.contents = null;
|
|
13083
13272
|
return true;
|
|
13084
13273
|
}
|
|
13085
|
-
return assertCollection(this.contents) ? this.contents.deleteIn(
|
|
13274
|
+
return assertCollection(this.contents) ? this.contents.deleteIn(path31) : false;
|
|
13086
13275
|
}
|
|
13087
13276
|
/**
|
|
13088
13277
|
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
|
@@ -13097,10 +13286,10 @@ var require_Document = __commonJS({
|
|
|
13097
13286
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
13098
13287
|
* `true` (collections are always returned intact).
|
|
13099
13288
|
*/
|
|
13100
|
-
getIn(
|
|
13101
|
-
if (Collection.isEmptyPath(
|
|
13289
|
+
getIn(path31, keepScalar) {
|
|
13290
|
+
if (Collection.isEmptyPath(path31))
|
|
13102
13291
|
return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
|
|
13103
|
-
return identity.isCollection(this.contents) ? this.contents.getIn(
|
|
13292
|
+
return identity.isCollection(this.contents) ? this.contents.getIn(path31, keepScalar) : void 0;
|
|
13104
13293
|
}
|
|
13105
13294
|
/**
|
|
13106
13295
|
* Checks if the document includes a value with the key `key`.
|
|
@@ -13111,10 +13300,10 @@ var require_Document = __commonJS({
|
|
|
13111
13300
|
/**
|
|
13112
13301
|
* Checks if the document includes a value at `path`.
|
|
13113
13302
|
*/
|
|
13114
|
-
hasIn(
|
|
13115
|
-
if (Collection.isEmptyPath(
|
|
13303
|
+
hasIn(path31) {
|
|
13304
|
+
if (Collection.isEmptyPath(path31))
|
|
13116
13305
|
return this.contents !== void 0;
|
|
13117
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
13306
|
+
return identity.isCollection(this.contents) ? this.contents.hasIn(path31) : false;
|
|
13118
13307
|
}
|
|
13119
13308
|
/**
|
|
13120
13309
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
@@ -13131,13 +13320,13 @@ var require_Document = __commonJS({
|
|
|
13131
13320
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
13132
13321
|
* boolean to add/remove the item from the set.
|
|
13133
13322
|
*/
|
|
13134
|
-
setIn(
|
|
13135
|
-
if (Collection.isEmptyPath(
|
|
13323
|
+
setIn(path31, value) {
|
|
13324
|
+
if (Collection.isEmptyPath(path31)) {
|
|
13136
13325
|
this.contents = value;
|
|
13137
13326
|
} else if (this.contents == null) {
|
|
13138
|
-
this.contents = Collection.collectionFromPath(this.schema, Array.from(
|
|
13327
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path31), value);
|
|
13139
13328
|
} else if (assertCollection(this.contents)) {
|
|
13140
|
-
this.contents.setIn(
|
|
13329
|
+
this.contents.setIn(path31, value);
|
|
13141
13330
|
}
|
|
13142
13331
|
}
|
|
13143
13332
|
/**
|
|
@@ -15114,9 +15303,9 @@ var require_cst_visit = __commonJS({
|
|
|
15114
15303
|
visit.BREAK = BREAK;
|
|
15115
15304
|
visit.SKIP = SKIP;
|
|
15116
15305
|
visit.REMOVE = REMOVE;
|
|
15117
|
-
visit.itemAtPath = (cst,
|
|
15306
|
+
visit.itemAtPath = (cst, path31) => {
|
|
15118
15307
|
let item = cst;
|
|
15119
|
-
for (const [field, index] of
|
|
15308
|
+
for (const [field, index] of path31) {
|
|
15120
15309
|
const tok = item?.[field];
|
|
15121
15310
|
if (tok && "items" in tok) {
|
|
15122
15311
|
item = tok.items[index];
|
|
@@ -15125,23 +15314,23 @@ var require_cst_visit = __commonJS({
|
|
|
15125
15314
|
}
|
|
15126
15315
|
return item;
|
|
15127
15316
|
};
|
|
15128
|
-
visit.parentCollection = (cst,
|
|
15129
|
-
const parent = visit.itemAtPath(cst,
|
|
15130
|
-
const field =
|
|
15317
|
+
visit.parentCollection = (cst, path31) => {
|
|
15318
|
+
const parent = visit.itemAtPath(cst, path31.slice(0, -1));
|
|
15319
|
+
const field = path31[path31.length - 1][0];
|
|
15131
15320
|
const coll = parent?.[field];
|
|
15132
15321
|
if (coll && "items" in coll)
|
|
15133
15322
|
return coll;
|
|
15134
15323
|
throw new Error("Parent collection not found");
|
|
15135
15324
|
};
|
|
15136
|
-
function _visit(
|
|
15137
|
-
let ctrl = visitor(item,
|
|
15325
|
+
function _visit(path31, item, visitor) {
|
|
15326
|
+
let ctrl = visitor(item, path31);
|
|
15138
15327
|
if (typeof ctrl === "symbol")
|
|
15139
15328
|
return ctrl;
|
|
15140
15329
|
for (const field of ["key", "value"]) {
|
|
15141
15330
|
const token = item[field];
|
|
15142
15331
|
if (token && "items" in token) {
|
|
15143
15332
|
for (let i = 0; i < token.items.length; ++i) {
|
|
15144
|
-
const ci = _visit(Object.freeze(
|
|
15333
|
+
const ci = _visit(Object.freeze(path31.concat([[field, i]])), token.items[i], visitor);
|
|
15145
15334
|
if (typeof ci === "number")
|
|
15146
15335
|
i = ci - 1;
|
|
15147
15336
|
else if (ci === BREAK)
|
|
@@ -15152,10 +15341,10 @@ var require_cst_visit = __commonJS({
|
|
|
15152
15341
|
}
|
|
15153
15342
|
}
|
|
15154
15343
|
if (typeof ctrl === "function" && field === "key")
|
|
15155
|
-
ctrl = ctrl(item,
|
|
15344
|
+
ctrl = ctrl(item, path31);
|
|
15156
15345
|
}
|
|
15157
15346
|
}
|
|
15158
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
15347
|
+
return typeof ctrl === "function" ? ctrl(item, path31) : ctrl;
|
|
15159
15348
|
}
|
|
15160
15349
|
exports.visit = visit;
|
|
15161
15350
|
}
|
|
@@ -16444,14 +16633,14 @@ var require_parser = __commonJS({
|
|
|
16444
16633
|
case "scalar":
|
|
16445
16634
|
case "single-quoted-scalar":
|
|
16446
16635
|
case "double-quoted-scalar": {
|
|
16447
|
-
const
|
|
16636
|
+
const fs36 = this.flowScalar(this.type);
|
|
16448
16637
|
if (atNextItem || it.value) {
|
|
16449
|
-
map3.items.push({ start, key:
|
|
16638
|
+
map3.items.push({ start, key: fs36, sep: [] });
|
|
16450
16639
|
this.onKeyLine = true;
|
|
16451
16640
|
} else if (it.sep) {
|
|
16452
|
-
this.stack.push(
|
|
16641
|
+
this.stack.push(fs36);
|
|
16453
16642
|
} else {
|
|
16454
|
-
Object.assign(it, { key:
|
|
16643
|
+
Object.assign(it, { key: fs36, sep: [] });
|
|
16455
16644
|
this.onKeyLine = true;
|
|
16456
16645
|
}
|
|
16457
16646
|
return;
|
|
@@ -16579,13 +16768,13 @@ var require_parser = __commonJS({
|
|
|
16579
16768
|
case "scalar":
|
|
16580
16769
|
case "single-quoted-scalar":
|
|
16581
16770
|
case "double-quoted-scalar": {
|
|
16582
|
-
const
|
|
16771
|
+
const fs36 = this.flowScalar(this.type);
|
|
16583
16772
|
if (!it || it.value)
|
|
16584
|
-
fc.items.push({ start: [], key:
|
|
16773
|
+
fc.items.push({ start: [], key: fs36, sep: [] });
|
|
16585
16774
|
else if (it.sep)
|
|
16586
|
-
this.stack.push(
|
|
16775
|
+
this.stack.push(fs36);
|
|
16587
16776
|
else
|
|
16588
|
-
Object.assign(it, { key:
|
|
16777
|
+
Object.assign(it, { key: fs36, sep: [] });
|
|
16589
16778
|
return;
|
|
16590
16779
|
}
|
|
16591
16780
|
case "flow-map-end":
|
|
@@ -20184,8 +20373,8 @@ var require_utils2 = __commonJS({
|
|
|
20184
20373
|
}
|
|
20185
20374
|
return ind;
|
|
20186
20375
|
}
|
|
20187
|
-
function removeDotSegments(
|
|
20188
|
-
let input =
|
|
20376
|
+
function removeDotSegments(path31) {
|
|
20377
|
+
let input = path31;
|
|
20189
20378
|
const output = [];
|
|
20190
20379
|
let nextSlash = -1;
|
|
20191
20380
|
let len = 0;
|
|
@@ -20385,8 +20574,8 @@ var require_schemes = __commonJS({
|
|
|
20385
20574
|
wsComponent.secure = void 0;
|
|
20386
20575
|
}
|
|
20387
20576
|
if (wsComponent.resourceName) {
|
|
20388
|
-
const [
|
|
20389
|
-
wsComponent.path =
|
|
20577
|
+
const [path31, query] = wsComponent.resourceName.split("?");
|
|
20578
|
+
wsComponent.path = path31 && path31 !== "/" ? path31 : void 0;
|
|
20390
20579
|
wsComponent.query = query;
|
|
20391
20580
|
wsComponent.resourceName = void 0;
|
|
20392
20581
|
}
|
|
@@ -20445,7 +20634,7 @@ var require_schemes = __commonJS({
|
|
|
20445
20634
|
urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
|
|
20446
20635
|
return urnComponent;
|
|
20447
20636
|
}
|
|
20448
|
-
var
|
|
20637
|
+
var http8 = (
|
|
20449
20638
|
/** @type {SchemeHandler} */
|
|
20450
20639
|
{
|
|
20451
20640
|
scheme: "http",
|
|
@@ -20454,11 +20643,11 @@ var require_schemes = __commonJS({
|
|
|
20454
20643
|
serialize: httpSerialize
|
|
20455
20644
|
}
|
|
20456
20645
|
);
|
|
20457
|
-
var
|
|
20646
|
+
var https7 = (
|
|
20458
20647
|
/** @type {SchemeHandler} */
|
|
20459
20648
|
{
|
|
20460
20649
|
scheme: "https",
|
|
20461
|
-
domainHost:
|
|
20650
|
+
domainHost: http8.domainHost,
|
|
20462
20651
|
parse: httpParse,
|
|
20463
20652
|
serialize: httpSerialize
|
|
20464
20653
|
}
|
|
@@ -20502,8 +20691,8 @@ var require_schemes = __commonJS({
|
|
|
20502
20691
|
var SCHEMES = (
|
|
20503
20692
|
/** @type {Record<SchemeName, SchemeHandler>} */
|
|
20504
20693
|
{
|
|
20505
|
-
http:
|
|
20506
|
-
https:
|
|
20694
|
+
http: http8,
|
|
20695
|
+
https: https7,
|
|
20507
20696
|
ws,
|
|
20508
20697
|
wss,
|
|
20509
20698
|
urn,
|
|
@@ -24193,6 +24382,10 @@ function isNotCondition(c) {
|
|
|
24193
24382
|
function isLlmCondition(c) {
|
|
24194
24383
|
return c.llm !== void 0 && typeof c.llm === "object";
|
|
24195
24384
|
}
|
|
24385
|
+
function isEventCountCondition(c) {
|
|
24386
|
+
const ec = c.event_count;
|
|
24387
|
+
return ec !== void 0 && typeof ec === "object" && typeof ec.device === "string";
|
|
24388
|
+
}
|
|
24196
24389
|
var init_types = __esm({
|
|
24197
24390
|
"src/rules/types.ts"() {
|
|
24198
24391
|
"use strict";
|
|
@@ -24476,7 +24669,7 @@ function locateError(doc, lineCounter, err) {
|
|
|
24476
24669
|
return { line: pos.line, col: pos.col };
|
|
24477
24670
|
}
|
|
24478
24671
|
function humanMessage(err) {
|
|
24479
|
-
const
|
|
24672
|
+
const path31 = err.instancePath || "(root)";
|
|
24480
24673
|
switch (err.keyword) {
|
|
24481
24674
|
case "required":
|
|
24482
24675
|
return `missing required property "${err.params.missingProperty}"`;
|
|
@@ -24484,21 +24677,21 @@ function humanMessage(err) {
|
|
|
24484
24677
|
return `unknown property "${err.params.additionalProperty}"`;
|
|
24485
24678
|
case "dependentRequired": {
|
|
24486
24679
|
const { property, missingProperty } = err.params;
|
|
24487
|
-
const parent =
|
|
24680
|
+
const parent = path31 === "(root)" ? "" : `${path31}: `;
|
|
24488
24681
|
return `${parent}when "${property}" is set, "${missingProperty}" is also required`;
|
|
24489
24682
|
}
|
|
24490
24683
|
case "pattern":
|
|
24491
|
-
return `${
|
|
24684
|
+
return `${path31} does not match pattern ${err.params.pattern}`;
|
|
24492
24685
|
case "const":
|
|
24493
|
-
return `${
|
|
24686
|
+
return `${path31} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
|
|
24494
24687
|
case "enum":
|
|
24495
|
-
return `${
|
|
24688
|
+
return `${path31} must be one of ${JSON.stringify(err.params.allowedValues)}`;
|
|
24496
24689
|
case "type":
|
|
24497
|
-
return `${
|
|
24690
|
+
return `${path31} must be ${err.params.type}`;
|
|
24498
24691
|
case "not":
|
|
24499
|
-
return `${
|
|
24692
|
+
return `${path31} is not allowed here`;
|
|
24500
24693
|
default:
|
|
24501
|
-
return `${
|
|
24694
|
+
return `${path31} ${err.message ?? "is invalid"}`;
|
|
24502
24695
|
}
|
|
24503
24696
|
}
|
|
24504
24697
|
function hintFor(err) {
|
|
@@ -24554,8 +24747,8 @@ function escapeJsonPointerSegment(segment) {
|
|
|
24554
24747
|
function isPlausibleDeviceId(value) {
|
|
24555
24748
|
return HEX_MAC_DEVICE_ID_RE.test(value) || HYPHENATED_DEVICE_ID_RE.test(value);
|
|
24556
24749
|
}
|
|
24557
|
-
function hasErrorAtPath(errors,
|
|
24558
|
-
return errors.some((err) => err.path ===
|
|
24750
|
+
function hasErrorAtPath(errors, path31) {
|
|
24751
|
+
return errors.some((err) => err.path === path31);
|
|
24559
24752
|
}
|
|
24560
24753
|
function resolvePolicyDeviceRef(raw, aliases) {
|
|
24561
24754
|
if (!raw) return { ok: false, reason: "missing-device" };
|
|
@@ -24578,25 +24771,25 @@ function isDeviceStateConditionLike(value) {
|
|
|
24578
24771
|
const candidate = value;
|
|
24579
24772
|
return typeof candidate.device === "string" && typeof candidate.field === "string" && typeof candidate.op === "string";
|
|
24580
24773
|
}
|
|
24581
|
-
function collectConditionDeviceRefs(condition,
|
|
24774
|
+
function collectConditionDeviceRefs(condition, path31) {
|
|
24582
24775
|
if (!condition || typeof condition !== "object" || Array.isArray(condition)) return [];
|
|
24583
24776
|
const out = [];
|
|
24584
24777
|
if (isDeviceStateConditionLike(condition)) {
|
|
24585
|
-
out.push({ path: `${
|
|
24778
|
+
out.push({ path: `${path31}/device`, ref: condition.device });
|
|
24586
24779
|
}
|
|
24587
24780
|
const candidate = condition;
|
|
24588
24781
|
if (Array.isArray(candidate.all)) {
|
|
24589
24782
|
for (let i = 0; i < candidate.all.length; i++) {
|
|
24590
|
-
out.push(...collectConditionDeviceRefs(candidate.all[i], `${
|
|
24783
|
+
out.push(...collectConditionDeviceRefs(candidate.all[i], `${path31}/all/${i}`));
|
|
24591
24784
|
}
|
|
24592
24785
|
}
|
|
24593
24786
|
if (Array.isArray(candidate.any)) {
|
|
24594
24787
|
for (let i = 0; i < candidate.any.length; i++) {
|
|
24595
|
-
out.push(...collectConditionDeviceRefs(candidate.any[i], `${
|
|
24788
|
+
out.push(...collectConditionDeviceRefs(candidate.any[i], `${path31}/any/${i}`));
|
|
24596
24789
|
}
|
|
24597
24790
|
}
|
|
24598
24791
|
if (candidate.not !== void 0) {
|
|
24599
|
-
out.push(...collectConditionDeviceRefs(candidate.not, `${
|
|
24792
|
+
out.push(...collectConditionDeviceRefs(candidate.not, `${path31}/not`));
|
|
24600
24793
|
}
|
|
24601
24794
|
return out;
|
|
24602
24795
|
}
|
|
@@ -24643,12 +24836,12 @@ function collectOfflineSemanticErrors(loaded, existingErrors) {
|
|
|
24643
24836
|
const out = [];
|
|
24644
24837
|
const aliases = collectAliasMap(data);
|
|
24645
24838
|
for (const [aliasName, deviceId] of Object.entries(aliases)) {
|
|
24646
|
-
const
|
|
24647
|
-
if (hasErrorAtPath(existingErrors,
|
|
24839
|
+
const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
|
|
24840
|
+
if (hasErrorAtPath(existingErrors, path31)) continue;
|
|
24648
24841
|
if (isPlausibleDeviceId(deviceId)) continue;
|
|
24649
|
-
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter,
|
|
24842
|
+
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
|
|
24650
24843
|
out.push({
|
|
24651
|
-
path:
|
|
24844
|
+
path: path31,
|
|
24652
24845
|
line,
|
|
24653
24846
|
col,
|
|
24654
24847
|
keyword: "alias-device-id",
|
|
@@ -24782,12 +24975,12 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
|
|
|
24782
24975
|
inventoryById.set(remote.deviceId, { typeName: remote.remoteType });
|
|
24783
24976
|
}
|
|
24784
24977
|
for (const [aliasName, deviceId] of Object.entries(aliases)) {
|
|
24785
|
-
const
|
|
24786
|
-
if (hasErrorAtPath(errors,
|
|
24978
|
+
const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
|
|
24979
|
+
if (hasErrorAtPath(errors, path31)) continue;
|
|
24787
24980
|
if (!inventoryById.has(deviceId)) {
|
|
24788
|
-
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter,
|
|
24981
|
+
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
|
|
24789
24982
|
errors.push({
|
|
24790
|
-
path:
|
|
24983
|
+
path: path31,
|
|
24791
24984
|
line,
|
|
24792
24985
|
col,
|
|
24793
24986
|
keyword: "alias-live-device-not-found",
|
|
@@ -24851,10 +25044,10 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
|
|
|
24851
25044
|
if (!effectiveDeviceId) continue;
|
|
24852
25045
|
const target = inventoryById.get(effectiveDeviceId);
|
|
24853
25046
|
if (!target) {
|
|
24854
|
-
const
|
|
24855
|
-
const { line: line2, col: col2 } = locateInstancePath(loaded.doc, loaded.lineCounter,
|
|
25047
|
+
const path31 = typeof action?.device === "string" ? devicePath : commandPath;
|
|
25048
|
+
const { line: line2, col: col2 } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
|
|
24856
25049
|
errors.push({
|
|
24857
|
-
path:
|
|
25050
|
+
path: path31,
|
|
24858
25051
|
line: line2,
|
|
24859
25052
|
col: col2,
|
|
24860
25053
|
keyword: "rule-live-device-not-found",
|
|
@@ -24960,6 +25153,32 @@ var init_validate = __esm({
|
|
|
24960
25153
|
}
|
|
24961
25154
|
});
|
|
24962
25155
|
|
|
25156
|
+
// src/llm/pricing.ts
|
|
25157
|
+
function calculateCostUsd(model, tokensIn, tokensOut) {
|
|
25158
|
+
const pricing = PRICING[model];
|
|
25159
|
+
if (!pricing) return void 0;
|
|
25160
|
+
const inputCost = tokensIn / 1e6 * pricing.inUsdPer1M;
|
|
25161
|
+
const outputCost = tokensOut / 1e6 * pricing.outUsdPer1M;
|
|
25162
|
+
return inputCost + outputCost;
|
|
25163
|
+
}
|
|
25164
|
+
var PRICING;
|
|
25165
|
+
var init_pricing = __esm({
|
|
25166
|
+
"src/llm/pricing.ts"() {
|
|
25167
|
+
"use strict";
|
|
25168
|
+
init_cjs_shim();
|
|
25169
|
+
PRICING = {
|
|
25170
|
+
// OpenAI — https://openai.com/api/pricing/
|
|
25171
|
+
"gpt-4o-mini": { inUsdPer1M: 0.15, outUsdPer1M: 0.6 },
|
|
25172
|
+
"gpt-4o": { inUsdPer1M: 2.5, outUsdPer1M: 10 },
|
|
25173
|
+
"gpt-4-turbo": { inUsdPer1M: 10, outUsdPer1M: 30 },
|
|
25174
|
+
// Anthropic — https://www.anthropic.com/pricing
|
|
25175
|
+
"claude-haiku-4-5-20251001": { inUsdPer1M: 1, outUsdPer1M: 5 },
|
|
25176
|
+
"claude-sonnet-4-6": { inUsdPer1M: 3, outUsdPer1M: 15 },
|
|
25177
|
+
"claude-opus-4-7": { inUsdPer1M: 15, outUsdPer1M: 75 }
|
|
25178
|
+
};
|
|
25179
|
+
}
|
|
25180
|
+
});
|
|
25181
|
+
|
|
24963
25182
|
// src/llm/providers/openai.ts
|
|
24964
25183
|
import https from "node:https";
|
|
24965
25184
|
import http from "node:http";
|
|
@@ -24968,9 +25187,11 @@ var init_openai = __esm({
|
|
|
24968
25187
|
"src/llm/providers/openai.ts"() {
|
|
24969
25188
|
"use strict";
|
|
24970
25189
|
init_cjs_shim();
|
|
25190
|
+
init_pricing();
|
|
24971
25191
|
OpenAIProvider = class {
|
|
24972
25192
|
name = "openai";
|
|
24973
25193
|
model;
|
|
25194
|
+
capabilities = { toolUse: true };
|
|
24974
25195
|
apiKey;
|
|
24975
25196
|
baseUrl;
|
|
24976
25197
|
timeoutMs;
|
|
@@ -25095,7 +25316,10 @@ var init_openai = __esm({
|
|
|
25095
25316
|
if (!toolCall) throw new Error("OpenAI decide: no tool call in response");
|
|
25096
25317
|
const args = JSON.parse(toolCall.function.arguments);
|
|
25097
25318
|
if (typeof args.pass !== "boolean") throw new Error("OpenAI decide: malformed function-call response");
|
|
25098
|
-
|
|
25319
|
+
const tokensIn = json3.usage?.prompt_tokens ?? 0;
|
|
25320
|
+
const tokensOut = json3.usage?.completion_tokens ?? 0;
|
|
25321
|
+
const usage = json3.usage ? { tokensIn, tokensOut, costUsd: calculateCostUsd(this.model, tokensIn, tokensOut) } : void 0;
|
|
25322
|
+
return { pass: args.pass, reason: String(args.reason ?? "").slice(0, 200), usage };
|
|
25099
25323
|
}
|
|
25100
25324
|
};
|
|
25101
25325
|
}
|
|
@@ -25108,9 +25332,11 @@ var init_anthropic = __esm({
|
|
|
25108
25332
|
"src/llm/providers/anthropic.ts"() {
|
|
25109
25333
|
"use strict";
|
|
25110
25334
|
init_cjs_shim();
|
|
25335
|
+
init_pricing();
|
|
25111
25336
|
AnthropicProvider = class {
|
|
25112
25337
|
name = "anthropic";
|
|
25113
25338
|
model;
|
|
25339
|
+
capabilities = { toolUse: true };
|
|
25114
25340
|
apiKey;
|
|
25115
25341
|
timeoutMs;
|
|
25116
25342
|
maxTokens;
|
|
@@ -25225,7 +25451,352 @@ var init_anthropic = __esm({
|
|
|
25225
25451
|
if (!toolUse?.input || typeof toolUse.input.pass !== "boolean") {
|
|
25226
25452
|
throw new Error("Anthropic decide: malformed tool-use response");
|
|
25227
25453
|
}
|
|
25228
|
-
|
|
25454
|
+
const tokensIn = json3.usage?.input_tokens ?? 0;
|
|
25455
|
+
const tokensOut = json3.usage?.output_tokens ?? 0;
|
|
25456
|
+
const usage = json3.usage ? { tokensIn, tokensOut, costUsd: calculateCostUsd(this.model, tokensIn, tokensOut) } : void 0;
|
|
25457
|
+
return { pass: toolUse.input.pass, reason: String(toolUse.input.reason ?? "").slice(0, 200), usage };
|
|
25458
|
+
}
|
|
25459
|
+
};
|
|
25460
|
+
}
|
|
25461
|
+
});
|
|
25462
|
+
|
|
25463
|
+
// src/llm/providers/structured-output-fallback.ts
|
|
25464
|
+
import https3 from "node:https";
|
|
25465
|
+
import http2 from "node:http";
|
|
25466
|
+
async function decideViaStructuredOutput(opts) {
|
|
25467
|
+
const messages = [
|
|
25468
|
+
{ role: "system", content: `${SYSTEM_INSTRUCTION} ${FEW_SHOT_EXAMPLE}` },
|
|
25469
|
+
{ role: "user", content: opts.prompt }
|
|
25470
|
+
];
|
|
25471
|
+
const first = await chatCompletion(opts, messages);
|
|
25472
|
+
const parsed = tryParseDecision(first.text);
|
|
25473
|
+
if (parsed) {
|
|
25474
|
+
return finalize3(parsed, first.usage, opts);
|
|
25475
|
+
}
|
|
25476
|
+
messages.push({ role: "assistant", content: first.text });
|
|
25477
|
+
messages.push({ role: "user", content: REPAIR_INSTRUCTION });
|
|
25478
|
+
const second = await chatCompletion(opts, messages);
|
|
25479
|
+
const repaired = tryParseDecision(second.text);
|
|
25480
|
+
if (repaired) {
|
|
25481
|
+
const merged = mergeUsage(first.usage, second.usage);
|
|
25482
|
+
return finalize3(repaired, merged, opts);
|
|
25483
|
+
}
|
|
25484
|
+
throw new Error(`Structured output fallback could not parse a JSON decision after repair retry. Last response: ${second.text.slice(0, 200)}`);
|
|
25485
|
+
}
|
|
25486
|
+
async function chatCompletion(opts, messages) {
|
|
25487
|
+
const body = JSON.stringify({
|
|
25488
|
+
model: opts.model,
|
|
25489
|
+
max_tokens: opts.maxTokens ?? 256,
|
|
25490
|
+
temperature: 0,
|
|
25491
|
+
messages
|
|
25492
|
+
});
|
|
25493
|
+
const url2 = new URL(`${opts.baseUrl.replace(/\/+$/, "")}/v1/chat/completions`);
|
|
25494
|
+
const isHttps = url2.protocol === "https:";
|
|
25495
|
+
const responseBody = await new Promise((resolve2, reject) => {
|
|
25496
|
+
const req = (isHttps ? https3 : http2).request(
|
|
25497
|
+
{
|
|
25498
|
+
hostname: url2.hostname,
|
|
25499
|
+
port: url2.port || (isHttps ? 443 : 80),
|
|
25500
|
+
path: url2.pathname,
|
|
25501
|
+
method: "POST",
|
|
25502
|
+
headers: {
|
|
25503
|
+
...opts.apiKey ? { "Authorization": `Bearer ${opts.apiKey}` } : {},
|
|
25504
|
+
"Content-Type": "application/json",
|
|
25505
|
+
"Content-Length": Buffer.byteLength(body)
|
|
25506
|
+
},
|
|
25507
|
+
timeout: opts.timeoutMs
|
|
25508
|
+
},
|
|
25509
|
+
(res) => {
|
|
25510
|
+
const chunks = [];
|
|
25511
|
+
res.on("data", (c) => chunks.push(c));
|
|
25512
|
+
res.on("end", () => {
|
|
25513
|
+
const text2 = Buffer.concat(chunks).toString("utf-8");
|
|
25514
|
+
if (res.statusCode !== void 0 && res.statusCode >= 400) {
|
|
25515
|
+
reject(new Error(`Local LLM API error ${res.statusCode}: ${text2.slice(0, 200)}`));
|
|
25516
|
+
} else {
|
|
25517
|
+
resolve2(text2);
|
|
25518
|
+
}
|
|
25519
|
+
});
|
|
25520
|
+
}
|
|
25521
|
+
);
|
|
25522
|
+
req.on("error", reject);
|
|
25523
|
+
req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
|
|
25524
|
+
req.write(body);
|
|
25525
|
+
req.end();
|
|
25526
|
+
});
|
|
25527
|
+
const json3 = JSON.parse(responseBody);
|
|
25528
|
+
const text = json3.choices?.[0]?.message?.content ?? "";
|
|
25529
|
+
if (!text) throw new Error("Local LLM returned empty content");
|
|
25530
|
+
const usage = json3.usage ? { input_tokens: json3.usage.prompt_tokens ?? 0, output_tokens: json3.usage.completion_tokens ?? 0 } : void 0;
|
|
25531
|
+
return { text, usage };
|
|
25532
|
+
}
|
|
25533
|
+
function tryParseDecision(text) {
|
|
25534
|
+
const stripped = text.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
25535
|
+
const candidates = [stripped, ...extractAllJsonObjects(stripped)];
|
|
25536
|
+
for (const candidate of candidates) {
|
|
25537
|
+
if (!candidate) continue;
|
|
25538
|
+
try {
|
|
25539
|
+
const obj = JSON.parse(candidate);
|
|
25540
|
+
if (typeof obj.pass !== "boolean") continue;
|
|
25541
|
+
const reason = typeof obj.reason === "string" ? obj.reason : "";
|
|
25542
|
+
return { pass: obj.pass, reason: reason.slice(0, 200) };
|
|
25543
|
+
} catch {
|
|
25544
|
+
}
|
|
25545
|
+
}
|
|
25546
|
+
return null;
|
|
25547
|
+
}
|
|
25548
|
+
function extractAllJsonObjects(text) {
|
|
25549
|
+
const out = [];
|
|
25550
|
+
let i = 0;
|
|
25551
|
+
while (i < text.length) {
|
|
25552
|
+
const start = text.indexOf("{", i);
|
|
25553
|
+
if (start === -1) break;
|
|
25554
|
+
const block = readBalancedBraces(text, start);
|
|
25555
|
+
if (!block) {
|
|
25556
|
+
i = start + 1;
|
|
25557
|
+
continue;
|
|
25558
|
+
}
|
|
25559
|
+
out.push(block);
|
|
25560
|
+
i = start + block.length;
|
|
25561
|
+
}
|
|
25562
|
+
return out;
|
|
25563
|
+
}
|
|
25564
|
+
function readBalancedBraces(text, start) {
|
|
25565
|
+
let depth = 0;
|
|
25566
|
+
let inString = false;
|
|
25567
|
+
let escape2 = false;
|
|
25568
|
+
for (let i = start; i < text.length; i++) {
|
|
25569
|
+
const ch = text[i];
|
|
25570
|
+
if (escape2) {
|
|
25571
|
+
escape2 = false;
|
|
25572
|
+
continue;
|
|
25573
|
+
}
|
|
25574
|
+
if (ch === "\\" && inString) {
|
|
25575
|
+
escape2 = true;
|
|
25576
|
+
continue;
|
|
25577
|
+
}
|
|
25578
|
+
if (ch === '"') {
|
|
25579
|
+
inString = !inString;
|
|
25580
|
+
continue;
|
|
25581
|
+
}
|
|
25582
|
+
if (inString) continue;
|
|
25583
|
+
if (ch === "{") depth++;
|
|
25584
|
+
else if (ch === "}") {
|
|
25585
|
+
depth--;
|
|
25586
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
25587
|
+
}
|
|
25588
|
+
}
|
|
25589
|
+
return null;
|
|
25590
|
+
}
|
|
25591
|
+
function mergeUsage(a, b2) {
|
|
25592
|
+
if (!a && !b2) return void 0;
|
|
25593
|
+
return {
|
|
25594
|
+
input_tokens: (a?.input_tokens ?? 0) + (b2?.input_tokens ?? 0),
|
|
25595
|
+
output_tokens: (a?.output_tokens ?? 0) + (b2?.output_tokens ?? 0)
|
|
25596
|
+
};
|
|
25597
|
+
}
|
|
25598
|
+
function finalize3(decision, rawUsage, opts) {
|
|
25599
|
+
let usage;
|
|
25600
|
+
if (rawUsage) {
|
|
25601
|
+
const tokensIn = rawUsage.input_tokens;
|
|
25602
|
+
const tokensOut = rawUsage.output_tokens;
|
|
25603
|
+
const costUsd = opts.computeCostUsd ? opts.computeCostUsd(opts.model, tokensIn, tokensOut) : void 0;
|
|
25604
|
+
usage = { tokensIn, tokensOut, costUsd };
|
|
25605
|
+
}
|
|
25606
|
+
return { pass: decision.pass, reason: decision.reason, usage };
|
|
25607
|
+
}
|
|
25608
|
+
var SYSTEM_INSTRUCTION, FEW_SHOT_EXAMPLE, REPAIR_INSTRUCTION;
|
|
25609
|
+
var init_structured_output_fallback = __esm({
|
|
25610
|
+
"src/llm/providers/structured-output-fallback.ts"() {
|
|
25611
|
+
"use strict";
|
|
25612
|
+
init_cjs_shim();
|
|
25613
|
+
SYSTEM_INSTRUCTION = [
|
|
25614
|
+
"You are a yes/no decision endpoint for a smart-home rule engine.",
|
|
25615
|
+
"Read the user prompt and reply with ONLY a JSON object in this exact shape:",
|
|
25616
|
+
'{"pass": <true|false>, "reason": "<brief explanation, \u2264200 chars>"}',
|
|
25617
|
+
"No prose, no markdown fences, no extra fields. The JSON must be valid and parseable."
|
|
25618
|
+
].join(" ");
|
|
25619
|
+
FEW_SHOT_EXAMPLE = [
|
|
25620
|
+
'Example input: "Is the front door locked? Status: lockState=lock"',
|
|
25621
|
+
'Example output: {"pass": true, "reason": "lockState reports lock"}'
|
|
25622
|
+
].join(" ");
|
|
25623
|
+
REPAIR_INSTRUCTION = [
|
|
25624
|
+
"Your previous response was not valid JSON.",
|
|
25625
|
+
'Reply with ONLY the JSON object: {"pass": <true|false>, "reason": "<short>"}.',
|
|
25626
|
+
"No prose, no markdown."
|
|
25627
|
+
].join(" ");
|
|
25628
|
+
}
|
|
25629
|
+
});
|
|
25630
|
+
|
|
25631
|
+
// src/llm/providers/local.ts
|
|
25632
|
+
import https4 from "node:https";
|
|
25633
|
+
import http3 from "node:http";
|
|
25634
|
+
function stripTrailingV1(url2) {
|
|
25635
|
+
return url2.replace(/\/v1\/?$/, "").replace(/\/+$/, "");
|
|
25636
|
+
}
|
|
25637
|
+
function parseBoolEnv(v2) {
|
|
25638
|
+
if (!v2) return void 0;
|
|
25639
|
+
const lower = v2.trim().toLowerCase();
|
|
25640
|
+
if (["1", "true", "yes", "on"].includes(lower)) return true;
|
|
25641
|
+
if (["0", "false", "no", "off"].includes(lower)) return false;
|
|
25642
|
+
return void 0;
|
|
25643
|
+
}
|
|
25644
|
+
var DEFAULT_LOCAL_BASE_URL, LocalProvider;
|
|
25645
|
+
var init_local = __esm({
|
|
25646
|
+
"src/llm/providers/local.ts"() {
|
|
25647
|
+
"use strict";
|
|
25648
|
+
init_cjs_shim();
|
|
25649
|
+
init_pricing();
|
|
25650
|
+
init_structured_output_fallback();
|
|
25651
|
+
DEFAULT_LOCAL_BASE_URL = "http://localhost:11434/v1";
|
|
25652
|
+
LocalProvider = class {
|
|
25653
|
+
name = "local";
|
|
25654
|
+
model;
|
|
25655
|
+
capabilities;
|
|
25656
|
+
apiKey;
|
|
25657
|
+
baseUrl;
|
|
25658
|
+
timeoutMs;
|
|
25659
|
+
maxTokens;
|
|
25660
|
+
constructor(opts = {}) {
|
|
25661
|
+
this.apiKey = process.env.SWITCHBOT_LOCAL_LLM_API_KEY ?? process.env.LOCAL_LLM_API_KEY ?? "";
|
|
25662
|
+
this.model = opts.model ?? process.env.SWITCHBOT_LOCAL_LLM_MODEL ?? "llama3.2";
|
|
25663
|
+
this.baseUrl = stripTrailingV1(opts.baseUrl ?? process.env.SWITCHBOT_LOCAL_LLM_URL ?? DEFAULT_LOCAL_BASE_URL);
|
|
25664
|
+
this.timeoutMs = opts.timeoutMs ?? 6e4;
|
|
25665
|
+
this.maxTokens = opts.maxTokens ?? 1024;
|
|
25666
|
+
const envToolUse = parseBoolEnv(process.env.SWITCHBOT_LOCAL_LLM_TOOL_USE);
|
|
25667
|
+
const toolUse = opts.toolUse ?? envToolUse ?? false;
|
|
25668
|
+
this.capabilities = { toolUse };
|
|
25669
|
+
}
|
|
25670
|
+
async generateYaml(systemPrompt, userIntent) {
|
|
25671
|
+
const body = JSON.stringify({
|
|
25672
|
+
model: this.model,
|
|
25673
|
+
messages: [
|
|
25674
|
+
{ role: "system", content: systemPrompt },
|
|
25675
|
+
{ role: "user", content: userIntent }
|
|
25676
|
+
],
|
|
25677
|
+
max_tokens: this.maxTokens,
|
|
25678
|
+
temperature: 0
|
|
25679
|
+
});
|
|
25680
|
+
const url2 = new URL(`${this.baseUrl}/v1/chat/completions`);
|
|
25681
|
+
const isHttps = url2.protocol === "https:";
|
|
25682
|
+
const responseBody = await new Promise((resolve2, reject) => {
|
|
25683
|
+
const req = (isHttps ? https4 : http3).request(
|
|
25684
|
+
{
|
|
25685
|
+
hostname: url2.hostname,
|
|
25686
|
+
port: url2.port || (isHttps ? 443 : 80),
|
|
25687
|
+
path: url2.pathname,
|
|
25688
|
+
method: "POST",
|
|
25689
|
+
headers: {
|
|
25690
|
+
...this.apiKey ? { "Authorization": `Bearer ${this.apiKey}` } : {},
|
|
25691
|
+
"Content-Type": "application/json",
|
|
25692
|
+
"Content-Length": Buffer.byteLength(body)
|
|
25693
|
+
},
|
|
25694
|
+
timeout: this.timeoutMs
|
|
25695
|
+
},
|
|
25696
|
+
(res) => {
|
|
25697
|
+
const chunks = [];
|
|
25698
|
+
res.on("data", (c) => chunks.push(c));
|
|
25699
|
+
res.on("end", () => {
|
|
25700
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
25701
|
+
if (res.statusCode !== void 0 && res.statusCode >= 400) {
|
|
25702
|
+
reject(new Error(`Local LLM API error ${res.statusCode}: ${text.slice(0, 200)}`));
|
|
25703
|
+
} else {
|
|
25704
|
+
resolve2(text);
|
|
25705
|
+
}
|
|
25706
|
+
});
|
|
25707
|
+
}
|
|
25708
|
+
);
|
|
25709
|
+
req.on("error", reject);
|
|
25710
|
+
req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
|
|
25711
|
+
req.write(body);
|
|
25712
|
+
req.end();
|
|
25713
|
+
});
|
|
25714
|
+
const json3 = JSON.parse(responseBody);
|
|
25715
|
+
const content = json3.choices?.[0]?.message?.content;
|
|
25716
|
+
if (!content) throw new Error("Local LLM returned empty content");
|
|
25717
|
+
return content.replace(/^```ya?ml\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
25718
|
+
}
|
|
25719
|
+
async decide(prompt2, opts = {}) {
|
|
25720
|
+
const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
|
|
25721
|
+
if (!this.capabilities.toolUse) {
|
|
25722
|
+
return decideViaStructuredOutput({
|
|
25723
|
+
prompt: prompt2,
|
|
25724
|
+
apiKey: this.apiKey,
|
|
25725
|
+
baseUrl: this.baseUrl,
|
|
25726
|
+
model: this.model,
|
|
25727
|
+
timeoutMs,
|
|
25728
|
+
maxTokens: 256,
|
|
25729
|
+
computeCostUsd: calculateCostUsd
|
|
25730
|
+
});
|
|
25731
|
+
}
|
|
25732
|
+
const body = JSON.stringify({
|
|
25733
|
+
model: this.model,
|
|
25734
|
+
max_tokens: 256,
|
|
25735
|
+
tools: [{
|
|
25736
|
+
type: "function",
|
|
25737
|
+
function: {
|
|
25738
|
+
name: "decide",
|
|
25739
|
+
description: "Return a boolean pass/fail decision with a brief reason.",
|
|
25740
|
+
parameters: {
|
|
25741
|
+
type: "object",
|
|
25742
|
+
properties: {
|
|
25743
|
+
pass: { type: "boolean" },
|
|
25744
|
+
reason: { type: "string" }
|
|
25745
|
+
},
|
|
25746
|
+
required: ["pass", "reason"]
|
|
25747
|
+
}
|
|
25748
|
+
}
|
|
25749
|
+
}],
|
|
25750
|
+
tool_choice: { type: "function", function: { name: "decide" } },
|
|
25751
|
+
messages: [{ role: "user", content: prompt2 }]
|
|
25752
|
+
});
|
|
25753
|
+
const url2 = new URL(`${this.baseUrl}/v1/chat/completions`);
|
|
25754
|
+
const isHttps = url2.protocol === "https:";
|
|
25755
|
+
const responseBody = await new Promise((resolve2, reject) => {
|
|
25756
|
+
const req = (isHttps ? https4 : http3).request(
|
|
25757
|
+
{
|
|
25758
|
+
hostname: url2.hostname,
|
|
25759
|
+
port: url2.port || (isHttps ? 443 : 80),
|
|
25760
|
+
path: url2.pathname,
|
|
25761
|
+
method: "POST",
|
|
25762
|
+
headers: {
|
|
25763
|
+
...this.apiKey ? { "Authorization": `Bearer ${this.apiKey}` } : {},
|
|
25764
|
+
"Content-Type": "application/json",
|
|
25765
|
+
"Content-Length": Buffer.byteLength(body)
|
|
25766
|
+
},
|
|
25767
|
+
timeout: timeoutMs
|
|
25768
|
+
},
|
|
25769
|
+
(res) => {
|
|
25770
|
+
const chunks = [];
|
|
25771
|
+
res.on("data", (c) => chunks.push(c));
|
|
25772
|
+
res.on("end", () => {
|
|
25773
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
25774
|
+
if (res.statusCode !== void 0 && res.statusCode >= 400) {
|
|
25775
|
+
reject(new Error(`Local LLM API error ${res.statusCode}: ${text.slice(0, 200)}`));
|
|
25776
|
+
} else {
|
|
25777
|
+
resolve2(text);
|
|
25778
|
+
}
|
|
25779
|
+
});
|
|
25780
|
+
}
|
|
25781
|
+
);
|
|
25782
|
+
req.on("error", reject);
|
|
25783
|
+
req.on("timeout", () => req.destroy(new Error("LLM request timeout")));
|
|
25784
|
+
req.write(body);
|
|
25785
|
+
req.end();
|
|
25786
|
+
});
|
|
25787
|
+
const json3 = JSON.parse(responseBody);
|
|
25788
|
+
const toolCall = json3.choices?.[0]?.message?.tool_calls?.find((tc) => tc.function.name === "decide");
|
|
25789
|
+
if (!toolCall) throw new Error("Local LLM decide: no tool call in response");
|
|
25790
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
25791
|
+
if (typeof args.pass !== "boolean") throw new Error("Local LLM decide: malformed function-call response");
|
|
25792
|
+
const tokensIn = json3.usage?.prompt_tokens ?? 0;
|
|
25793
|
+
const tokensOut = json3.usage?.completion_tokens ?? 0;
|
|
25794
|
+
const usage = json3.usage ? { tokensIn, tokensOut, costUsd: calculateCostUsd(this.model, tokensIn, tokensOut) } : void 0;
|
|
25795
|
+
return { pass: args.pass, reason: String(args.reason ?? "").slice(0, 200), usage };
|
|
25796
|
+
}
|
|
25797
|
+
/** Exposed for doctor `local-llm-reachable` check. */
|
|
25798
|
+
getEndpoint() {
|
|
25799
|
+
return this.baseUrl;
|
|
25229
25800
|
}
|
|
25230
25801
|
};
|
|
25231
25802
|
}
|
|
@@ -25267,8 +25838,9 @@ __export(llm_exports, {
|
|
|
25267
25838
|
scoreIntentComplexity: () => scoreIntentComplexity
|
|
25268
25839
|
});
|
|
25269
25840
|
function createLLMProvider(backend, opts = {}) {
|
|
25270
|
-
if (backend === "openai"
|
|
25841
|
+
if (backend === "openai") return new OpenAIProvider(opts);
|
|
25271
25842
|
if (backend === "anthropic") return new AnthropicProvider(opts);
|
|
25843
|
+
if (backend === "local") return new LocalProvider(opts);
|
|
25272
25844
|
throw new Error(`Unknown LLM backend: ${backend}`);
|
|
25273
25845
|
}
|
|
25274
25846
|
var LLM_AUTO_THRESHOLD;
|
|
@@ -25278,6 +25850,7 @@ var init_llm = __esm({
|
|
|
25278
25850
|
init_cjs_shim();
|
|
25279
25851
|
init_openai();
|
|
25280
25852
|
init_anthropic();
|
|
25853
|
+
init_local();
|
|
25281
25854
|
init_complexity();
|
|
25282
25855
|
LLM_AUTO_THRESHOLD = 4;
|
|
25283
25856
|
}
|
|
@@ -25482,9 +26055,10 @@ async function evaluateSingle(c, now, ctx) {
|
|
|
25482
26055
|
};
|
|
25483
26056
|
}
|
|
25484
26057
|
try {
|
|
26058
|
+
const recent = await fetchRecentEvents(c.llm.recent_events, ctx);
|
|
25485
26059
|
const res = await ctx.llmEvaluator.evaluate(
|
|
25486
26060
|
c.llm,
|
|
25487
|
-
{ event: ctx.event },
|
|
26061
|
+
{ event: ctx.event, recentEvents: recent },
|
|
25488
26062
|
ctx.ruleVersion ?? "unknown",
|
|
25489
26063
|
ctx.globalLlmMaxCallsPerHour
|
|
25490
26064
|
);
|
|
@@ -25496,6 +26070,44 @@ async function evaluateSingle(c, now, ctx) {
|
|
|
25496
26070
|
return fail(`llm condition error: ${err instanceof Error ? err.message : String(err)}`);
|
|
25497
26071
|
}
|
|
25498
26072
|
}
|
|
26073
|
+
if (isEventCountCondition(c)) {
|
|
26074
|
+
if (!ctx.eventWindowFetcher) {
|
|
26075
|
+
return {
|
|
26076
|
+
matched: false,
|
|
26077
|
+
failures: [],
|
|
26078
|
+
unsupported: [{ keyword: "event_count", hint: "event_count evaluation requires a history fetcher; this call site did not provide one." }]
|
|
26079
|
+
};
|
|
26080
|
+
}
|
|
26081
|
+
const ec = c.event_count;
|
|
26082
|
+
const resolved = resolveDeviceRef(ec.device, ctx.aliases);
|
|
26083
|
+
if (!resolved) return fail(`event_count: could not resolve device "${ec.device}" to an id (no matching alias).`);
|
|
26084
|
+
const windowMs = parseDurationOrNull(ec.window);
|
|
26085
|
+
if (windowMs === null || windowMs <= 0) {
|
|
26086
|
+
return fail(`event_count: invalid window "${ec.window}" \u2014 expected e.g. "30s", "5m", "1h".`);
|
|
26087
|
+
}
|
|
26088
|
+
const untilMs = now.getTime();
|
|
26089
|
+
const sinceMs = untilMs - windowMs;
|
|
26090
|
+
try {
|
|
26091
|
+
const events = await ctx.eventWindowFetcher(resolved, {
|
|
26092
|
+
sinceMs,
|
|
26093
|
+
untilMs,
|
|
26094
|
+
limit: Math.max(ec.min, ec.max ?? ec.min) + 1,
|
|
26095
|
+
eventName: ec.event
|
|
26096
|
+
});
|
|
26097
|
+
const count = events.length;
|
|
26098
|
+
const min = ec.min;
|
|
26099
|
+
const max = ec.max;
|
|
26100
|
+
const ceilingViolated = max !== void 0 && count > max;
|
|
26101
|
+
const floorViolated = count < min;
|
|
26102
|
+
if (floorViolated || ceilingViolated) {
|
|
26103
|
+
const range = max !== void 0 ? `${min}\u2013${max}` : `\u2265 ${min}`;
|
|
26104
|
+
return fail(`event_count ${ec.device}${ec.event ? ` ${ec.event}` : ""} in ${ec.window}: ${count} (expected ${range})`);
|
|
26105
|
+
}
|
|
26106
|
+
return ok;
|
|
26107
|
+
} catch (err) {
|
|
26108
|
+
return fail(`event_count ${ec.device}: fetch failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
26109
|
+
}
|
|
26110
|
+
}
|
|
25499
26111
|
return {
|
|
25500
26112
|
matched: false,
|
|
25501
26113
|
failures: [],
|
|
@@ -25562,12 +26174,14 @@ function conditionKind(c) {
|
|
|
25562
26174
|
if (isTimeBetween(c)) return "time_between";
|
|
25563
26175
|
if (isDeviceState(c)) return "device_state";
|
|
25564
26176
|
if (isLlmCondition(c)) return "llm";
|
|
26177
|
+
if (isEventCountCondition(c)) return "event_count";
|
|
25565
26178
|
return "unknown";
|
|
25566
26179
|
}
|
|
25567
26180
|
function conditionConfig(c) {
|
|
25568
26181
|
if (isTimeBetween(c)) return c.time_between;
|
|
25569
26182
|
if (isDeviceState(c)) return { device: c.device, field: c.field, op: c.op, value: c.value };
|
|
25570
26183
|
if (isLlmCondition(c)) return { prompt: c.llm.prompt.slice(0, 80) };
|
|
26184
|
+
if (isEventCountCondition(c)) return c.event_count;
|
|
25571
26185
|
return void 0;
|
|
25572
26186
|
}
|
|
25573
26187
|
function pushConditionTrace(trace, c, sub) {
|
|
@@ -25577,6 +26191,31 @@ function pushConditionTrace(trace, c, sub) {
|
|
|
25577
26191
|
passed: sub.unsupported.length > 0 ? false : sub.matched
|
|
25578
26192
|
});
|
|
25579
26193
|
}
|
|
26194
|
+
function parseDurationOrNull(spec) {
|
|
26195
|
+
const m2 = String(spec ?? "").trim().match(/^(\d+)(ms|s|m|h|d)$/i);
|
|
26196
|
+
if (!m2) return null;
|
|
26197
|
+
const n = Number(m2[1]);
|
|
26198
|
+
const unit = m2[2].toLowerCase();
|
|
26199
|
+
const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
26200
|
+
return n * factor;
|
|
26201
|
+
}
|
|
26202
|
+
async function fetchRecentEvents(count, ctx) {
|
|
26203
|
+
if (!count || count <= 0) return void 0;
|
|
26204
|
+
if (!ctx.eventWindowFetcher || !ctx.event?.deviceId) return void 0;
|
|
26205
|
+
const untilMs = ctx.event.t.getTime();
|
|
26206
|
+
const sinceMs = untilMs - 24 * 60 * 60 * 1e3;
|
|
26207
|
+
try {
|
|
26208
|
+
const events = await ctx.eventWindowFetcher(ctx.event.deviceId, {
|
|
26209
|
+
sinceMs,
|
|
26210
|
+
untilMs,
|
|
26211
|
+
limit: count,
|
|
26212
|
+
eventName: ctx.event.event
|
|
26213
|
+
});
|
|
26214
|
+
return events.slice(-count);
|
|
26215
|
+
} catch {
|
|
26216
|
+
return void 0;
|
|
26217
|
+
}
|
|
26218
|
+
}
|
|
25580
26219
|
var EVENT_CLASSIFIERS;
|
|
25581
26220
|
var init_matcher = __esm({
|
|
25582
26221
|
"src/rules/matcher.ts"() {
|
|
@@ -26502,7 +27141,7 @@ var init_cron_scheduler = __esm({
|
|
|
26502
27141
|
});
|
|
26503
27142
|
|
|
26504
27143
|
// src/rules/webhook-listener.ts
|
|
26505
|
-
import
|
|
27144
|
+
import http4 from "node:http";
|
|
26506
27145
|
import { timingSafeEqual } from "node:crypto";
|
|
26507
27146
|
function normalisePath(p2) {
|
|
26508
27147
|
if (!p2) return "/";
|
|
@@ -26558,7 +27197,7 @@ var init_webhook_listener = __esm({
|
|
|
26558
27197
|
/** Start listening. Resolves once the server has bound a port. */
|
|
26559
27198
|
async start() {
|
|
26560
27199
|
if (this.server) return;
|
|
26561
|
-
const server =
|
|
27200
|
+
const server = http4.createServer((req, res) => {
|
|
26562
27201
|
this.handle(req, res).catch((err) => {
|
|
26563
27202
|
if (!res.headersSent) {
|
|
26564
27203
|
res.writeHead(500);
|
|
@@ -26698,8 +27337,8 @@ var init_webhook_listener = __esm({
|
|
|
26698
27337
|
// src/rules/notify.ts
|
|
26699
27338
|
import fs14 from "node:fs";
|
|
26700
27339
|
import path12 from "node:path";
|
|
26701
|
-
import
|
|
26702
|
-
import
|
|
27340
|
+
import http5 from "node:http";
|
|
27341
|
+
import https5 from "node:https";
|
|
26703
27342
|
function renderNotifyTemplate(template, vars) {
|
|
26704
27343
|
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_2, key) => {
|
|
26705
27344
|
const val = vars[key];
|
|
@@ -26782,7 +27421,7 @@ async function sendWebhook(url2, body) {
|
|
|
26782
27421
|
"User-Agent": "switchbot-cli/notify"
|
|
26783
27422
|
}
|
|
26784
27423
|
};
|
|
26785
|
-
const req = (isHttps ?
|
|
27424
|
+
const req = (isHttps ? https5 : http5).request(options, (res) => {
|
|
26786
27425
|
res.resume();
|
|
26787
27426
|
const status = res.statusCode ?? 0;
|
|
26788
27427
|
if (status >= 200 && status < 300) {
|
|
@@ -26969,10 +27608,250 @@ var init_trace = __esm({
|
|
|
26969
27608
|
}
|
|
26970
27609
|
});
|
|
26971
27610
|
|
|
27611
|
+
// src/rules/llm-condition.ts
|
|
27612
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
27613
|
+
function buildCacheKey(ruleVersion2, promptTemplate, context) {
|
|
27614
|
+
const contextSnapshot = {
|
|
27615
|
+
event: { source: context.event.source, event: context.event.event, deviceId: context.event.deviceId },
|
|
27616
|
+
recentEvents: (context.recentEvents ?? []).map((e) => ({
|
|
27617
|
+
source: e.source,
|
|
27618
|
+
event: e.event,
|
|
27619
|
+
deviceId: e.deviceId
|
|
27620
|
+
}))
|
|
27621
|
+
};
|
|
27622
|
+
const serialized = JSON.stringify([ruleVersion2, promptTemplate, deepSortedJson(contextSnapshot)]);
|
|
27623
|
+
return createHash4("sha256").update(serialized).digest("hex");
|
|
27624
|
+
}
|
|
27625
|
+
function buildPrompt(template, context) {
|
|
27626
|
+
const eventDesc = `Event: ${context.event.source} ${context.event.event}${context.event.deviceId ? ` on ${context.event.deviceId}` : ""}`;
|
|
27627
|
+
return `${template}
|
|
27628
|
+
|
|
27629
|
+
${eventDesc}`;
|
|
27630
|
+
}
|
|
27631
|
+
function parseCacheTtl(ttl) {
|
|
27632
|
+
if (ttl === "none") return 0;
|
|
27633
|
+
const match = /^(\d+)(s|m|h)$/.exec(ttl);
|
|
27634
|
+
if (!match) return 5 * 60 * 1e3;
|
|
27635
|
+
const n = parseInt(match[1], 10);
|
|
27636
|
+
if (match[2] === "s") return n * 1e3;
|
|
27637
|
+
if (match[2] === "m") return n * 60 * 1e3;
|
|
27638
|
+
return n * 60 * 60 * 1e3;
|
|
27639
|
+
}
|
|
27640
|
+
function resolveProvider(provider) {
|
|
27641
|
+
if (provider === "auto") {
|
|
27642
|
+
if (process.env.ANTHROPIC_API_KEY) return "anthropic";
|
|
27643
|
+
if (process.env.OPENAI_API_KEY || process.env.LLM_API_KEY) return "openai";
|
|
27644
|
+
throw new Error("No LLM API key found for llm condition. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.");
|
|
27645
|
+
}
|
|
27646
|
+
return provider;
|
|
27647
|
+
}
|
|
27648
|
+
function onErrorResult(onError, reason) {
|
|
27649
|
+
const pass = onError === "pass";
|
|
27650
|
+
return {
|
|
27651
|
+
pass,
|
|
27652
|
+
traceFields: {
|
|
27653
|
+
provider: "error",
|
|
27654
|
+
model: "error",
|
|
27655
|
+
latencyMs: 0,
|
|
27656
|
+
cacheHit: false,
|
|
27657
|
+
reason: reason.slice(0, 200),
|
|
27658
|
+
promptDigest: ""
|
|
27659
|
+
}
|
|
27660
|
+
};
|
|
27661
|
+
}
|
|
27662
|
+
var HOUR_MS, DAY_MS, LlmConditionEvaluator;
|
|
27663
|
+
var init_llm_condition = __esm({
|
|
27664
|
+
"src/rules/llm-condition.ts"() {
|
|
27665
|
+
"use strict";
|
|
27666
|
+
init_cjs_shim();
|
|
27667
|
+
init_trace();
|
|
27668
|
+
init_audit();
|
|
27669
|
+
HOUR_MS = 60 * 60 * 1e3;
|
|
27670
|
+
DAY_MS = 24 * HOUR_MS;
|
|
27671
|
+
LlmConditionEvaluator = class {
|
|
27672
|
+
cache = /* @__PURE__ */ new Map();
|
|
27673
|
+
budgetCounters = /* @__PURE__ */ new Map();
|
|
27674
|
+
async evaluate(condition, context, ruleVersion2, budgetCaps) {
|
|
27675
|
+
const caps = typeof budgetCaps === "number" ? { maxCallsPerHour: budgetCaps } : { ...budgetCaps ?? {} };
|
|
27676
|
+
if (condition.budget?.max_calls_per_hour !== void 0) caps.maxCallsPerHour = condition.budget.max_calls_per_hour;
|
|
27677
|
+
if (condition.budget?.max_tokens_per_hour !== void 0) caps.maxTokensPerHour = condition.budget.max_tokens_per_hour;
|
|
27678
|
+
if (condition.budget?.max_cost_per_day_usd !== void 0) caps.maxCostPerDayUsd = condition.budget.max_cost_per_day_usd;
|
|
27679
|
+
const cacheKey2 = buildCacheKey(ruleVersion2, condition.prompt, context);
|
|
27680
|
+
const ttlMs = parseCacheTtl(condition.cache_ttl ?? "5m");
|
|
27681
|
+
if (ttlMs > 0) {
|
|
27682
|
+
const cached2 = this.cache.get(cacheKey2);
|
|
27683
|
+
if (cached2 && Date.now() < cached2.expiresAt) {
|
|
27684
|
+
return {
|
|
27685
|
+
pass: cached2.result,
|
|
27686
|
+
traceFields: {
|
|
27687
|
+
provider: "cached",
|
|
27688
|
+
model: "cached",
|
|
27689
|
+
latencyMs: 0,
|
|
27690
|
+
cacheHit: true,
|
|
27691
|
+
reason: cached2.reason,
|
|
27692
|
+
promptDigest: cacheKey2.slice(0, 8)
|
|
27693
|
+
}
|
|
27694
|
+
};
|
|
27695
|
+
}
|
|
27696
|
+
}
|
|
27697
|
+
const budgetKey = `${ruleVersion2}:${condition.prompt.slice(0, 32)}`;
|
|
27698
|
+
const counter = this.rollCounter(budgetKey);
|
|
27699
|
+
const callViolation = this.checkPreCallBudget(counter, caps);
|
|
27700
|
+
if (callViolation) {
|
|
27701
|
+
this.emitBudgetExceeded(callViolation, context, condition);
|
|
27702
|
+
return onErrorResult(condition.on_error ?? "fail", `Budget exceeded (${callViolation.dimension})`);
|
|
27703
|
+
}
|
|
27704
|
+
const backend = resolveProvider(condition.provider ?? "auto");
|
|
27705
|
+
const { createLLMProvider: createLLMProvider2 } = await Promise.resolve().then(() => (init_llm(), llm_exports));
|
|
27706
|
+
const provider = createLLMProvider2(backend, {
|
|
27707
|
+
timeoutMs: condition.timeout_ms ?? 5e3
|
|
27708
|
+
});
|
|
27709
|
+
const prompt2 = buildPrompt(condition.prompt, context);
|
|
27710
|
+
const start = Date.now();
|
|
27711
|
+
try {
|
|
27712
|
+
const result = await provider.decide(prompt2, { timeoutMs: condition.timeout_ms ?? 5e3 });
|
|
27713
|
+
const latencyMs = Date.now() - start;
|
|
27714
|
+
counter.calls += 1;
|
|
27715
|
+
if (result.usage) {
|
|
27716
|
+
counter.tokens += result.usage.tokensIn + result.usage.tokensOut;
|
|
27717
|
+
if (result.usage.costUsd !== void 0) {
|
|
27718
|
+
counter.costUsd += result.usage.costUsd;
|
|
27719
|
+
}
|
|
27720
|
+
}
|
|
27721
|
+
if (ttlMs > 0) {
|
|
27722
|
+
this.cache.set(cacheKey2, {
|
|
27723
|
+
result: result.pass,
|
|
27724
|
+
reason: result.reason,
|
|
27725
|
+
expiresAt: Date.now() + ttlMs,
|
|
27726
|
+
usage: result.usage
|
|
27727
|
+
});
|
|
27728
|
+
}
|
|
27729
|
+
return {
|
|
27730
|
+
pass: result.pass,
|
|
27731
|
+
traceFields: {
|
|
27732
|
+
provider: provider.name,
|
|
27733
|
+
model: provider.model,
|
|
27734
|
+
latencyMs,
|
|
27735
|
+
cacheHit: false,
|
|
27736
|
+
reason: String(result.reason ?? "").slice(0, 200),
|
|
27737
|
+
promptDigest: cacheKey2.slice(0, 8),
|
|
27738
|
+
usage: result.usage
|
|
27739
|
+
}
|
|
27740
|
+
};
|
|
27741
|
+
} catch (err) {
|
|
27742
|
+
return onErrorResult(condition.on_error ?? "fail", String(err));
|
|
27743
|
+
}
|
|
27744
|
+
}
|
|
27745
|
+
rollCounter(key) {
|
|
27746
|
+
const now = Date.now();
|
|
27747
|
+
const existing = this.budgetCounters.get(key);
|
|
27748
|
+
if (!existing) {
|
|
27749
|
+
const fresh = { hourlyStart: now, calls: 0, tokens: 0, dailyStart: now, costUsd: 0 };
|
|
27750
|
+
this.budgetCounters.set(key, fresh);
|
|
27751
|
+
return fresh;
|
|
27752
|
+
}
|
|
27753
|
+
if (now - existing.hourlyStart >= HOUR_MS) {
|
|
27754
|
+
existing.hourlyStart = now;
|
|
27755
|
+
existing.calls = 0;
|
|
27756
|
+
existing.tokens = 0;
|
|
27757
|
+
}
|
|
27758
|
+
if (now - existing.dailyStart >= DAY_MS) {
|
|
27759
|
+
existing.dailyStart = now;
|
|
27760
|
+
existing.costUsd = 0;
|
|
27761
|
+
}
|
|
27762
|
+
return existing;
|
|
27763
|
+
}
|
|
27764
|
+
checkPreCallBudget(counter, caps) {
|
|
27765
|
+
if (caps.maxCallsPerHour !== void 0 && caps.maxCallsPerHour >= 0 && counter.calls >= caps.maxCallsPerHour) {
|
|
27766
|
+
return { dimension: "calls", limit: caps.maxCallsPerHour, observed: counter.calls };
|
|
27767
|
+
}
|
|
27768
|
+
if (caps.maxTokensPerHour !== void 0 && caps.maxTokensPerHour >= 0 && counter.tokens >= caps.maxTokensPerHour) {
|
|
27769
|
+
return { dimension: "tokens", limit: caps.maxTokensPerHour, observed: counter.tokens };
|
|
27770
|
+
}
|
|
27771
|
+
if (caps.maxCostPerDayUsd !== void 0 && caps.maxCostPerDayUsd >= 0 && counter.costUsd >= caps.maxCostPerDayUsd) {
|
|
27772
|
+
return { dimension: "cost", limit: caps.maxCostPerDayUsd, observed: counter.costUsd };
|
|
27773
|
+
}
|
|
27774
|
+
return null;
|
|
27775
|
+
}
|
|
27776
|
+
emitBudgetExceeded(violation, context, _condition) {
|
|
27777
|
+
writeAudit({
|
|
27778
|
+
auditVersion: 2,
|
|
27779
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27780
|
+
kind: "llm-budget-exceeded",
|
|
27781
|
+
deviceId: context.event.deviceId ?? "",
|
|
27782
|
+
command: "llm-condition",
|
|
27783
|
+
parameter: null,
|
|
27784
|
+
commandType: "command",
|
|
27785
|
+
dryRun: false,
|
|
27786
|
+
budgetDimension: violation.dimension,
|
|
27787
|
+
budgetLimit: violation.limit,
|
|
27788
|
+
budgetObserved: violation.observed
|
|
27789
|
+
});
|
|
27790
|
+
}
|
|
27791
|
+
};
|
|
27792
|
+
}
|
|
27793
|
+
});
|
|
27794
|
+
|
|
27795
|
+
// src/devices/history-window.ts
|
|
27796
|
+
import fs15 from "node:fs";
|
|
27797
|
+
import readline5 from "node:readline";
|
|
27798
|
+
async function queryEventWindow(deviceId, opts) {
|
|
27799
|
+
const { sinceMs, untilMs } = opts;
|
|
27800
|
+
if (!Number.isFinite(sinceMs) || !Number.isFinite(untilMs)) return [];
|
|
27801
|
+
if (sinceMs > untilMs) return [];
|
|
27802
|
+
const limit = Math.max(0, opts.limit ?? Number.POSITIVE_INFINITY);
|
|
27803
|
+
if (limit === 0) return [];
|
|
27804
|
+
const files = jsonlFilesForDevice(deviceId).slice().reverse();
|
|
27805
|
+
const out = [];
|
|
27806
|
+
for (const file2 of files) {
|
|
27807
|
+
let mtimeMs;
|
|
27808
|
+
try {
|
|
27809
|
+
mtimeMs = fs15.statSync(file2).mtimeMs;
|
|
27810
|
+
} catch {
|
|
27811
|
+
continue;
|
|
27812
|
+
}
|
|
27813
|
+
if (mtimeMs < sinceMs) break;
|
|
27814
|
+
const records = await readWindowFromFile(file2, sinceMs, untilMs, opts.eventFilter);
|
|
27815
|
+
out.push(...records);
|
|
27816
|
+
if (out.length >= limit) {
|
|
27817
|
+
return out.slice(0, limit);
|
|
27818
|
+
}
|
|
27819
|
+
}
|
|
27820
|
+
return out;
|
|
27821
|
+
}
|
|
27822
|
+
async function readWindowFromFile(file2, sinceMs, untilMs, eventFilter) {
|
|
27823
|
+
const stream = fs15.createReadStream(file2, { encoding: "utf-8" });
|
|
27824
|
+
const rl = readline5.createInterface({ input: stream, crlfDelay: Infinity });
|
|
27825
|
+
const out = [];
|
|
27826
|
+
for await (const line of rl) {
|
|
27827
|
+
if (!line) continue;
|
|
27828
|
+
let rec;
|
|
27829
|
+
try {
|
|
27830
|
+
rec = JSON.parse(line);
|
|
27831
|
+
} catch {
|
|
27832
|
+
continue;
|
|
27833
|
+
}
|
|
27834
|
+
const tMs = Date.parse(rec.t);
|
|
27835
|
+
if (!Number.isFinite(tMs)) continue;
|
|
27836
|
+
if (tMs < sinceMs || tMs > untilMs) continue;
|
|
27837
|
+
if (eventFilter && !eventFilter(rec)) continue;
|
|
27838
|
+
out.push(rec);
|
|
27839
|
+
}
|
|
27840
|
+
return out;
|
|
27841
|
+
}
|
|
27842
|
+
var init_history_window = __esm({
|
|
27843
|
+
"src/devices/history-window.ts"() {
|
|
27844
|
+
"use strict";
|
|
27845
|
+
init_cjs_shim();
|
|
27846
|
+
init_history_query();
|
|
27847
|
+
}
|
|
27848
|
+
});
|
|
27849
|
+
|
|
26972
27850
|
// src/rules/engine.ts
|
|
26973
27851
|
var engine_exports = {};
|
|
26974
27852
|
__export(engine_exports, {
|
|
26975
27853
|
RulesEngine: () => RulesEngine,
|
|
27854
|
+
defaultEventWindowFetcher: () => defaultEventWindowFetcher,
|
|
26976
27855
|
lintRules: () => lintRules
|
|
26977
27856
|
});
|
|
26978
27857
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
@@ -27190,6 +28069,25 @@ function lintRules(automation) {
|
|
|
27190
28069
|
message: "llm condition budget.max_calls_per_hour is 0 \u2014 condition will always take the on_error path."
|
|
27191
28070
|
});
|
|
27192
28071
|
}
|
|
28072
|
+
if (llm.budget?.max_tokens_per_hour === 0) {
|
|
28073
|
+
issues.push({
|
|
28074
|
+
rule: r.name,
|
|
28075
|
+
severity: "warning",
|
|
28076
|
+
code: "condition-llm-tokens-budget-zero",
|
|
28077
|
+
message: "llm condition budget.max_tokens_per_hour is 0 \u2014 condition will always take the on_error path."
|
|
28078
|
+
});
|
|
28079
|
+
}
|
|
28080
|
+
if (llm.budget?.max_cost_per_day_usd !== void 0 && llm.budget.max_cost_per_day_usd > 0) {
|
|
28081
|
+
const provider = llm.provider ?? "auto";
|
|
28082
|
+
if (provider === "auto") {
|
|
28083
|
+
issues.push({
|
|
28084
|
+
rule: r.name,
|
|
28085
|
+
severity: "warning",
|
|
28086
|
+
code: "condition-llm-cost-without-known-model",
|
|
28087
|
+
message: 'llm condition sets max_cost_per_day_usd but provider is "auto" \u2014 cost dimension is skipped for any model not in the pricing table.'
|
|
28088
|
+
});
|
|
28089
|
+
}
|
|
28090
|
+
}
|
|
27193
28091
|
if (llm.on_error === "pass") {
|
|
27194
28092
|
issues.push({
|
|
27195
28093
|
rule: r.name,
|
|
@@ -27199,6 +28097,26 @@ function lintRules(automation) {
|
|
|
27199
28097
|
});
|
|
27200
28098
|
}
|
|
27201
28099
|
}
|
|
28100
|
+
for (const c of r.conditions ?? []) {
|
|
28101
|
+
if (!isEventCountCondition(c)) continue;
|
|
28102
|
+
const ec = c.event_count;
|
|
28103
|
+
if (!/^\d+(ms|s|m|h|d)$/.test(String(ec.window ?? ""))) {
|
|
28104
|
+
issues.push({
|
|
28105
|
+
rule: r.name,
|
|
28106
|
+
severity: "error",
|
|
28107
|
+
code: "condition-event-count-bad-window",
|
|
28108
|
+
message: `event_count.window "${ec.window}" must match \`<digits>(ms|s|m|h|d)\` \u2014 e.g. "5m", "1h".`
|
|
28109
|
+
});
|
|
28110
|
+
}
|
|
28111
|
+
if (ec.max !== void 0 && ec.max < ec.min) {
|
|
28112
|
+
issues.push({
|
|
28113
|
+
rule: r.name,
|
|
28114
|
+
severity: "error",
|
|
28115
|
+
code: "condition-event-count-max-below-min",
|
|
28116
|
+
message: `event_count.max (${ec.max}) must be \u2265 min (${ec.min}).`
|
|
28117
|
+
});
|
|
28118
|
+
}
|
|
28119
|
+
}
|
|
27202
28120
|
const enabled = r.enabled !== false;
|
|
27203
28121
|
const hasError = issues.some((i) => i.severity === "error");
|
|
27204
28122
|
const hasUnsupported = issues.some((i) => i.code === "trigger-unsupported");
|
|
@@ -27211,6 +28129,21 @@ function lintRules(automation) {
|
|
|
27211
28129
|
unsupportedCount
|
|
27212
28130
|
};
|
|
27213
28131
|
}
|
|
28132
|
+
async function defaultEventWindowFetcher(deviceId, opts) {
|
|
28133
|
+
const records = await queryEventWindow(deviceId, {
|
|
28134
|
+
sinceMs: opts.sinceMs,
|
|
28135
|
+
untilMs: opts.untilMs,
|
|
28136
|
+
limit: opts.limit,
|
|
28137
|
+
eventFilter: opts.eventName ? (rec) => classifyMqttPayload(rec.payload).event === opts.eventName : void 0
|
|
28138
|
+
});
|
|
28139
|
+
return records.map((rec) => ({
|
|
28140
|
+
source: "mqtt",
|
|
28141
|
+
event: classifyMqttPayload(rec.payload).event,
|
|
28142
|
+
t: new Date(rec.t),
|
|
28143
|
+
deviceId,
|
|
28144
|
+
payload: rec.payload
|
|
28145
|
+
}));
|
|
28146
|
+
}
|
|
27214
28147
|
var RulesEngine;
|
|
27215
28148
|
var init_engine = __esm({
|
|
27216
28149
|
"src/rules/engine.ts"() {
|
|
@@ -27228,6 +28161,8 @@ var init_engine = __esm({
|
|
|
27228
28161
|
init_croner();
|
|
27229
28162
|
init_audit();
|
|
27230
28163
|
init_trace();
|
|
28164
|
+
init_llm_condition();
|
|
28165
|
+
init_history_window();
|
|
27231
28166
|
RulesEngine = class {
|
|
27232
28167
|
opts;
|
|
27233
28168
|
rules;
|
|
@@ -27248,6 +28183,8 @@ var init_engine = __esm({
|
|
|
27248
28183
|
* keeps the semantics of `max_per` honest.
|
|
27249
28184
|
*/
|
|
27250
28185
|
pendingChain = Promise.resolve();
|
|
28186
|
+
llmEvaluator = new LlmConditionEvaluator();
|
|
28187
|
+
eventWindowFetcher = defaultEventWindowFetcher;
|
|
27251
28188
|
stats = {
|
|
27252
28189
|
started: false,
|
|
27253
28190
|
rulesLoaded: 0,
|
|
@@ -27577,7 +28514,12 @@ var init_engine = __esm({
|
|
|
27577
28514
|
const cond = await evaluateConditions(rule.conditions, event.t, {
|
|
27578
28515
|
aliases: this.aliases,
|
|
27579
28516
|
fetchStatus,
|
|
27580
|
-
trace
|
|
28517
|
+
trace,
|
|
28518
|
+
event,
|
|
28519
|
+
llmEvaluator: this.llmEvaluator,
|
|
28520
|
+
ruleVersion: ruleVersion(rule),
|
|
28521
|
+
globalLlmMaxCallsPerHour: this.opts.automation?.llm_budget?.max_calls_per_hour,
|
|
28522
|
+
eventWindowFetcher: this.opts.eventWindowFetcher ?? this.eventWindowFetcher
|
|
27581
28523
|
});
|
|
27582
28524
|
if (!cond.matched) {
|
|
27583
28525
|
const hasHysteresis = rule.hysteresis ?? rule.requires_stable_for;
|
|
@@ -27778,6 +28720,162 @@ var init_engine = __esm({
|
|
|
27778
28720
|
}
|
|
27779
28721
|
});
|
|
27780
28722
|
|
|
28723
|
+
// src/daemon/socket-path.ts
|
|
28724
|
+
import os17 from "node:os";
|
|
28725
|
+
import fs23 from "node:fs";
|
|
28726
|
+
import path18 from "node:path";
|
|
28727
|
+
import { execSync } from "node:child_process";
|
|
28728
|
+
function getDaemonSocketPath() {
|
|
28729
|
+
if (process.platform === "win32") {
|
|
28730
|
+
return `\\\\.\\pipe\\switchbot-daemon-${getCurrentUserKey()}`;
|
|
28731
|
+
}
|
|
28732
|
+
return path18.join(os17.homedir(), ".switchbot", "daemon.sock");
|
|
28733
|
+
}
|
|
28734
|
+
function getCurrentUserKey() {
|
|
28735
|
+
if (cachedUserKey) return cachedUserKey;
|
|
28736
|
+
const fromEnv = process.env.USERNAME ?? process.env.USER;
|
|
28737
|
+
if (fromEnv) {
|
|
28738
|
+
cachedUserKey = sanitize(fromEnv);
|
|
28739
|
+
return cachedUserKey;
|
|
28740
|
+
}
|
|
28741
|
+
try {
|
|
28742
|
+
const userInfo = os17.userInfo();
|
|
28743
|
+
cachedUserKey = sanitize(userInfo.username);
|
|
28744
|
+
return cachedUserKey;
|
|
28745
|
+
} catch {
|
|
28746
|
+
}
|
|
28747
|
+
try {
|
|
28748
|
+
const out = execSync("whoami", { encoding: "utf-8", timeout: 2e3 }).trim();
|
|
28749
|
+
cachedUserKey = sanitize(out.split(/[\\\/]/).pop() ?? "unknown");
|
|
28750
|
+
return cachedUserKey;
|
|
28751
|
+
} catch {
|
|
28752
|
+
cachedUserKey = "unknown";
|
|
28753
|
+
return cachedUserKey;
|
|
28754
|
+
}
|
|
28755
|
+
}
|
|
28756
|
+
function sanitize(name) {
|
|
28757
|
+
return name.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "unknown";
|
|
28758
|
+
}
|
|
28759
|
+
var cachedUserKey;
|
|
28760
|
+
var init_socket_path = __esm({
|
|
28761
|
+
"src/daemon/socket-path.ts"() {
|
|
28762
|
+
"use strict";
|
|
28763
|
+
init_cjs_shim();
|
|
28764
|
+
cachedUserKey = null;
|
|
28765
|
+
}
|
|
28766
|
+
});
|
|
28767
|
+
|
|
28768
|
+
// src/daemon/client.ts
|
|
28769
|
+
var client_exports2 = {};
|
|
28770
|
+
__export(client_exports2, {
|
|
28771
|
+
IpcDaemonClient: () => IpcDaemonClient,
|
|
28772
|
+
IpcDaemonClientError: () => IpcDaemonClientError
|
|
28773
|
+
});
|
|
28774
|
+
import net from "node:net";
|
|
28775
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
28776
|
+
var IpcDaemonClientError, IpcDaemonClient;
|
|
28777
|
+
var init_client3 = __esm({
|
|
28778
|
+
"src/daemon/client.ts"() {
|
|
28779
|
+
"use strict";
|
|
28780
|
+
init_cjs_shim();
|
|
28781
|
+
init_socket_path();
|
|
28782
|
+
IpcDaemonClientError = class extends Error {
|
|
28783
|
+
constructor(message, code, data) {
|
|
28784
|
+
super(message);
|
|
28785
|
+
this.code = code;
|
|
28786
|
+
this.data = data;
|
|
28787
|
+
this.name = "IpcDaemonClientError";
|
|
28788
|
+
}
|
|
28789
|
+
code;
|
|
28790
|
+
data;
|
|
28791
|
+
};
|
|
28792
|
+
IpcDaemonClient = class {
|
|
28793
|
+
socketPath;
|
|
28794
|
+
timeoutMs;
|
|
28795
|
+
connectTimeoutMs;
|
|
28796
|
+
constructor(opts = {}) {
|
|
28797
|
+
this.socketPath = opts.socketPath ?? getDaemonSocketPath();
|
|
28798
|
+
this.timeoutMs = opts.timeoutMs ?? 5e3;
|
|
28799
|
+
this.connectTimeoutMs = opts.connectTimeoutMs ?? 2e3;
|
|
28800
|
+
}
|
|
28801
|
+
getSocketPath() {
|
|
28802
|
+
return this.socketPath;
|
|
28803
|
+
}
|
|
28804
|
+
/**
|
|
28805
|
+
* Sends a JSON-RPC request and resolves with the `result` field. Throws
|
|
28806
|
+
* IpcDaemonClientError on transport failure, parse failure, timeout, or
|
|
28807
|
+
* server-side error.
|
|
28808
|
+
*/
|
|
28809
|
+
async call(method, params) {
|
|
28810
|
+
const id = randomUUID6();
|
|
28811
|
+
const request = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
28812
|
+
return new Promise((resolve2, reject) => {
|
|
28813
|
+
const socket = net.createConnection(this.socketPath);
|
|
28814
|
+
let buffer = "";
|
|
28815
|
+
let settled = false;
|
|
28816
|
+
const finish = (err, value) => {
|
|
28817
|
+
if (settled) return;
|
|
28818
|
+
settled = true;
|
|
28819
|
+
clearTimeout(callTimer);
|
|
28820
|
+
clearTimeout(connectTimer);
|
|
28821
|
+
socket.destroy();
|
|
28822
|
+
if (err) reject(err);
|
|
28823
|
+
else resolve2(value);
|
|
28824
|
+
};
|
|
28825
|
+
const callTimer = setTimeout(() => {
|
|
28826
|
+
finish(new IpcDaemonClientError(`IPC call to ${method} timed out after ${this.timeoutMs}ms`));
|
|
28827
|
+
}, this.timeoutMs);
|
|
28828
|
+
const connectTimer = setTimeout(() => {
|
|
28829
|
+
finish(new IpcDaemonClientError(`IPC connect timed out after ${this.connectTimeoutMs}ms`));
|
|
28830
|
+
}, this.connectTimeoutMs);
|
|
28831
|
+
socket.setEncoding("utf-8");
|
|
28832
|
+
socket.on("connect", () => {
|
|
28833
|
+
clearTimeout(connectTimer);
|
|
28834
|
+
socket.write(request);
|
|
28835
|
+
});
|
|
28836
|
+
socket.on("data", (chunk) => {
|
|
28837
|
+
buffer += chunk;
|
|
28838
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
28839
|
+
if (newlineIdx === -1) return;
|
|
28840
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
28841
|
+
if (!line) return;
|
|
28842
|
+
try {
|
|
28843
|
+
const response = JSON.parse(line);
|
|
28844
|
+
if ("error" in response) {
|
|
28845
|
+
finish(new IpcDaemonClientError(
|
|
28846
|
+
response.error.message,
|
|
28847
|
+
response.error.code,
|
|
28848
|
+
response.error.data
|
|
28849
|
+
));
|
|
28850
|
+
return;
|
|
28851
|
+
}
|
|
28852
|
+
finish(null, response.result);
|
|
28853
|
+
} catch (err) {
|
|
28854
|
+
finish(new IpcDaemonClientError(`Malformed JSON-RPC response: ${err instanceof Error ? err.message : String(err)}`));
|
|
28855
|
+
}
|
|
28856
|
+
});
|
|
28857
|
+
socket.on("error", (err) => {
|
|
28858
|
+
const code = err.code === "ENOENT" || err.code === "ECONNREFUSED" ? `IPC daemon not listening at ${this.socketPath} (${err.code})` : `IPC socket error: ${err.message}`;
|
|
28859
|
+
finish(new IpcDaemonClientError(code));
|
|
28860
|
+
});
|
|
28861
|
+
socket.on("end", () => {
|
|
28862
|
+
if (!settled) finish(new IpcDaemonClientError("IPC server closed connection before responding"));
|
|
28863
|
+
});
|
|
28864
|
+
});
|
|
28865
|
+
}
|
|
28866
|
+
/**
|
|
28867
|
+
* Quick reachability probe. Resolves with the latency in milliseconds when
|
|
28868
|
+
* the daemon responds to `daemon.status`; rejects otherwise.
|
|
28869
|
+
*/
|
|
28870
|
+
async ping() {
|
|
28871
|
+
const start = Date.now();
|
|
28872
|
+
const status = await this.call("daemon.status");
|
|
28873
|
+
return { latencyMs: Date.now() - start, status };
|
|
28874
|
+
}
|
|
28875
|
+
};
|
|
28876
|
+
}
|
|
28877
|
+
});
|
|
28878
|
+
|
|
27781
28879
|
// src/devices/resources.ts
|
|
27782
28880
|
var COMMON_WEBHOOK_FIELDS, RESOURCE_CATALOG;
|
|
27783
28881
|
var init_resources = __esm({
|
|
@@ -28411,6 +29509,128 @@ var init_capabilities = __esm({
|
|
|
28411
29509
|
}
|
|
28412
29510
|
});
|
|
28413
29511
|
|
|
29512
|
+
// src/daemon/server.ts
|
|
29513
|
+
var server_exports = {};
|
|
29514
|
+
__export(server_exports, {
|
|
29515
|
+
startIpcServer: () => startIpcServer
|
|
29516
|
+
});
|
|
29517
|
+
import net2 from "node:net";
|
|
29518
|
+
import fs26 from "node:fs";
|
|
29519
|
+
import path22 from "node:path";
|
|
29520
|
+
async function startIpcServer(opts) {
|
|
29521
|
+
const socketPath = opts.socketPath ?? getDaemonSocketPath();
|
|
29522
|
+
if (process.platform !== "win32") {
|
|
29523
|
+
await ensureParentDir(socketPath);
|
|
29524
|
+
await removeStaleSocket(socketPath);
|
|
29525
|
+
}
|
|
29526
|
+
const server = net2.createServer((socket) => {
|
|
29527
|
+
let buffer = "";
|
|
29528
|
+
socket.setEncoding("utf-8");
|
|
29529
|
+
socket.on("data", (chunk) => {
|
|
29530
|
+
buffer += chunk;
|
|
29531
|
+
let newlineIdx = buffer.indexOf("\n");
|
|
29532
|
+
while (newlineIdx !== -1) {
|
|
29533
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
29534
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
29535
|
+
newlineIdx = buffer.indexOf("\n");
|
|
29536
|
+
if (!line) continue;
|
|
29537
|
+
void handleLine(line, socket, opts);
|
|
29538
|
+
}
|
|
29539
|
+
});
|
|
29540
|
+
socket.on("error", (err) => opts.onClientError?.(err));
|
|
29541
|
+
});
|
|
29542
|
+
await new Promise((resolve2, reject) => {
|
|
29543
|
+
server.once("error", reject);
|
|
29544
|
+
server.listen(socketPath, () => {
|
|
29545
|
+
server.removeListener("error", reject);
|
|
29546
|
+
resolve2();
|
|
29547
|
+
});
|
|
29548
|
+
});
|
|
29549
|
+
if (process.platform !== "win32") {
|
|
29550
|
+
try {
|
|
29551
|
+
fs26.chmodSync(socketPath, 384);
|
|
29552
|
+
} catch {
|
|
29553
|
+
}
|
|
29554
|
+
}
|
|
29555
|
+
return {
|
|
29556
|
+
socketPath,
|
|
29557
|
+
isListening: () => server.listening,
|
|
29558
|
+
close: () => new Promise((resolve2) => {
|
|
29559
|
+
server.close(() => {
|
|
29560
|
+
if (process.platform !== "win32") {
|
|
29561
|
+
try {
|
|
29562
|
+
fs26.unlinkSync(socketPath);
|
|
29563
|
+
} catch {
|
|
29564
|
+
}
|
|
29565
|
+
}
|
|
29566
|
+
resolve2();
|
|
29567
|
+
});
|
|
29568
|
+
})
|
|
29569
|
+
};
|
|
29570
|
+
}
|
|
29571
|
+
async function handleLine(line, socket, opts) {
|
|
29572
|
+
let req;
|
|
29573
|
+
let id = null;
|
|
29574
|
+
try {
|
|
29575
|
+
const parsed = JSON.parse(line);
|
|
29576
|
+
req = parsed;
|
|
29577
|
+
id = parsed.id ?? null;
|
|
29578
|
+
} catch (err) {
|
|
29579
|
+
send(socket, errorResponse(null, ERR_PARSE, "Parse error", String(err)));
|
|
29580
|
+
return;
|
|
29581
|
+
}
|
|
29582
|
+
if (req.jsonrpc !== "2.0" || typeof req.method !== "string") {
|
|
29583
|
+
send(socket, errorResponse(id, ERR_INVALID_REQUEST, 'Invalid Request: missing jsonrpc:"2.0" or method'));
|
|
29584
|
+
return;
|
|
29585
|
+
}
|
|
29586
|
+
const handler = opts.handlers[req.method];
|
|
29587
|
+
if (!handler) {
|
|
29588
|
+
send(socket, errorResponse(id, ERR_METHOD_NOT_FOUND, `Method not found: ${req.method}`));
|
|
29589
|
+
return;
|
|
29590
|
+
}
|
|
29591
|
+
try {
|
|
29592
|
+
const result = await handler(req.params);
|
|
29593
|
+
if (req.id !== void 0) {
|
|
29594
|
+
send(socket, { jsonrpc: "2.0", id, result });
|
|
29595
|
+
}
|
|
29596
|
+
} catch (err) {
|
|
29597
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29598
|
+
send(socket, errorResponse(id, ERR_INTERNAL, message));
|
|
29599
|
+
}
|
|
29600
|
+
}
|
|
29601
|
+
function send(socket, msg) {
|
|
29602
|
+
if (!socket.writable) return;
|
|
29603
|
+
socket.write(JSON.stringify(msg) + "\n");
|
|
29604
|
+
}
|
|
29605
|
+
function errorResponse(id, code, message, data) {
|
|
29606
|
+
return { jsonrpc: "2.0", id, error: data === void 0 ? { code, message } : { code, message, data } };
|
|
29607
|
+
}
|
|
29608
|
+
async function ensureParentDir(socketPath) {
|
|
29609
|
+
const parent = path22.dirname(socketPath);
|
|
29610
|
+
await fs26.promises.mkdir(parent, { recursive: true });
|
|
29611
|
+
}
|
|
29612
|
+
async function removeStaleSocket(socketPath) {
|
|
29613
|
+
try {
|
|
29614
|
+
await fs26.promises.unlink(socketPath);
|
|
29615
|
+
} catch (err) {
|
|
29616
|
+
if (err.code !== "ENOENT") {
|
|
29617
|
+
throw err;
|
|
29618
|
+
}
|
|
29619
|
+
}
|
|
29620
|
+
}
|
|
29621
|
+
var ERR_PARSE, ERR_INVALID_REQUEST, ERR_METHOD_NOT_FOUND, ERR_INTERNAL;
|
|
29622
|
+
var init_server = __esm({
|
|
29623
|
+
"src/daemon/server.ts"() {
|
|
29624
|
+
"use strict";
|
|
29625
|
+
init_cjs_shim();
|
|
29626
|
+
init_socket_path();
|
|
29627
|
+
ERR_PARSE = -32700;
|
|
29628
|
+
ERR_INVALID_REQUEST = -32600;
|
|
29629
|
+
ERR_METHOD_NOT_FOUND = -32601;
|
|
29630
|
+
ERR_INTERNAL = -32603;
|
|
29631
|
+
}
|
|
29632
|
+
});
|
|
29633
|
+
|
|
28414
29634
|
// src/index.ts
|
|
28415
29635
|
init_cjs_shim();
|
|
28416
29636
|
init_esm();
|
|
@@ -36466,10 +37686,10 @@ function mergeDefs(...defs) {
|
|
|
36466
37686
|
function cloneDef(schema2) {
|
|
36467
37687
|
return mergeDefs(schema2._zod.def);
|
|
36468
37688
|
}
|
|
36469
|
-
function getElementAtPath(obj,
|
|
36470
|
-
if (!
|
|
37689
|
+
function getElementAtPath(obj, path31) {
|
|
37690
|
+
if (!path31)
|
|
36471
37691
|
return obj;
|
|
36472
|
-
return
|
|
37692
|
+
return path31.reduce((acc, key) => acc?.[key], obj);
|
|
36473
37693
|
}
|
|
36474
37694
|
function promiseAllObject(promisesObj) {
|
|
36475
37695
|
const keys = Object.keys(promisesObj);
|
|
@@ -36852,11 +38072,11 @@ function aborted(x2, startIndex = 0) {
|
|
|
36852
38072
|
}
|
|
36853
38073
|
return false;
|
|
36854
38074
|
}
|
|
36855
|
-
function prefixIssues(
|
|
38075
|
+
function prefixIssues(path31, issues) {
|
|
36856
38076
|
return issues.map((iss) => {
|
|
36857
38077
|
var _a2;
|
|
36858
38078
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
36859
|
-
iss.path.unshift(
|
|
38079
|
+
iss.path.unshift(path31);
|
|
36860
38080
|
return iss;
|
|
36861
38081
|
});
|
|
36862
38082
|
}
|
|
@@ -37039,7 +38259,7 @@ function formatError2(error48, mapper = (issue2) => issue2.message) {
|
|
|
37039
38259
|
}
|
|
37040
38260
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
37041
38261
|
const result = { errors: [] };
|
|
37042
|
-
const processError = (error49,
|
|
38262
|
+
const processError = (error49, path31 = []) => {
|
|
37043
38263
|
var _a2, _b;
|
|
37044
38264
|
for (const issue2 of error49.issues) {
|
|
37045
38265
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -37049,7 +38269,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
37049
38269
|
} else if (issue2.code === "invalid_element") {
|
|
37050
38270
|
processError({ issues: issue2.issues }, issue2.path);
|
|
37051
38271
|
} else {
|
|
37052
|
-
const fullpath = [...
|
|
38272
|
+
const fullpath = [...path31, ...issue2.path];
|
|
37053
38273
|
if (fullpath.length === 0) {
|
|
37054
38274
|
result.errors.push(mapper(issue2));
|
|
37055
38275
|
continue;
|
|
@@ -37081,8 +38301,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
37081
38301
|
}
|
|
37082
38302
|
function toDotPath(_path) {
|
|
37083
38303
|
const segs = [];
|
|
37084
|
-
const
|
|
37085
|
-
for (const seg of
|
|
38304
|
+
const path31 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
38305
|
+
for (const seg of path31) {
|
|
37086
38306
|
if (typeof seg === "number")
|
|
37087
38307
|
segs.push(`[${seg}]`);
|
|
37088
38308
|
else if (typeof seg === "symbol")
|
|
@@ -49137,13 +50357,13 @@ function resolveRef(ref, ctx) {
|
|
|
49137
50357
|
if (!ref.startsWith("#")) {
|
|
49138
50358
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
49139
50359
|
}
|
|
49140
|
-
const
|
|
49141
|
-
if (
|
|
50360
|
+
const path31 = ref.slice(1).split("/").filter(Boolean);
|
|
50361
|
+
if (path31.length === 0) {
|
|
49142
50362
|
return ctx.rootSchema;
|
|
49143
50363
|
}
|
|
49144
50364
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
49145
|
-
if (
|
|
49146
|
-
const key =
|
|
50365
|
+
if (path31[0] === defsKey) {
|
|
50366
|
+
const key = path31[1];
|
|
49147
50367
|
if (!key || !ctx.defs[key]) {
|
|
49148
50368
|
throw new Error(`Reference not found: ${ref}`);
|
|
49149
50369
|
}
|
|
@@ -49926,157 +51146,12 @@ function resolveToolProfile(name) {
|
|
|
49926
51146
|
throw new Error(`Unknown tool profile "${name}". Valid profiles: ${valid}`);
|
|
49927
51147
|
}
|
|
49928
51148
|
|
|
49929
|
-
// src/
|
|
49930
|
-
|
|
49931
|
-
import fs10 from "node:fs";
|
|
49932
|
-
import path10 from "node:path";
|
|
49933
|
-
import os11 from "node:os";
|
|
49934
|
-
import readline2 from "node:readline";
|
|
49935
|
-
var DEFAULT_LIMIT = 1e3;
|
|
49936
|
-
function historyDir2() {
|
|
49937
|
-
return path10.join(os11.homedir(), ".switchbot", "device-history");
|
|
49938
|
-
}
|
|
49939
|
-
function parseDurationToMs2(spec) {
|
|
49940
|
-
const m2 = spec.trim().match(/^(\d+)(ms|s|m|h|d)$/i);
|
|
49941
|
-
if (!m2) return null;
|
|
49942
|
-
const n = Number(m2[1]);
|
|
49943
|
-
const unit = m2[2].toLowerCase();
|
|
49944
|
-
const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
49945
|
-
return n * factor;
|
|
49946
|
-
}
|
|
49947
|
-
function parseInstantToMs(spec) {
|
|
49948
|
-
const ms = Date.parse(spec);
|
|
49949
|
-
return Number.isFinite(ms) ? ms : null;
|
|
49950
|
-
}
|
|
49951
|
-
function resolveRange(opts) {
|
|
49952
|
-
let fromMs = Number.NEGATIVE_INFINITY;
|
|
49953
|
-
let toMs = Number.POSITIVE_INFINITY;
|
|
49954
|
-
if (opts.since && (opts.from || opts.to)) {
|
|
49955
|
-
throw new Error("--since is mutually exclusive with --from/--to.");
|
|
49956
|
-
}
|
|
49957
|
-
if (opts.since) {
|
|
49958
|
-
const durMs = parseDurationToMs2(opts.since);
|
|
49959
|
-
if (durMs === null) {
|
|
49960
|
-
throw new Error(`Invalid --since value "${opts.since}". Expected e.g. "30s", "15m", "1h", "7d".`);
|
|
49961
|
-
}
|
|
49962
|
-
fromMs = Date.now() - durMs;
|
|
49963
|
-
} else {
|
|
49964
|
-
if (opts.from) {
|
|
49965
|
-
const parsed = parseInstantToMs(opts.from);
|
|
49966
|
-
if (parsed === null) throw new Error(`Invalid --from value "${opts.from}". Expected ISO-8601 timestamp.`);
|
|
49967
|
-
fromMs = parsed;
|
|
49968
|
-
}
|
|
49969
|
-
if (opts.to) {
|
|
49970
|
-
const parsed = parseInstantToMs(opts.to);
|
|
49971
|
-
if (parsed === null) throw new Error(`Invalid --to value "${opts.to}". Expected ISO-8601 timestamp.`);
|
|
49972
|
-
toMs = parsed;
|
|
49973
|
-
}
|
|
49974
|
-
if (fromMs > toMs) throw new Error("--from must be <= --to.");
|
|
49975
|
-
}
|
|
49976
|
-
return { fromMs, toMs };
|
|
49977
|
-
}
|
|
49978
|
-
function jsonlFilesForDevice(deviceId, baseDir = historyDir2()) {
|
|
49979
|
-
const out = [];
|
|
49980
|
-
if (!fs10.existsSync(baseDir)) return out;
|
|
49981
|
-
for (let i = 3; i >= 1; i--) {
|
|
49982
|
-
const p2 = path10.join(baseDir, `${deviceId}.jsonl.${i}`);
|
|
49983
|
-
if (fs10.existsSync(p2)) out.push(p2);
|
|
49984
|
-
}
|
|
49985
|
-
const current = path10.join(baseDir, `${deviceId}.jsonl`);
|
|
49986
|
-
if (fs10.existsSync(current)) out.push(current);
|
|
49987
|
-
return out;
|
|
49988
|
-
}
|
|
49989
|
-
function projectFields(record2, fields) {
|
|
49990
|
-
if (fields.length === 0) return record2;
|
|
49991
|
-
const projected = {};
|
|
49992
|
-
const payload = record2.payload ?? {};
|
|
49993
|
-
for (const f2 of fields) {
|
|
49994
|
-
if (f2 in payload) projected[f2] = payload[f2];
|
|
49995
|
-
}
|
|
49996
|
-
return { t: record2.t, topic: record2.topic, deviceType: record2.deviceType, payload: projected };
|
|
49997
|
-
}
|
|
49998
|
-
async function queryDeviceHistory(deviceId, opts = {}) {
|
|
49999
|
-
const { fromMs, toMs } = resolveRange(opts);
|
|
50000
|
-
const limit = Math.max(0, opts.limit ?? DEFAULT_LIMIT);
|
|
50001
|
-
const fields = opts.fields ?? [];
|
|
50002
|
-
const files = jsonlFilesForDevice(deviceId);
|
|
50003
|
-
const out = [];
|
|
50004
|
-
for (const file2 of files) {
|
|
50005
|
-
try {
|
|
50006
|
-
const stat = fs10.statSync(file2);
|
|
50007
|
-
if (stat.mtimeMs < fromMs) continue;
|
|
50008
|
-
} catch {
|
|
50009
|
-
continue;
|
|
50010
|
-
}
|
|
50011
|
-
const stream = fs10.createReadStream(file2, { encoding: "utf-8" });
|
|
50012
|
-
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
50013
|
-
for await (const line of rl) {
|
|
50014
|
-
if (!line) continue;
|
|
50015
|
-
let rec;
|
|
50016
|
-
try {
|
|
50017
|
-
rec = JSON.parse(line);
|
|
50018
|
-
} catch {
|
|
50019
|
-
continue;
|
|
50020
|
-
}
|
|
50021
|
-
const tMs = Date.parse(rec.t);
|
|
50022
|
-
if (!Number.isFinite(tMs)) continue;
|
|
50023
|
-
if (tMs < fromMs || tMs > toMs) continue;
|
|
50024
|
-
out.push(projectFields(rec, fields));
|
|
50025
|
-
if (out.length >= limit) {
|
|
50026
|
-
rl.close();
|
|
50027
|
-
stream.destroy();
|
|
50028
|
-
return out;
|
|
50029
|
-
}
|
|
50030
|
-
}
|
|
50031
|
-
}
|
|
50032
|
-
return out;
|
|
50033
|
-
}
|
|
50034
|
-
function queryDeviceHistoryStats(deviceId) {
|
|
50035
|
-
const dir = historyDir2();
|
|
50036
|
-
const files = jsonlFilesForDevice(deviceId);
|
|
50037
|
-
let totalBytes = 0;
|
|
50038
|
-
let oldest = null;
|
|
50039
|
-
let newest = null;
|
|
50040
|
-
let count = 0;
|
|
50041
|
-
for (const file2 of files) {
|
|
50042
|
-
try {
|
|
50043
|
-
totalBytes += fs10.statSync(file2).size;
|
|
50044
|
-
} catch {
|
|
50045
|
-
}
|
|
50046
|
-
}
|
|
50047
|
-
for (const file2 of files) {
|
|
50048
|
-
try {
|
|
50049
|
-
const lines = fs10.readFileSync(file2, "utf-8").split("\n");
|
|
50050
|
-
for (const line of lines) {
|
|
50051
|
-
if (!line) continue;
|
|
50052
|
-
count += 1;
|
|
50053
|
-
try {
|
|
50054
|
-
const rec = JSON.parse(line);
|
|
50055
|
-
const tMs = Date.parse(rec.t);
|
|
50056
|
-
if (Number.isFinite(tMs)) {
|
|
50057
|
-
if (oldest === null || tMs < oldest) oldest = tMs;
|
|
50058
|
-
if (newest === null || tMs > newest) newest = tMs;
|
|
50059
|
-
}
|
|
50060
|
-
} catch {
|
|
50061
|
-
}
|
|
50062
|
-
}
|
|
50063
|
-
} catch {
|
|
50064
|
-
}
|
|
50065
|
-
}
|
|
50066
|
-
return {
|
|
50067
|
-
deviceId,
|
|
50068
|
-
fileCount: files.length,
|
|
50069
|
-
totalBytes,
|
|
50070
|
-
recordCount: count,
|
|
50071
|
-
oldest: oldest !== null ? new Date(oldest).toISOString() : void 0,
|
|
50072
|
-
newest: newest !== null ? new Date(newest).toISOString() : void 0,
|
|
50073
|
-
jsonlFiles: files.map((f2) => path10.basename(f2)),
|
|
50074
|
-
historyDir: dir
|
|
50075
|
-
};
|
|
50076
|
-
}
|
|
51149
|
+
// src/commands/mcp.ts
|
|
51150
|
+
init_history_query();
|
|
50077
51151
|
|
|
50078
51152
|
// src/devices/history-agg.ts
|
|
50079
51153
|
init_cjs_shim();
|
|
51154
|
+
init_history_query();
|
|
50080
51155
|
import fs11 from "node:fs";
|
|
50081
51156
|
import readline3 from "node:readline";
|
|
50082
51157
|
var ALL_AGG_FNS = ["count", "min", "max", "avg", "sum", "p50", "p95"];
|
|
@@ -51200,7 +52275,7 @@ var import_yaml5 = __toESM(require_dist(), 1);
|
|
|
51200
52275
|
var import_yaml6 = __toESM(require_dist(), 1);
|
|
51201
52276
|
init_load();
|
|
51202
52277
|
init_validate();
|
|
51203
|
-
import
|
|
52278
|
+
import fs16 from "node:fs";
|
|
51204
52279
|
var AddRuleError = class extends Error {
|
|
51205
52280
|
constructor(message, code) {
|
|
51206
52281
|
super(message);
|
|
@@ -51316,7 +52391,7 @@ ${msgs}`,
|
|
|
51316
52391
|
function addRuleToPolicyFile(opts) {
|
|
51317
52392
|
const result = addRuleToPolicySource(opts);
|
|
51318
52393
|
if (!opts.dryRun) {
|
|
51319
|
-
|
|
52394
|
+
fs16.writeFileSync(opts.policyPath, result.nextSource, "utf8");
|
|
51320
52395
|
return { ...result, written: true };
|
|
51321
52396
|
}
|
|
51322
52397
|
return { ...result, written: false };
|
|
@@ -51325,15 +52400,15 @@ function addRuleToPolicyFile(opts) {
|
|
|
51325
52400
|
// src/rules/explain.ts
|
|
51326
52401
|
init_cjs_shim();
|
|
51327
52402
|
init_trace();
|
|
51328
|
-
import
|
|
52403
|
+
import fs17 from "node:fs";
|
|
51329
52404
|
function loadTraceRecords(auditFile, opts = {}) {
|
|
51330
|
-
if (!
|
|
51331
|
-
const lines =
|
|
52405
|
+
if (!fs17.existsSync(auditFile)) return [];
|
|
52406
|
+
const lines = fs17.readFileSync(auditFile, "utf-8").split(/\r?\n/);
|
|
51332
52407
|
return filterTraceRecords(lines, opts);
|
|
51333
52408
|
}
|
|
51334
52409
|
function loadRelatedAudit(auditFile, fireId) {
|
|
51335
|
-
if (!
|
|
51336
|
-
const raw =
|
|
52410
|
+
if (!fs17.existsSync(auditFile)) return [];
|
|
52411
|
+
const raw = fs17.readFileSync(auditFile, "utf-8");
|
|
51337
52412
|
const out = [];
|
|
51338
52413
|
for (const line of raw.split(/\r?\n/)) {
|
|
51339
52414
|
const trimmed = line.trim();
|
|
@@ -51417,17 +52492,18 @@ init_throttle();
|
|
|
51417
52492
|
init_trace();
|
|
51418
52493
|
init_trace();
|
|
51419
52494
|
init_matcher();
|
|
51420
|
-
|
|
52495
|
+
init_engine();
|
|
52496
|
+
import fs18 from "node:fs";
|
|
51421
52497
|
import path14 from "node:path";
|
|
51422
52498
|
import os13 from "node:os";
|
|
51423
52499
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
51424
|
-
var
|
|
52500
|
+
var HOUR_MS2 = 60 * 60 * 1e3;
|
|
51425
52501
|
var DEVICE_HISTORY_DIR = path14.join(os13.homedir(), ".switchbot", "device-history");
|
|
51426
52502
|
async function simulateRule(opts) {
|
|
51427
52503
|
const { rule, aliases = {}, liveLlm = false } = opts;
|
|
51428
52504
|
const rv = ruleVersion(rule);
|
|
51429
52505
|
const events = loadSourceEvents(opts);
|
|
51430
|
-
const windowStart = events.length > 0 ? new Date(Math.min(...events.map((e) => e.t.getTime()))) : new Date(Date.now() - 24 *
|
|
52506
|
+
const windowStart = events.length > 0 ? new Date(Math.min(...events.map((e) => e.t.getTime()))) : new Date(Date.now() - 24 * HOUR_MS2);
|
|
51431
52507
|
const windowEnd = events.length > 0 ? new Date(Math.max(...events.map((e) => e.t.getTime()))) : /* @__PURE__ */ new Date();
|
|
51432
52508
|
const counts = { wouldFire: 0, blocked: 0, throttled: 0, errored: 0, skippedLlm: 0 };
|
|
51433
52509
|
const blockReasons = /* @__PURE__ */ new Map();
|
|
@@ -51477,7 +52553,8 @@ async function simulateRule(opts) {
|
|
|
51477
52553
|
aliases,
|
|
51478
52554
|
fetchStatus: statusFetcher,
|
|
51479
52555
|
event,
|
|
51480
|
-
ruleVersion: rv
|
|
52556
|
+
ruleVersion: rv,
|
|
52557
|
+
eventWindowFetcher: defaultEventWindowFetcher
|
|
51481
52558
|
});
|
|
51482
52559
|
} catch (err) {
|
|
51483
52560
|
counts.errored++;
|
|
@@ -51526,8 +52603,8 @@ async function simulateRule(opts) {
|
|
|
51526
52603
|
}
|
|
51527
52604
|
function loadSourceEvents(opts) {
|
|
51528
52605
|
if (opts.against) {
|
|
51529
|
-
if (!
|
|
51530
|
-
const lines2 =
|
|
52606
|
+
if (!fs18.existsSync(opts.against)) return [];
|
|
52607
|
+
const lines2 = fs18.readFileSync(opts.against, "utf-8").split(/\r?\n/);
|
|
51531
52608
|
const events = [];
|
|
51532
52609
|
for (const line of lines2) {
|
|
51533
52610
|
const trimmed = line.trim();
|
|
@@ -51547,10 +52624,10 @@ function loadSourceEvents(opts) {
|
|
|
51547
52624
|
return events;
|
|
51548
52625
|
}
|
|
51549
52626
|
const auditLog = opts.auditLog;
|
|
51550
|
-
if (!auditLog || !
|
|
51551
|
-
const sinceMs = opts.since ? parseSince(opts.since) : Date.now() - 24 *
|
|
52627
|
+
if (!auditLog || !fs18.existsSync(auditLog)) return [];
|
|
52628
|
+
const sinceMs = opts.since ? parseSince(opts.since) : Date.now() - 24 * HOUR_MS2;
|
|
51552
52629
|
const sinceIso = new Date(sinceMs).toISOString();
|
|
51553
|
-
const lines =
|
|
52630
|
+
const lines = fs18.readFileSync(auditLog, "utf-8").split(/\r?\n/);
|
|
51554
52631
|
const traceRecords = filterTraceRecords(lines, {
|
|
51555
52632
|
ruleName: opts.rule.name,
|
|
51556
52633
|
since: sinceIso
|
|
@@ -51574,13 +52651,13 @@ function parseSince(since) {
|
|
|
51574
52651
|
const unitMs = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
51575
52652
|
return Date.now() - n * unitMs;
|
|
51576
52653
|
}
|
|
51577
|
-
return Date.now() - 24 *
|
|
52654
|
+
return Date.now() - 24 * HOUR_MS2;
|
|
51578
52655
|
}
|
|
51579
52656
|
function buildStatusFetcher(asOf) {
|
|
51580
52657
|
return async (deviceId) => {
|
|
51581
52658
|
const histFile = path14.join(DEVICE_HISTORY_DIR, `${deviceId}.jsonl`);
|
|
51582
|
-
if (!
|
|
51583
|
-
const lines =
|
|
52659
|
+
if (!fs18.existsSync(histFile)) return {};
|
|
52660
|
+
const lines = fs18.readFileSync(histFile, "utf-8").split(/\r?\n/);
|
|
51584
52661
|
const asOfMs = asOf.getTime();
|
|
51585
52662
|
let best;
|
|
51586
52663
|
for (const line of lines) {
|
|
@@ -51616,13 +52693,13 @@ function collectPolicyDiff(left, right, at, out, limit) {
|
|
|
51616
52693
|
const maxLen = Math.max(left.length, right.length);
|
|
51617
52694
|
for (let i = 0; i < maxLen; i++) {
|
|
51618
52695
|
if (out.length >= limit) return;
|
|
51619
|
-
const
|
|
52696
|
+
const path31 = `${at}[${i}]`;
|
|
51620
52697
|
if (i >= left.length) {
|
|
51621
|
-
out.push({ path:
|
|
52698
|
+
out.push({ path: path31, kind: "added", after: right[i] });
|
|
51622
52699
|
} else if (i >= right.length) {
|
|
51623
|
-
out.push({ path:
|
|
52700
|
+
out.push({ path: path31, kind: "removed", before: left[i] });
|
|
51624
52701
|
} else {
|
|
51625
|
-
collectPolicyDiff(left[i], right[i],
|
|
52702
|
+
collectPolicyDiff(left[i], right[i], path31, out, limit);
|
|
51626
52703
|
}
|
|
51627
52704
|
}
|
|
51628
52705
|
return;
|
|
@@ -51631,15 +52708,15 @@ function collectPolicyDiff(left, right, at, out, limit) {
|
|
|
51631
52708
|
const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
51632
52709
|
for (const key of [...keys].sort()) {
|
|
51633
52710
|
if (out.length >= limit) return;
|
|
51634
|
-
const
|
|
52711
|
+
const path31 = at === "$" ? `$.${key}` : `${at}.${key}`;
|
|
51635
52712
|
const leftHas = Object.prototype.hasOwnProperty.call(left, key);
|
|
51636
52713
|
const rightHas = Object.prototype.hasOwnProperty.call(right, key);
|
|
51637
52714
|
if (!leftHas && rightHas) {
|
|
51638
|
-
out.push({ path:
|
|
52715
|
+
out.push({ path: path31, kind: "added", after: right[key] });
|
|
51639
52716
|
} else if (leftHas && !rightHas) {
|
|
51640
|
-
out.push({ path:
|
|
52717
|
+
out.push({ path: path31, kind: "removed", before: left[key] });
|
|
51641
52718
|
} else {
|
|
51642
|
-
collectPolicyDiff(left[key], right[key],
|
|
52719
|
+
collectPolicyDiff(left[key], right[key], path31, out, limit);
|
|
51643
52720
|
}
|
|
51644
52721
|
}
|
|
51645
52722
|
return;
|
|
@@ -51693,7 +52770,7 @@ function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges
|
|
|
51693
52770
|
init_embedded_assets();
|
|
51694
52771
|
import { dirname as pathDirname, join as pathJoin } from "node:path";
|
|
51695
52772
|
import os14 from "node:os";
|
|
51696
|
-
import
|
|
52773
|
+
import fs19 from "node:fs";
|
|
51697
52774
|
var LATEST_SUPPORTED_VERSION = SUPPORTED_POLICY_SCHEMA_VERSIONS[SUPPORTED_POLICY_SCHEMA_VERSIONS.length - 1];
|
|
51698
52775
|
function mcpError(kind, code, message, options) {
|
|
51699
52776
|
const obj = { code, kind, message };
|
|
@@ -52669,15 +53746,15 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
|
|
|
52669
53746
|
async ({ path: pathArg, force }) => {
|
|
52670
53747
|
const policyPath = resolvePolicyPath({ flag: pathArg });
|
|
52671
53748
|
const doForce = force === true;
|
|
52672
|
-
if (
|
|
53749
|
+
if (fs19.existsSync(policyPath) && !doForce) {
|
|
52673
53750
|
return mcpError("guard", 5, `refusing to overwrite existing policy at ${policyPath}`, {
|
|
52674
53751
|
hint: "pass force=true to overwrite, or choose a different path",
|
|
52675
53752
|
context: { policyPath }
|
|
52676
53753
|
});
|
|
52677
53754
|
}
|
|
52678
53755
|
const template = readPolicyExampleYaml();
|
|
52679
|
-
|
|
52680
|
-
|
|
53756
|
+
fs19.mkdirSync(pathDirname(policyPath), { recursive: true });
|
|
53757
|
+
fs19.writeFileSync(policyPath, template, { encoding: "utf-8" });
|
|
52681
53758
|
const structured = {
|
|
52682
53759
|
policyPath,
|
|
52683
53760
|
schemaVersion: CURRENT_POLICY_SCHEMA_VERSION,
|
|
@@ -52866,7 +53943,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
|
|
|
52866
53943
|
let leftSource = "";
|
|
52867
53944
|
let rightSource = "";
|
|
52868
53945
|
try {
|
|
52869
|
-
leftSource =
|
|
53946
|
+
leftSource = fs19.readFileSync(left_path, "utf-8");
|
|
52870
53947
|
} catch (err) {
|
|
52871
53948
|
if (err?.code === "ENOENT") {
|
|
52872
53949
|
return mcpError("usage", 2, `policy file not found: ${left_path}`, {
|
|
@@ -52876,7 +53953,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
|
|
|
52876
53953
|
return mcpError("runtime", 1, `failed to read ${left_path}: ${String(err)}`);
|
|
52877
53954
|
}
|
|
52878
53955
|
try {
|
|
52879
|
-
rightSource =
|
|
53956
|
+
rightSource = fs19.readFileSync(right_path, "utf-8");
|
|
52880
53957
|
} catch (err) {
|
|
52881
53958
|
if (err?.code === "ENOENT") {
|
|
52882
53959
|
return mcpError("usage", 2, `policy file not found: ${right_path}`, {
|
|
@@ -53751,7 +54828,7 @@ process_uptime_seconds ${Math.floor(process.uptime())}
|
|
|
53751
54828
|
}
|
|
53752
54829
|
if (profile) {
|
|
53753
54830
|
const envCredsPresent = !!(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
|
|
53754
|
-
if (!envCredsPresent && !
|
|
54831
|
+
if (!envCredsPresent && !fs19.existsSync(profileFilePath(profile))) {
|
|
53755
54832
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
53756
54833
|
res.end(JSON.stringify({
|
|
53757
54834
|
jsonrpc: "2.0",
|
|
@@ -54357,7 +55434,7 @@ init_cjs_shim();
|
|
|
54357
55434
|
init_output();
|
|
54358
55435
|
init_arg_parsers();
|
|
54359
55436
|
init_flags();
|
|
54360
|
-
import
|
|
55437
|
+
import http6 from "node:http";
|
|
54361
55438
|
import crypto4 from "node:crypto";
|
|
54362
55439
|
init_client2();
|
|
54363
55440
|
init_credential();
|
|
@@ -54388,18 +55465,18 @@ var StdoutSink = class {
|
|
|
54388
55465
|
|
|
54389
55466
|
// src/sinks/file.ts
|
|
54390
55467
|
init_cjs_shim();
|
|
54391
|
-
import
|
|
55468
|
+
import fs20 from "node:fs";
|
|
54392
55469
|
import path15 from "node:path";
|
|
54393
55470
|
var FileSink = class {
|
|
54394
55471
|
filePath;
|
|
54395
55472
|
constructor(filePath) {
|
|
54396
55473
|
this.filePath = path15.resolve(filePath);
|
|
54397
55474
|
const dir = path15.dirname(this.filePath);
|
|
54398
|
-
if (!
|
|
55475
|
+
if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
|
|
54399
55476
|
}
|
|
54400
55477
|
async write(event) {
|
|
54401
55478
|
try {
|
|
54402
|
-
|
|
55479
|
+
fs20.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
|
|
54403
55480
|
} catch {
|
|
54404
55481
|
}
|
|
54405
55482
|
}
|
|
@@ -54653,7 +55730,7 @@ function parseFilter2(flag) {
|
|
|
54653
55730
|
}
|
|
54654
55731
|
}
|
|
54655
55732
|
function startReceiver(port, pathMatch, filter, onEvent) {
|
|
54656
|
-
const server =
|
|
55733
|
+
const server = http6.createServer((req, res) => {
|
|
54657
55734
|
if (req.method !== "POST") {
|
|
54658
55735
|
res.statusCode = 405;
|
|
54659
55736
|
res.end("method not allowed");
|
|
@@ -55068,10 +56145,10 @@ init_catalog();
|
|
|
55068
56145
|
init_config();
|
|
55069
56146
|
init_cache();
|
|
55070
56147
|
init_quota();
|
|
55071
|
-
import
|
|
55072
|
-
import
|
|
55073
|
-
import
|
|
55074
|
-
import { execSync } from "node:child_process";
|
|
56148
|
+
import fs24 from "node:fs";
|
|
56149
|
+
import os18 from "node:os";
|
|
56150
|
+
import path19 from "node:path";
|
|
56151
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
55075
56152
|
|
|
55076
56153
|
// src/commands/agent-bootstrap.ts
|
|
55077
56154
|
init_cjs_shim();
|
|
@@ -55306,7 +56383,7 @@ init_request_context();
|
|
|
55306
56383
|
|
|
55307
56384
|
// src/lib/daemon-state.ts
|
|
55308
56385
|
init_cjs_shim();
|
|
55309
|
-
import
|
|
56386
|
+
import fs21 from "node:fs";
|
|
55310
56387
|
import os15 from "node:os";
|
|
55311
56388
|
import path16 from "node:path";
|
|
55312
56389
|
function getStateDir() {
|
|
@@ -55329,15 +56406,15 @@ var DAEMON_LOG_FILE = getDaemonLogFile();
|
|
|
55329
56406
|
var DAEMON_STATE_FILE = getDaemonStateFile();
|
|
55330
56407
|
var HEALTHZ_PID_FILE = getHealthzPidFile();
|
|
55331
56408
|
function ensureStateDir() {
|
|
55332
|
-
|
|
56409
|
+
fs21.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
|
|
55333
56410
|
}
|
|
55334
56411
|
function writeDaemonState(state) {
|
|
55335
56412
|
ensureStateDir();
|
|
55336
|
-
|
|
56413
|
+
fs21.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
|
|
55337
56414
|
}
|
|
55338
56415
|
function readDaemonState() {
|
|
55339
56416
|
try {
|
|
55340
|
-
const raw =
|
|
56417
|
+
const raw = fs21.readFileSync(getDaemonStateFile(), "utf-8");
|
|
55341
56418
|
return JSON.parse(raw);
|
|
55342
56419
|
} catch {
|
|
55343
56420
|
return null;
|
|
@@ -55346,7 +56423,7 @@ function readDaemonState() {
|
|
|
55346
56423
|
|
|
55347
56424
|
// src/rules/pid-file.ts
|
|
55348
56425
|
init_cjs_shim();
|
|
55349
|
-
import
|
|
56426
|
+
import fs22 from "node:fs";
|
|
55350
56427
|
import os16 from "node:os";
|
|
55351
56428
|
import path17 from "node:path";
|
|
55352
56429
|
var DEFAULT_DIR = path17.join(os16.homedir(), ".switchbot");
|
|
@@ -55359,13 +56436,13 @@ function getDefaultPidFilePaths() {
|
|
|
55359
56436
|
}
|
|
55360
56437
|
function writePidFile(pidFile, pid = process.pid) {
|
|
55361
56438
|
const dir = path17.dirname(pidFile);
|
|
55362
|
-
|
|
55363
|
-
|
|
56439
|
+
fs22.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
56440
|
+
fs22.writeFileSync(pidFile, `${pid}
|
|
55364
56441
|
`, { mode: 384 });
|
|
55365
56442
|
}
|
|
55366
56443
|
function readPidFile(pidFile) {
|
|
55367
56444
|
try {
|
|
55368
|
-
const raw =
|
|
56445
|
+
const raw = fs22.readFileSync(pidFile, "utf-8").trim();
|
|
55369
56446
|
const n = Number(raw);
|
|
55370
56447
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
55371
56448
|
} catch {
|
|
@@ -55375,20 +56452,20 @@ function readPidFile(pidFile) {
|
|
|
55375
56452
|
function clearPidFile(pidFile, pid = process.pid) {
|
|
55376
56453
|
try {
|
|
55377
56454
|
const existing = readPidFile(pidFile);
|
|
55378
|
-
if (existing === pid)
|
|
56455
|
+
if (existing === pid) fs22.unlinkSync(pidFile);
|
|
55379
56456
|
} catch {
|
|
55380
56457
|
}
|
|
55381
56458
|
}
|
|
55382
56459
|
function writeReloadSentinel(reloadFile) {
|
|
55383
56460
|
const dir = path17.dirname(reloadFile);
|
|
55384
|
-
|
|
55385
|
-
|
|
56461
|
+
fs22.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
56462
|
+
fs22.writeFileSync(reloadFile, `${Date.now()}
|
|
55386
56463
|
`, { mode: 384 });
|
|
55387
56464
|
}
|
|
55388
56465
|
function consumeReloadSentinel(reloadFile) {
|
|
55389
56466
|
try {
|
|
55390
|
-
if (!
|
|
55391
|
-
|
|
56467
|
+
if (!fs22.existsSync(reloadFile)) return false;
|
|
56468
|
+
fs22.unlinkSync(reloadFile);
|
|
55392
56469
|
return true;
|
|
55393
56470
|
} catch {
|
|
55394
56471
|
return false;
|
|
@@ -55459,7 +56536,7 @@ async function checkCredentials() {
|
|
|
55459
56536
|
};
|
|
55460
56537
|
}
|
|
55461
56538
|
const file2 = configFilePath();
|
|
55462
|
-
if (!
|
|
56539
|
+
if (!fs24.existsSync(file2)) {
|
|
55463
56540
|
return {
|
|
55464
56541
|
name: "credentials",
|
|
55465
56542
|
status: "fail",
|
|
@@ -55474,7 +56551,7 @@ async function checkCredentials() {
|
|
|
55474
56551
|
};
|
|
55475
56552
|
}
|
|
55476
56553
|
try {
|
|
55477
|
-
const raw =
|
|
56554
|
+
const raw = fs24.readFileSync(file2, "utf-8");
|
|
55478
56555
|
const cfg = JSON.parse(raw);
|
|
55479
56556
|
if (!cfg.token || !cfg.secret) {
|
|
55480
56557
|
return {
|
|
@@ -55521,8 +56598,8 @@ async function checkCredentials() {
|
|
|
55521
56598
|
}
|
|
55522
56599
|
}
|
|
55523
56600
|
function checkProfiles() {
|
|
55524
|
-
const dir =
|
|
55525
|
-
if (!
|
|
56601
|
+
const dir = path19.join(os18.homedir(), ".switchbot", "profiles");
|
|
56602
|
+
if (!fs24.existsSync(dir)) {
|
|
55526
56603
|
return { name: "profiles", status: "ok", detail: "no profile dir (default profile only)" };
|
|
55527
56604
|
}
|
|
55528
56605
|
const profiles = listProfiles();
|
|
@@ -55648,8 +56725,8 @@ function checkCache() {
|
|
|
55648
56725
|
}
|
|
55649
56726
|
}
|
|
55650
56727
|
function checkQuotaFile() {
|
|
55651
|
-
const p2 =
|
|
55652
|
-
if (!
|
|
56728
|
+
const p2 = path19.join(os18.homedir(), ".switchbot", "quota.json");
|
|
56729
|
+
if (!fs24.existsSync(p2)) {
|
|
55653
56730
|
return {
|
|
55654
56731
|
name: "quota",
|
|
55655
56732
|
status: "ok",
|
|
@@ -55662,7 +56739,7 @@ function checkQuotaFile() {
|
|
|
55662
56739
|
};
|
|
55663
56740
|
}
|
|
55664
56741
|
try {
|
|
55665
|
-
const raw =
|
|
56742
|
+
const raw = fs24.readFileSync(p2, "utf-8");
|
|
55666
56743
|
JSON.parse(raw);
|
|
55667
56744
|
} catch {
|
|
55668
56745
|
return {
|
|
@@ -55743,8 +56820,8 @@ function checkInventoryConsistency() {
|
|
|
55743
56820
|
};
|
|
55744
56821
|
}
|
|
55745
56822
|
function checkAudit() {
|
|
55746
|
-
const p2 =
|
|
55747
|
-
if (!
|
|
56823
|
+
const p2 = path19.join(os18.homedir(), ".switchbot", "audit.log");
|
|
56824
|
+
if (!fs24.existsSync(p2)) {
|
|
55748
56825
|
return {
|
|
55749
56826
|
name: "audit",
|
|
55750
56827
|
status: "ok",
|
|
@@ -55756,7 +56833,7 @@ function checkAudit() {
|
|
|
55756
56833
|
};
|
|
55757
56834
|
}
|
|
55758
56835
|
try {
|
|
55759
|
-
const raw =
|
|
56836
|
+
const raw = fs24.readFileSync(p2, "utf-8");
|
|
55760
56837
|
const since = Date.now() - 24 * 60 * 60 * 1e3;
|
|
55761
56838
|
const recent = [];
|
|
55762
56839
|
let total = 0;
|
|
@@ -55955,14 +57032,14 @@ function checkPathDiscoverability() {
|
|
|
55955
57032
|
const binaryName = isWindows ? "switchbot.cmd" : "switchbot";
|
|
55956
57033
|
let npmBinDir = null;
|
|
55957
57034
|
try {
|
|
55958
|
-
const prefix =
|
|
55959
|
-
npmBinDir = isWindows ? prefix :
|
|
57035
|
+
const prefix = execSync2("npm prefix -g", { timeout: 4e3, encoding: "utf-8" }).trim();
|
|
57036
|
+
npmBinDir = isWindows ? prefix : path19.join(prefix, "bin");
|
|
55960
57037
|
} catch {
|
|
55961
57038
|
}
|
|
55962
57039
|
let binaryOnPath = false;
|
|
55963
57040
|
let resolvedPath = null;
|
|
55964
57041
|
try {
|
|
55965
|
-
const which =
|
|
57042
|
+
const which = execSync2(
|
|
55966
57043
|
isWindows ? `where ${binaryName}` : `which ${binaryName}`,
|
|
55967
57044
|
{ timeout: 3e3, encoding: "utf-8" }
|
|
55968
57045
|
).trim().split(/\r?\n/)[0];
|
|
@@ -55986,7 +57063,7 @@ function checkPathDiscoverability() {
|
|
|
55986
57063
|
};
|
|
55987
57064
|
}
|
|
55988
57065
|
const currentPath = process.env.PATH ?? "";
|
|
55989
|
-
const missingSegment = npmBinDir && !currentPath.split(
|
|
57066
|
+
const missingSegment = npmBinDir && !currentPath.split(path19.delimiter).includes(npmBinDir) ? npmBinDir : null;
|
|
55990
57067
|
const currentShell = detectShellFlavor();
|
|
55991
57068
|
const shellFix = buildPathFix(currentShell, missingSegment, npmBinDir);
|
|
55992
57069
|
return {
|
|
@@ -56087,9 +57164,9 @@ function checkMqtt() {
|
|
|
56087
57164
|
};
|
|
56088
57165
|
}
|
|
56089
57166
|
const file2 = configFilePath();
|
|
56090
|
-
if (
|
|
57167
|
+
if (fs24.existsSync(file2)) {
|
|
56091
57168
|
try {
|
|
56092
|
-
const cfg = JSON.parse(
|
|
57169
|
+
const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
|
|
56093
57170
|
if (cfg.token && cfg.secret) {
|
|
56094
57171
|
return {
|
|
56095
57172
|
name: "mqtt",
|
|
@@ -56116,9 +57193,9 @@ async function checkMqttProbe() {
|
|
|
56116
57193
|
creds = { token, secret };
|
|
56117
57194
|
} else {
|
|
56118
57195
|
const file2 = configFilePath();
|
|
56119
|
-
if (
|
|
57196
|
+
if (fs24.existsSync(file2)) {
|
|
56120
57197
|
try {
|
|
56121
|
-
const cfg = JSON.parse(
|
|
57198
|
+
const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
|
|
56122
57199
|
if (cfg.token && cfg.secret) {
|
|
56123
57200
|
creds = { token: cfg.token, secret: cfg.secret };
|
|
56124
57201
|
}
|
|
@@ -56232,6 +57309,112 @@ function checkNotifyConnectivity() {
|
|
|
56232
57309
|
detail: { webhookCount: webhookUrls.length, message: `${webhookUrls.length} webhook URL(s) configured (live probe not run \u2014 use --probe to test connectivity)` }
|
|
56233
57310
|
};
|
|
56234
57311
|
}
|
|
57312
|
+
async function checkLocalLlmReachable() {
|
|
57313
|
+
const policyPath = resolvePolicyPath();
|
|
57314
|
+
let loaded;
|
|
57315
|
+
try {
|
|
57316
|
+
loaded = loadPolicyFile(policyPath);
|
|
57317
|
+
} catch {
|
|
57318
|
+
return { name: "local-llm-reachable", status: "ok", detail: { present: false, message: "no policy or policy unreadable \u2014 check skipped" } };
|
|
57319
|
+
}
|
|
57320
|
+
const policy = loaded.data;
|
|
57321
|
+
const automation = policy?.automation;
|
|
57322
|
+
const ruleArr = Array.isArray(automation?.rules) ? automation.rules : [];
|
|
57323
|
+
const usesLocal = ruleArr.some((rule) => {
|
|
57324
|
+
const conds = Array.isArray(rule.conditions) ? rule.conditions : [];
|
|
57325
|
+
return conds.some((c) => {
|
|
57326
|
+
const llm = c.llm;
|
|
57327
|
+
return llm && llm.provider === "local";
|
|
57328
|
+
});
|
|
57329
|
+
});
|
|
57330
|
+
if (!usesLocal) {
|
|
57331
|
+
return { name: "local-llm-reachable", status: "ok", detail: { applicable: false, message: "no policy reference to provider:local \u2014 check skipped" } };
|
|
57332
|
+
}
|
|
57333
|
+
const baseUrl = (process.env.SWITCHBOT_LOCAL_LLM_URL ?? "http://localhost:11434/v1").replace(/\/v1\/?$/, "").replace(/\/+$/, "");
|
|
57334
|
+
const start = Date.now();
|
|
57335
|
+
try {
|
|
57336
|
+
const reachable = await probeLocalLlmEndpoint(baseUrl);
|
|
57337
|
+
const latencyMs = Date.now() - start;
|
|
57338
|
+
if (!reachable) {
|
|
57339
|
+
return {
|
|
57340
|
+
name: "local-llm-reachable",
|
|
57341
|
+
status: "fail",
|
|
57342
|
+
detail: { baseUrl, latencyMs, message: "endpoint did not respond \u2014 start your local LLM server (e.g. `ollama serve`)" }
|
|
57343
|
+
};
|
|
57344
|
+
}
|
|
57345
|
+
return { name: "local-llm-reachable", status: "ok", detail: { baseUrl, latencyMs } };
|
|
57346
|
+
} catch (err) {
|
|
57347
|
+
return {
|
|
57348
|
+
name: "local-llm-reachable",
|
|
57349
|
+
status: "fail",
|
|
57350
|
+
detail: { baseUrl, message: err instanceof Error ? err.message : String(err) }
|
|
57351
|
+
};
|
|
57352
|
+
}
|
|
57353
|
+
}
|
|
57354
|
+
async function probeLocalLlmEndpoint(baseUrl) {
|
|
57355
|
+
const httpMod = await import("node:http");
|
|
57356
|
+
const httpsMod = await import("node:https");
|
|
57357
|
+
return new Promise((resolve2) => {
|
|
57358
|
+
let url2;
|
|
57359
|
+
try {
|
|
57360
|
+
url2 = new URL(baseUrl);
|
|
57361
|
+
} catch {
|
|
57362
|
+
resolve2(false);
|
|
57363
|
+
return;
|
|
57364
|
+
}
|
|
57365
|
+
const isHttps = url2.protocol === "https:";
|
|
57366
|
+
const lib = isHttps ? httpsMod.default : httpMod.default;
|
|
57367
|
+
const req = lib.request(
|
|
57368
|
+
{
|
|
57369
|
+
hostname: url2.hostname,
|
|
57370
|
+
port: url2.port || (isHttps ? 443 : 80),
|
|
57371
|
+
path: url2.pathname || "/",
|
|
57372
|
+
method: "GET",
|
|
57373
|
+
timeout: 3e3
|
|
57374
|
+
},
|
|
57375
|
+
(res) => {
|
|
57376
|
+
res.on("data", () => {
|
|
57377
|
+
});
|
|
57378
|
+
res.on("end", () => resolve2(true));
|
|
57379
|
+
res.resume();
|
|
57380
|
+
}
|
|
57381
|
+
);
|
|
57382
|
+
req.on("error", () => resolve2(false));
|
|
57383
|
+
req.on("timeout", () => {
|
|
57384
|
+
req.destroy();
|
|
57385
|
+
resolve2(false);
|
|
57386
|
+
});
|
|
57387
|
+
req.end();
|
|
57388
|
+
});
|
|
57389
|
+
}
|
|
57390
|
+
async function checkDaemonIpc() {
|
|
57391
|
+
const daemonPid = readPidFile(DAEMON_PID_FILE);
|
|
57392
|
+
if (!daemonPid || !isPidAlive(daemonPid)) {
|
|
57393
|
+
return { name: "daemon-ipc", status: "ok", detail: { applicable: false, message: "daemon not running \u2014 check skipped" } };
|
|
57394
|
+
}
|
|
57395
|
+
try {
|
|
57396
|
+
const { IpcDaemonClient: IpcDaemonClient2 } = await Promise.resolve().then(() => (init_client3(), client_exports2));
|
|
57397
|
+
const client = new IpcDaemonClient2({ timeoutMs: 1500, connectTimeoutMs: 500 });
|
|
57398
|
+
const start = Date.now();
|
|
57399
|
+
const result = await client.ping();
|
|
57400
|
+
const latencyMs = Date.now() - start;
|
|
57401
|
+
return {
|
|
57402
|
+
name: "daemon-ipc",
|
|
57403
|
+
status: "ok",
|
|
57404
|
+
detail: {
|
|
57405
|
+
socketPath: client.getSocketPath(),
|
|
57406
|
+
latencyMs,
|
|
57407
|
+
ipcStatus: result.status
|
|
57408
|
+
}
|
|
57409
|
+
};
|
|
57410
|
+
} catch (err) {
|
|
57411
|
+
return {
|
|
57412
|
+
name: "daemon-ipc",
|
|
57413
|
+
status: "fail",
|
|
57414
|
+
detail: { message: err instanceof Error ? err.message : String(err) }
|
|
57415
|
+
};
|
|
57416
|
+
}
|
|
57417
|
+
}
|
|
56235
57418
|
var CHECK_REGISTRY = [
|
|
56236
57419
|
{ name: "node", description: "Node.js version compatibility", run: () => checkNodeVersion() },
|
|
56237
57420
|
{ name: "path", description: "switchbot binary reachable on PATH", run: () => checkPathDiscoverability() },
|
|
@@ -56256,7 +57439,9 @@ var CHECK_REGISTRY = [
|
|
|
56256
57439
|
{ name: "audit", description: "recent command errors (last 24h)", run: () => checkAudit() },
|
|
56257
57440
|
{ name: "daemon", description: "daemon state file + runtime status", run: () => checkDaemon() },
|
|
56258
57441
|
{ name: "health", description: "health endpoint availability (daemon --healthz-port)", run: () => checkHealthEndpoint() },
|
|
56259
|
-
{ name: "notify-connectivity", description: "webhook URLs from notify actions in policy.yaml", run: () => checkNotifyConnectivity() }
|
|
57442
|
+
{ name: "notify-connectivity", description: "webhook URLs from notify actions in policy.yaml", run: () => checkNotifyConnectivity() },
|
|
57443
|
+
{ name: "local-llm-reachable", description: "local LLM endpoint reachable (only when policy uses provider:local)", run: () => checkLocalLlmReachable() },
|
|
57444
|
+
{ name: "daemon-ipc", description: "daemon JSON-RPC IPC socket reachable (only when daemon is running)", run: () => checkDaemonIpc() }
|
|
56260
57445
|
];
|
|
56261
57446
|
function applyFixes(checks, writeOk) {
|
|
56262
57447
|
const results = [];
|
|
@@ -56594,9 +57779,10 @@ init_arg_parsers();
|
|
|
56594
57779
|
init_output();
|
|
56595
57780
|
init_audit();
|
|
56596
57781
|
init_devices();
|
|
56597
|
-
|
|
56598
|
-
import
|
|
56599
|
-
|
|
57782
|
+
init_history_query();
|
|
57783
|
+
import path20 from "node:path";
|
|
57784
|
+
import os19 from "node:os";
|
|
57785
|
+
var DEFAULT_AUDIT = path20.join(os19.homedir(), ".switchbot", "audit.log");
|
|
56600
57786
|
function registerHistoryCommand(program3) {
|
|
56601
57787
|
const history = program3.command("history").description("View and replay SwitchBot commands recorded via --audit-log").addHelpText("after", `
|
|
56602
57788
|
Every 'devices command' run with --audit-log is appended as JSONL to the
|
|
@@ -57454,9 +58640,9 @@ init_load();
|
|
|
57454
58640
|
init_validate();
|
|
57455
58641
|
init_types();
|
|
57456
58642
|
init_engine();
|
|
57457
|
-
import
|
|
57458
|
-
import
|
|
57459
|
-
import
|
|
58643
|
+
import fs27 from "node:fs";
|
|
58644
|
+
import os21 from "node:os";
|
|
58645
|
+
import path23 from "node:path";
|
|
57460
58646
|
|
|
57461
58647
|
// src/rules/conflict-analyzer.ts
|
|
57462
58648
|
init_cjs_shim();
|
|
@@ -57619,9 +58805,9 @@ init_client2();
|
|
|
57619
58805
|
|
|
57620
58806
|
// src/rules/webhook-token.ts
|
|
57621
58807
|
init_cjs_shim();
|
|
57622
|
-
import
|
|
57623
|
-
import
|
|
57624
|
-
import
|
|
58808
|
+
import fs25 from "node:fs";
|
|
58809
|
+
import os20 from "node:os";
|
|
58810
|
+
import path21 from "node:path";
|
|
57625
58811
|
import { randomBytes } from "node:crypto";
|
|
57626
58812
|
var ENV_TOKEN = "SWITCHBOT_WEBHOOK_TOKEN";
|
|
57627
58813
|
var DEFAULT_FILE = ".switchbot/webhook-token";
|
|
@@ -57629,7 +58815,7 @@ var WebhookTokenStore = class {
|
|
|
57629
58815
|
filePath;
|
|
57630
58816
|
envLookup;
|
|
57631
58817
|
constructor(opts = {}) {
|
|
57632
|
-
this.filePath = opts.filePath ??
|
|
58818
|
+
this.filePath = opts.filePath ?? path21.join(os20.homedir(), DEFAULT_FILE);
|
|
57633
58819
|
this.envLookup = opts.envLookup ?? (() => process.env[ENV_TOKEN]);
|
|
57634
58820
|
}
|
|
57635
58821
|
/**
|
|
@@ -57653,7 +58839,7 @@ var WebhookTokenStore = class {
|
|
|
57653
58839
|
*/
|
|
57654
58840
|
readFromDisk() {
|
|
57655
58841
|
try {
|
|
57656
|
-
const raw =
|
|
58842
|
+
const raw = fs25.readFileSync(this.filePath, "utf-8").trim();
|
|
57657
58843
|
return raw.length > 0 ? raw : null;
|
|
57658
58844
|
} catch (err) {
|
|
57659
58845
|
if (err.code === "ENOENT") return null;
|
|
@@ -57670,12 +58856,12 @@ var WebhookTokenStore = class {
|
|
|
57670
58856
|
return this.filePath;
|
|
57671
58857
|
}
|
|
57672
58858
|
writeToDisk(token) {
|
|
57673
|
-
const dir =
|
|
57674
|
-
|
|
57675
|
-
|
|
58859
|
+
const dir = path21.dirname(this.filePath);
|
|
58860
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
58861
|
+
fs25.writeFileSync(this.filePath, `${token}
|
|
57676
58862
|
`, { mode: 384 });
|
|
57677
58863
|
try {
|
|
57678
|
-
|
|
58864
|
+
fs25.chmodSync(this.filePath, 384);
|
|
57679
58865
|
} catch {
|
|
57680
58866
|
}
|
|
57681
58867
|
}
|
|
@@ -57760,18 +58946,19 @@ function aggregateRuleAudits(entries) {
|
|
|
57760
58946
|
}
|
|
57761
58947
|
|
|
57762
58948
|
// src/commands/rules.ts
|
|
57763
|
-
|
|
58949
|
+
init_history_query();
|
|
58950
|
+
var DEFAULT_AUDIT_PATH2 = path23.join(os21.homedir(), ".switchbot", "audit.log");
|
|
57764
58951
|
function loadAutomation(policyPathFlag) {
|
|
57765
|
-
const
|
|
58952
|
+
const path31 = resolvePolicyPath({ flag: policyPathFlag });
|
|
57766
58953
|
let loaded;
|
|
57767
58954
|
try {
|
|
57768
|
-
loaded = loadPolicyFile(
|
|
58955
|
+
loaded = loadPolicyFile(path31);
|
|
57769
58956
|
} catch (err) {
|
|
57770
58957
|
if (err instanceof PolicyFileNotFoundError) {
|
|
57771
58958
|
exitWithError({
|
|
57772
58959
|
code: 2,
|
|
57773
58960
|
kind: "usage",
|
|
57774
|
-
message: `policy file not found: ${
|
|
58961
|
+
message: `policy file not found: ${path31}`,
|
|
57775
58962
|
extra: { subKind: "file-not-found" }
|
|
57776
58963
|
});
|
|
57777
58964
|
}
|
|
@@ -57779,7 +58966,7 @@ function loadAutomation(policyPathFlag) {
|
|
|
57779
58966
|
exitWithError({
|
|
57780
58967
|
code: 3,
|
|
57781
58968
|
kind: "runtime",
|
|
57782
|
-
message: `YAML parse error in ${
|
|
58969
|
+
message: `YAML parse error in ${path31}: ${err.message}`,
|
|
57783
58970
|
extra: { subKind: "yaml-parse", errors: err.yamlErrors }
|
|
57784
58971
|
});
|
|
57785
58972
|
}
|
|
@@ -57791,7 +58978,7 @@ function loadAutomation(policyPathFlag) {
|
|
|
57791
58978
|
code: 4,
|
|
57792
58979
|
kind: "runtime",
|
|
57793
58980
|
message: "policy file failed schema validation. Run `switchbot policy validate` for details.",
|
|
57794
|
-
extra: { subKind: "invalid-policy", path:
|
|
58981
|
+
extra: { subKind: "invalid-policy", path: path31 }
|
|
57795
58982
|
});
|
|
57796
58983
|
}
|
|
57797
58984
|
const data = loaded.data ?? {};
|
|
@@ -57805,7 +58992,7 @@ function loadAutomation(policyPathFlag) {
|
|
|
57805
58992
|
}
|
|
57806
58993
|
const rawQH = data.quiet_hours;
|
|
57807
58994
|
const quietHours = rawQH && typeof rawQH.start === "string" && typeof rawQH.end === "string" ? { start: rawQH.start, end: rawQH.end } : null;
|
|
57808
|
-
return { path:
|
|
58995
|
+
return { path: path31, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
|
|
57809
58996
|
}
|
|
57810
58997
|
function describeTrigger(rule) {
|
|
57811
58998
|
const t = rule.when;
|
|
@@ -57940,6 +59127,8 @@ function registerRun(rules) {
|
|
|
57940
59127
|
let stopping = false;
|
|
57941
59128
|
const pidPaths = getDefaultPidFilePaths();
|
|
57942
59129
|
writePidFile(pidPaths.pidFile);
|
|
59130
|
+
const ipcStartedAt = /* @__PURE__ */ new Date();
|
|
59131
|
+
let ipcServerHandle = null;
|
|
57943
59132
|
const cleanup = () => {
|
|
57944
59133
|
clearPidFile(pidPaths.pidFile);
|
|
57945
59134
|
consumeReloadSentinel(pidPaths.reloadFile);
|
|
@@ -57948,6 +59137,12 @@ function registerRun(rules) {
|
|
|
57948
59137
|
if (stopping) return;
|
|
57949
59138
|
stopping = true;
|
|
57950
59139
|
try {
|
|
59140
|
+
if (ipcServerHandle) {
|
|
59141
|
+
try {
|
|
59142
|
+
await ipcServerHandle.close();
|
|
59143
|
+
} catch {
|
|
59144
|
+
}
|
|
59145
|
+
}
|
|
57951
59146
|
await engine.stop();
|
|
57952
59147
|
await client.disconnect();
|
|
57953
59148
|
} finally {
|
|
@@ -58006,6 +59201,34 @@ function registerRun(rules) {
|
|
|
58006
59201
|
}
|
|
58007
59202
|
}, 2e3);
|
|
58008
59203
|
reloadPoll.unref();
|
|
59204
|
+
try {
|
|
59205
|
+
const { startIpcServer: startIpcServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
59206
|
+
ipcServerHandle = await startIpcServer2({
|
|
59207
|
+
handlers: {
|
|
59208
|
+
"daemon.status": () => ({
|
|
59209
|
+
status: "running",
|
|
59210
|
+
pid: process.pid,
|
|
59211
|
+
startedAt: ipcStartedAt.toISOString(),
|
|
59212
|
+
rulesActive: engine.getStats().rulesActive,
|
|
59213
|
+
globalDryRun: opts.dryRun === true
|
|
59214
|
+
}),
|
|
59215
|
+
"daemon.ping": () => ({ ok: true, t: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
59216
|
+
"daemon.reload": async () => {
|
|
59217
|
+
await doReload("ipc");
|
|
59218
|
+
return { ok: true, rulesActive: engine.getStats().rulesActive };
|
|
59219
|
+
}
|
|
59220
|
+
},
|
|
59221
|
+
onClientError: () => {
|
|
59222
|
+
}
|
|
59223
|
+
});
|
|
59224
|
+
if (!isJsonMode()) {
|
|
59225
|
+
console.error(`IPC: listening on ${ipcServerHandle.socketPath}`);
|
|
59226
|
+
}
|
|
59227
|
+
} catch (err) {
|
|
59228
|
+
if (!isJsonMode()) {
|
|
59229
|
+
console.error(`IPC: failed to start (${err instanceof Error ? err.message : String(err)}) \u2014 daemon will run without IPC`);
|
|
59230
|
+
}
|
|
59231
|
+
}
|
|
58009
59232
|
if (!isJsonMode()) {
|
|
58010
59233
|
console.error(
|
|
58011
59234
|
`Rules engine started \u2014 ${engine.getStats().rulesActive} active rule(s), ${opts.dryRun ? "global dry-run" : "live"}.`
|
|
@@ -58068,7 +59291,7 @@ function registerTail(rules) {
|
|
|
58068
59291
|
rules.command("tail").description("Stream rule-* entries from the audit log.").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--since <duration>", "Only entries newer than this window (e.g. 1h, 30m, 7d).").option("--rule <name>", "Filter to a single rule name.").option("-f, --follow", "Keep the process open and stream new lines as they arrive.").action(async (opts) => {
|
|
58069
59292
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58070
59293
|
const sinceMs = resolveSinceMs(opts.since);
|
|
58071
|
-
const existing =
|
|
59294
|
+
const existing = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
58072
59295
|
const filtered = filterRuleAudits(existing, { sinceMs, ruleName: opts.rule });
|
|
58073
59296
|
if (isJsonMode()) {
|
|
58074
59297
|
for (const e of filtered) console.log(JSON.stringify(e));
|
|
@@ -58080,7 +59303,7 @@ function registerTail(rules) {
|
|
|
58080
59303
|
for (const e of filtered) console.log(formatAuditLine(e));
|
|
58081
59304
|
}
|
|
58082
59305
|
if (!opts.follow) return;
|
|
58083
|
-
let offset =
|
|
59306
|
+
let offset = fs27.existsSync(file2) ? fs27.statSync(file2).size : 0;
|
|
58084
59307
|
let buffer = "";
|
|
58085
59308
|
const emit = (line) => {
|
|
58086
59309
|
const trimmed = line.trim();
|
|
@@ -58097,21 +59320,21 @@ function registerTail(rules) {
|
|
|
58097
59320
|
else console.log(formatAuditLine(entry));
|
|
58098
59321
|
};
|
|
58099
59322
|
const poll = setInterval(() => {
|
|
58100
|
-
if (!
|
|
58101
|
-
const size =
|
|
59323
|
+
if (!fs27.existsSync(file2)) return;
|
|
59324
|
+
const size = fs27.statSync(file2).size;
|
|
58102
59325
|
if (size < offset) {
|
|
58103
59326
|
offset = 0;
|
|
58104
59327
|
buffer = "";
|
|
58105
59328
|
}
|
|
58106
59329
|
if (size === offset) return;
|
|
58107
|
-
const fd =
|
|
59330
|
+
const fd = fs27.openSync(file2, "r");
|
|
58108
59331
|
try {
|
|
58109
59332
|
const chunk = Buffer.alloc(size - offset);
|
|
58110
|
-
|
|
59333
|
+
fs27.readSync(fd, chunk, 0, chunk.length, offset);
|
|
58111
59334
|
offset = size;
|
|
58112
59335
|
buffer += chunk.toString("utf-8");
|
|
58113
59336
|
} finally {
|
|
58114
|
-
|
|
59337
|
+
fs27.closeSync(fd);
|
|
58115
59338
|
}
|
|
58116
59339
|
let newline = buffer.indexOf("\n");
|
|
58117
59340
|
while (newline !== -1) {
|
|
@@ -58151,7 +59374,7 @@ function formatReplayTable(report) {
|
|
|
58151
59374
|
function registerReplay(rules) {
|
|
58152
59375
|
rules.command("replay").description("Aggregate rule-* audit entries per rule (fire/throttle/error counts).").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--since <duration>", "Only entries newer than this window (e.g. 1h, 7d).").option("--rule <name>", "Filter to a single rule name.").action((opts) => {
|
|
58153
59376
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58154
|
-
const entries =
|
|
59377
|
+
const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
58155
59378
|
const sinceMs = resolveSinceMs(opts.since);
|
|
58156
59379
|
const filtered = filterRuleAudits(entries, {
|
|
58157
59380
|
sinceMs,
|
|
@@ -58271,7 +59494,7 @@ function registerSuggest(rules) {
|
|
|
58271
59494
|
for (const w2 of warnings) process.stderr.write(`warning: ${w2}
|
|
58272
59495
|
`);
|
|
58273
59496
|
if (opts.out) {
|
|
58274
|
-
|
|
59497
|
+
fs27.writeFileSync(opts.out, ruleYaml, "utf8");
|
|
58275
59498
|
if (!isJsonMode()) console.log(`\u2713 rule YAML written to ${opts.out}`);
|
|
58276
59499
|
} else if (isJsonMode()) {
|
|
58277
59500
|
printJson({ rule, rule_yaml: ruleYaml, warnings });
|
|
@@ -58346,7 +59569,7 @@ overall: ${overall ? "ok" : "issues found"}`);
|
|
|
58346
59569
|
function registerSummary(rules) {
|
|
58347
59570
|
rules.command("summary").description("Aggregate rule-* audit entries per rule over a time window (fires, throttled, errors).").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--since <duration>", "Only entries newer than this window (default: 24h). E.g. 1h, 7d.").option("--rule <name>", "Filter to a single rule name.").action((opts) => {
|
|
58348
59571
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58349
|
-
const entries =
|
|
59572
|
+
const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
58350
59573
|
const sinceMs = resolveSinceMs(opts.since ?? "24h");
|
|
58351
59574
|
const filtered = filterRuleAudits(entries, { sinceMs, ruleName: opts.rule });
|
|
58352
59575
|
const report = aggregateRuleAudits(filtered);
|
|
@@ -58377,7 +59600,7 @@ function registerLastFired(rules) {
|
|
|
58377
59600
|
rules.command("last-fired").description("Show the N most recently fired rule-fire entries from the audit log.").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2})`).option("--rule <name>", "Filter to a single rule name.").option("-n <count>", "Number of entries to show (default: 10).", (v2) => Number.parseInt(v2, 10)).action((opts) => {
|
|
58378
59601
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58379
59602
|
const n = opts.n ?? 10;
|
|
58380
|
-
const entries =
|
|
59603
|
+
const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
58381
59604
|
const fires = filterRuleAudits(entries, {
|
|
58382
59605
|
ruleName: opts.rule,
|
|
58383
59606
|
kinds: ["rule-fire", "rule-fire-dry"]
|
|
@@ -58419,7 +59642,7 @@ function registerExplain(rules) {
|
|
|
58419
59642
|
return;
|
|
58420
59643
|
}
|
|
58421
59644
|
const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58422
|
-
const entries =
|
|
59645
|
+
const entries = fs27.existsSync(auditFile) ? readAudit(auditFile) : [];
|
|
58423
59646
|
const fires = filterRuleAudits(entries, { ruleName: name, kinds: ["rule-fire", "rule-fire-dry"] });
|
|
58424
59647
|
const lastFired = fires.length > 0 ? fires[fires.length - 1].t : null;
|
|
58425
59648
|
const detail = {
|
|
@@ -58460,7 +59683,7 @@ function registerTraceExplain(rules) {
|
|
|
58460
59683
|
rules.command("trace-explain [fireId]").description("Show why a rule evaluation fired or was blocked (reads rule-evaluate trace records).").option("--rule <name>", "Filter to a specific rule name.").option("--last", "Show the most recent evaluation for the rule (requires --rule).").option("--since <duration>", "Show evaluations in this window (e.g. 1h, 7d).").option("--all", "Include evaluations that fired (default: show all evaluations).").option("--file <path>", `Audit log path (default ${DEFAULT_AUDIT_PATH2}).`).action(
|
|
58461
59684
|
(fireIdArg, opts) => {
|
|
58462
59685
|
const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58463
|
-
if (!
|
|
59686
|
+
if (!fs27.existsSync(auditFile)) {
|
|
58464
59687
|
exitWithError({ code: 1, kind: "usage", message: `Audit log not found: ${auditFile}. Make sure trace recording is enabled (automation.audit.evaluate_trace: sampled or full).` });
|
|
58465
59688
|
return;
|
|
58466
59689
|
}
|
|
@@ -58495,14 +59718,14 @@ function registerSimulate(rules) {
|
|
|
58495
59718
|
async (ruleOrPolicy, opts) => {
|
|
58496
59719
|
let rule;
|
|
58497
59720
|
const auditLog = opts.auditLog ?? DEFAULT_AUDIT_PATH2;
|
|
58498
|
-
if (!
|
|
59721
|
+
if (!fs27.existsSync(ruleOrPolicy)) {
|
|
58499
59722
|
exitWithError({ code: 2, kind: "usage", message: `File not found: ${ruleOrPolicy}` });
|
|
58500
59723
|
return;
|
|
58501
59724
|
}
|
|
58502
59725
|
let parsed;
|
|
58503
59726
|
try {
|
|
58504
59727
|
const { parse: yamlParse5 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
|
|
58505
|
-
parsed = yamlParse5(
|
|
59728
|
+
parsed = yamlParse5(fs27.readFileSync(ruleOrPolicy, "utf-8"));
|
|
58506
59729
|
} catch {
|
|
58507
59730
|
exitWithError({ code: 2, kind: "usage", message: `Could not parse YAML file: ${ruleOrPolicy}` });
|
|
58508
59731
|
return;
|
|
@@ -58533,7 +59756,7 @@ function registerSimulate(rules) {
|
|
|
58533
59756
|
liveLlm: opts.liveLlm ?? false
|
|
58534
59757
|
});
|
|
58535
59758
|
if (opts.reportOut) {
|
|
58536
|
-
|
|
59759
|
+
fs27.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
|
|
58537
59760
|
console.log(`Report written to ${opts.reportOut}`);
|
|
58538
59761
|
}
|
|
58539
59762
|
if (isJsonMode()) {
|
|
@@ -58624,10 +59847,10 @@ init_output();
|
|
|
58624
59847
|
init_arg_parsers();
|
|
58625
59848
|
init_request_context();
|
|
58626
59849
|
init_keychain();
|
|
58627
|
-
import
|
|
58628
|
-
import
|
|
58629
|
-
import
|
|
58630
|
-
import
|
|
59850
|
+
import fs28 from "node:fs";
|
|
59851
|
+
import path24 from "node:path";
|
|
59852
|
+
import os22 from "node:os";
|
|
59853
|
+
import readline6 from "node:readline";
|
|
58631
59854
|
function activeProfile() {
|
|
58632
59855
|
return getActiveProfile() ?? "default";
|
|
58633
59856
|
}
|
|
@@ -58639,7 +59862,7 @@ function maskValue2(value) {
|
|
|
58639
59862
|
return `${head}${"*".repeat(Math.max(4, value.length - 4))}${tail}`;
|
|
58640
59863
|
}
|
|
58641
59864
|
async function promptSecret2(question) {
|
|
58642
|
-
const rl =
|
|
59865
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
58643
59866
|
return new Promise((resolve2) => {
|
|
58644
59867
|
process.stderr.write(question);
|
|
58645
59868
|
const stdin = process.stdin;
|
|
@@ -58672,7 +59895,7 @@ async function promptSecret2(question) {
|
|
|
58672
59895
|
});
|
|
58673
59896
|
}
|
|
58674
59897
|
function readStdinFile(filePath) {
|
|
58675
|
-
if (!
|
|
59898
|
+
if (!fs28.existsSync(filePath)) {
|
|
58676
59899
|
exitWithError({
|
|
58677
59900
|
code: 2,
|
|
58678
59901
|
kind: "usage",
|
|
@@ -58681,7 +59904,7 @@ function readStdinFile(filePath) {
|
|
|
58681
59904
|
}
|
|
58682
59905
|
let parsed;
|
|
58683
59906
|
try {
|
|
58684
|
-
parsed = JSON.parse(
|
|
59907
|
+
parsed = JSON.parse(fs28.readFileSync(filePath, "utf-8"));
|
|
58685
59908
|
} catch (err) {
|
|
58686
59909
|
exitWithError({
|
|
58687
59910
|
code: 2,
|
|
@@ -58711,10 +59934,10 @@ function cleanupMigratedSourceFile(sourceFile, parsed) {
|
|
|
58711
59934
|
delete next.token;
|
|
58712
59935
|
delete next.secret;
|
|
58713
59936
|
if (Object.keys(next).length === 0) {
|
|
58714
|
-
|
|
59937
|
+
fs28.unlinkSync(sourceFile);
|
|
58715
59938
|
return "deleted";
|
|
58716
59939
|
}
|
|
58717
|
-
|
|
59940
|
+
fs28.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
|
|
58718
59941
|
return "scrubbed";
|
|
58719
59942
|
}
|
|
58720
59943
|
function registerAuthCommand(program3) {
|
|
@@ -58845,8 +60068,8 @@ function registerAuthCommand(program3) {
|
|
|
58845
60068
|
message: `backend "${store.name}" is not writable on this machine`
|
|
58846
60069
|
});
|
|
58847
60070
|
}
|
|
58848
|
-
const sourceFile = profile === "default" ?
|
|
58849
|
-
if (!
|
|
60071
|
+
const sourceFile = profile === "default" ? path24.join(os22.homedir(), ".switchbot", "config.json") : path24.join(os22.homedir(), ".switchbot", "profiles", `${profile}.json`);
|
|
60072
|
+
if (!fs28.existsSync(sourceFile)) {
|
|
58850
60073
|
exitWithError({
|
|
58851
60074
|
code: 2,
|
|
58852
60075
|
kind: "usage",
|
|
@@ -58856,7 +60079,7 @@ function registerAuthCommand(program3) {
|
|
|
58856
60079
|
}
|
|
58857
60080
|
let parsed;
|
|
58858
60081
|
try {
|
|
58859
|
-
const raw = JSON.parse(
|
|
60082
|
+
const raw = JSON.parse(fs28.readFileSync(sourceFile, "utf-8"));
|
|
58860
60083
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
58861
60084
|
throw new Error("expected a JSON object");
|
|
58862
60085
|
}
|
|
@@ -58918,8 +60141,8 @@ function registerAuthCommand(program3) {
|
|
|
58918
60141
|
init_cjs_shim();
|
|
58919
60142
|
init_esm();
|
|
58920
60143
|
init_load();
|
|
58921
|
-
import
|
|
58922
|
-
import
|
|
60144
|
+
import fs31 from "node:fs";
|
|
60145
|
+
import path27 from "node:path";
|
|
58923
60146
|
|
|
58924
60147
|
// src/install/steps.ts
|
|
58925
60148
|
init_cjs_shim();
|
|
@@ -58971,9 +60194,9 @@ init_cjs_shim();
|
|
|
58971
60194
|
init_load();
|
|
58972
60195
|
init_validate();
|
|
58973
60196
|
init_keychain();
|
|
58974
|
-
import
|
|
58975
|
-
import
|
|
58976
|
-
import
|
|
60197
|
+
import fs29 from "node:fs";
|
|
60198
|
+
import path25 from "node:path";
|
|
60199
|
+
import os23 from "node:os";
|
|
58977
60200
|
function parseMajor(version2) {
|
|
58978
60201
|
const m2 = /^v?(\d+)\./.exec(version2);
|
|
58979
60202
|
if (!m2) return null;
|
|
@@ -59063,10 +60286,10 @@ async function checkKeychain2() {
|
|
|
59063
60286
|
}
|
|
59064
60287
|
}
|
|
59065
60288
|
function checkHomeDirWritable() {
|
|
59066
|
-
const home =
|
|
59067
|
-
const switchbotDir =
|
|
60289
|
+
const home = os23.homedir();
|
|
60290
|
+
const switchbotDir = path25.join(home, ".switchbot");
|
|
59068
60291
|
try {
|
|
59069
|
-
const homeStat =
|
|
60292
|
+
const homeStat = fs29.statSync(home);
|
|
59070
60293
|
if (!homeStat.isDirectory()) {
|
|
59071
60294
|
return {
|
|
59072
60295
|
name: "home",
|
|
@@ -59075,8 +60298,8 @@ function checkHomeDirWritable() {
|
|
|
59075
60298
|
hint: "check your HOME/USERPROFILE environment configuration"
|
|
59076
60299
|
};
|
|
59077
60300
|
}
|
|
59078
|
-
if (
|
|
59079
|
-
const sbStat =
|
|
60301
|
+
if (fs29.existsSync(switchbotDir)) {
|
|
60302
|
+
const sbStat = fs29.statSync(switchbotDir);
|
|
59080
60303
|
if (!sbStat.isDirectory()) {
|
|
59081
60304
|
return {
|
|
59082
60305
|
name: "home",
|
|
@@ -59085,10 +60308,10 @@ function checkHomeDirWritable() {
|
|
|
59085
60308
|
hint: "move the file aside and re-run install"
|
|
59086
60309
|
};
|
|
59087
60310
|
}
|
|
59088
|
-
|
|
60311
|
+
fs29.accessSync(switchbotDir, fs29.constants.W_OK);
|
|
59089
60312
|
return { name: "home", status: "ok", message: `writable: ${switchbotDir}` };
|
|
59090
60313
|
}
|
|
59091
|
-
|
|
60314
|
+
fs29.accessSync(home, fs29.constants.W_OK);
|
|
59092
60315
|
return { name: "home", status: "ok", message: `writable: ${home}` };
|
|
59093
60316
|
} catch (err) {
|
|
59094
60317
|
return {
|
|
@@ -59102,8 +60325,8 @@ function checkHomeDirWritable() {
|
|
|
59102
60325
|
function nearestExistingPath(target) {
|
|
59103
60326
|
let cur = target;
|
|
59104
60327
|
while (true) {
|
|
59105
|
-
if (
|
|
59106
|
-
const parent =
|
|
60328
|
+
if (fs29.existsSync(cur)) return cur;
|
|
60329
|
+
const parent = path25.dirname(cur);
|
|
59107
60330
|
if (parent === cur) return null;
|
|
59108
60331
|
cur = parent;
|
|
59109
60332
|
}
|
|
@@ -59111,8 +60334,8 @@ function nearestExistingPath(target) {
|
|
|
59111
60334
|
function checkAgentSkillDirWritable(opts) {
|
|
59112
60335
|
const shouldCheck = opts.agent === "claude-code" && (opts.expectSkillLink ?? true);
|
|
59113
60336
|
if (!shouldCheck) return null;
|
|
59114
|
-
const home =
|
|
59115
|
-
const target =
|
|
60337
|
+
const home = os23.homedir();
|
|
60338
|
+
const target = path25.join(home, ".claude", "skills");
|
|
59116
60339
|
try {
|
|
59117
60340
|
const existing = nearestExistingPath(target);
|
|
59118
60341
|
if (!existing) {
|
|
@@ -59123,7 +60346,7 @@ function checkAgentSkillDirWritable(opts) {
|
|
|
59123
60346
|
hint: "check your home directory path and permissions"
|
|
59124
60347
|
};
|
|
59125
60348
|
}
|
|
59126
|
-
const stat =
|
|
60349
|
+
const stat = fs29.statSync(existing);
|
|
59127
60350
|
if (!stat.isDirectory()) {
|
|
59128
60351
|
return {
|
|
59129
60352
|
name: "agent-skills-dir",
|
|
@@ -59132,7 +60355,7 @@ function checkAgentSkillDirWritable(opts) {
|
|
|
59132
60355
|
hint: "move the blocking file aside and re-run install"
|
|
59133
60356
|
};
|
|
59134
60357
|
}
|
|
59135
|
-
|
|
60358
|
+
fs29.accessSync(existing, fs29.constants.W_OK);
|
|
59136
60359
|
return { name: "agent-skills-dir", status: "ok", message: `writable: ${target}` };
|
|
59137
60360
|
} catch (err) {
|
|
59138
60361
|
return {
|
|
@@ -59157,9 +60380,9 @@ async function runPreflight(options = {}) {
|
|
|
59157
60380
|
|
|
59158
60381
|
// src/install/default-steps.ts
|
|
59159
60382
|
init_cjs_shim();
|
|
59160
|
-
import
|
|
59161
|
-
import
|
|
59162
|
-
import
|
|
60383
|
+
import fs30 from "node:fs";
|
|
60384
|
+
import path26 from "node:path";
|
|
60385
|
+
import os24 from "node:os";
|
|
59163
60386
|
import { spawnSync } from "node:child_process";
|
|
59164
60387
|
init_keychain();
|
|
59165
60388
|
function stepPromptCredentials() {
|
|
@@ -59234,15 +60457,15 @@ function stepScaffoldPolicy() {
|
|
|
59234
60457
|
const r = ctx.policyScaffoldResult;
|
|
59235
60458
|
if (!r || r.skipped) return;
|
|
59236
60459
|
try {
|
|
59237
|
-
|
|
60460
|
+
fs30.unlinkSync(r.policyPath);
|
|
59238
60461
|
} catch {
|
|
59239
60462
|
}
|
|
59240
60463
|
}
|
|
59241
60464
|
};
|
|
59242
60465
|
}
|
|
59243
|
-
function skillLinkPathFor(agent, home =
|
|
60466
|
+
function skillLinkPathFor(agent, home = os24.homedir()) {
|
|
59244
60467
|
if (agent === "claude-code") {
|
|
59245
|
-
return
|
|
60468
|
+
return path26.join(home, ".claude", "skills", "switchbot");
|
|
59246
60469
|
}
|
|
59247
60470
|
return null;
|
|
59248
60471
|
}
|
|
@@ -59256,11 +60479,11 @@ function stepSymlinkSkill(opts = {}) {
|
|
|
59256
60479
|
ctx.skillRecipePrinted = true;
|
|
59257
60480
|
return;
|
|
59258
60481
|
}
|
|
59259
|
-
const target =
|
|
59260
|
-
if (!
|
|
60482
|
+
const target = path26.resolve(ctx.skillPath);
|
|
60483
|
+
if (!fs30.existsSync(target)) {
|
|
59261
60484
|
throw new Error(`--skill-path does not exist: ${target}`);
|
|
59262
60485
|
}
|
|
59263
|
-
const stat =
|
|
60486
|
+
const stat = fs30.statSync(target);
|
|
59264
60487
|
if (!stat.isDirectory()) {
|
|
59265
60488
|
throw new Error(`--skill-path is not a directory: ${target}`);
|
|
59266
60489
|
}
|
|
@@ -59269,17 +60492,17 @@ function stepSymlinkSkill(opts = {}) {
|
|
|
59269
60492
|
ctx.skillRecipePrinted = true;
|
|
59270
60493
|
return;
|
|
59271
60494
|
}
|
|
59272
|
-
if (!opts.force && !
|
|
60495
|
+
if (!opts.force && !fs30.existsSync(path26.join(target, "SKILL.md"))) {
|
|
59273
60496
|
throw new Error(
|
|
59274
60497
|
`${target} does not look like a skill (no SKILL.md at the root). Pass --force if you really mean to link this directory.`
|
|
59275
60498
|
);
|
|
59276
60499
|
}
|
|
59277
|
-
if (
|
|
59278
|
-
const st =
|
|
60500
|
+
if (fs30.existsSync(linkPath)) {
|
|
60501
|
+
const st = fs30.lstatSync(linkPath);
|
|
59279
60502
|
if (st.isSymbolicLink()) {
|
|
59280
60503
|
let existingTarget = null;
|
|
59281
60504
|
try {
|
|
59282
|
-
existingTarget =
|
|
60505
|
+
existingTarget = path26.resolve(path26.dirname(linkPath), fs30.readlinkSync(linkPath));
|
|
59283
60506
|
} catch {
|
|
59284
60507
|
existingTarget = null;
|
|
59285
60508
|
}
|
|
@@ -59293,23 +60516,23 @@ function stepSymlinkSkill(opts = {}) {
|
|
|
59293
60516
|
`${linkPath} already links to ${existingTarget ?? "(unreadable)"}; pass --force to replace it, or run \`switchbot uninstall\` first.`
|
|
59294
60517
|
);
|
|
59295
60518
|
}
|
|
59296
|
-
|
|
60519
|
+
fs30.unlinkSync(linkPath);
|
|
59297
60520
|
} else {
|
|
59298
60521
|
throw new Error(
|
|
59299
60522
|
`${linkPath} exists and is not a symlink; refusing to clobber (move it aside and re-run)`
|
|
59300
60523
|
);
|
|
59301
60524
|
}
|
|
59302
60525
|
}
|
|
59303
|
-
|
|
60526
|
+
fs30.mkdirSync(path26.dirname(linkPath), { recursive: true });
|
|
59304
60527
|
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
59305
|
-
|
|
60528
|
+
fs30.symlinkSync(target, linkPath, linkType);
|
|
59306
60529
|
ctx.skillLinkPath = linkPath;
|
|
59307
60530
|
ctx.skillLinkCreated = true;
|
|
59308
60531
|
},
|
|
59309
60532
|
undo(ctx) {
|
|
59310
60533
|
if (!ctx.skillLinkCreated || !ctx.skillLinkPath) return;
|
|
59311
60534
|
try {
|
|
59312
|
-
|
|
60535
|
+
fs30.unlinkSync(ctx.skillLinkPath);
|
|
59313
60536
|
} catch {
|
|
59314
60537
|
}
|
|
59315
60538
|
}
|
|
@@ -59452,8 +60675,8 @@ Examples:
|
|
|
59452
60675
|
const agent = parseAgent(opts.agent);
|
|
59453
60676
|
const profile = getActiveProfile() ?? "default";
|
|
59454
60677
|
const skip = parseSkipList(opts.skip);
|
|
59455
|
-
const skillPath = opts.skillPath ?
|
|
59456
|
-
const tokenFile = opts.tokenFile ?
|
|
60678
|
+
const skillPath = opts.skillPath ? path27.resolve(opts.skillPath) : void 0;
|
|
60679
|
+
const tokenFile = opts.tokenFile ? path27.resolve(opts.tokenFile) : void 0;
|
|
59457
60680
|
const force = Boolean(opts.force);
|
|
59458
60681
|
const verify = Boolean(opts.verify);
|
|
59459
60682
|
const globalOpts = command.parent?.opts() ?? {};
|
|
@@ -59497,7 +60720,7 @@ Examples:
|
|
|
59497
60720
|
const report = await runInstall(steps, { context: ctx });
|
|
59498
60721
|
if (report.ok && tokenFile) {
|
|
59499
60722
|
try {
|
|
59500
|
-
|
|
60723
|
+
fs31.unlinkSync(tokenFile);
|
|
59501
60724
|
} catch {
|
|
59502
60725
|
}
|
|
59503
60726
|
}
|
|
@@ -59556,8 +60779,8 @@ Examples:
|
|
|
59556
60779
|
init_cjs_shim();
|
|
59557
60780
|
init_esm();
|
|
59558
60781
|
init_load();
|
|
59559
|
-
import
|
|
59560
|
-
import
|
|
60782
|
+
import fs32 from "node:fs";
|
|
60783
|
+
import readline7 from "node:readline";
|
|
59561
60784
|
init_keychain();
|
|
59562
60785
|
init_output();
|
|
59563
60786
|
init_request_context();
|
|
@@ -59572,7 +60795,7 @@ function parseAgent2(value) {
|
|
|
59572
60795
|
}
|
|
59573
60796
|
async function prompt(question, defaultYes) {
|
|
59574
60797
|
if (!process.stdin.isTTY) return defaultYes;
|
|
59575
|
-
const rl =
|
|
60798
|
+
const rl = readline7.createInterface({ input: process.stdin, output: process.stderr });
|
|
59576
60799
|
return new Promise((resolve2) => {
|
|
59577
60800
|
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
59578
60801
|
rl.question(question + suffix, (ans) => {
|
|
@@ -59621,10 +60844,10 @@ Examples:
|
|
|
59621
60844
|
action: "remove-skill-link",
|
|
59622
60845
|
detail: skillLink,
|
|
59623
60846
|
run: async () => {
|
|
59624
|
-
if (!
|
|
60847
|
+
if (!fs32.existsSync(skillLink)) {
|
|
59625
60848
|
return { action: "remove-skill-link", status: "absent", detail: skillLink };
|
|
59626
60849
|
}
|
|
59627
|
-
const stat =
|
|
60850
|
+
const stat = fs32.lstatSync(skillLink);
|
|
59628
60851
|
if (!stat.isSymbolicLink()) {
|
|
59629
60852
|
return {
|
|
59630
60853
|
action: "remove-skill-link",
|
|
@@ -59635,7 +60858,7 @@ Examples:
|
|
|
59635
60858
|
const ok = yes ? true : await prompt(`Remove skill link ${skillLink}?`, true);
|
|
59636
60859
|
if (!ok) return { action: "remove-skill-link", status: "skipped", detail: skillLink };
|
|
59637
60860
|
try {
|
|
59638
|
-
|
|
60861
|
+
fs32.unlinkSync(skillLink);
|
|
59639
60862
|
return { action: "remove-skill-link", status: "removed", detail: skillLink };
|
|
59640
60863
|
} catch (err) {
|
|
59641
60864
|
return {
|
|
@@ -59690,13 +60913,13 @@ Examples:
|
|
|
59690
60913
|
detail: "pass --remove-policy to delete policy.yaml"
|
|
59691
60914
|
};
|
|
59692
60915
|
}
|
|
59693
|
-
if (!
|
|
60916
|
+
if (!fs32.existsSync(policyPath)) {
|
|
59694
60917
|
return { action: "remove-policy", status: "absent", detail: policyPath };
|
|
59695
60918
|
}
|
|
59696
60919
|
const ok = yes ? true : await prompt(`Delete policy file ${policyPath}?`, false);
|
|
59697
60920
|
if (!ok) return { action: "remove-policy", status: "skipped", detail: policyPath };
|
|
59698
60921
|
try {
|
|
59699
|
-
|
|
60922
|
+
fs32.unlinkSync(policyPath);
|
|
59700
60923
|
return { action: "remove-policy", status: "removed", detail: policyPath };
|
|
59701
60924
|
} catch (err) {
|
|
59702
60925
|
return {
|
|
@@ -59759,9 +60982,9 @@ init_request_context();
|
|
|
59759
60982
|
init_output();
|
|
59760
60983
|
init_flags();
|
|
59761
60984
|
import { spawn as spawn4, spawnSync as spawnSync2 } from "node:child_process";
|
|
59762
|
-
import
|
|
59763
|
-
import
|
|
59764
|
-
import
|
|
60985
|
+
import fs33 from "node:fs";
|
|
60986
|
+
import os25 from "node:os";
|
|
60987
|
+
import path28 from "node:path";
|
|
59765
60988
|
var DEFAULT_OPENCLAW_URL = "http://localhost:18789";
|
|
59766
60989
|
function resolveStatusSyncRuntime(options) {
|
|
59767
60990
|
if (!tryLoadConfig()) {
|
|
@@ -59895,14 +61118,14 @@ async function probeStatusSyncStart(options = {}) {
|
|
|
59895
61118
|
};
|
|
59896
61119
|
}
|
|
59897
61120
|
function resolveStatusSyncPaths(explicitStateDir) {
|
|
59898
|
-
const stateDir =
|
|
59899
|
-
explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ??
|
|
61121
|
+
const stateDir = path28.resolve(
|
|
61122
|
+
explicitStateDir ?? process.env.SWITCHBOT_STATUS_SYNC_HOME ?? path28.join(os25.homedir(), ".switchbot", "status-sync")
|
|
59900
61123
|
);
|
|
59901
61124
|
return {
|
|
59902
61125
|
stateDir,
|
|
59903
|
-
stateFile:
|
|
59904
|
-
stdoutLog:
|
|
59905
|
-
stderrLog:
|
|
61126
|
+
stateFile: path28.join(stateDir, "state.json"),
|
|
61127
|
+
stdoutLog: path28.join(stateDir, "stdout.log"),
|
|
61128
|
+
stderrLog: path28.join(stateDir, "stderr.log")
|
|
59906
61129
|
};
|
|
59907
61130
|
}
|
|
59908
61131
|
function buildStatusSyncChildArgs(options) {
|
|
@@ -59910,11 +61133,11 @@ function buildStatusSyncChildArgs(options) {
|
|
|
59910
61133
|
if (!scriptPath) {
|
|
59911
61134
|
throw new Error("Cannot determine the current CLI entrypoint path.");
|
|
59912
61135
|
}
|
|
59913
|
-
const args = [
|
|
61136
|
+
const args = [path28.resolve(scriptPath)];
|
|
59914
61137
|
const configPath = getConfigPath();
|
|
59915
61138
|
const profile = getActiveProfile();
|
|
59916
61139
|
if (configPath) {
|
|
59917
|
-
args.push("--config",
|
|
61140
|
+
args.push("--config", path28.resolve(configPath));
|
|
59918
61141
|
} else if (profile) {
|
|
59919
61142
|
args.push("--profile", profile);
|
|
59920
61143
|
}
|
|
@@ -59935,7 +61158,7 @@ function buildStatusSyncChildArgs(options) {
|
|
|
59935
61158
|
}
|
|
59936
61159
|
function safeUnlink(filePath) {
|
|
59937
61160
|
try {
|
|
59938
|
-
|
|
61161
|
+
fs33.unlinkSync(filePath);
|
|
59939
61162
|
} catch {
|
|
59940
61163
|
}
|
|
59941
61164
|
}
|
|
@@ -59950,9 +61173,9 @@ function isProcessRunning(pid) {
|
|
|
59950
61173
|
}
|
|
59951
61174
|
}
|
|
59952
61175
|
function readStateFile(paths) {
|
|
59953
|
-
if (!
|
|
61176
|
+
if (!fs33.existsSync(paths.stateFile)) return null;
|
|
59954
61177
|
try {
|
|
59955
|
-
const raw = JSON.parse(
|
|
61178
|
+
const raw = JSON.parse(fs33.readFileSync(paths.stateFile, "utf-8"));
|
|
59956
61179
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
59957
61180
|
safeUnlink(paths.stateFile);
|
|
59958
61181
|
return null;
|
|
@@ -60071,14 +61294,14 @@ function startStatusSync(options = {}) {
|
|
|
60071
61294
|
}
|
|
60072
61295
|
stopStatusSync({ stateDir: paths.stateDir });
|
|
60073
61296
|
}
|
|
60074
|
-
|
|
61297
|
+
fs33.mkdirSync(paths.stateDir, { recursive: true });
|
|
60075
61298
|
const configPath = getConfigPath();
|
|
60076
61299
|
const command = buildStatusSyncChildArgs(runtime);
|
|
60077
61300
|
let stdoutFd = null;
|
|
60078
61301
|
let stderrFd = null;
|
|
60079
61302
|
try {
|
|
60080
|
-
stdoutFd =
|
|
60081
|
-
stderrFd =
|
|
61303
|
+
stdoutFd = fs33.openSync(paths.stdoutLog, "a");
|
|
61304
|
+
stderrFd = fs33.openSync(paths.stderrLog, "a");
|
|
60082
61305
|
const child = spawn4(process.execPath, command, {
|
|
60083
61306
|
detached: true,
|
|
60084
61307
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
@@ -60096,16 +61319,16 @@ function startStatusSync(options = {}) {
|
|
|
60096
61319
|
openclawUrl: runtime.openclawUrl,
|
|
60097
61320
|
openclawModel: runtime.openclawModel,
|
|
60098
61321
|
topic: runtime.topic ?? null,
|
|
60099
|
-
configPath: configPath ?
|
|
61322
|
+
configPath: configPath ? path28.resolve(configPath) : null,
|
|
60100
61323
|
profile: configPath ? null : getActiveProfile() ?? null,
|
|
60101
61324
|
stdoutLog: paths.stdoutLog,
|
|
60102
61325
|
stderrLog: paths.stderrLog
|
|
60103
61326
|
};
|
|
60104
|
-
|
|
61327
|
+
fs33.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
|
|
60105
61328
|
return toStatus(paths, state, true);
|
|
60106
61329
|
} finally {
|
|
60107
|
-
if (stdoutFd !== null)
|
|
60108
|
-
if (stderrFd !== null)
|
|
61330
|
+
if (stdoutFd !== null) fs33.closeSync(stdoutFd);
|
|
61331
|
+
if (stderrFd !== null) fs33.closeSync(stderrFd);
|
|
60109
61332
|
}
|
|
60110
61333
|
}
|
|
60111
61334
|
async function runStatusSyncForeground(options = {}) {
|
|
@@ -60245,17 +61468,17 @@ Examples:
|
|
|
60245
61468
|
// src/commands/health.ts
|
|
60246
61469
|
init_cjs_shim();
|
|
60247
61470
|
init_output();
|
|
60248
|
-
import
|
|
61471
|
+
import http7 from "node:http";
|
|
60249
61472
|
|
|
60250
61473
|
// src/utils/health.ts
|
|
60251
61474
|
init_cjs_shim();
|
|
60252
61475
|
init_quota();
|
|
60253
61476
|
init_audit();
|
|
60254
61477
|
init_client();
|
|
60255
|
-
import
|
|
60256
|
-
import
|
|
60257
|
-
import
|
|
60258
|
-
var DEFAULT_AUDIT_PATH3 =
|
|
61478
|
+
import fs34 from "node:fs";
|
|
61479
|
+
import os26 from "node:os";
|
|
61480
|
+
import path29 from "node:path";
|
|
61481
|
+
var DEFAULT_AUDIT_PATH3 = path29.join(os26.homedir(), ".switchbot", "audit.log");
|
|
60259
61482
|
var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
60260
61483
|
var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set([161, 171, 190]);
|
|
60261
61484
|
function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
|
|
@@ -60277,7 +61500,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
|
|
|
60277
61500
|
status: pct >= 90 ? "critical" : pct >= 70 ? "warn" : "ok"
|
|
60278
61501
|
};
|
|
60279
61502
|
let auditHealth;
|
|
60280
|
-
if (!
|
|
61503
|
+
if (!fs34.existsSync(auditPath)) {
|
|
60281
61504
|
auditHealth = {
|
|
60282
61505
|
present: false,
|
|
60283
61506
|
recentErrors: 0,
|
|
@@ -60447,7 +61670,7 @@ Example:
|
|
|
60447
61670
|
const opts = cmd.optsWithGlobals();
|
|
60448
61671
|
const port = parseInt(opts.port, 10);
|
|
60449
61672
|
const handler = createHealthHandler(opts.auditLog);
|
|
60450
|
-
const server =
|
|
61673
|
+
const server = http7.createServer(handler);
|
|
60451
61674
|
server.on("error", (err) => {
|
|
60452
61675
|
if (err.code === "EADDRINUSE") {
|
|
60453
61676
|
handleError(Object.assign(new Error(`Port ${port} is already in use. Choose a different port with --port.`), { code: err.code }));
|
|
@@ -60478,13 +61701,13 @@ Example:
|
|
|
60478
61701
|
init_cjs_shim();
|
|
60479
61702
|
init_output();
|
|
60480
61703
|
init_source();
|
|
60481
|
-
import
|
|
61704
|
+
import https6 from "node:https";
|
|
60482
61705
|
var pkgName = "@switchbot/openapi-cli";
|
|
60483
61706
|
function fetchLatestVersion(packageName, timeoutMs = 8e3) {
|
|
60484
61707
|
const encoded = packageName.replace("/", "%2F");
|
|
60485
61708
|
const url2 = `https://registry.npmjs.org/${encoded}/latest`;
|
|
60486
61709
|
return new Promise((resolve2, reject) => {
|
|
60487
|
-
const req =
|
|
61710
|
+
const req = https6.get(url2, { timeout: timeoutMs }, (res) => {
|
|
60488
61711
|
const chunks = [];
|
|
60489
61712
|
res.on("data", (c) => chunks.push(c));
|
|
60490
61713
|
res.on("end", () => {
|
|
@@ -60581,8 +61804,8 @@ function registerUpgradeCheckCommand(program3) {
|
|
|
60581
61804
|
init_cjs_shim();
|
|
60582
61805
|
init_output();
|
|
60583
61806
|
import { spawn as spawn5 } from "node:child_process";
|
|
60584
|
-
import
|
|
60585
|
-
import
|
|
61807
|
+
import fs35 from "node:fs";
|
|
61808
|
+
import path30 from "node:path";
|
|
60586
61809
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
60587
61810
|
init_arg_parsers();
|
|
60588
61811
|
init_source();
|
|
@@ -60644,7 +61867,7 @@ function persistState(partial2) {
|
|
|
60644
61867
|
}
|
|
60645
61868
|
function readLastLines(filePath, n = 20) {
|
|
60646
61869
|
try {
|
|
60647
|
-
const content =
|
|
61870
|
+
const content = fs35.readFileSync(filePath, "utf-8");
|
|
60648
61871
|
const lines = content.split("\n");
|
|
60649
61872
|
return lines.slice(Math.max(0, lines.length - n)).join("\n").trim();
|
|
60650
61873
|
} catch {
|
|
@@ -60734,10 +61957,10 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60734
61957
|
}
|
|
60735
61958
|
}
|
|
60736
61959
|
const thisFile = fileURLToPath3(import.meta.url);
|
|
60737
|
-
const cliEntry =
|
|
61960
|
+
const cliEntry = path30.basename(thisFile) === "index.js" ? thisFile : path30.resolve(path30.dirname(thisFile), "..", "index.js");
|
|
60738
61961
|
const args = ["rules", "run"];
|
|
60739
61962
|
if (opts.policy) args.push(opts.policy);
|
|
60740
|
-
|
|
61963
|
+
fs35.mkdirSync(path30.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
|
|
60741
61964
|
persistState({
|
|
60742
61965
|
status: "starting",
|
|
60743
61966
|
pid: null,
|
|
@@ -60749,14 +61972,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60749
61972
|
healthzPid: null,
|
|
60750
61973
|
healthzPidFile: HEALTHZ_PID_FILE
|
|
60751
61974
|
});
|
|
60752
|
-
const logFd =
|
|
61975
|
+
const logFd = fs35.openSync(DAEMON_LOG_FILE, "a");
|
|
60753
61976
|
const child = spawn5(process.execPath, [cliEntry, ...args], {
|
|
60754
61977
|
detached: true,
|
|
60755
61978
|
stdio: ["ignore", logFd, logFd],
|
|
60756
61979
|
env: { ...process.env }
|
|
60757
61980
|
});
|
|
60758
61981
|
child.unref();
|
|
60759
|
-
|
|
61982
|
+
fs35.closeSync(logFd);
|
|
60760
61983
|
await probeLiveness({
|
|
60761
61984
|
child,
|
|
60762
61985
|
delayMs: 300,
|
|
@@ -60779,14 +62002,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60779
62002
|
let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
|
|
60780
62003
|
if (healthzPort !== null) {
|
|
60781
62004
|
const healthArgs = ["health", "serve", "--port", String(healthzPort)];
|
|
60782
|
-
const healthLogFd =
|
|
62005
|
+
const healthLogFd = fs35.openSync(DAEMON_LOG_FILE, "a");
|
|
60783
62006
|
const healthChild = spawn5(process.execPath, [cliEntry, ...healthArgs], {
|
|
60784
62007
|
detached: true,
|
|
60785
62008
|
stdio: ["ignore", healthLogFd, healthLogFd],
|
|
60786
62009
|
env: { ...process.env }
|
|
60787
62010
|
});
|
|
60788
62011
|
healthChild.unref();
|
|
60789
|
-
|
|
62012
|
+
fs35.closeSync(healthLogFd);
|
|
60790
62013
|
if (healthChild.pid) {
|
|
60791
62014
|
const healthAlive = await probeLiveness({ child: healthChild, delayMs: 200, fatal: false });
|
|
60792
62015
|
if (healthAlive) {
|
|
@@ -60849,11 +62072,11 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60849
62072
|
});
|
|
60850
62073
|
}
|
|
60851
62074
|
try {
|
|
60852
|
-
|
|
62075
|
+
fs35.unlinkSync(DAEMON_PID_FILE);
|
|
60853
62076
|
} catch {
|
|
60854
62077
|
}
|
|
60855
62078
|
try {
|
|
60856
|
-
|
|
62079
|
+
fs35.unlinkSync(HEALTHZ_PID_FILE);
|
|
60857
62080
|
} catch {
|
|
60858
62081
|
}
|
|
60859
62082
|
persistState({
|