@switchbot/openapi-cli 3.6.0 → 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 +1954 -623
- package/dist/policy/schema/v0.2.json +33 -1
- package/package.json +3 -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
|
}
|
|
@@ -7797,7 +7797,9 @@ var init_catalog = __esm({
|
|
|
7797
7797
|
moveDetected: "Motion detected (boolean)",
|
|
7798
7798
|
openState: "Contact sensor open/closed",
|
|
7799
7799
|
status: "Device-specific status word",
|
|
7800
|
-
lightLevel: "Ambient light level"
|
|
7800
|
+
lightLevel: "Ambient light level",
|
|
7801
|
+
detected: "Presence detected (boolean)",
|
|
7802
|
+
hubDeviceId: "Hub device identifier for hub-bound devices"
|
|
7801
7803
|
};
|
|
7802
7804
|
onOff = [
|
|
7803
7805
|
{ command: "turnOn", parameter: "\u2014", description: "Power on", idempotent: true },
|
|
@@ -7851,9 +7853,9 @@ var init_catalog = __esm({
|
|
|
7851
7853
|
{
|
|
7852
7854
|
type: "Smart Lock",
|
|
7853
7855
|
category: "physical",
|
|
7854
|
-
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").',
|
|
7855
7857
|
role: "security",
|
|
7856
|
-
aliases: ["Lock", "Smart Lock Pro", "Lock Pro"],
|
|
7858
|
+
aliases: ["Lock", "Smart Lock Pro", "Lock Pro", "Smart Lock Pro Wifi"],
|
|
7857
7859
|
commands: [
|
|
7858
7860
|
{ command: "lock", parameter: "\u2014", description: "Lock the door", idempotent: true },
|
|
7859
7861
|
{ command: "unlock", parameter: "\u2014", description: "Unlock the door", idempotent: true, safetyTier: "destructive", safetyReason: "Physically unlocks the door \u2014 anyone nearby can open it." },
|
|
@@ -7886,6 +7888,29 @@ var init_catalog = __esm({
|
|
|
7886
7888
|
],
|
|
7887
7889
|
statusFields: ["battery", "version", "lockState", "doorState", "calibrate"]
|
|
7888
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
|
+
},
|
|
7889
7914
|
{
|
|
7890
7915
|
type: "Plug",
|
|
7891
7916
|
category: "physical",
|
|
@@ -7918,12 +7943,12 @@ var init_catalog = __esm({
|
|
|
7918
7943
|
{
|
|
7919
7944
|
type: "Relay Switch 2PM",
|
|
7920
7945
|
category: "physical",
|
|
7921
|
-
description:
|
|
7946
|
+
description: 'Dual-channel relay switch with per-channel on/off/toggle and optional roller-shade mode. IMPORTANT: turnOn/turnOff/toggle require a channel parameter ("1" or "2") \u2014 omitting it is a usage error.',
|
|
7922
7947
|
role: "power",
|
|
7923
7948
|
commands: [
|
|
7924
|
-
{ command: "turnOn", parameter: "1 | 2 (channel)", description: "Turn on channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
|
|
7925
|
-
{ command: "turnOff", parameter: "1 | 2 (channel)", description: "Turn off channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
|
|
7926
|
-
{ command: "toggle", parameter: "1 | 2 (channel)", description: "Toggle channel 1 or 2", idempotent: false, exampleParams: ["1", "2"] },
|
|
7949
|
+
{ command: "turnOn", parameter: "1 | 2 (channel \u2014 required)", description: "Turn on channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
|
|
7950
|
+
{ command: "turnOff", parameter: "1 | 2 (channel \u2014 required)", description: "Turn off channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
|
|
7951
|
+
{ command: "toggle", parameter: "1 | 2 (channel \u2014 required)", description: "Toggle channel 1 or 2", idempotent: false, exampleParams: ["1", "2"] },
|
|
7927
7952
|
{ command: "setMode", parameter: '"<channel>;<mode>" e.g. "1;0"', description: "Per-channel mode (see Relay Switch 1 modes)", idempotent: true, exampleParams: ["1;0", "2;3"] },
|
|
7928
7953
|
{ command: "setPosition", parameter: "0-100 (roller percentage)", description: "Roller-shade-pair mode only", idempotent: true, exampleParams: ["0", "50", "100"] }
|
|
7929
7954
|
],
|
|
@@ -7945,7 +7970,7 @@ var init_catalog = __esm({
|
|
|
7945
7970
|
category: "physical",
|
|
7946
7971
|
description: "Evaporative humidifier with multiple speed/auto/sleep/humidity modes and child lock.",
|
|
7947
7972
|
role: "climate",
|
|
7948
|
-
aliases: ["Evaporative Humidifier"],
|
|
7973
|
+
aliases: ["Evaporative Humidifier", "Evaporative Humidifier (Auto-refill)"],
|
|
7949
7974
|
commands: [
|
|
7950
7975
|
...onOff,
|
|
7951
7976
|
{ command: "setMode", parameter: `'{"mode":1-8,"targetHumidify":0-100}'`, description: "mode: 1=lv4 2=lv3 3=lv2 4=lv1 5=humidity 6=sleep 7=auto 8=drying", idempotent: true, exampleParams: ['{"mode":7,"targetHumidify":50}'] },
|
|
@@ -8064,7 +8089,7 @@ var init_catalog = __esm({
|
|
|
8064
8089
|
category: "physical",
|
|
8065
8090
|
description: "Entry-level robot vacuum with start/stop/dock and four suction power levels.",
|
|
8066
8091
|
role: "cleaning",
|
|
8067
|
-
aliases: ["Robot Vacuum", "Robot Vacuum Cleaner S1 Plus", "K10+", "K10+ Pro"],
|
|
8092
|
+
aliases: ["Robot Vacuum", "S1", "S1 Plus", "Robot Vacuum Cleaner S1 Plus", "K10+", "K10+ Pro"],
|
|
8068
8093
|
commands: [
|
|
8069
8094
|
{ command: "start", parameter: "\u2014", description: "Start cleaning", idempotent: true },
|
|
8070
8095
|
{ command: "stop", parameter: "\u2014", description: "Stop cleaning", idempotent: true },
|
|
@@ -8078,7 +8103,7 @@ var init_catalog = __esm({
|
|
|
8078
8103
|
category: "physical",
|
|
8079
8104
|
description: "Compact robot vacuum and mop combo with sweep/mop sessions, fan level, and water level.",
|
|
8080
8105
|
role: "cleaning",
|
|
8081
|
-
aliases: ["K10+ Pro Combo", "K20+ Pro", "K11+", "Robot Vacuum Cleaner K11+"],
|
|
8106
|
+
aliases: ["K10+ Pro Combo", "K20+ Pro", "Robot Vacuum Cleaner K20 Plus Pro", "K11+", "Robot Vacuum Cleaner K11+"],
|
|
8082
8107
|
commands: [
|
|
8083
8108
|
{ command: "startClean", parameter: `'{"action":"sweep"|"mop","param":{"fanLevel":1-4,"times":1-2639999}}'`, description: "Begin a cleaning session", idempotent: false, exampleParams: ['{"action":"sweep","param":{"fanLevel":2,"times":1}}'] },
|
|
8084
8109
|
{ command: "pause", parameter: "\u2014", description: "Pause cleaning", idempotent: true },
|
|
@@ -8093,7 +8118,7 @@ var init_catalog = __esm({
|
|
|
8093
8118
|
category: "physical",
|
|
8094
8119
|
description: "Advanced floor cleaning robot with sweep/mop modes, self-wash dock, and humidifier refill.",
|
|
8095
8120
|
role: "cleaning",
|
|
8096
|
-
aliases: ["Robot Vacuum Cleaner S10", "Robot Vacuum Cleaner S20", "S20"],
|
|
8121
|
+
aliases: ["S10", "Robot Vacuum Cleaner S10", "Robot Vacuum Cleaner S20", "S20"],
|
|
8097
8122
|
commands: [
|
|
8098
8123
|
{ command: "startClean", parameter: `'{"action":"sweep"|"sweep_mop","param":{"fanLevel":1-4,"waterLevel":1-2,"times":1-2639999}}'`, description: "Begin a cleaning session", idempotent: false, exampleParams: ['{"action":"sweep","param":{"fanLevel":2,"waterLevel":1,"times":1}}'] },
|
|
8099
8124
|
{ command: "pause", parameter: "\u2014", description: "Pause cleaning", idempotent: true },
|
|
@@ -8193,11 +8218,12 @@ var init_catalog = __esm({
|
|
|
8193
8218
|
{
|
|
8194
8219
|
type: "AI Art Frame",
|
|
8195
8220
|
category: "physical",
|
|
8196
|
-
description: "Digital art frame that can switch
|
|
8221
|
+
description: "Digital art frame that can switch images and accept new artwork uploads.",
|
|
8197
8222
|
role: "other",
|
|
8198
8223
|
commands: [
|
|
8199
8224
|
{ command: "next", parameter: "\u2014", description: "Switch to the next image", idempotent: false },
|
|
8200
|
-
{ 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"] }
|
|
8201
8227
|
],
|
|
8202
8228
|
statusFields: ["version"]
|
|
8203
8229
|
},
|
|
@@ -8208,10 +8234,31 @@ var init_catalog = __esm({
|
|
|
8208
8234
|
description: "Battery-powered temperature and humidity sensor; read-only, no control commands.",
|
|
8209
8235
|
role: "sensor",
|
|
8210
8236
|
readOnly: true,
|
|
8211
|
-
aliases: [
|
|
8237
|
+
aliases: [
|
|
8238
|
+
"MeterPlus",
|
|
8239
|
+
"Meter Plus",
|
|
8240
|
+
"Meter Plus (JP)",
|
|
8241
|
+
"Meter Plus (US)",
|
|
8242
|
+
"Outdoor Meter",
|
|
8243
|
+
"MeterPro",
|
|
8244
|
+
"Meter Pro",
|
|
8245
|
+
"MeterPro(CO2)",
|
|
8246
|
+
"Meter Pro (CO2 Monitor)",
|
|
8247
|
+
"WoIOSensor"
|
|
8248
|
+
],
|
|
8212
8249
|
commands: [],
|
|
8213
8250
|
statusFields: ["temperature", "humidity", "battery", "version"]
|
|
8214
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
|
+
},
|
|
8215
8262
|
{
|
|
8216
8263
|
type: "Motion Sensor",
|
|
8217
8264
|
category: "physical",
|
|
@@ -8221,6 +8268,16 @@ var init_catalog = __esm({
|
|
|
8221
8268
|
commands: [],
|
|
8222
8269
|
statusFields: ["battery", "version", "moveDetected", "brightness", "openState"]
|
|
8223
8270
|
},
|
|
8271
|
+
{
|
|
8272
|
+
type: "Presence Sensor",
|
|
8273
|
+
category: "physical",
|
|
8274
|
+
description: "Presence detector that reports occupancy and ambient light through a paired hub; read-only.",
|
|
8275
|
+
role: "sensor",
|
|
8276
|
+
readOnly: true,
|
|
8277
|
+
aliases: ["Human Presence Sensor", "Prensence Sensor"],
|
|
8278
|
+
commands: [],
|
|
8279
|
+
statusFields: ["version", "battery", "lightLevel", "detected", "hubDeviceId"]
|
|
8280
|
+
},
|
|
8224
8281
|
{
|
|
8225
8282
|
type: "Contact Sensor",
|
|
8226
8283
|
category: "physical",
|
|
@@ -8256,7 +8313,7 @@ var init_catalog = __esm({
|
|
|
8256
8313
|
description: "IR hub that bridges BLE devices to the cloud and learns IR remotes; no direct control commands.",
|
|
8257
8314
|
role: "hub",
|
|
8258
8315
|
readOnly: true,
|
|
8259
|
-
aliases: ["Hub Mini2"],
|
|
8316
|
+
aliases: ["Hub", "Hub Plus", "Hub Mini2"],
|
|
8260
8317
|
commands: [],
|
|
8261
8318
|
statusFields: ["version"]
|
|
8262
8319
|
},
|
|
@@ -8305,6 +8362,41 @@ var init_catalog = __esm({
|
|
|
8305
8362
|
commands: [],
|
|
8306
8363
|
statusFields: ["battery", "version"]
|
|
8307
8364
|
},
|
|
8365
|
+
{
|
|
8366
|
+
type: "Indoor Cam",
|
|
8367
|
+
category: "physical",
|
|
8368
|
+
description: "Indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
|
|
8369
|
+
role: "security",
|
|
8370
|
+
readOnly: true,
|
|
8371
|
+
commands: []
|
|
8372
|
+
},
|
|
8373
|
+
{
|
|
8374
|
+
type: "Pan/Tilt Cam",
|
|
8375
|
+
category: "physical",
|
|
8376
|
+
description: "Pan/tilt indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
|
|
8377
|
+
role: "security",
|
|
8378
|
+
readOnly: true,
|
|
8379
|
+
aliases: ["Pan/Tilt Cam 2K"],
|
|
8380
|
+
commands: []
|
|
8381
|
+
},
|
|
8382
|
+
{
|
|
8383
|
+
type: "Pan/Tilt Cam Plus 3K",
|
|
8384
|
+
category: "physical",
|
|
8385
|
+
description: "Pan/tilt indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
|
|
8386
|
+
role: "security",
|
|
8387
|
+
readOnly: true,
|
|
8388
|
+
aliases: ["Pan/Tilt Cam Plus 2K", "Pan/Tilt Cam Plus", "Pan Tilt Cam Plus 3K"],
|
|
8389
|
+
commands: []
|
|
8390
|
+
},
|
|
8391
|
+
{
|
|
8392
|
+
type: "Remote",
|
|
8393
|
+
category: "physical",
|
|
8394
|
+
description: "Bluetooth wireless button remote paired to a hub or device; listed by the cloud API but exposes no cloud status or control commands.",
|
|
8395
|
+
role: "other",
|
|
8396
|
+
readOnly: true,
|
|
8397
|
+
aliases: ["SwitchBot Remote", "Remote Button", "Wireless Remote"],
|
|
8398
|
+
commands: []
|
|
8399
|
+
},
|
|
8308
8400
|
// ---------- Virtual IR remotes ----------
|
|
8309
8401
|
{
|
|
8310
8402
|
type: "Air Conditioner",
|
|
@@ -9463,6 +9555,161 @@ var init_credential = __esm({
|
|
|
9463
9555
|
}
|
|
9464
9556
|
});
|
|
9465
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
|
+
|
|
9466
9713
|
// node_modules/yaml/dist/nodes/identity.js
|
|
9467
9714
|
var require_identity = __commonJS({
|
|
9468
9715
|
"node_modules/yaml/dist/nodes/identity.js"(exports) {
|
|
@@ -9542,17 +9789,17 @@ var require_visit = __commonJS({
|
|
|
9542
9789
|
visit.BREAK = BREAK;
|
|
9543
9790
|
visit.SKIP = SKIP;
|
|
9544
9791
|
visit.REMOVE = REMOVE;
|
|
9545
|
-
function visit_(key, node, visitor,
|
|
9546
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
9792
|
+
function visit_(key, node, visitor, path31) {
|
|
9793
|
+
const ctrl = callVisitor(key, node, visitor, path31);
|
|
9547
9794
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
9548
|
-
replaceNode(key,
|
|
9549
|
-
return visit_(key, ctrl, visitor,
|
|
9795
|
+
replaceNode(key, path31, ctrl);
|
|
9796
|
+
return visit_(key, ctrl, visitor, path31);
|
|
9550
9797
|
}
|
|
9551
9798
|
if (typeof ctrl !== "symbol") {
|
|
9552
9799
|
if (identity.isCollection(node)) {
|
|
9553
|
-
|
|
9800
|
+
path31 = Object.freeze(path31.concat(node));
|
|
9554
9801
|
for (let i = 0; i < node.items.length; ++i) {
|
|
9555
|
-
const ci = visit_(i, node.items[i], visitor,
|
|
9802
|
+
const ci = visit_(i, node.items[i], visitor, path31);
|
|
9556
9803
|
if (typeof ci === "number")
|
|
9557
9804
|
i = ci - 1;
|
|
9558
9805
|
else if (ci === BREAK)
|
|
@@ -9563,13 +9810,13 @@ var require_visit = __commonJS({
|
|
|
9563
9810
|
}
|
|
9564
9811
|
}
|
|
9565
9812
|
} else if (identity.isPair(node)) {
|
|
9566
|
-
|
|
9567
|
-
const ck = visit_("key", node.key, visitor,
|
|
9813
|
+
path31 = Object.freeze(path31.concat(node));
|
|
9814
|
+
const ck = visit_("key", node.key, visitor, path31);
|
|
9568
9815
|
if (ck === BREAK)
|
|
9569
9816
|
return BREAK;
|
|
9570
9817
|
else if (ck === REMOVE)
|
|
9571
9818
|
node.key = null;
|
|
9572
|
-
const cv = visit_("value", node.value, visitor,
|
|
9819
|
+
const cv = visit_("value", node.value, visitor, path31);
|
|
9573
9820
|
if (cv === BREAK)
|
|
9574
9821
|
return BREAK;
|
|
9575
9822
|
else if (cv === REMOVE)
|
|
@@ -9590,17 +9837,17 @@ var require_visit = __commonJS({
|
|
|
9590
9837
|
visitAsync.BREAK = BREAK;
|
|
9591
9838
|
visitAsync.SKIP = SKIP;
|
|
9592
9839
|
visitAsync.REMOVE = REMOVE;
|
|
9593
|
-
async function visitAsync_(key, node, visitor,
|
|
9594
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
9840
|
+
async function visitAsync_(key, node, visitor, path31) {
|
|
9841
|
+
const ctrl = await callVisitor(key, node, visitor, path31);
|
|
9595
9842
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
9596
|
-
replaceNode(key,
|
|
9597
|
-
return visitAsync_(key, ctrl, visitor,
|
|
9843
|
+
replaceNode(key, path31, ctrl);
|
|
9844
|
+
return visitAsync_(key, ctrl, visitor, path31);
|
|
9598
9845
|
}
|
|
9599
9846
|
if (typeof ctrl !== "symbol") {
|
|
9600
9847
|
if (identity.isCollection(node)) {
|
|
9601
|
-
|
|
9848
|
+
path31 = Object.freeze(path31.concat(node));
|
|
9602
9849
|
for (let i = 0; i < node.items.length; ++i) {
|
|
9603
|
-
const ci = await visitAsync_(i, node.items[i], visitor,
|
|
9850
|
+
const ci = await visitAsync_(i, node.items[i], visitor, path31);
|
|
9604
9851
|
if (typeof ci === "number")
|
|
9605
9852
|
i = ci - 1;
|
|
9606
9853
|
else if (ci === BREAK)
|
|
@@ -9611,13 +9858,13 @@ var require_visit = __commonJS({
|
|
|
9611
9858
|
}
|
|
9612
9859
|
}
|
|
9613
9860
|
} else if (identity.isPair(node)) {
|
|
9614
|
-
|
|
9615
|
-
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);
|
|
9616
9863
|
if (ck === BREAK)
|
|
9617
9864
|
return BREAK;
|
|
9618
9865
|
else if (ck === REMOVE)
|
|
9619
9866
|
node.key = null;
|
|
9620
|
-
const cv = await visitAsync_("value", node.value, visitor,
|
|
9867
|
+
const cv = await visitAsync_("value", node.value, visitor, path31);
|
|
9621
9868
|
if (cv === BREAK)
|
|
9622
9869
|
return BREAK;
|
|
9623
9870
|
else if (cv === REMOVE)
|
|
@@ -9644,23 +9891,23 @@ var require_visit = __commonJS({
|
|
|
9644
9891
|
}
|
|
9645
9892
|
return visitor;
|
|
9646
9893
|
}
|
|
9647
|
-
function callVisitor(key, node, visitor,
|
|
9894
|
+
function callVisitor(key, node, visitor, path31) {
|
|
9648
9895
|
if (typeof visitor === "function")
|
|
9649
|
-
return visitor(key, node,
|
|
9896
|
+
return visitor(key, node, path31);
|
|
9650
9897
|
if (identity.isMap(node))
|
|
9651
|
-
return visitor.Map?.(key, node,
|
|
9898
|
+
return visitor.Map?.(key, node, path31);
|
|
9652
9899
|
if (identity.isSeq(node))
|
|
9653
|
-
return visitor.Seq?.(key, node,
|
|
9900
|
+
return visitor.Seq?.(key, node, path31);
|
|
9654
9901
|
if (identity.isPair(node))
|
|
9655
|
-
return visitor.Pair?.(key, node,
|
|
9902
|
+
return visitor.Pair?.(key, node, path31);
|
|
9656
9903
|
if (identity.isScalar(node))
|
|
9657
|
-
return visitor.Scalar?.(key, node,
|
|
9904
|
+
return visitor.Scalar?.(key, node, path31);
|
|
9658
9905
|
if (identity.isAlias(node))
|
|
9659
|
-
return visitor.Alias?.(key, node,
|
|
9906
|
+
return visitor.Alias?.(key, node, path31);
|
|
9660
9907
|
return void 0;
|
|
9661
9908
|
}
|
|
9662
|
-
function replaceNode(key,
|
|
9663
|
-
const parent =
|
|
9909
|
+
function replaceNode(key, path31, node) {
|
|
9910
|
+
const parent = path31[path31.length - 1];
|
|
9664
9911
|
if (identity.isCollection(parent)) {
|
|
9665
9912
|
parent.items[key] = node;
|
|
9666
9913
|
} else if (identity.isPair(parent)) {
|
|
@@ -10277,10 +10524,10 @@ var require_Collection = __commonJS({
|
|
|
10277
10524
|
var createNode = require_createNode();
|
|
10278
10525
|
var identity = require_identity();
|
|
10279
10526
|
var Node = require_Node();
|
|
10280
|
-
function collectionFromPath(schema2,
|
|
10527
|
+
function collectionFromPath(schema2, path31, value) {
|
|
10281
10528
|
let v2 = value;
|
|
10282
|
-
for (let i =
|
|
10283
|
-
const k2 =
|
|
10529
|
+
for (let i = path31.length - 1; i >= 0; --i) {
|
|
10530
|
+
const k2 = path31[i];
|
|
10284
10531
|
if (typeof k2 === "number" && Number.isInteger(k2) && k2 >= 0) {
|
|
10285
10532
|
const a = [];
|
|
10286
10533
|
a[k2] = v2;
|
|
@@ -10299,7 +10546,7 @@ var require_Collection = __commonJS({
|
|
|
10299
10546
|
sourceObjects: /* @__PURE__ */ new Map()
|
|
10300
10547
|
});
|
|
10301
10548
|
}
|
|
10302
|
-
var isEmptyPath = (
|
|
10549
|
+
var isEmptyPath = (path31) => path31 == null || typeof path31 === "object" && !!path31[Symbol.iterator]().next().done;
|
|
10303
10550
|
var Collection = class extends Node.NodeBase {
|
|
10304
10551
|
constructor(type2, schema2) {
|
|
10305
10552
|
super(type2);
|
|
@@ -10329,11 +10576,11 @@ var require_Collection = __commonJS({
|
|
|
10329
10576
|
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
|
10330
10577
|
* that already exists in the map.
|
|
10331
10578
|
*/
|
|
10332
|
-
addIn(
|
|
10333
|
-
if (isEmptyPath(
|
|
10579
|
+
addIn(path31, value) {
|
|
10580
|
+
if (isEmptyPath(path31))
|
|
10334
10581
|
this.add(value);
|
|
10335
10582
|
else {
|
|
10336
|
-
const [key, ...rest] =
|
|
10583
|
+
const [key, ...rest] = path31;
|
|
10337
10584
|
const node = this.get(key, true);
|
|
10338
10585
|
if (identity.isCollection(node))
|
|
10339
10586
|
node.addIn(rest, value);
|
|
@@ -10347,8 +10594,8 @@ var require_Collection = __commonJS({
|
|
|
10347
10594
|
* Removes a value from the collection.
|
|
10348
10595
|
* @returns `true` if the item was found and removed.
|
|
10349
10596
|
*/
|
|
10350
|
-
deleteIn(
|
|
10351
|
-
const [key, ...rest] =
|
|
10597
|
+
deleteIn(path31) {
|
|
10598
|
+
const [key, ...rest] = path31;
|
|
10352
10599
|
if (rest.length === 0)
|
|
10353
10600
|
return this.delete(key);
|
|
10354
10601
|
const node = this.get(key, true);
|
|
@@ -10362,8 +10609,8 @@ var require_Collection = __commonJS({
|
|
|
10362
10609
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
10363
10610
|
* `true` (collections are always returned intact).
|
|
10364
10611
|
*/
|
|
10365
|
-
getIn(
|
|
10366
|
-
const [key, ...rest] =
|
|
10612
|
+
getIn(path31, keepScalar) {
|
|
10613
|
+
const [key, ...rest] = path31;
|
|
10367
10614
|
const node = this.get(key, true);
|
|
10368
10615
|
if (rest.length === 0)
|
|
10369
10616
|
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
|
@@ -10381,8 +10628,8 @@ var require_Collection = __commonJS({
|
|
|
10381
10628
|
/**
|
|
10382
10629
|
* Checks if the collection includes a value with the key `key`.
|
|
10383
10630
|
*/
|
|
10384
|
-
hasIn(
|
|
10385
|
-
const [key, ...rest] =
|
|
10631
|
+
hasIn(path31) {
|
|
10632
|
+
const [key, ...rest] = path31;
|
|
10386
10633
|
if (rest.length === 0)
|
|
10387
10634
|
return this.has(key);
|
|
10388
10635
|
const node = this.get(key, true);
|
|
@@ -10392,8 +10639,8 @@ var require_Collection = __commonJS({
|
|
|
10392
10639
|
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
|
10393
10640
|
* boolean to add/remove the item from the set.
|
|
10394
10641
|
*/
|
|
10395
|
-
setIn(
|
|
10396
|
-
const [key, ...rest] =
|
|
10642
|
+
setIn(path31, value) {
|
|
10643
|
+
const [key, ...rest] = path31;
|
|
10397
10644
|
if (rest.length === 0) {
|
|
10398
10645
|
this.set(key, value);
|
|
10399
10646
|
} else {
|
|
@@ -12940,9 +13187,9 @@ var require_Document = __commonJS({
|
|
|
12940
13187
|
this.contents.add(value);
|
|
12941
13188
|
}
|
|
12942
13189
|
/** Adds a value to the document. */
|
|
12943
|
-
addIn(
|
|
13190
|
+
addIn(path31, value) {
|
|
12944
13191
|
if (assertCollection(this.contents))
|
|
12945
|
-
this.contents.addIn(
|
|
13192
|
+
this.contents.addIn(path31, value);
|
|
12946
13193
|
}
|
|
12947
13194
|
/**
|
|
12948
13195
|
* Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
|
@@ -13017,14 +13264,14 @@ var require_Document = __commonJS({
|
|
|
13017
13264
|
* Removes a value from the document.
|
|
13018
13265
|
* @returns `true` if the item was found and removed.
|
|
13019
13266
|
*/
|
|
13020
|
-
deleteIn(
|
|
13021
|
-
if (Collection.isEmptyPath(
|
|
13267
|
+
deleteIn(path31) {
|
|
13268
|
+
if (Collection.isEmptyPath(path31)) {
|
|
13022
13269
|
if (this.contents == null)
|
|
13023
13270
|
return false;
|
|
13024
13271
|
this.contents = null;
|
|
13025
13272
|
return true;
|
|
13026
13273
|
}
|
|
13027
|
-
return assertCollection(this.contents) ? this.contents.deleteIn(
|
|
13274
|
+
return assertCollection(this.contents) ? this.contents.deleteIn(path31) : false;
|
|
13028
13275
|
}
|
|
13029
13276
|
/**
|
|
13030
13277
|
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
|
@@ -13039,10 +13286,10 @@ var require_Document = __commonJS({
|
|
|
13039
13286
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
13040
13287
|
* `true` (collections are always returned intact).
|
|
13041
13288
|
*/
|
|
13042
|
-
getIn(
|
|
13043
|
-
if (Collection.isEmptyPath(
|
|
13289
|
+
getIn(path31, keepScalar) {
|
|
13290
|
+
if (Collection.isEmptyPath(path31))
|
|
13044
13291
|
return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
|
|
13045
|
-
return identity.isCollection(this.contents) ? this.contents.getIn(
|
|
13292
|
+
return identity.isCollection(this.contents) ? this.contents.getIn(path31, keepScalar) : void 0;
|
|
13046
13293
|
}
|
|
13047
13294
|
/**
|
|
13048
13295
|
* Checks if the document includes a value with the key `key`.
|
|
@@ -13053,10 +13300,10 @@ var require_Document = __commonJS({
|
|
|
13053
13300
|
/**
|
|
13054
13301
|
* Checks if the document includes a value at `path`.
|
|
13055
13302
|
*/
|
|
13056
|
-
hasIn(
|
|
13057
|
-
if (Collection.isEmptyPath(
|
|
13303
|
+
hasIn(path31) {
|
|
13304
|
+
if (Collection.isEmptyPath(path31))
|
|
13058
13305
|
return this.contents !== void 0;
|
|
13059
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
13306
|
+
return identity.isCollection(this.contents) ? this.contents.hasIn(path31) : false;
|
|
13060
13307
|
}
|
|
13061
13308
|
/**
|
|
13062
13309
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
@@ -13073,13 +13320,13 @@ var require_Document = __commonJS({
|
|
|
13073
13320
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
13074
13321
|
* boolean to add/remove the item from the set.
|
|
13075
13322
|
*/
|
|
13076
|
-
setIn(
|
|
13077
|
-
if (Collection.isEmptyPath(
|
|
13323
|
+
setIn(path31, value) {
|
|
13324
|
+
if (Collection.isEmptyPath(path31)) {
|
|
13078
13325
|
this.contents = value;
|
|
13079
13326
|
} else if (this.contents == null) {
|
|
13080
|
-
this.contents = Collection.collectionFromPath(this.schema, Array.from(
|
|
13327
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path31), value);
|
|
13081
13328
|
} else if (assertCollection(this.contents)) {
|
|
13082
|
-
this.contents.setIn(
|
|
13329
|
+
this.contents.setIn(path31, value);
|
|
13083
13330
|
}
|
|
13084
13331
|
}
|
|
13085
13332
|
/**
|
|
@@ -15056,9 +15303,9 @@ var require_cst_visit = __commonJS({
|
|
|
15056
15303
|
visit.BREAK = BREAK;
|
|
15057
15304
|
visit.SKIP = SKIP;
|
|
15058
15305
|
visit.REMOVE = REMOVE;
|
|
15059
|
-
visit.itemAtPath = (cst,
|
|
15306
|
+
visit.itemAtPath = (cst, path31) => {
|
|
15060
15307
|
let item = cst;
|
|
15061
|
-
for (const [field, index] of
|
|
15308
|
+
for (const [field, index] of path31) {
|
|
15062
15309
|
const tok = item?.[field];
|
|
15063
15310
|
if (tok && "items" in tok) {
|
|
15064
15311
|
item = tok.items[index];
|
|
@@ -15067,23 +15314,23 @@ var require_cst_visit = __commonJS({
|
|
|
15067
15314
|
}
|
|
15068
15315
|
return item;
|
|
15069
15316
|
};
|
|
15070
|
-
visit.parentCollection = (cst,
|
|
15071
|
-
const parent = visit.itemAtPath(cst,
|
|
15072
|
-
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];
|
|
15073
15320
|
const coll = parent?.[field];
|
|
15074
15321
|
if (coll && "items" in coll)
|
|
15075
15322
|
return coll;
|
|
15076
15323
|
throw new Error("Parent collection not found");
|
|
15077
15324
|
};
|
|
15078
|
-
function _visit(
|
|
15079
|
-
let ctrl = visitor(item,
|
|
15325
|
+
function _visit(path31, item, visitor) {
|
|
15326
|
+
let ctrl = visitor(item, path31);
|
|
15080
15327
|
if (typeof ctrl === "symbol")
|
|
15081
15328
|
return ctrl;
|
|
15082
15329
|
for (const field of ["key", "value"]) {
|
|
15083
15330
|
const token = item[field];
|
|
15084
15331
|
if (token && "items" in token) {
|
|
15085
15332
|
for (let i = 0; i < token.items.length; ++i) {
|
|
15086
|
-
const ci = _visit(Object.freeze(
|
|
15333
|
+
const ci = _visit(Object.freeze(path31.concat([[field, i]])), token.items[i], visitor);
|
|
15087
15334
|
if (typeof ci === "number")
|
|
15088
15335
|
i = ci - 1;
|
|
15089
15336
|
else if (ci === BREAK)
|
|
@@ -15094,10 +15341,10 @@ var require_cst_visit = __commonJS({
|
|
|
15094
15341
|
}
|
|
15095
15342
|
}
|
|
15096
15343
|
if (typeof ctrl === "function" && field === "key")
|
|
15097
|
-
ctrl = ctrl(item,
|
|
15344
|
+
ctrl = ctrl(item, path31);
|
|
15098
15345
|
}
|
|
15099
15346
|
}
|
|
15100
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
15347
|
+
return typeof ctrl === "function" ? ctrl(item, path31) : ctrl;
|
|
15101
15348
|
}
|
|
15102
15349
|
exports.visit = visit;
|
|
15103
15350
|
}
|
|
@@ -16386,14 +16633,14 @@ var require_parser = __commonJS({
|
|
|
16386
16633
|
case "scalar":
|
|
16387
16634
|
case "single-quoted-scalar":
|
|
16388
16635
|
case "double-quoted-scalar": {
|
|
16389
|
-
const
|
|
16636
|
+
const fs36 = this.flowScalar(this.type);
|
|
16390
16637
|
if (atNextItem || it.value) {
|
|
16391
|
-
map3.items.push({ start, key:
|
|
16638
|
+
map3.items.push({ start, key: fs36, sep: [] });
|
|
16392
16639
|
this.onKeyLine = true;
|
|
16393
16640
|
} else if (it.sep) {
|
|
16394
|
-
this.stack.push(
|
|
16641
|
+
this.stack.push(fs36);
|
|
16395
16642
|
} else {
|
|
16396
|
-
Object.assign(it, { key:
|
|
16643
|
+
Object.assign(it, { key: fs36, sep: [] });
|
|
16397
16644
|
this.onKeyLine = true;
|
|
16398
16645
|
}
|
|
16399
16646
|
return;
|
|
@@ -16521,13 +16768,13 @@ var require_parser = __commonJS({
|
|
|
16521
16768
|
case "scalar":
|
|
16522
16769
|
case "single-quoted-scalar":
|
|
16523
16770
|
case "double-quoted-scalar": {
|
|
16524
|
-
const
|
|
16771
|
+
const fs36 = this.flowScalar(this.type);
|
|
16525
16772
|
if (!it || it.value)
|
|
16526
|
-
fc.items.push({ start: [], key:
|
|
16773
|
+
fc.items.push({ start: [], key: fs36, sep: [] });
|
|
16527
16774
|
else if (it.sep)
|
|
16528
|
-
this.stack.push(
|
|
16775
|
+
this.stack.push(fs36);
|
|
16529
16776
|
else
|
|
16530
|
-
Object.assign(it, { key:
|
|
16777
|
+
Object.assign(it, { key: fs36, sep: [] });
|
|
16531
16778
|
return;
|
|
16532
16779
|
}
|
|
16533
16780
|
case "flow-map-end":
|
|
@@ -20126,8 +20373,8 @@ var require_utils2 = __commonJS({
|
|
|
20126
20373
|
}
|
|
20127
20374
|
return ind;
|
|
20128
20375
|
}
|
|
20129
|
-
function removeDotSegments(
|
|
20130
|
-
let input =
|
|
20376
|
+
function removeDotSegments(path31) {
|
|
20377
|
+
let input = path31;
|
|
20131
20378
|
const output = [];
|
|
20132
20379
|
let nextSlash = -1;
|
|
20133
20380
|
let len = 0;
|
|
@@ -20327,8 +20574,8 @@ var require_schemes = __commonJS({
|
|
|
20327
20574
|
wsComponent.secure = void 0;
|
|
20328
20575
|
}
|
|
20329
20576
|
if (wsComponent.resourceName) {
|
|
20330
|
-
const [
|
|
20331
|
-
wsComponent.path =
|
|
20577
|
+
const [path31, query] = wsComponent.resourceName.split("?");
|
|
20578
|
+
wsComponent.path = path31 && path31 !== "/" ? path31 : void 0;
|
|
20332
20579
|
wsComponent.query = query;
|
|
20333
20580
|
wsComponent.resourceName = void 0;
|
|
20334
20581
|
}
|
|
@@ -20387,7 +20634,7 @@ var require_schemes = __commonJS({
|
|
|
20387
20634
|
urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
|
|
20388
20635
|
return urnComponent;
|
|
20389
20636
|
}
|
|
20390
|
-
var
|
|
20637
|
+
var http8 = (
|
|
20391
20638
|
/** @type {SchemeHandler} */
|
|
20392
20639
|
{
|
|
20393
20640
|
scheme: "http",
|
|
@@ -20396,11 +20643,11 @@ var require_schemes = __commonJS({
|
|
|
20396
20643
|
serialize: httpSerialize
|
|
20397
20644
|
}
|
|
20398
20645
|
);
|
|
20399
|
-
var
|
|
20646
|
+
var https7 = (
|
|
20400
20647
|
/** @type {SchemeHandler} */
|
|
20401
20648
|
{
|
|
20402
20649
|
scheme: "https",
|
|
20403
|
-
domainHost:
|
|
20650
|
+
domainHost: http8.domainHost,
|
|
20404
20651
|
parse: httpParse,
|
|
20405
20652
|
serialize: httpSerialize
|
|
20406
20653
|
}
|
|
@@ -20444,8 +20691,8 @@ var require_schemes = __commonJS({
|
|
|
20444
20691
|
var SCHEMES = (
|
|
20445
20692
|
/** @type {Record<SchemeName, SchemeHandler>} */
|
|
20446
20693
|
{
|
|
20447
|
-
http:
|
|
20448
|
-
https:
|
|
20694
|
+
http: http8,
|
|
20695
|
+
https: https7,
|
|
20449
20696
|
ws,
|
|
20450
20697
|
wss,
|
|
20451
20698
|
urn,
|
|
@@ -24135,6 +24382,10 @@ function isNotCondition(c) {
|
|
|
24135
24382
|
function isLlmCondition(c) {
|
|
24136
24383
|
return c.llm !== void 0 && typeof c.llm === "object";
|
|
24137
24384
|
}
|
|
24385
|
+
function isEventCountCondition(c) {
|
|
24386
|
+
const ec = c.event_count;
|
|
24387
|
+
return ec !== void 0 && typeof ec === "object" && typeof ec.device === "string";
|
|
24388
|
+
}
|
|
24138
24389
|
var init_types = __esm({
|
|
24139
24390
|
"src/rules/types.ts"() {
|
|
24140
24391
|
"use strict";
|
|
@@ -24418,7 +24669,7 @@ function locateError(doc, lineCounter, err) {
|
|
|
24418
24669
|
return { line: pos.line, col: pos.col };
|
|
24419
24670
|
}
|
|
24420
24671
|
function humanMessage(err) {
|
|
24421
|
-
const
|
|
24672
|
+
const path31 = err.instancePath || "(root)";
|
|
24422
24673
|
switch (err.keyword) {
|
|
24423
24674
|
case "required":
|
|
24424
24675
|
return `missing required property "${err.params.missingProperty}"`;
|
|
@@ -24426,21 +24677,21 @@ function humanMessage(err) {
|
|
|
24426
24677
|
return `unknown property "${err.params.additionalProperty}"`;
|
|
24427
24678
|
case "dependentRequired": {
|
|
24428
24679
|
const { property, missingProperty } = err.params;
|
|
24429
|
-
const parent =
|
|
24680
|
+
const parent = path31 === "(root)" ? "" : `${path31}: `;
|
|
24430
24681
|
return `${parent}when "${property}" is set, "${missingProperty}" is also required`;
|
|
24431
24682
|
}
|
|
24432
24683
|
case "pattern":
|
|
24433
|
-
return `${
|
|
24684
|
+
return `${path31} does not match pattern ${err.params.pattern}`;
|
|
24434
24685
|
case "const":
|
|
24435
|
-
return `${
|
|
24686
|
+
return `${path31} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
|
|
24436
24687
|
case "enum":
|
|
24437
|
-
return `${
|
|
24688
|
+
return `${path31} must be one of ${JSON.stringify(err.params.allowedValues)}`;
|
|
24438
24689
|
case "type":
|
|
24439
|
-
return `${
|
|
24690
|
+
return `${path31} must be ${err.params.type}`;
|
|
24440
24691
|
case "not":
|
|
24441
|
-
return `${
|
|
24692
|
+
return `${path31} is not allowed here`;
|
|
24442
24693
|
default:
|
|
24443
|
-
return `${
|
|
24694
|
+
return `${path31} ${err.message ?? "is invalid"}`;
|
|
24444
24695
|
}
|
|
24445
24696
|
}
|
|
24446
24697
|
function hintFor(err) {
|
|
@@ -24496,8 +24747,8 @@ function escapeJsonPointerSegment(segment) {
|
|
|
24496
24747
|
function isPlausibleDeviceId(value) {
|
|
24497
24748
|
return HEX_MAC_DEVICE_ID_RE.test(value) || HYPHENATED_DEVICE_ID_RE.test(value);
|
|
24498
24749
|
}
|
|
24499
|
-
function hasErrorAtPath(errors,
|
|
24500
|
-
return errors.some((err) => err.path ===
|
|
24750
|
+
function hasErrorAtPath(errors, path31) {
|
|
24751
|
+
return errors.some((err) => err.path === path31);
|
|
24501
24752
|
}
|
|
24502
24753
|
function resolvePolicyDeviceRef(raw, aliases) {
|
|
24503
24754
|
if (!raw) return { ok: false, reason: "missing-device" };
|
|
@@ -24520,25 +24771,25 @@ function isDeviceStateConditionLike(value) {
|
|
|
24520
24771
|
const candidate = value;
|
|
24521
24772
|
return typeof candidate.device === "string" && typeof candidate.field === "string" && typeof candidate.op === "string";
|
|
24522
24773
|
}
|
|
24523
|
-
function collectConditionDeviceRefs(condition,
|
|
24774
|
+
function collectConditionDeviceRefs(condition, path31) {
|
|
24524
24775
|
if (!condition || typeof condition !== "object" || Array.isArray(condition)) return [];
|
|
24525
24776
|
const out = [];
|
|
24526
24777
|
if (isDeviceStateConditionLike(condition)) {
|
|
24527
|
-
out.push({ path: `${
|
|
24778
|
+
out.push({ path: `${path31}/device`, ref: condition.device });
|
|
24528
24779
|
}
|
|
24529
24780
|
const candidate = condition;
|
|
24530
24781
|
if (Array.isArray(candidate.all)) {
|
|
24531
24782
|
for (let i = 0; i < candidate.all.length; i++) {
|
|
24532
|
-
out.push(...collectConditionDeviceRefs(candidate.all[i], `${
|
|
24783
|
+
out.push(...collectConditionDeviceRefs(candidate.all[i], `${path31}/all/${i}`));
|
|
24533
24784
|
}
|
|
24534
24785
|
}
|
|
24535
24786
|
if (Array.isArray(candidate.any)) {
|
|
24536
24787
|
for (let i = 0; i < candidate.any.length; i++) {
|
|
24537
|
-
out.push(...collectConditionDeviceRefs(candidate.any[i], `${
|
|
24788
|
+
out.push(...collectConditionDeviceRefs(candidate.any[i], `${path31}/any/${i}`));
|
|
24538
24789
|
}
|
|
24539
24790
|
}
|
|
24540
24791
|
if (candidate.not !== void 0) {
|
|
24541
|
-
out.push(...collectConditionDeviceRefs(candidate.not, `${
|
|
24792
|
+
out.push(...collectConditionDeviceRefs(candidate.not, `${path31}/not`));
|
|
24542
24793
|
}
|
|
24543
24794
|
return out;
|
|
24544
24795
|
}
|
|
@@ -24585,12 +24836,12 @@ function collectOfflineSemanticErrors(loaded, existingErrors) {
|
|
|
24585
24836
|
const out = [];
|
|
24586
24837
|
const aliases = collectAliasMap(data);
|
|
24587
24838
|
for (const [aliasName, deviceId] of Object.entries(aliases)) {
|
|
24588
|
-
const
|
|
24589
|
-
if (hasErrorAtPath(existingErrors,
|
|
24839
|
+
const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
|
|
24840
|
+
if (hasErrorAtPath(existingErrors, path31)) continue;
|
|
24590
24841
|
if (isPlausibleDeviceId(deviceId)) continue;
|
|
24591
|
-
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter,
|
|
24842
|
+
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
|
|
24592
24843
|
out.push({
|
|
24593
|
-
path:
|
|
24844
|
+
path: path31,
|
|
24594
24845
|
line,
|
|
24595
24846
|
col,
|
|
24596
24847
|
keyword: "alias-device-id",
|
|
@@ -24724,12 +24975,12 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
|
|
|
24724
24975
|
inventoryById.set(remote.deviceId, { typeName: remote.remoteType });
|
|
24725
24976
|
}
|
|
24726
24977
|
for (const [aliasName, deviceId] of Object.entries(aliases)) {
|
|
24727
|
-
const
|
|
24728
|
-
if (hasErrorAtPath(errors,
|
|
24978
|
+
const path31 = `/aliases/${escapeJsonPointerSegment(aliasName)}`;
|
|
24979
|
+
if (hasErrorAtPath(errors, path31)) continue;
|
|
24729
24980
|
if (!inventoryById.has(deviceId)) {
|
|
24730
|
-
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter,
|
|
24981
|
+
const { line, col } = locateInstancePath(loaded.doc, loaded.lineCounter, path31);
|
|
24731
24982
|
errors.push({
|
|
24732
|
-
path:
|
|
24983
|
+
path: path31,
|
|
24733
24984
|
line,
|
|
24734
24985
|
col,
|
|
24735
24986
|
keyword: "alias-live-device-not-found",
|
|
@@ -24793,10 +25044,10 @@ function validateLoadedPolicyAgainstInventory(loaded, inventory) {
|
|
|
24793
25044
|
if (!effectiveDeviceId) continue;
|
|
24794
25045
|
const target = inventoryById.get(effectiveDeviceId);
|
|
24795
25046
|
if (!target) {
|
|
24796
|
-
const
|
|
24797
|
-
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);
|
|
24798
25049
|
errors.push({
|
|
24799
|
-
path:
|
|
25050
|
+
path: path31,
|
|
24800
25051
|
line: line2,
|
|
24801
25052
|
col: col2,
|
|
24802
25053
|
keyword: "rule-live-device-not-found",
|
|
@@ -24902,6 +25153,32 @@ var init_validate = __esm({
|
|
|
24902
25153
|
}
|
|
24903
25154
|
});
|
|
24904
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
|
+
|
|
24905
25182
|
// src/llm/providers/openai.ts
|
|
24906
25183
|
import https from "node:https";
|
|
24907
25184
|
import http from "node:http";
|
|
@@ -24910,9 +25187,11 @@ var init_openai = __esm({
|
|
|
24910
25187
|
"src/llm/providers/openai.ts"() {
|
|
24911
25188
|
"use strict";
|
|
24912
25189
|
init_cjs_shim();
|
|
25190
|
+
init_pricing();
|
|
24913
25191
|
OpenAIProvider = class {
|
|
24914
25192
|
name = "openai";
|
|
24915
25193
|
model;
|
|
25194
|
+
capabilities = { toolUse: true };
|
|
24916
25195
|
apiKey;
|
|
24917
25196
|
baseUrl;
|
|
24918
25197
|
timeoutMs;
|
|
@@ -25037,7 +25316,10 @@ var init_openai = __esm({
|
|
|
25037
25316
|
if (!toolCall) throw new Error("OpenAI decide: no tool call in response");
|
|
25038
25317
|
const args = JSON.parse(toolCall.function.arguments);
|
|
25039
25318
|
if (typeof args.pass !== "boolean") throw new Error("OpenAI decide: malformed function-call response");
|
|
25040
|
-
|
|
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 };
|
|
25041
25323
|
}
|
|
25042
25324
|
};
|
|
25043
25325
|
}
|
|
@@ -25050,9 +25332,11 @@ var init_anthropic = __esm({
|
|
|
25050
25332
|
"src/llm/providers/anthropic.ts"() {
|
|
25051
25333
|
"use strict";
|
|
25052
25334
|
init_cjs_shim();
|
|
25335
|
+
init_pricing();
|
|
25053
25336
|
AnthropicProvider = class {
|
|
25054
25337
|
name = "anthropic";
|
|
25055
25338
|
model;
|
|
25339
|
+
capabilities = { toolUse: true };
|
|
25056
25340
|
apiKey;
|
|
25057
25341
|
timeoutMs;
|
|
25058
25342
|
maxTokens;
|
|
@@ -25167,7 +25451,352 @@ var init_anthropic = __esm({
|
|
|
25167
25451
|
if (!toolUse?.input || typeof toolUse.input.pass !== "boolean") {
|
|
25168
25452
|
throw new Error("Anthropic decide: malformed tool-use response");
|
|
25169
25453
|
}
|
|
25170
|
-
|
|
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;
|
|
25171
25800
|
}
|
|
25172
25801
|
};
|
|
25173
25802
|
}
|
|
@@ -25209,8 +25838,9 @@ __export(llm_exports, {
|
|
|
25209
25838
|
scoreIntentComplexity: () => scoreIntentComplexity
|
|
25210
25839
|
});
|
|
25211
25840
|
function createLLMProvider(backend, opts = {}) {
|
|
25212
|
-
if (backend === "openai"
|
|
25841
|
+
if (backend === "openai") return new OpenAIProvider(opts);
|
|
25213
25842
|
if (backend === "anthropic") return new AnthropicProvider(opts);
|
|
25843
|
+
if (backend === "local") return new LocalProvider(opts);
|
|
25214
25844
|
throw new Error(`Unknown LLM backend: ${backend}`);
|
|
25215
25845
|
}
|
|
25216
25846
|
var LLM_AUTO_THRESHOLD;
|
|
@@ -25220,6 +25850,7 @@ var init_llm = __esm({
|
|
|
25220
25850
|
init_cjs_shim();
|
|
25221
25851
|
init_openai();
|
|
25222
25852
|
init_anthropic();
|
|
25853
|
+
init_local();
|
|
25223
25854
|
init_complexity();
|
|
25224
25855
|
LLM_AUTO_THRESHOLD = 4;
|
|
25225
25856
|
}
|
|
@@ -25424,9 +26055,10 @@ async function evaluateSingle(c, now, ctx) {
|
|
|
25424
26055
|
};
|
|
25425
26056
|
}
|
|
25426
26057
|
try {
|
|
26058
|
+
const recent = await fetchRecentEvents(c.llm.recent_events, ctx);
|
|
25427
26059
|
const res = await ctx.llmEvaluator.evaluate(
|
|
25428
26060
|
c.llm,
|
|
25429
|
-
{ event: ctx.event },
|
|
26061
|
+
{ event: ctx.event, recentEvents: recent },
|
|
25430
26062
|
ctx.ruleVersion ?? "unknown",
|
|
25431
26063
|
ctx.globalLlmMaxCallsPerHour
|
|
25432
26064
|
);
|
|
@@ -25438,6 +26070,44 @@ async function evaluateSingle(c, now, ctx) {
|
|
|
25438
26070
|
return fail(`llm condition error: ${err instanceof Error ? err.message : String(err)}`);
|
|
25439
26071
|
}
|
|
25440
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
|
+
}
|
|
25441
26111
|
return {
|
|
25442
26112
|
matched: false,
|
|
25443
26113
|
failures: [],
|
|
@@ -25504,12 +26174,14 @@ function conditionKind(c) {
|
|
|
25504
26174
|
if (isTimeBetween(c)) return "time_between";
|
|
25505
26175
|
if (isDeviceState(c)) return "device_state";
|
|
25506
26176
|
if (isLlmCondition(c)) return "llm";
|
|
26177
|
+
if (isEventCountCondition(c)) return "event_count";
|
|
25507
26178
|
return "unknown";
|
|
25508
26179
|
}
|
|
25509
26180
|
function conditionConfig(c) {
|
|
25510
26181
|
if (isTimeBetween(c)) return c.time_between;
|
|
25511
26182
|
if (isDeviceState(c)) return { device: c.device, field: c.field, op: c.op, value: c.value };
|
|
25512
26183
|
if (isLlmCondition(c)) return { prompt: c.llm.prompt.slice(0, 80) };
|
|
26184
|
+
if (isEventCountCondition(c)) return c.event_count;
|
|
25513
26185
|
return void 0;
|
|
25514
26186
|
}
|
|
25515
26187
|
function pushConditionTrace(trace, c, sub) {
|
|
@@ -25519,6 +26191,31 @@ function pushConditionTrace(trace, c, sub) {
|
|
|
25519
26191
|
passed: sub.unsupported.length > 0 ? false : sub.matched
|
|
25520
26192
|
});
|
|
25521
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
|
+
}
|
|
25522
26219
|
var EVENT_CLASSIFIERS;
|
|
25523
26220
|
var init_matcher = __esm({
|
|
25524
26221
|
"src/rules/matcher.ts"() {
|
|
@@ -26444,7 +27141,7 @@ var init_cron_scheduler = __esm({
|
|
|
26444
27141
|
});
|
|
26445
27142
|
|
|
26446
27143
|
// src/rules/webhook-listener.ts
|
|
26447
|
-
import
|
|
27144
|
+
import http4 from "node:http";
|
|
26448
27145
|
import { timingSafeEqual } from "node:crypto";
|
|
26449
27146
|
function normalisePath(p2) {
|
|
26450
27147
|
if (!p2) return "/";
|
|
@@ -26500,7 +27197,7 @@ var init_webhook_listener = __esm({
|
|
|
26500
27197
|
/** Start listening. Resolves once the server has bound a port. */
|
|
26501
27198
|
async start() {
|
|
26502
27199
|
if (this.server) return;
|
|
26503
|
-
const server =
|
|
27200
|
+
const server = http4.createServer((req, res) => {
|
|
26504
27201
|
this.handle(req, res).catch((err) => {
|
|
26505
27202
|
if (!res.headersSent) {
|
|
26506
27203
|
res.writeHead(500);
|
|
@@ -26640,8 +27337,8 @@ var init_webhook_listener = __esm({
|
|
|
26640
27337
|
// src/rules/notify.ts
|
|
26641
27338
|
import fs14 from "node:fs";
|
|
26642
27339
|
import path12 from "node:path";
|
|
26643
|
-
import
|
|
26644
|
-
import
|
|
27340
|
+
import http5 from "node:http";
|
|
27341
|
+
import https5 from "node:https";
|
|
26645
27342
|
function renderNotifyTemplate(template, vars) {
|
|
26646
27343
|
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_2, key) => {
|
|
26647
27344
|
const val = vars[key];
|
|
@@ -26724,7 +27421,7 @@ async function sendWebhook(url2, body) {
|
|
|
26724
27421
|
"User-Agent": "switchbot-cli/notify"
|
|
26725
27422
|
}
|
|
26726
27423
|
};
|
|
26727
|
-
const req = (isHttps ?
|
|
27424
|
+
const req = (isHttps ? https5 : http5).request(options, (res) => {
|
|
26728
27425
|
res.resume();
|
|
26729
27426
|
const status = res.statusCode ?? 0;
|
|
26730
27427
|
if (status >= 200 && status < 300) {
|
|
@@ -26911,10 +27608,250 @@ var init_trace = __esm({
|
|
|
26911
27608
|
}
|
|
26912
27609
|
});
|
|
26913
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
|
+
|
|
26914
27850
|
// src/rules/engine.ts
|
|
26915
27851
|
var engine_exports = {};
|
|
26916
27852
|
__export(engine_exports, {
|
|
26917
27853
|
RulesEngine: () => RulesEngine,
|
|
27854
|
+
defaultEventWindowFetcher: () => defaultEventWindowFetcher,
|
|
26918
27855
|
lintRules: () => lintRules
|
|
26919
27856
|
});
|
|
26920
27857
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
@@ -27132,6 +28069,25 @@ function lintRules(automation) {
|
|
|
27132
28069
|
message: "llm condition budget.max_calls_per_hour is 0 \u2014 condition will always take the on_error path."
|
|
27133
28070
|
});
|
|
27134
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
|
+
}
|
|
27135
28091
|
if (llm.on_error === "pass") {
|
|
27136
28092
|
issues.push({
|
|
27137
28093
|
rule: r.name,
|
|
@@ -27141,6 +28097,26 @@ function lintRules(automation) {
|
|
|
27141
28097
|
});
|
|
27142
28098
|
}
|
|
27143
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
|
+
}
|
|
27144
28120
|
const enabled = r.enabled !== false;
|
|
27145
28121
|
const hasError = issues.some((i) => i.severity === "error");
|
|
27146
28122
|
const hasUnsupported = issues.some((i) => i.code === "trigger-unsupported");
|
|
@@ -27153,6 +28129,21 @@ function lintRules(automation) {
|
|
|
27153
28129
|
unsupportedCount
|
|
27154
28130
|
};
|
|
27155
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
|
+
}
|
|
27156
28147
|
var RulesEngine;
|
|
27157
28148
|
var init_engine = __esm({
|
|
27158
28149
|
"src/rules/engine.ts"() {
|
|
@@ -27170,6 +28161,8 @@ var init_engine = __esm({
|
|
|
27170
28161
|
init_croner();
|
|
27171
28162
|
init_audit();
|
|
27172
28163
|
init_trace();
|
|
28164
|
+
init_llm_condition();
|
|
28165
|
+
init_history_window();
|
|
27173
28166
|
RulesEngine = class {
|
|
27174
28167
|
opts;
|
|
27175
28168
|
rules;
|
|
@@ -27190,6 +28183,8 @@ var init_engine = __esm({
|
|
|
27190
28183
|
* keeps the semantics of `max_per` honest.
|
|
27191
28184
|
*/
|
|
27192
28185
|
pendingChain = Promise.resolve();
|
|
28186
|
+
llmEvaluator = new LlmConditionEvaluator();
|
|
28187
|
+
eventWindowFetcher = defaultEventWindowFetcher;
|
|
27193
28188
|
stats = {
|
|
27194
28189
|
started: false,
|
|
27195
28190
|
rulesLoaded: 0,
|
|
@@ -27519,7 +28514,12 @@ var init_engine = __esm({
|
|
|
27519
28514
|
const cond = await evaluateConditions(rule.conditions, event.t, {
|
|
27520
28515
|
aliases: this.aliases,
|
|
27521
28516
|
fetchStatus,
|
|
27522
|
-
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
|
|
27523
28523
|
});
|
|
27524
28524
|
if (!cond.matched) {
|
|
27525
28525
|
const hasHysteresis = rule.hysteresis ?? rule.requires_stable_for;
|
|
@@ -27720,6 +28720,162 @@ var init_engine = __esm({
|
|
|
27720
28720
|
}
|
|
27721
28721
|
});
|
|
27722
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
|
+
|
|
27723
28879
|
// src/devices/resources.ts
|
|
27724
28880
|
var COMMON_WEBHOOK_FIELDS, RESOURCE_CATALOG;
|
|
27725
28881
|
var init_resources = __esm({
|
|
@@ -28353,6 +29509,128 @@ var init_capabilities = __esm({
|
|
|
28353
29509
|
}
|
|
28354
29510
|
});
|
|
28355
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
|
+
|
|
28356
29634
|
// src/index.ts
|
|
28357
29635
|
init_cjs_shim();
|
|
28358
29636
|
init_esm();
|
|
@@ -32221,15 +33499,15 @@ function validateSetBrightness(raw, deviceType) {
|
|
|
32221
33499
|
error: `setBrightness requires an integer ${min}-${max} (percent). Example: "50".`
|
|
32222
33500
|
};
|
|
32223
33501
|
}
|
|
32224
|
-
const
|
|
32225
|
-
if (
|
|
33502
|
+
const parsed = parseStrictInt(raw);
|
|
33503
|
+
if (!parsed.ok) {
|
|
32226
33504
|
return {
|
|
32227
33505
|
ok: false,
|
|
32228
33506
|
error: `setBrightness must be an integer ${min}-${max}, got ${JSON.stringify(raw)}. ${hintBrightnessRetry(min, max)}`
|
|
32229
33507
|
};
|
|
32230
33508
|
}
|
|
32231
|
-
const n =
|
|
32232
|
-
if (
|
|
33509
|
+
const n = parsed.value;
|
|
33510
|
+
if (n < min || n > max) {
|
|
32233
33511
|
return {
|
|
32234
33512
|
ok: false,
|
|
32235
33513
|
error: `setBrightness must be an integer ${min}-${max}, got "${raw}". ${hintBrightnessRetry(min, max)}`
|
|
@@ -32321,15 +33599,15 @@ function validateSetColorTemperature(raw) {
|
|
|
32321
33599
|
error: `setColorTemperature requires an integer Kelvin value 2700-6500. Example: "4000".`
|
|
32322
33600
|
};
|
|
32323
33601
|
}
|
|
32324
|
-
const
|
|
32325
|
-
if (
|
|
33602
|
+
const parsed = parseStrictInt(raw);
|
|
33603
|
+
if (!parsed.ok) {
|
|
32326
33604
|
return {
|
|
32327
33605
|
ok: false,
|
|
32328
33606
|
error: `setColorTemperature must be an integer 2700-6500, got ${JSON.stringify(raw)}.`
|
|
32329
33607
|
};
|
|
32330
33608
|
}
|
|
32331
|
-
const n =
|
|
32332
|
-
if (
|
|
33609
|
+
const n = parsed.value;
|
|
33610
|
+
if (n < 2700 || n > 6500) {
|
|
32333
33611
|
return {
|
|
32334
33612
|
ok: false,
|
|
32335
33613
|
error: `setColorTemperature must be an integer 2700-6500, got "${raw}".`
|
|
@@ -32359,27 +33637,30 @@ function validateAcSetAll(raw) {
|
|
|
32359
33637
|
};
|
|
32360
33638
|
}
|
|
32361
33639
|
const [tempStr, modeStr, fanStr, powerStr] = parts.map((s2) => s2.trim());
|
|
32362
|
-
const
|
|
32363
|
-
if (!
|
|
33640
|
+
const tempP = parseStrictInt(tempStr);
|
|
33641
|
+
if (!tempP.ok || tempP.value < 16 || tempP.value > 30) {
|
|
32364
33642
|
return {
|
|
32365
33643
|
ok: false,
|
|
32366
33644
|
error: `setAll field 1 (temp) must be an integer 16-30, got "${tempStr}". Example: "26,2,2,on".`
|
|
32367
33645
|
};
|
|
32368
33646
|
}
|
|
32369
|
-
const
|
|
32370
|
-
|
|
33647
|
+
const temp = tempP.value;
|
|
33648
|
+
const modeP = parseStrictInt(modeStr);
|
|
33649
|
+
if (!modeP.ok || modeP.value < 1 || modeP.value > 5) {
|
|
32371
33650
|
return {
|
|
32372
33651
|
ok: false,
|
|
32373
33652
|
error: `setAll field 2 (mode) must be 1-5 (1=auto 2=cool 3=dry 4=fan 5=heat), got "${modeStr}". Example: "26,2,2,on".`
|
|
32374
33653
|
};
|
|
32375
33654
|
}
|
|
32376
|
-
const
|
|
32377
|
-
|
|
33655
|
+
const mode = modeP.value;
|
|
33656
|
+
const fanP = parseStrictInt(fanStr);
|
|
33657
|
+
if (!fanP.ok || fanP.value < 1 || fanP.value > 4) {
|
|
32378
33658
|
return {
|
|
32379
33659
|
ok: false,
|
|
32380
33660
|
error: `setAll field 3 (fan) must be 1-4 (1=auto 2=low 3=mid 4=high), got "${fanStr}". Example: "26,2,2,on".`
|
|
32381
33661
|
};
|
|
32382
33662
|
}
|
|
33663
|
+
const fan = fanP.value;
|
|
32383
33664
|
const power = powerStr.toLowerCase();
|
|
32384
33665
|
if (power !== "on" && power !== "off") {
|
|
32385
33666
|
return {
|
|
@@ -32398,14 +33679,14 @@ function validateCurtainSetPosition(raw) {
|
|
|
32398
33679
|
}
|
|
32399
33680
|
const stripped = stripQuotes(raw.trim());
|
|
32400
33681
|
if (!stripped.includes(",")) {
|
|
32401
|
-
const
|
|
32402
|
-
if (!
|
|
33682
|
+
const posP2 = parseStrictInt(stripped);
|
|
33683
|
+
if (!posP2.ok || posP2.value < 0 || posP2.value > 100) {
|
|
32403
33684
|
return {
|
|
32404
33685
|
ok: false,
|
|
32405
33686
|
error: `setPosition must be an integer 0-100, got "${raw}". Example: "50".`
|
|
32406
33687
|
};
|
|
32407
33688
|
}
|
|
32408
|
-
return { ok: true, normalized: String(
|
|
33689
|
+
return { ok: true, normalized: String(posP2.value) };
|
|
32409
33690
|
}
|
|
32410
33691
|
const parts = stripped.split(",").map((s2) => s2.trim());
|
|
32411
33692
|
if (parts.length !== 3) {
|
|
@@ -32415,13 +33696,14 @@ function validateCurtainSetPosition(raw) {
|
|
|
32415
33696
|
};
|
|
32416
33697
|
}
|
|
32417
33698
|
const [idxStr, modeStr, posStr] = parts;
|
|
32418
|
-
const
|
|
32419
|
-
if (!
|
|
33699
|
+
const idxP = parseStrictInt(idxStr);
|
|
33700
|
+
if (!idxP.ok || idxP.value < 0) {
|
|
32420
33701
|
return {
|
|
32421
33702
|
ok: false,
|
|
32422
33703
|
error: `setPosition field 1 (index) must be a non-negative integer, got "${idxStr}".`
|
|
32423
33704
|
};
|
|
32424
33705
|
}
|
|
33706
|
+
const idx = idxP.value;
|
|
32425
33707
|
const modeLower = modeStr.toLowerCase();
|
|
32426
33708
|
if (!["ff", "0", "1"].includes(modeLower)) {
|
|
32427
33709
|
return {
|
|
@@ -32429,13 +33711,14 @@ function validateCurtainSetPosition(raw) {
|
|
|
32429
33711
|
error: `setPosition field 2 (mode) must be "ff", "0", or "1", got "${modeStr}". (ff=default, 0=performance, 1=silent)`
|
|
32430
33712
|
};
|
|
32431
33713
|
}
|
|
32432
|
-
const
|
|
32433
|
-
if (!
|
|
33714
|
+
const posP = parseStrictInt(posStr);
|
|
33715
|
+
if (!posP.ok || posP.value < 0 || posP.value > 100) {
|
|
32434
33716
|
return {
|
|
32435
33717
|
ok: false,
|
|
32436
33718
|
error: `setPosition field 3 (position) must be an integer 0-100, got "${posStr}".`
|
|
32437
33719
|
};
|
|
32438
33720
|
}
|
|
33721
|
+
const pos = posP.value;
|
|
32439
33722
|
return { ok: true, normalized: `${idx},${modeLower},${pos}` };
|
|
32440
33723
|
}
|
|
32441
33724
|
function validateBlindTiltSetPosition(raw) {
|
|
@@ -32460,13 +33743,14 @@ function validateBlindTiltSetPosition(raw) {
|
|
|
32460
33743
|
error: `Blind Tilt setPosition direction must be "up" or "down", got "${parts[0]}".`
|
|
32461
33744
|
};
|
|
32462
33745
|
}
|
|
32463
|
-
const
|
|
32464
|
-
if (!
|
|
33746
|
+
const angleP = parseStrictInt(parts[1]);
|
|
33747
|
+
if (!angleP.ok || angleP.value < 0 || angleP.value > 100) {
|
|
32465
33748
|
return {
|
|
32466
33749
|
ok: false,
|
|
32467
33750
|
error: `Blind Tilt setPosition angle must be an integer 0-100, got "${parts[1]}".`
|
|
32468
33751
|
};
|
|
32469
33752
|
}
|
|
33753
|
+
const angle = angleP.value;
|
|
32470
33754
|
if (angle % 2 !== 0) {
|
|
32471
33755
|
return {
|
|
32472
33756
|
ok: false,
|
|
@@ -32490,21 +33774,22 @@ function validateRelay2PmSetMode(raw) {
|
|
|
32490
33774
|
error: `Relay Switch setMode expects "<channel>;<mode>", got ${JSON.stringify(raw)}. Example: "1;1".`
|
|
32491
33775
|
};
|
|
32492
33776
|
}
|
|
32493
|
-
const
|
|
32494
|
-
if (
|
|
33777
|
+
const chP = parseStrictInt(parts[0]);
|
|
33778
|
+
if (!chP.ok || chP.value !== 1 && chP.value !== 2) {
|
|
32495
33779
|
return {
|
|
32496
33780
|
ok: false,
|
|
32497
33781
|
error: `Relay Switch setMode channel must be 1 or 2, got "${parts[0]}".`
|
|
32498
33782
|
};
|
|
32499
33783
|
}
|
|
32500
|
-
const
|
|
32501
|
-
|
|
33784
|
+
const ch = chP.value;
|
|
33785
|
+
const modeP = parseStrictInt(parts[1]);
|
|
33786
|
+
if (!modeP.ok || modeP.value < 0 || modeP.value > 3) {
|
|
32502
33787
|
return {
|
|
32503
33788
|
ok: false,
|
|
32504
33789
|
error: `Relay Switch setMode mode must be 0-3 (0=toggle 1=edge 2=detached 3=momentary), got "${parts[1]}".`
|
|
32505
33790
|
};
|
|
32506
33791
|
}
|
|
32507
|
-
return { ok: true, normalized: `${ch};${
|
|
33792
|
+
return { ok: true, normalized: `${ch};${modeP.value}` };
|
|
32508
33793
|
}
|
|
32509
33794
|
function validateRelayChannel(raw) {
|
|
32510
33795
|
if (raw === void 0 || raw === "" || raw === "default") {
|
|
@@ -32528,6 +33813,11 @@ function stripQuotes(s2) {
|
|
|
32528
33813
|
}
|
|
32529
33814
|
return s2;
|
|
32530
33815
|
}
|
|
33816
|
+
function parseStrictInt(raw) {
|
|
33817
|
+
const cleaned = stripQuotes(raw);
|
|
33818
|
+
if (!/^[+-]?(?:0|[1-9]\d*)$/.test(cleaned)) return { ok: false };
|
|
33819
|
+
return { ok: true, value: Number(cleaned) };
|
|
33820
|
+
}
|
|
32531
33821
|
function validateIntRange(raw, command, min, max, label) {
|
|
32532
33822
|
if (raw === void 0 || raw === "" || raw === "default") {
|
|
32533
33823
|
return {
|
|
@@ -32535,14 +33825,14 @@ function validateIntRange(raw, command, min, max, label) {
|
|
|
32535
33825
|
error: `${command} requires an integer ${min}-${max} (${label}). Example: "${Math.round((min + max) / 2)}".`
|
|
32536
33826
|
};
|
|
32537
33827
|
}
|
|
32538
|
-
const
|
|
32539
|
-
if (
|
|
33828
|
+
const parsed = parseStrictInt(raw);
|
|
33829
|
+
if (!parsed.ok) {
|
|
32540
33830
|
return {
|
|
32541
33831
|
ok: false,
|
|
32542
33832
|
error: `${command} must be an integer ${min}-${max} (${label}), got ${JSON.stringify(raw)}.`
|
|
32543
33833
|
};
|
|
32544
33834
|
}
|
|
32545
|
-
const n =
|
|
33835
|
+
const n = parsed.value;
|
|
32546
33836
|
if (n < min || n > max) {
|
|
32547
33837
|
return {
|
|
32548
33838
|
ok: false,
|
|
@@ -33205,9 +34495,9 @@ Examples:
|
|
|
33205
34495
|
printJson(result);
|
|
33206
34496
|
} else {
|
|
33207
34497
|
if (dryRunned.length > 0) {
|
|
33208
|
-
console.
|
|
34498
|
+
console.error(`
|
|
33209
34499
|
Planned (dry-run): ${dryRunned.length} device(s)`);
|
|
33210
|
-
for (const d of dryRunned) console.
|
|
34500
|
+
for (const d of dryRunned) console.error(` - ${d.deviceId}`);
|
|
33211
34501
|
}
|
|
33212
34502
|
if (preSkipped.length > 0) {
|
|
33213
34503
|
console.log(`
|
|
@@ -33842,7 +35132,7 @@ Examples:
|
|
|
33842
35132
|
if (isJsonMode()) {
|
|
33843
35133
|
printJson({ ok: true, dryRun: true, command, deviceId });
|
|
33844
35134
|
} else {
|
|
33845
|
-
console.
|
|
35135
|
+
console.error(`\u25E6 dry-run: ${command} would be sent to ${deviceId}`);
|
|
33846
35136
|
}
|
|
33847
35137
|
return;
|
|
33848
35138
|
}
|
|
@@ -34204,7 +35494,7 @@ Total: ${totalLabel}`);
|
|
|
34204
35494
|
handleError(error48);
|
|
34205
35495
|
}
|
|
34206
35496
|
});
|
|
34207
|
-
devices.command("status").description("Query the real-time status of a specific device").argument("[deviceId]", 'Device ID from "devices list" (or use --name or --ids)').option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: fuzzy)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--ids <list>", "Comma-separated device IDs for batch status (incompatible with --name)", stringArg("--ids")).addHelpText("after", `
|
|
35497
|
+
devices.command("status").description("Query the real-time status of a specific device").argument("[deviceId...]", 'Device ID(s) from "devices list" (or use --name or --ids)').option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: fuzzy)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--ids <list>", "Comma-separated device IDs for batch status (incompatible with --name)", stringArg("--ids")).addHelpText("after", `
|
|
34208
35498
|
Status fields vary by device type. To discover them without a live call:
|
|
34209
35499
|
|
|
34210
35500
|
switchbot devices commands <type> (prints the "Status fields" section)
|
|
@@ -34214,6 +35504,7 @@ all field names returned by your specific device, then narrow with --fields.
|
|
|
34214
35504
|
|
|
34215
35505
|
Examples:
|
|
34216
35506
|
$ switchbot devices status ABC123DEF456
|
|
35507
|
+
$ switchbot devices status ABC123 DEF456 GHI789
|
|
34217
35508
|
$ switchbot devices status --name "Living Room AC"
|
|
34218
35509
|
$ switchbot devices status ABC123DEF456 --json
|
|
34219
35510
|
$ switchbot devices status ABC123DEF456 --format yaml
|
|
@@ -34221,12 +35512,13 @@ Examples:
|
|
|
34221
35512
|
$ switchbot devices status ABC123DEF456 --json | jq '.data.battery'
|
|
34222
35513
|
$ switchbot devices status --ids ABC123,DEF456,GHI789
|
|
34223
35514
|
$ switchbot devices status --ids ABC123,DEF456 --fields power,battery
|
|
34224
|
-
`).action(async (
|
|
35515
|
+
`).action(async (deviceIdArgs, options) => {
|
|
34225
35516
|
try {
|
|
34226
|
-
|
|
35517
|
+
const batchIds = options.ids ? options.ids.split(",").map((s2) => s2.trim()).filter(Boolean) : deviceIdArgs.length > 1 ? deviceIdArgs : void 0;
|
|
35518
|
+
if (batchIds) {
|
|
34227
35519
|
if (options.name) throw new UsageError("--ids and --name cannot be used together.");
|
|
34228
|
-
|
|
34229
|
-
|
|
35520
|
+
if (batchIds.length === 0) throw new UsageError("--ids requires at least one device ID.");
|
|
35521
|
+
const ids = batchIds;
|
|
34230
35522
|
const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
|
|
34231
35523
|
const fetchedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
34232
35524
|
const batch = results.map(
|
|
@@ -34258,7 +35550,7 @@ Examples:
|
|
|
34258
35550
|
}
|
|
34259
35551
|
return;
|
|
34260
35552
|
}
|
|
34261
|
-
const deviceId = resolveDeviceId(
|
|
35553
|
+
const deviceId = resolveDeviceId(deviceIdArgs[0], options.name, {
|
|
34262
35554
|
strategy: options.nameStrategy ?? "fuzzy",
|
|
34263
35555
|
type: options.nameType,
|
|
34264
35556
|
category: options.nameCategory,
|
|
@@ -34536,7 +35828,7 @@ ${extra}` : extra;
|
|
|
34536
35828
|
if (isJsonMode()) {
|
|
34537
35829
|
printJson({ dryRun: true, wouldSend });
|
|
34538
35830
|
} else {
|
|
34539
|
-
console.
|
|
35831
|
+
console.error(`\u25E6 dry-run intercepted for ${_cmd} on ${_deviceId}; see stderr preview for the HTTP request.`);
|
|
34540
35832
|
}
|
|
34541
35833
|
return;
|
|
34542
35834
|
}
|
|
@@ -34787,12 +36079,15 @@ function renderCatalogEntry(entry) {
|
|
|
34787
36079
|
console.log(`Type: ${entry.type}`);
|
|
34788
36080
|
console.log(`Category: ${entry.category === "ir" ? "IR remote" : "Physical device"}`);
|
|
34789
36081
|
if (entry.role) console.log(`Role: ${entry.role}`);
|
|
34790
|
-
|
|
36082
|
+
const hasStatusFields = (entry.statusFields?.length ?? 0) > 0;
|
|
36083
|
+
if (entry.readOnly) {
|
|
36084
|
+
console.log(hasStatusFields ? `ReadOnly: yes (status-only device, no control commands)` : `ReadOnly: yes (no cloud control commands cataloged)`);
|
|
36085
|
+
}
|
|
34791
36086
|
if (entry.aliases && entry.aliases.length > 0) {
|
|
34792
36087
|
console.log(`Aliases: ${entry.aliases.join(", ")}`);
|
|
34793
36088
|
}
|
|
34794
36089
|
if (entry.commands.length === 0) {
|
|
34795
|
-
console.log("\nCommands: (none \u2014 status-only device)");
|
|
36090
|
+
console.log(hasStatusFields ? "\nCommands: (none \u2014 status-only device)" : "\nCommands: (none \u2014 no cloud control commands cataloged)");
|
|
34796
36091
|
} else {
|
|
34797
36092
|
console.log("\nCommands:");
|
|
34798
36093
|
const hasExamples = entry.commands.some((c) => c.exampleParams && c.exampleParams.length > 0);
|
|
@@ -34897,7 +36192,7 @@ Example:
|
|
|
34897
36192
|
if (isJsonMode()) {
|
|
34898
36193
|
printJson({ dryRun: true, wouldSend });
|
|
34899
36194
|
} else {
|
|
34900
|
-
console.
|
|
36195
|
+
console.error(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
|
|
34901
36196
|
}
|
|
34902
36197
|
return;
|
|
34903
36198
|
}
|
|
@@ -35055,7 +36350,7 @@ Example:
|
|
|
35055
36350
|
console.log(`idempotent: unknown (scene steps not exposed by API)`);
|
|
35056
36351
|
console.log(`toExecute: ${explanation.toExecute}`);
|
|
35057
36352
|
if (explanation.dryRun) {
|
|
35058
|
-
console.
|
|
36353
|
+
console.error(`dryRun: true (pass --dry-run to execute would be a no-op)`);
|
|
35059
36354
|
}
|
|
35060
36355
|
console.log(`note: ${explanation.note}`);
|
|
35061
36356
|
} catch (error48) {
|
|
@@ -36391,10 +37686,10 @@ function mergeDefs(...defs) {
|
|
|
36391
37686
|
function cloneDef(schema2) {
|
|
36392
37687
|
return mergeDefs(schema2._zod.def);
|
|
36393
37688
|
}
|
|
36394
|
-
function getElementAtPath(obj,
|
|
36395
|
-
if (!
|
|
37689
|
+
function getElementAtPath(obj, path31) {
|
|
37690
|
+
if (!path31)
|
|
36396
37691
|
return obj;
|
|
36397
|
-
return
|
|
37692
|
+
return path31.reduce((acc, key) => acc?.[key], obj);
|
|
36398
37693
|
}
|
|
36399
37694
|
function promiseAllObject(promisesObj) {
|
|
36400
37695
|
const keys = Object.keys(promisesObj);
|
|
@@ -36777,11 +38072,11 @@ function aborted(x2, startIndex = 0) {
|
|
|
36777
38072
|
}
|
|
36778
38073
|
return false;
|
|
36779
38074
|
}
|
|
36780
|
-
function prefixIssues(
|
|
38075
|
+
function prefixIssues(path31, issues) {
|
|
36781
38076
|
return issues.map((iss) => {
|
|
36782
38077
|
var _a2;
|
|
36783
38078
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
36784
|
-
iss.path.unshift(
|
|
38079
|
+
iss.path.unshift(path31);
|
|
36785
38080
|
return iss;
|
|
36786
38081
|
});
|
|
36787
38082
|
}
|
|
@@ -36964,7 +38259,7 @@ function formatError2(error48, mapper = (issue2) => issue2.message) {
|
|
|
36964
38259
|
}
|
|
36965
38260
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
36966
38261
|
const result = { errors: [] };
|
|
36967
|
-
const processError = (error49,
|
|
38262
|
+
const processError = (error49, path31 = []) => {
|
|
36968
38263
|
var _a2, _b;
|
|
36969
38264
|
for (const issue2 of error49.issues) {
|
|
36970
38265
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -36974,7 +38269,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
36974
38269
|
} else if (issue2.code === "invalid_element") {
|
|
36975
38270
|
processError({ issues: issue2.issues }, issue2.path);
|
|
36976
38271
|
} else {
|
|
36977
|
-
const fullpath = [...
|
|
38272
|
+
const fullpath = [...path31, ...issue2.path];
|
|
36978
38273
|
if (fullpath.length === 0) {
|
|
36979
38274
|
result.errors.push(mapper(issue2));
|
|
36980
38275
|
continue;
|
|
@@ -37006,8 +38301,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
37006
38301
|
}
|
|
37007
38302
|
function toDotPath(_path) {
|
|
37008
38303
|
const segs = [];
|
|
37009
|
-
const
|
|
37010
|
-
for (const seg of
|
|
38304
|
+
const path31 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
38305
|
+
for (const seg of path31) {
|
|
37011
38306
|
if (typeof seg === "number")
|
|
37012
38307
|
segs.push(`[${seg}]`);
|
|
37013
38308
|
else if (typeof seg === "symbol")
|
|
@@ -49062,13 +50357,13 @@ function resolveRef(ref, ctx) {
|
|
|
49062
50357
|
if (!ref.startsWith("#")) {
|
|
49063
50358
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
49064
50359
|
}
|
|
49065
|
-
const
|
|
49066
|
-
if (
|
|
50360
|
+
const path31 = ref.slice(1).split("/").filter(Boolean);
|
|
50361
|
+
if (path31.length === 0) {
|
|
49067
50362
|
return ctx.rootSchema;
|
|
49068
50363
|
}
|
|
49069
50364
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
49070
|
-
if (
|
|
49071
|
-
const key =
|
|
50365
|
+
if (path31[0] === defsKey) {
|
|
50366
|
+
const key = path31[1];
|
|
49072
50367
|
if (!key || !ctx.defs[key]) {
|
|
49073
50368
|
throw new Error(`Reference not found: ${ref}`);
|
|
49074
50369
|
}
|
|
@@ -49851,157 +51146,12 @@ function resolveToolProfile(name) {
|
|
|
49851
51146
|
throw new Error(`Unknown tool profile "${name}". Valid profiles: ${valid}`);
|
|
49852
51147
|
}
|
|
49853
51148
|
|
|
49854
|
-
// src/
|
|
49855
|
-
|
|
49856
|
-
import fs10 from "node:fs";
|
|
49857
|
-
import path10 from "node:path";
|
|
49858
|
-
import os11 from "node:os";
|
|
49859
|
-
import readline2 from "node:readline";
|
|
49860
|
-
var DEFAULT_LIMIT = 1e3;
|
|
49861
|
-
function historyDir2() {
|
|
49862
|
-
return path10.join(os11.homedir(), ".switchbot", "device-history");
|
|
49863
|
-
}
|
|
49864
|
-
function parseDurationToMs2(spec) {
|
|
49865
|
-
const m2 = spec.trim().match(/^(\d+)(ms|s|m|h|d)$/i);
|
|
49866
|
-
if (!m2) return null;
|
|
49867
|
-
const n = Number(m2[1]);
|
|
49868
|
-
const unit = m2[2].toLowerCase();
|
|
49869
|
-
const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
49870
|
-
return n * factor;
|
|
49871
|
-
}
|
|
49872
|
-
function parseInstantToMs(spec) {
|
|
49873
|
-
const ms = Date.parse(spec);
|
|
49874
|
-
return Number.isFinite(ms) ? ms : null;
|
|
49875
|
-
}
|
|
49876
|
-
function resolveRange(opts) {
|
|
49877
|
-
let fromMs = Number.NEGATIVE_INFINITY;
|
|
49878
|
-
let toMs = Number.POSITIVE_INFINITY;
|
|
49879
|
-
if (opts.since && (opts.from || opts.to)) {
|
|
49880
|
-
throw new Error("--since is mutually exclusive with --from/--to.");
|
|
49881
|
-
}
|
|
49882
|
-
if (opts.since) {
|
|
49883
|
-
const durMs = parseDurationToMs2(opts.since);
|
|
49884
|
-
if (durMs === null) {
|
|
49885
|
-
throw new Error(`Invalid --since value "${opts.since}". Expected e.g. "30s", "15m", "1h", "7d".`);
|
|
49886
|
-
}
|
|
49887
|
-
fromMs = Date.now() - durMs;
|
|
49888
|
-
} else {
|
|
49889
|
-
if (opts.from) {
|
|
49890
|
-
const parsed = parseInstantToMs(opts.from);
|
|
49891
|
-
if (parsed === null) throw new Error(`Invalid --from value "${opts.from}". Expected ISO-8601 timestamp.`);
|
|
49892
|
-
fromMs = parsed;
|
|
49893
|
-
}
|
|
49894
|
-
if (opts.to) {
|
|
49895
|
-
const parsed = parseInstantToMs(opts.to);
|
|
49896
|
-
if (parsed === null) throw new Error(`Invalid --to value "${opts.to}". Expected ISO-8601 timestamp.`);
|
|
49897
|
-
toMs = parsed;
|
|
49898
|
-
}
|
|
49899
|
-
if (fromMs > toMs) throw new Error("--from must be <= --to.");
|
|
49900
|
-
}
|
|
49901
|
-
return { fromMs, toMs };
|
|
49902
|
-
}
|
|
49903
|
-
function jsonlFilesForDevice(deviceId, baseDir = historyDir2()) {
|
|
49904
|
-
const out = [];
|
|
49905
|
-
if (!fs10.existsSync(baseDir)) return out;
|
|
49906
|
-
for (let i = 3; i >= 1; i--) {
|
|
49907
|
-
const p2 = path10.join(baseDir, `${deviceId}.jsonl.${i}`);
|
|
49908
|
-
if (fs10.existsSync(p2)) out.push(p2);
|
|
49909
|
-
}
|
|
49910
|
-
const current = path10.join(baseDir, `${deviceId}.jsonl`);
|
|
49911
|
-
if (fs10.existsSync(current)) out.push(current);
|
|
49912
|
-
return out;
|
|
49913
|
-
}
|
|
49914
|
-
function projectFields(record2, fields) {
|
|
49915
|
-
if (fields.length === 0) return record2;
|
|
49916
|
-
const projected = {};
|
|
49917
|
-
const payload = record2.payload ?? {};
|
|
49918
|
-
for (const f2 of fields) {
|
|
49919
|
-
if (f2 in payload) projected[f2] = payload[f2];
|
|
49920
|
-
}
|
|
49921
|
-
return { t: record2.t, topic: record2.topic, deviceType: record2.deviceType, payload: projected };
|
|
49922
|
-
}
|
|
49923
|
-
async function queryDeviceHistory(deviceId, opts = {}) {
|
|
49924
|
-
const { fromMs, toMs } = resolveRange(opts);
|
|
49925
|
-
const limit = Math.max(0, opts.limit ?? DEFAULT_LIMIT);
|
|
49926
|
-
const fields = opts.fields ?? [];
|
|
49927
|
-
const files = jsonlFilesForDevice(deviceId);
|
|
49928
|
-
const out = [];
|
|
49929
|
-
for (const file2 of files) {
|
|
49930
|
-
try {
|
|
49931
|
-
const stat = fs10.statSync(file2);
|
|
49932
|
-
if (stat.mtimeMs < fromMs) continue;
|
|
49933
|
-
} catch {
|
|
49934
|
-
continue;
|
|
49935
|
-
}
|
|
49936
|
-
const stream = fs10.createReadStream(file2, { encoding: "utf-8" });
|
|
49937
|
-
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
49938
|
-
for await (const line of rl) {
|
|
49939
|
-
if (!line) continue;
|
|
49940
|
-
let rec;
|
|
49941
|
-
try {
|
|
49942
|
-
rec = JSON.parse(line);
|
|
49943
|
-
} catch {
|
|
49944
|
-
continue;
|
|
49945
|
-
}
|
|
49946
|
-
const tMs = Date.parse(rec.t);
|
|
49947
|
-
if (!Number.isFinite(tMs)) continue;
|
|
49948
|
-
if (tMs < fromMs || tMs > toMs) continue;
|
|
49949
|
-
out.push(projectFields(rec, fields));
|
|
49950
|
-
if (out.length >= limit) {
|
|
49951
|
-
rl.close();
|
|
49952
|
-
stream.destroy();
|
|
49953
|
-
return out;
|
|
49954
|
-
}
|
|
49955
|
-
}
|
|
49956
|
-
}
|
|
49957
|
-
return out;
|
|
49958
|
-
}
|
|
49959
|
-
function queryDeviceHistoryStats(deviceId) {
|
|
49960
|
-
const dir = historyDir2();
|
|
49961
|
-
const files = jsonlFilesForDevice(deviceId);
|
|
49962
|
-
let totalBytes = 0;
|
|
49963
|
-
let oldest = null;
|
|
49964
|
-
let newest = null;
|
|
49965
|
-
let count = 0;
|
|
49966
|
-
for (const file2 of files) {
|
|
49967
|
-
try {
|
|
49968
|
-
totalBytes += fs10.statSync(file2).size;
|
|
49969
|
-
} catch {
|
|
49970
|
-
}
|
|
49971
|
-
}
|
|
49972
|
-
for (const file2 of files) {
|
|
49973
|
-
try {
|
|
49974
|
-
const lines = fs10.readFileSync(file2, "utf-8").split("\n");
|
|
49975
|
-
for (const line of lines) {
|
|
49976
|
-
if (!line) continue;
|
|
49977
|
-
count += 1;
|
|
49978
|
-
try {
|
|
49979
|
-
const rec = JSON.parse(line);
|
|
49980
|
-
const tMs = Date.parse(rec.t);
|
|
49981
|
-
if (Number.isFinite(tMs)) {
|
|
49982
|
-
if (oldest === null || tMs < oldest) oldest = tMs;
|
|
49983
|
-
if (newest === null || tMs > newest) newest = tMs;
|
|
49984
|
-
}
|
|
49985
|
-
} catch {
|
|
49986
|
-
}
|
|
49987
|
-
}
|
|
49988
|
-
} catch {
|
|
49989
|
-
}
|
|
49990
|
-
}
|
|
49991
|
-
return {
|
|
49992
|
-
deviceId,
|
|
49993
|
-
fileCount: files.length,
|
|
49994
|
-
totalBytes,
|
|
49995
|
-
recordCount: count,
|
|
49996
|
-
oldest: oldest !== null ? new Date(oldest).toISOString() : void 0,
|
|
49997
|
-
newest: newest !== null ? new Date(newest).toISOString() : void 0,
|
|
49998
|
-
jsonlFiles: files.map((f2) => path10.basename(f2)),
|
|
49999
|
-
historyDir: dir
|
|
50000
|
-
};
|
|
50001
|
-
}
|
|
51149
|
+
// src/commands/mcp.ts
|
|
51150
|
+
init_history_query();
|
|
50002
51151
|
|
|
50003
51152
|
// src/devices/history-agg.ts
|
|
50004
51153
|
init_cjs_shim();
|
|
51154
|
+
init_history_query();
|
|
50005
51155
|
import fs11 from "node:fs";
|
|
50006
51156
|
import readline3 from "node:readline";
|
|
50007
51157
|
var ALL_AGG_FNS = ["count", "min", "max", "avg", "sum", "p50", "p95"];
|
|
@@ -50545,7 +51695,7 @@ async function executePlanSteps(plan, planId, options) {
|
|
|
50545
51695
|
if (err instanceof Error && err.name === "DryRunSignal") {
|
|
50546
51696
|
out.results.push({ step: idx, type: "command", deviceId: resolvedDeviceId, command: step.command, status: "dry-run" });
|
|
50547
51697
|
out.summary.dryRun++;
|
|
50548
|
-
if (!isJsonMode()) console.
|
|
51698
|
+
if (!isJsonMode()) console.error(` ${idx}. \u25E6 dry-run ${step.command} on ${resolvedDeviceId}`);
|
|
50549
51699
|
continue;
|
|
50550
51700
|
}
|
|
50551
51701
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -51125,7 +52275,7 @@ var import_yaml5 = __toESM(require_dist(), 1);
|
|
|
51125
52275
|
var import_yaml6 = __toESM(require_dist(), 1);
|
|
51126
52276
|
init_load();
|
|
51127
52277
|
init_validate();
|
|
51128
|
-
import
|
|
52278
|
+
import fs16 from "node:fs";
|
|
51129
52279
|
var AddRuleError = class extends Error {
|
|
51130
52280
|
constructor(message, code) {
|
|
51131
52281
|
super(message);
|
|
@@ -51241,7 +52391,7 @@ ${msgs}`,
|
|
|
51241
52391
|
function addRuleToPolicyFile(opts) {
|
|
51242
52392
|
const result = addRuleToPolicySource(opts);
|
|
51243
52393
|
if (!opts.dryRun) {
|
|
51244
|
-
|
|
52394
|
+
fs16.writeFileSync(opts.policyPath, result.nextSource, "utf8");
|
|
51245
52395
|
return { ...result, written: true };
|
|
51246
52396
|
}
|
|
51247
52397
|
return { ...result, written: false };
|
|
@@ -51250,15 +52400,15 @@ function addRuleToPolicyFile(opts) {
|
|
|
51250
52400
|
// src/rules/explain.ts
|
|
51251
52401
|
init_cjs_shim();
|
|
51252
52402
|
init_trace();
|
|
51253
|
-
import
|
|
52403
|
+
import fs17 from "node:fs";
|
|
51254
52404
|
function loadTraceRecords(auditFile, opts = {}) {
|
|
51255
|
-
if (!
|
|
51256
|
-
const lines =
|
|
52405
|
+
if (!fs17.existsSync(auditFile)) return [];
|
|
52406
|
+
const lines = fs17.readFileSync(auditFile, "utf-8").split(/\r?\n/);
|
|
51257
52407
|
return filterTraceRecords(lines, opts);
|
|
51258
52408
|
}
|
|
51259
52409
|
function loadRelatedAudit(auditFile, fireId) {
|
|
51260
|
-
if (!
|
|
51261
|
-
const raw =
|
|
52410
|
+
if (!fs17.existsSync(auditFile)) return [];
|
|
52411
|
+
const raw = fs17.readFileSync(auditFile, "utf-8");
|
|
51262
52412
|
const out = [];
|
|
51263
52413
|
for (const line of raw.split(/\r?\n/)) {
|
|
51264
52414
|
const trimmed = line.trim();
|
|
@@ -51342,17 +52492,18 @@ init_throttle();
|
|
|
51342
52492
|
init_trace();
|
|
51343
52493
|
init_trace();
|
|
51344
52494
|
init_matcher();
|
|
51345
|
-
|
|
52495
|
+
init_engine();
|
|
52496
|
+
import fs18 from "node:fs";
|
|
51346
52497
|
import path14 from "node:path";
|
|
51347
52498
|
import os13 from "node:os";
|
|
51348
52499
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
51349
|
-
var
|
|
52500
|
+
var HOUR_MS2 = 60 * 60 * 1e3;
|
|
51350
52501
|
var DEVICE_HISTORY_DIR = path14.join(os13.homedir(), ".switchbot", "device-history");
|
|
51351
52502
|
async function simulateRule(opts) {
|
|
51352
52503
|
const { rule, aliases = {}, liveLlm = false } = opts;
|
|
51353
52504
|
const rv = ruleVersion(rule);
|
|
51354
52505
|
const events = loadSourceEvents(opts);
|
|
51355
|
-
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);
|
|
51356
52507
|
const windowEnd = events.length > 0 ? new Date(Math.max(...events.map((e) => e.t.getTime()))) : /* @__PURE__ */ new Date();
|
|
51357
52508
|
const counts = { wouldFire: 0, blocked: 0, throttled: 0, errored: 0, skippedLlm: 0 };
|
|
51358
52509
|
const blockReasons = /* @__PURE__ */ new Map();
|
|
@@ -51402,7 +52553,8 @@ async function simulateRule(opts) {
|
|
|
51402
52553
|
aliases,
|
|
51403
52554
|
fetchStatus: statusFetcher,
|
|
51404
52555
|
event,
|
|
51405
|
-
ruleVersion: rv
|
|
52556
|
+
ruleVersion: rv,
|
|
52557
|
+
eventWindowFetcher: defaultEventWindowFetcher
|
|
51406
52558
|
});
|
|
51407
52559
|
} catch (err) {
|
|
51408
52560
|
counts.errored++;
|
|
@@ -51451,8 +52603,8 @@ async function simulateRule(opts) {
|
|
|
51451
52603
|
}
|
|
51452
52604
|
function loadSourceEvents(opts) {
|
|
51453
52605
|
if (opts.against) {
|
|
51454
|
-
if (!
|
|
51455
|
-
const lines2 =
|
|
52606
|
+
if (!fs18.existsSync(opts.against)) return [];
|
|
52607
|
+
const lines2 = fs18.readFileSync(opts.against, "utf-8").split(/\r?\n/);
|
|
51456
52608
|
const events = [];
|
|
51457
52609
|
for (const line of lines2) {
|
|
51458
52610
|
const trimmed = line.trim();
|
|
@@ -51472,10 +52624,10 @@ function loadSourceEvents(opts) {
|
|
|
51472
52624
|
return events;
|
|
51473
52625
|
}
|
|
51474
52626
|
const auditLog = opts.auditLog;
|
|
51475
|
-
if (!auditLog || !
|
|
51476
|
-
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;
|
|
51477
52629
|
const sinceIso = new Date(sinceMs).toISOString();
|
|
51478
|
-
const lines =
|
|
52630
|
+
const lines = fs18.readFileSync(auditLog, "utf-8").split(/\r?\n/);
|
|
51479
52631
|
const traceRecords = filterTraceRecords(lines, {
|
|
51480
52632
|
ruleName: opts.rule.name,
|
|
51481
52633
|
since: sinceIso
|
|
@@ -51499,13 +52651,13 @@ function parseSince(since) {
|
|
|
51499
52651
|
const unitMs = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
51500
52652
|
return Date.now() - n * unitMs;
|
|
51501
52653
|
}
|
|
51502
|
-
return Date.now() - 24 *
|
|
52654
|
+
return Date.now() - 24 * HOUR_MS2;
|
|
51503
52655
|
}
|
|
51504
52656
|
function buildStatusFetcher(asOf) {
|
|
51505
52657
|
return async (deviceId) => {
|
|
51506
52658
|
const histFile = path14.join(DEVICE_HISTORY_DIR, `${deviceId}.jsonl`);
|
|
51507
|
-
if (!
|
|
51508
|
-
const lines =
|
|
52659
|
+
if (!fs18.existsSync(histFile)) return {};
|
|
52660
|
+
const lines = fs18.readFileSync(histFile, "utf-8").split(/\r?\n/);
|
|
51509
52661
|
const asOfMs = asOf.getTime();
|
|
51510
52662
|
let best;
|
|
51511
52663
|
for (const line of lines) {
|
|
@@ -51541,13 +52693,13 @@ function collectPolicyDiff(left, right, at, out, limit) {
|
|
|
51541
52693
|
const maxLen = Math.max(left.length, right.length);
|
|
51542
52694
|
for (let i = 0; i < maxLen; i++) {
|
|
51543
52695
|
if (out.length >= limit) return;
|
|
51544
|
-
const
|
|
52696
|
+
const path31 = `${at}[${i}]`;
|
|
51545
52697
|
if (i >= left.length) {
|
|
51546
|
-
out.push({ path:
|
|
52698
|
+
out.push({ path: path31, kind: "added", after: right[i] });
|
|
51547
52699
|
} else if (i >= right.length) {
|
|
51548
|
-
out.push({ path:
|
|
52700
|
+
out.push({ path: path31, kind: "removed", before: left[i] });
|
|
51549
52701
|
} else {
|
|
51550
|
-
collectPolicyDiff(left[i], right[i],
|
|
52702
|
+
collectPolicyDiff(left[i], right[i], path31, out, limit);
|
|
51551
52703
|
}
|
|
51552
52704
|
}
|
|
51553
52705
|
return;
|
|
@@ -51556,15 +52708,15 @@ function collectPolicyDiff(left, right, at, out, limit) {
|
|
|
51556
52708
|
const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
51557
52709
|
for (const key of [...keys].sort()) {
|
|
51558
52710
|
if (out.length >= limit) return;
|
|
51559
|
-
const
|
|
52711
|
+
const path31 = at === "$" ? `$.${key}` : `${at}.${key}`;
|
|
51560
52712
|
const leftHas = Object.prototype.hasOwnProperty.call(left, key);
|
|
51561
52713
|
const rightHas = Object.prototype.hasOwnProperty.call(right, key);
|
|
51562
52714
|
if (!leftHas && rightHas) {
|
|
51563
|
-
out.push({ path:
|
|
52715
|
+
out.push({ path: path31, kind: "added", after: right[key] });
|
|
51564
52716
|
} else if (leftHas && !rightHas) {
|
|
51565
|
-
out.push({ path:
|
|
52717
|
+
out.push({ path: path31, kind: "removed", before: left[key] });
|
|
51566
52718
|
} else {
|
|
51567
|
-
collectPolicyDiff(left[key], right[key],
|
|
52719
|
+
collectPolicyDiff(left[key], right[key], path31, out, limit);
|
|
51568
52720
|
}
|
|
51569
52721
|
}
|
|
51570
52722
|
return;
|
|
@@ -51618,7 +52770,7 @@ function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges
|
|
|
51618
52770
|
init_embedded_assets();
|
|
51619
52771
|
import { dirname as pathDirname, join as pathJoin } from "node:path";
|
|
51620
52772
|
import os14 from "node:os";
|
|
51621
|
-
import
|
|
52773
|
+
import fs19 from "node:fs";
|
|
51622
52774
|
var LATEST_SUPPORTED_VERSION = SUPPORTED_POLICY_SCHEMA_VERSIONS[SUPPORTED_POLICY_SCHEMA_VERSIONS.length - 1];
|
|
51623
52775
|
function mcpError(kind, code, message, options) {
|
|
51624
52776
|
const obj = { code, kind, message };
|
|
@@ -52594,15 +53746,15 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
|
|
|
52594
53746
|
async ({ path: pathArg, force }) => {
|
|
52595
53747
|
const policyPath = resolvePolicyPath({ flag: pathArg });
|
|
52596
53748
|
const doForce = force === true;
|
|
52597
|
-
if (
|
|
53749
|
+
if (fs19.existsSync(policyPath) && !doForce) {
|
|
52598
53750
|
return mcpError("guard", 5, `refusing to overwrite existing policy at ${policyPath}`, {
|
|
52599
53751
|
hint: "pass force=true to overwrite, or choose a different path",
|
|
52600
53752
|
context: { policyPath }
|
|
52601
53753
|
});
|
|
52602
53754
|
}
|
|
52603
53755
|
const template = readPolicyExampleYaml();
|
|
52604
|
-
|
|
52605
|
-
|
|
53756
|
+
fs19.mkdirSync(pathDirname(policyPath), { recursive: true });
|
|
53757
|
+
fs19.writeFileSync(policyPath, template, { encoding: "utf-8" });
|
|
52606
53758
|
const structured = {
|
|
52607
53759
|
policyPath,
|
|
52608
53760
|
schemaVersion: CURRENT_POLICY_SCHEMA_VERSION,
|
|
@@ -52791,7 +53943,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
|
|
|
52791
53943
|
let leftSource = "";
|
|
52792
53944
|
let rightSource = "";
|
|
52793
53945
|
try {
|
|
52794
|
-
leftSource =
|
|
53946
|
+
leftSource = fs19.readFileSync(left_path, "utf-8");
|
|
52795
53947
|
} catch (err) {
|
|
52796
53948
|
if (err?.code === "ENOENT") {
|
|
52797
53949
|
return mcpError("usage", 2, `policy file not found: ${left_path}`, {
|
|
@@ -52801,7 +53953,7 @@ Tool profile: ${profileName} (${allowedTools.size} tools loaded).${profileName !
|
|
|
52801
53953
|
return mcpError("runtime", 1, `failed to read ${left_path}: ${String(err)}`);
|
|
52802
53954
|
}
|
|
52803
53955
|
try {
|
|
52804
|
-
rightSource =
|
|
53956
|
+
rightSource = fs19.readFileSync(right_path, "utf-8");
|
|
52805
53957
|
} catch (err) {
|
|
52806
53958
|
if (err?.code === "ENOENT") {
|
|
52807
53959
|
return mcpError("usage", 2, `policy file not found: ${right_path}`, {
|
|
@@ -53515,21 +54667,21 @@ Example Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
|
|
|
53515
54667
|
Inspect locally:
|
|
53516
54668
|
$ npx @modelcontextprotocol/inspector switchbot mcp serve
|
|
53517
54669
|
`);
|
|
53518
|
-
mcp.command("tools").description("Print the registered MCP tools in human or JSON form").option("--tools <profile>",
|
|
54670
|
+
mcp.command("tools").description("Print the registered MCP tools in human or JSON form").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24). Lists all when omitted', stringArg("--tools"), "all").action((opts) => {
|
|
53519
54671
|
try {
|
|
53520
54672
|
printMcpToolDirectory(resolveToolProfile(opts.tools));
|
|
53521
54673
|
} catch (e) {
|
|
53522
54674
|
handleError(e);
|
|
53523
54675
|
}
|
|
53524
54676
|
});
|
|
53525
|
-
mcp.command("list-tools").description("Alias of `mcp tools`").option("--tools <profile>",
|
|
54677
|
+
mcp.command("list-tools").description("Alias of `mcp tools`").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24). Lists all when omitted', stringArg("--tools"), "all").action((opts) => {
|
|
53526
54678
|
try {
|
|
53527
54679
|
printMcpToolDirectory(resolveToolProfile(opts.tools));
|
|
53528
54680
|
} catch (e) {
|
|
53529
54681
|
handleError(e);
|
|
53530
54682
|
}
|
|
53531
54683
|
});
|
|
53532
|
-
mcp.command("serve").description("Start the MCP server on stdio (default) or HTTP (--port)").option("--port <n>", "Listen on HTTP instead of stdio (Streamable HTTP transport)", intArg("--port", { min: 1, max: 65535 })).option("--bind <host>", "IP address to bind (default 127.0.0.1; use 0.0.0.0 to accept external connections)", stringArg("--bind"), "127.0.0.1").option("--auth-token <token>", "Bearer token for HTTP requests (required for --bind 0.0.0.0; falls back to SWITCHBOT_MCP_TOKEN env var)", stringArg("--auth-token")).option("--cors-origin <url>", "Allowed CORS origin(s) for HTTP (repeatable)", stringArg("--cors-origin")).option("--rate-limit <n>", "Max requests per minute per profile (default 60)", intArg("--rate-limit", { min: 1 }), "60").option("--tools <profile>",
|
|
54684
|
+
mcp.command("serve").description("Start the MCP server on stdio (default) or HTTP (--port)").option("--port <n>", "Listen on HTTP instead of stdio (Streamable HTTP transport)", intArg("--port", { min: 1, max: 65535 })).option("--bind <host>", "IP address to bind (default 127.0.0.1; use 0.0.0.0 to accept external connections)", stringArg("--bind"), "127.0.0.1").option("--auth-token <token>", "Bearer token for HTTP requests (required for --bind 0.0.0.0; falls back to SWITCHBOT_MCP_TOKEN env var)", stringArg("--auth-token")).option("--cors-origin <url>", "Allowed CORS origin(s) for HTTP (repeatable)", stringArg("--cors-origin")).option("--rate-limit <n>", "Max requests per minute per profile (default 60)", intArg("--rate-limit", { min: 1 }), "60").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24)', stringArg("--tools"), "default").addHelpText("after", `
|
|
53533
54685
|
Examples:
|
|
53534
54686
|
$ switchbot mcp serve
|
|
53535
54687
|
$ switchbot mcp serve --tools all
|
|
@@ -53676,7 +54828,7 @@ process_uptime_seconds ${Math.floor(process.uptime())}
|
|
|
53676
54828
|
}
|
|
53677
54829
|
if (profile) {
|
|
53678
54830
|
const envCredsPresent = !!(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
|
|
53679
|
-
if (!envCredsPresent && !
|
|
54831
|
+
if (!envCredsPresent && !fs19.existsSync(profileFilePath(profile))) {
|
|
53680
54832
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
53681
54833
|
res.end(JSON.stringify({
|
|
53682
54834
|
jsonrpc: "2.0",
|
|
@@ -53977,6 +55129,7 @@ Total: ${entries.length} device type(s) (source: ${source})`);
|
|
|
53977
55129
|
});
|
|
53978
55130
|
catalog.command("search").description("Fuzzy search the effective catalog by type name, alias, role, or command name").argument("<keyword>", "Substring to match (case-insensitive) against type, alias, role, or command").option("--strict", "Only return entries whose type name matches (skip alias/role/command fallbacks)").action((keyword, options) => {
|
|
53979
55131
|
try {
|
|
55132
|
+
if (!keyword.trim()) throw new UsageError("catalog search requires a non-empty keyword.");
|
|
53980
55133
|
const q = keyword.toLowerCase();
|
|
53981
55134
|
const entries = getEffectiveCatalog();
|
|
53982
55135
|
const strict = options.strict === true;
|
|
@@ -54281,7 +55434,7 @@ init_cjs_shim();
|
|
|
54281
55434
|
init_output();
|
|
54282
55435
|
init_arg_parsers();
|
|
54283
55436
|
init_flags();
|
|
54284
|
-
import
|
|
55437
|
+
import http6 from "node:http";
|
|
54285
55438
|
import crypto4 from "node:crypto";
|
|
54286
55439
|
init_client2();
|
|
54287
55440
|
init_credential();
|
|
@@ -54312,18 +55465,18 @@ var StdoutSink = class {
|
|
|
54312
55465
|
|
|
54313
55466
|
// src/sinks/file.ts
|
|
54314
55467
|
init_cjs_shim();
|
|
54315
|
-
import
|
|
55468
|
+
import fs20 from "node:fs";
|
|
54316
55469
|
import path15 from "node:path";
|
|
54317
55470
|
var FileSink = class {
|
|
54318
55471
|
filePath;
|
|
54319
55472
|
constructor(filePath) {
|
|
54320
55473
|
this.filePath = path15.resolve(filePath);
|
|
54321
55474
|
const dir = path15.dirname(this.filePath);
|
|
54322
|
-
if (!
|
|
55475
|
+
if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
|
|
54323
55476
|
}
|
|
54324
55477
|
async write(event) {
|
|
54325
55478
|
try {
|
|
54326
|
-
|
|
55479
|
+
fs20.appendFileSync(this.filePath, JSON.stringify(event) + "\n", { encoding: "utf-8" });
|
|
54327
55480
|
} catch {
|
|
54328
55481
|
}
|
|
54329
55482
|
}
|
|
@@ -54577,7 +55730,7 @@ function parseFilter2(flag) {
|
|
|
54577
55730
|
}
|
|
54578
55731
|
}
|
|
54579
55732
|
function startReceiver(port, pathMatch, filter, onEvent) {
|
|
54580
|
-
const server =
|
|
55733
|
+
const server = http6.createServer((req, res) => {
|
|
54581
55734
|
if (req.method !== "POST") {
|
|
54582
55735
|
res.statusCode = 405;
|
|
54583
55736
|
res.end("method not allowed");
|
|
@@ -54992,10 +56145,10 @@ init_catalog();
|
|
|
54992
56145
|
init_config();
|
|
54993
56146
|
init_cache();
|
|
54994
56147
|
init_quota();
|
|
54995
|
-
import
|
|
54996
|
-
import
|
|
54997
|
-
import
|
|
54998
|
-
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";
|
|
54999
56152
|
|
|
55000
56153
|
// src/commands/agent-bootstrap.ts
|
|
55001
56154
|
init_cjs_shim();
|
|
@@ -55230,7 +56383,7 @@ init_request_context();
|
|
|
55230
56383
|
|
|
55231
56384
|
// src/lib/daemon-state.ts
|
|
55232
56385
|
init_cjs_shim();
|
|
55233
|
-
import
|
|
56386
|
+
import fs21 from "node:fs";
|
|
55234
56387
|
import os15 from "node:os";
|
|
55235
56388
|
import path16 from "node:path";
|
|
55236
56389
|
function getStateDir() {
|
|
@@ -55253,15 +56406,15 @@ var DAEMON_LOG_FILE = getDaemonLogFile();
|
|
|
55253
56406
|
var DAEMON_STATE_FILE = getDaemonStateFile();
|
|
55254
56407
|
var HEALTHZ_PID_FILE = getHealthzPidFile();
|
|
55255
56408
|
function ensureStateDir() {
|
|
55256
|
-
|
|
56409
|
+
fs21.mkdirSync(getStateDir(), { recursive: true, mode: 448 });
|
|
55257
56410
|
}
|
|
55258
56411
|
function writeDaemonState(state) {
|
|
55259
56412
|
ensureStateDir();
|
|
55260
|
-
|
|
56413
|
+
fs21.writeFileSync(getDaemonStateFile(), JSON.stringify(state, null, 2), { mode: 384 });
|
|
55261
56414
|
}
|
|
55262
56415
|
function readDaemonState() {
|
|
55263
56416
|
try {
|
|
55264
|
-
const raw =
|
|
56417
|
+
const raw = fs21.readFileSync(getDaemonStateFile(), "utf-8");
|
|
55265
56418
|
return JSON.parse(raw);
|
|
55266
56419
|
} catch {
|
|
55267
56420
|
return null;
|
|
@@ -55270,7 +56423,7 @@ function readDaemonState() {
|
|
|
55270
56423
|
|
|
55271
56424
|
// src/rules/pid-file.ts
|
|
55272
56425
|
init_cjs_shim();
|
|
55273
|
-
import
|
|
56426
|
+
import fs22 from "node:fs";
|
|
55274
56427
|
import os16 from "node:os";
|
|
55275
56428
|
import path17 from "node:path";
|
|
55276
56429
|
var DEFAULT_DIR = path17.join(os16.homedir(), ".switchbot");
|
|
@@ -55283,13 +56436,13 @@ function getDefaultPidFilePaths() {
|
|
|
55283
56436
|
}
|
|
55284
56437
|
function writePidFile(pidFile, pid = process.pid) {
|
|
55285
56438
|
const dir = path17.dirname(pidFile);
|
|
55286
|
-
|
|
55287
|
-
|
|
56439
|
+
fs22.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
56440
|
+
fs22.writeFileSync(pidFile, `${pid}
|
|
55288
56441
|
`, { mode: 384 });
|
|
55289
56442
|
}
|
|
55290
56443
|
function readPidFile(pidFile) {
|
|
55291
56444
|
try {
|
|
55292
|
-
const raw =
|
|
56445
|
+
const raw = fs22.readFileSync(pidFile, "utf-8").trim();
|
|
55293
56446
|
const n = Number(raw);
|
|
55294
56447
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
55295
56448
|
} catch {
|
|
@@ -55299,20 +56452,20 @@ function readPidFile(pidFile) {
|
|
|
55299
56452
|
function clearPidFile(pidFile, pid = process.pid) {
|
|
55300
56453
|
try {
|
|
55301
56454
|
const existing = readPidFile(pidFile);
|
|
55302
|
-
if (existing === pid)
|
|
56455
|
+
if (existing === pid) fs22.unlinkSync(pidFile);
|
|
55303
56456
|
} catch {
|
|
55304
56457
|
}
|
|
55305
56458
|
}
|
|
55306
56459
|
function writeReloadSentinel(reloadFile) {
|
|
55307
56460
|
const dir = path17.dirname(reloadFile);
|
|
55308
|
-
|
|
55309
|
-
|
|
56461
|
+
fs22.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
56462
|
+
fs22.writeFileSync(reloadFile, `${Date.now()}
|
|
55310
56463
|
`, { mode: 384 });
|
|
55311
56464
|
}
|
|
55312
56465
|
function consumeReloadSentinel(reloadFile) {
|
|
55313
56466
|
try {
|
|
55314
|
-
if (!
|
|
55315
|
-
|
|
56467
|
+
if (!fs22.existsSync(reloadFile)) return false;
|
|
56468
|
+
fs22.unlinkSync(reloadFile);
|
|
55316
56469
|
return true;
|
|
55317
56470
|
} catch {
|
|
55318
56471
|
return false;
|
|
@@ -55383,7 +56536,7 @@ async function checkCredentials() {
|
|
|
55383
56536
|
};
|
|
55384
56537
|
}
|
|
55385
56538
|
const file2 = configFilePath();
|
|
55386
|
-
if (!
|
|
56539
|
+
if (!fs24.existsSync(file2)) {
|
|
55387
56540
|
return {
|
|
55388
56541
|
name: "credentials",
|
|
55389
56542
|
status: "fail",
|
|
@@ -55398,7 +56551,7 @@ async function checkCredentials() {
|
|
|
55398
56551
|
};
|
|
55399
56552
|
}
|
|
55400
56553
|
try {
|
|
55401
|
-
const raw =
|
|
56554
|
+
const raw = fs24.readFileSync(file2, "utf-8");
|
|
55402
56555
|
const cfg = JSON.parse(raw);
|
|
55403
56556
|
if (!cfg.token || !cfg.secret) {
|
|
55404
56557
|
return {
|
|
@@ -55445,8 +56598,8 @@ async function checkCredentials() {
|
|
|
55445
56598
|
}
|
|
55446
56599
|
}
|
|
55447
56600
|
function checkProfiles() {
|
|
55448
|
-
const dir =
|
|
55449
|
-
if (!
|
|
56601
|
+
const dir = path19.join(os18.homedir(), ".switchbot", "profiles");
|
|
56602
|
+
if (!fs24.existsSync(dir)) {
|
|
55450
56603
|
return { name: "profiles", status: "ok", detail: "no profile dir (default profile only)" };
|
|
55451
56604
|
}
|
|
55452
56605
|
const profiles = listProfiles();
|
|
@@ -55541,6 +56694,25 @@ function checkCatalog() {
|
|
|
55541
56694
|
detail: `${catalog.length} types loaded${missingRole > 0 ? `, ${missingRole} missing role` : ""}`
|
|
55542
56695
|
};
|
|
55543
56696
|
}
|
|
56697
|
+
function checkCatalogCoverage() {
|
|
56698
|
+
const cache2 = loadCache();
|
|
56699
|
+
if (!cache2) {
|
|
56700
|
+
return { name: "catalog-coverage", status: "ok", detail: 'no device cache \u2014 run "switchbot devices list" first' };
|
|
56701
|
+
}
|
|
56702
|
+
const catalog = getEffectiveCatalog();
|
|
56703
|
+
const catalogTypes = new Set(catalog.map((e) => e.type.toLowerCase()));
|
|
56704
|
+
const aliases = new Set(catalog.flatMap((e) => (e.aliases ?? []).map((a) => a.toLowerCase())));
|
|
56705
|
+
const deviceTypes = [...new Set(Object.values(cache2.devices).map((d) => d.type))];
|
|
56706
|
+
const missing = deviceTypes.filter((t) => !catalogTypes.has(t.toLowerCase()) && !aliases.has(t.toLowerCase()));
|
|
56707
|
+
if (missing.length === 0) {
|
|
56708
|
+
return { name: "catalog-coverage", status: "ok", detail: `all ${deviceTypes.length} device types have catalog entries` };
|
|
56709
|
+
}
|
|
56710
|
+
return {
|
|
56711
|
+
name: "catalog-coverage",
|
|
56712
|
+
status: "warn",
|
|
56713
|
+
detail: { missing, message: `${missing.length} device type(s) without catalog entry` }
|
|
56714
|
+
};
|
|
56715
|
+
}
|
|
55544
56716
|
function checkCache() {
|
|
55545
56717
|
try {
|
|
55546
56718
|
const info = describeCache();
|
|
@@ -55553,8 +56725,8 @@ function checkCache() {
|
|
|
55553
56725
|
}
|
|
55554
56726
|
}
|
|
55555
56727
|
function checkQuotaFile() {
|
|
55556
|
-
const p2 =
|
|
55557
|
-
if (!
|
|
56728
|
+
const p2 = path19.join(os18.homedir(), ".switchbot", "quota.json");
|
|
56729
|
+
if (!fs24.existsSync(p2)) {
|
|
55558
56730
|
return {
|
|
55559
56731
|
name: "quota",
|
|
55560
56732
|
status: "ok",
|
|
@@ -55567,7 +56739,7 @@ function checkQuotaFile() {
|
|
|
55567
56739
|
};
|
|
55568
56740
|
}
|
|
55569
56741
|
try {
|
|
55570
|
-
const raw =
|
|
56742
|
+
const raw = fs24.readFileSync(p2, "utf-8");
|
|
55571
56743
|
JSON.parse(raw);
|
|
55572
56744
|
} catch {
|
|
55573
56745
|
return {
|
|
@@ -55642,13 +56814,14 @@ function checkInventoryConsistency() {
|
|
|
55642
56814
|
status: "warn",
|
|
55643
56815
|
detail: {
|
|
55644
56816
|
message: `${dangling.length} device(s) reference a hubDeviceId that is not present in the current inventory`,
|
|
56817
|
+
hint: "This usually means the hub was removed or replaced. Re-pair affected devices in the SwitchBot app, or ignore if the devices still work.",
|
|
55645
56818
|
dangling: dangling.slice(0, 10)
|
|
55646
56819
|
}
|
|
55647
56820
|
};
|
|
55648
56821
|
}
|
|
55649
56822
|
function checkAudit() {
|
|
55650
|
-
const p2 =
|
|
55651
|
-
if (!
|
|
56823
|
+
const p2 = path19.join(os18.homedir(), ".switchbot", "audit.log");
|
|
56824
|
+
if (!fs24.existsSync(p2)) {
|
|
55652
56825
|
return {
|
|
55653
56826
|
name: "audit",
|
|
55654
56827
|
status: "ok",
|
|
@@ -55660,7 +56833,7 @@ function checkAudit() {
|
|
|
55660
56833
|
};
|
|
55661
56834
|
}
|
|
55662
56835
|
try {
|
|
55663
|
-
const raw =
|
|
56836
|
+
const raw = fs24.readFileSync(p2, "utf-8");
|
|
55664
56837
|
const since = Date.now() - 24 * 60 * 60 * 1e3;
|
|
55665
56838
|
const recent = [];
|
|
55666
56839
|
let total = 0;
|
|
@@ -55859,14 +57032,14 @@ function checkPathDiscoverability() {
|
|
|
55859
57032
|
const binaryName = isWindows ? "switchbot.cmd" : "switchbot";
|
|
55860
57033
|
let npmBinDir = null;
|
|
55861
57034
|
try {
|
|
55862
|
-
const prefix =
|
|
55863
|
-
npmBinDir = isWindows ? prefix :
|
|
57035
|
+
const prefix = execSync2("npm prefix -g", { timeout: 4e3, encoding: "utf-8" }).trim();
|
|
57036
|
+
npmBinDir = isWindows ? prefix : path19.join(prefix, "bin");
|
|
55864
57037
|
} catch {
|
|
55865
57038
|
}
|
|
55866
57039
|
let binaryOnPath = false;
|
|
55867
57040
|
let resolvedPath = null;
|
|
55868
57041
|
try {
|
|
55869
|
-
const which =
|
|
57042
|
+
const which = execSync2(
|
|
55870
57043
|
isWindows ? `where ${binaryName}` : `which ${binaryName}`,
|
|
55871
57044
|
{ timeout: 3e3, encoding: "utf-8" }
|
|
55872
57045
|
).trim().split(/\r?\n/)[0];
|
|
@@ -55890,7 +57063,7 @@ function checkPathDiscoverability() {
|
|
|
55890
57063
|
};
|
|
55891
57064
|
}
|
|
55892
57065
|
const currentPath = process.env.PATH ?? "";
|
|
55893
|
-
const missingSegment = npmBinDir && !currentPath.split(
|
|
57066
|
+
const missingSegment = npmBinDir && !currentPath.split(path19.delimiter).includes(npmBinDir) ? npmBinDir : null;
|
|
55894
57067
|
const currentShell = detectShellFlavor();
|
|
55895
57068
|
const shellFix = buildPathFix(currentShell, missingSegment, npmBinDir);
|
|
55896
57069
|
return {
|
|
@@ -55991,9 +57164,9 @@ function checkMqtt() {
|
|
|
55991
57164
|
};
|
|
55992
57165
|
}
|
|
55993
57166
|
const file2 = configFilePath();
|
|
55994
|
-
if (
|
|
57167
|
+
if (fs24.existsSync(file2)) {
|
|
55995
57168
|
try {
|
|
55996
|
-
const cfg = JSON.parse(
|
|
57169
|
+
const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
|
|
55997
57170
|
if (cfg.token && cfg.secret) {
|
|
55998
57171
|
return {
|
|
55999
57172
|
name: "mqtt",
|
|
@@ -56020,9 +57193,9 @@ async function checkMqttProbe() {
|
|
|
56020
57193
|
creds = { token, secret };
|
|
56021
57194
|
} else {
|
|
56022
57195
|
const file2 = configFilePath();
|
|
56023
|
-
if (
|
|
57196
|
+
if (fs24.existsSync(file2)) {
|
|
56024
57197
|
try {
|
|
56025
|
-
const cfg = JSON.parse(
|
|
57198
|
+
const cfg = JSON.parse(fs24.readFileSync(file2, "utf-8"));
|
|
56026
57199
|
if (cfg.token && cfg.secret) {
|
|
56027
57200
|
creds = { token: cfg.token, secret: cfg.secret };
|
|
56028
57201
|
}
|
|
@@ -56136,6 +57309,112 @@ function checkNotifyConnectivity() {
|
|
|
56136
57309
|
detail: { webhookCount: webhookUrls.length, message: `${webhookUrls.length} webhook URL(s) configured (live probe not run \u2014 use --probe to test connectivity)` }
|
|
56137
57310
|
};
|
|
56138
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
|
+
}
|
|
56139
57418
|
var CHECK_REGISTRY = [
|
|
56140
57419
|
{ name: "node", description: "Node.js version compatibility", run: () => checkNodeVersion() },
|
|
56141
57420
|
{ name: "path", description: "switchbot binary reachable on PATH", run: () => checkPathDiscoverability() },
|
|
@@ -56143,6 +57422,7 @@ var CHECK_REGISTRY = [
|
|
|
56143
57422
|
{ name: "keychain", description: "OS keychain backend availability and usage", run: () => checkKeychain() },
|
|
56144
57423
|
{ name: "profiles", description: "profile definitions valid", run: () => checkProfiles() },
|
|
56145
57424
|
{ name: "catalog", description: "catalog loads", run: () => checkCatalog() },
|
|
57425
|
+
{ name: "catalog-coverage", description: "all cached device types have catalog entries", run: () => checkCatalogCoverage() },
|
|
56146
57426
|
{ name: "catalog-schema", description: "catalog vs agent-bootstrap version aligned", run: () => checkCatalogSchema() },
|
|
56147
57427
|
{ name: "inventory", description: "cached inventory graph consistency (hubDeviceId references)", run: () => checkInventoryConsistency() },
|
|
56148
57428
|
{ name: "cache", description: "device cache state", run: () => checkCache() },
|
|
@@ -56159,7 +57439,9 @@ var CHECK_REGISTRY = [
|
|
|
56159
57439
|
{ name: "audit", description: "recent command errors (last 24h)", run: () => checkAudit() },
|
|
56160
57440
|
{ name: "daemon", description: "daemon state file + runtime status", run: () => checkDaemon() },
|
|
56161
57441
|
{ name: "health", description: "health endpoint availability (daemon --healthz-port)", run: () => checkHealthEndpoint() },
|
|
56162
|
-
{ 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() }
|
|
56163
57445
|
];
|
|
56164
57446
|
function applyFixes(checks, writeOk) {
|
|
56165
57447
|
const results = [];
|
|
@@ -56204,7 +57486,7 @@ function applyFixes(checks, writeOk) {
|
|
|
56204
57486
|
return results;
|
|
56205
57487
|
}
|
|
56206
57488
|
function registerDoctorCommand(program3) {
|
|
56207
|
-
program3.command("doctor").description("Self-check the SwitchBot CLI setup: credentials, catalog, cache, quota, MQTT, daemon, health, MCP").option("--section <names>", "Comma-separated list of checks to run (see --list for names)").option("--list", "Print the registered check names and exit 0 without running any check").option("--fix", "Apply safe, reversible remediations for failing checks (e.g. clear stale cache)").option("--yes", "Required together with --fix to confirm write actions").option("--probe", "Perform live-probe variant of checks that support it (mqtt)").addHelpText("after", `
|
|
57489
|
+
program3.command("doctor").description("Self-check the SwitchBot CLI setup: credentials, catalog, cache, quota, MQTT, daemon, health, MCP").option("--section <names>", "Comma-separated list of checks to run (see --list for names)").option("--list", "Print the registered check names and exit 0 without running any check").option("--fix", "Apply safe, reversible remediations for failing checks (e.g. clear stale cache)").option("--yes", "Required together with --fix to confirm write actions").option("--probe", "Perform live-probe variant of checks that support it (mqtt)").option("-q, --quiet", "Only show warn/fail checks, hide passing checks").addHelpText("after", `
|
|
56208
57490
|
Runs a battery of local sanity checks and exits with code 0 only when every
|
|
56209
57491
|
check is 'ok'. 'warn' \u2192 exit 0 (informational); 'fail' \u2192 exit 1.
|
|
56210
57492
|
|
|
@@ -56281,7 +57563,9 @@ Examples:
|
|
|
56281
57563
|
if (fixes !== void 0) payload.fixes = fixes;
|
|
56282
57564
|
printJson(payload);
|
|
56283
57565
|
} else {
|
|
57566
|
+
const quiet = Boolean(opts.quiet);
|
|
56284
57567
|
for (const c of checks) {
|
|
57568
|
+
if (quiet && c.status === "ok") continue;
|
|
56285
57569
|
const icon2 = c.status === "ok" ? "\u2713" : c.status === "warn" ? "!" : "\u2717";
|
|
56286
57570
|
const detailStr = typeof c.detail === "string" ? c.detail : typeof c.detail.message === "string" ? c.detail.message : JSON.stringify(c.detail);
|
|
56287
57571
|
console.log(`${icon2} ${c.name.padEnd(12)} ${detailStr}`);
|
|
@@ -56495,9 +57779,10 @@ init_arg_parsers();
|
|
|
56495
57779
|
init_output();
|
|
56496
57780
|
init_audit();
|
|
56497
57781
|
init_devices();
|
|
56498
|
-
|
|
56499
|
-
import
|
|
56500
|
-
|
|
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");
|
|
56501
57786
|
function registerHistoryCommand(program3) {
|
|
56502
57787
|
const history = program3.command("history").description("View and replay SwitchBot commands recorded via --audit-log").addHelpText("after", `
|
|
56503
57788
|
Every 'devices command' run with --audit-log is appended as JSONL to the
|
|
@@ -57114,8 +58399,8 @@ Examples:
|
|
|
57114
58399
|
if (opts.dryRun) {
|
|
57115
58400
|
if (isJsonMode()) printJson(finalPayload);
|
|
57116
58401
|
else {
|
|
57117
|
-
console.
|
|
57118
|
-
console.
|
|
58402
|
+
console.error(`\u2022 dry-run: would upgrade ${policyPath} (v${plan.fromVersion} \u2192 v${plan.toVersion})`);
|
|
58403
|
+
console.error(` bytes: ${bytesWritten}`);
|
|
57119
58404
|
console.log(` precheck: valid against v${target}`);
|
|
57120
58405
|
}
|
|
57121
58406
|
return;
|
|
@@ -57232,7 +58517,7 @@ Reads rule YAML from stdin. Combine with 'rules suggest' for a full pipeline:
|
|
|
57232
58517
|
if (result.written) {
|
|
57233
58518
|
console.log(`\u2713 rule "${result.ruleName}" added to ${policyPath}`);
|
|
57234
58519
|
} else {
|
|
57235
|
-
console.
|
|
58520
|
+
console.error(`\u2022 dry-run: rule "${result.ruleName}" not written`);
|
|
57236
58521
|
}
|
|
57237
58522
|
}
|
|
57238
58523
|
} catch (err) {
|
|
@@ -57355,9 +58640,9 @@ init_load();
|
|
|
57355
58640
|
init_validate();
|
|
57356
58641
|
init_types();
|
|
57357
58642
|
init_engine();
|
|
57358
|
-
import
|
|
57359
|
-
import
|
|
57360
|
-
import
|
|
58643
|
+
import fs27 from "node:fs";
|
|
58644
|
+
import os21 from "node:os";
|
|
58645
|
+
import path23 from "node:path";
|
|
57361
58646
|
|
|
57362
58647
|
// src/rules/conflict-analyzer.ts
|
|
57363
58648
|
init_cjs_shim();
|
|
@@ -57520,9 +58805,9 @@ init_client2();
|
|
|
57520
58805
|
|
|
57521
58806
|
// src/rules/webhook-token.ts
|
|
57522
58807
|
init_cjs_shim();
|
|
57523
|
-
import
|
|
57524
|
-
import
|
|
57525
|
-
import
|
|
58808
|
+
import fs25 from "node:fs";
|
|
58809
|
+
import os20 from "node:os";
|
|
58810
|
+
import path21 from "node:path";
|
|
57526
58811
|
import { randomBytes } from "node:crypto";
|
|
57527
58812
|
var ENV_TOKEN = "SWITCHBOT_WEBHOOK_TOKEN";
|
|
57528
58813
|
var DEFAULT_FILE = ".switchbot/webhook-token";
|
|
@@ -57530,7 +58815,7 @@ var WebhookTokenStore = class {
|
|
|
57530
58815
|
filePath;
|
|
57531
58816
|
envLookup;
|
|
57532
58817
|
constructor(opts = {}) {
|
|
57533
|
-
this.filePath = opts.filePath ??
|
|
58818
|
+
this.filePath = opts.filePath ?? path21.join(os20.homedir(), DEFAULT_FILE);
|
|
57534
58819
|
this.envLookup = opts.envLookup ?? (() => process.env[ENV_TOKEN]);
|
|
57535
58820
|
}
|
|
57536
58821
|
/**
|
|
@@ -57554,7 +58839,7 @@ var WebhookTokenStore = class {
|
|
|
57554
58839
|
*/
|
|
57555
58840
|
readFromDisk() {
|
|
57556
58841
|
try {
|
|
57557
|
-
const raw =
|
|
58842
|
+
const raw = fs25.readFileSync(this.filePath, "utf-8").trim();
|
|
57558
58843
|
return raw.length > 0 ? raw : null;
|
|
57559
58844
|
} catch (err) {
|
|
57560
58845
|
if (err.code === "ENOENT") return null;
|
|
@@ -57571,12 +58856,12 @@ var WebhookTokenStore = class {
|
|
|
57571
58856
|
return this.filePath;
|
|
57572
58857
|
}
|
|
57573
58858
|
writeToDisk(token) {
|
|
57574
|
-
const dir =
|
|
57575
|
-
|
|
57576
|
-
|
|
58859
|
+
const dir = path21.dirname(this.filePath);
|
|
58860
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
58861
|
+
fs25.writeFileSync(this.filePath, `${token}
|
|
57577
58862
|
`, { mode: 384 });
|
|
57578
58863
|
try {
|
|
57579
|
-
|
|
58864
|
+
fs25.chmodSync(this.filePath, 384);
|
|
57580
58865
|
} catch {
|
|
57581
58866
|
}
|
|
57582
58867
|
}
|
|
@@ -57661,18 +58946,19 @@ function aggregateRuleAudits(entries) {
|
|
|
57661
58946
|
}
|
|
57662
58947
|
|
|
57663
58948
|
// src/commands/rules.ts
|
|
57664
|
-
|
|
58949
|
+
init_history_query();
|
|
58950
|
+
var DEFAULT_AUDIT_PATH2 = path23.join(os21.homedir(), ".switchbot", "audit.log");
|
|
57665
58951
|
function loadAutomation(policyPathFlag) {
|
|
57666
|
-
const
|
|
58952
|
+
const path31 = resolvePolicyPath({ flag: policyPathFlag });
|
|
57667
58953
|
let loaded;
|
|
57668
58954
|
try {
|
|
57669
|
-
loaded = loadPolicyFile(
|
|
58955
|
+
loaded = loadPolicyFile(path31);
|
|
57670
58956
|
} catch (err) {
|
|
57671
58957
|
if (err instanceof PolicyFileNotFoundError) {
|
|
57672
58958
|
exitWithError({
|
|
57673
58959
|
code: 2,
|
|
57674
58960
|
kind: "usage",
|
|
57675
|
-
message: `policy file not found: ${
|
|
58961
|
+
message: `policy file not found: ${path31}`,
|
|
57676
58962
|
extra: { subKind: "file-not-found" }
|
|
57677
58963
|
});
|
|
57678
58964
|
}
|
|
@@ -57680,7 +58966,7 @@ function loadAutomation(policyPathFlag) {
|
|
|
57680
58966
|
exitWithError({
|
|
57681
58967
|
code: 3,
|
|
57682
58968
|
kind: "runtime",
|
|
57683
|
-
message: `YAML parse error in ${
|
|
58969
|
+
message: `YAML parse error in ${path31}: ${err.message}`,
|
|
57684
58970
|
extra: { subKind: "yaml-parse", errors: err.yamlErrors }
|
|
57685
58971
|
});
|
|
57686
58972
|
}
|
|
@@ -57692,7 +58978,7 @@ function loadAutomation(policyPathFlag) {
|
|
|
57692
58978
|
code: 4,
|
|
57693
58979
|
kind: "runtime",
|
|
57694
58980
|
message: "policy file failed schema validation. Run `switchbot policy validate` for details.",
|
|
57695
|
-
extra: { subKind: "invalid-policy", path:
|
|
58981
|
+
extra: { subKind: "invalid-policy", path: path31 }
|
|
57696
58982
|
});
|
|
57697
58983
|
}
|
|
57698
58984
|
const data = loaded.data ?? {};
|
|
@@ -57706,7 +58992,7 @@ function loadAutomation(policyPathFlag) {
|
|
|
57706
58992
|
}
|
|
57707
58993
|
const rawQH = data.quiet_hours;
|
|
57708
58994
|
const quietHours = rawQH && typeof rawQH.start === "string" && typeof rawQH.end === "string" ? { start: rawQH.start, end: rawQH.end } : null;
|
|
57709
|
-
return { path:
|
|
58995
|
+
return { path: path31, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
|
|
57710
58996
|
}
|
|
57711
58997
|
function describeTrigger(rule) {
|
|
57712
58998
|
const t = rule.when;
|
|
@@ -57841,6 +59127,8 @@ function registerRun(rules) {
|
|
|
57841
59127
|
let stopping = false;
|
|
57842
59128
|
const pidPaths = getDefaultPidFilePaths();
|
|
57843
59129
|
writePidFile(pidPaths.pidFile);
|
|
59130
|
+
const ipcStartedAt = /* @__PURE__ */ new Date();
|
|
59131
|
+
let ipcServerHandle = null;
|
|
57844
59132
|
const cleanup = () => {
|
|
57845
59133
|
clearPidFile(pidPaths.pidFile);
|
|
57846
59134
|
consumeReloadSentinel(pidPaths.reloadFile);
|
|
@@ -57849,6 +59137,12 @@ function registerRun(rules) {
|
|
|
57849
59137
|
if (stopping) return;
|
|
57850
59138
|
stopping = true;
|
|
57851
59139
|
try {
|
|
59140
|
+
if (ipcServerHandle) {
|
|
59141
|
+
try {
|
|
59142
|
+
await ipcServerHandle.close();
|
|
59143
|
+
} catch {
|
|
59144
|
+
}
|
|
59145
|
+
}
|
|
57852
59146
|
await engine.stop();
|
|
57853
59147
|
await client.disconnect();
|
|
57854
59148
|
} finally {
|
|
@@ -57907,6 +59201,34 @@ function registerRun(rules) {
|
|
|
57907
59201
|
}
|
|
57908
59202
|
}, 2e3);
|
|
57909
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
|
+
}
|
|
57910
59232
|
if (!isJsonMode()) {
|
|
57911
59233
|
console.error(
|
|
57912
59234
|
`Rules engine started \u2014 ${engine.getStats().rulesActive} active rule(s), ${opts.dryRun ? "global dry-run" : "live"}.`
|
|
@@ -57969,7 +59291,7 @@ function registerTail(rules) {
|
|
|
57969
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) => {
|
|
57970
59292
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
57971
59293
|
const sinceMs = resolveSinceMs(opts.since);
|
|
57972
|
-
const existing =
|
|
59294
|
+
const existing = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
57973
59295
|
const filtered = filterRuleAudits(existing, { sinceMs, ruleName: opts.rule });
|
|
57974
59296
|
if (isJsonMode()) {
|
|
57975
59297
|
for (const e of filtered) console.log(JSON.stringify(e));
|
|
@@ -57981,7 +59303,7 @@ function registerTail(rules) {
|
|
|
57981
59303
|
for (const e of filtered) console.log(formatAuditLine(e));
|
|
57982
59304
|
}
|
|
57983
59305
|
if (!opts.follow) return;
|
|
57984
|
-
let offset =
|
|
59306
|
+
let offset = fs27.existsSync(file2) ? fs27.statSync(file2).size : 0;
|
|
57985
59307
|
let buffer = "";
|
|
57986
59308
|
const emit = (line) => {
|
|
57987
59309
|
const trimmed = line.trim();
|
|
@@ -57998,21 +59320,21 @@ function registerTail(rules) {
|
|
|
57998
59320
|
else console.log(formatAuditLine(entry));
|
|
57999
59321
|
};
|
|
58000
59322
|
const poll = setInterval(() => {
|
|
58001
|
-
if (!
|
|
58002
|
-
const size =
|
|
59323
|
+
if (!fs27.existsSync(file2)) return;
|
|
59324
|
+
const size = fs27.statSync(file2).size;
|
|
58003
59325
|
if (size < offset) {
|
|
58004
59326
|
offset = 0;
|
|
58005
59327
|
buffer = "";
|
|
58006
59328
|
}
|
|
58007
59329
|
if (size === offset) return;
|
|
58008
|
-
const fd =
|
|
59330
|
+
const fd = fs27.openSync(file2, "r");
|
|
58009
59331
|
try {
|
|
58010
59332
|
const chunk = Buffer.alloc(size - offset);
|
|
58011
|
-
|
|
59333
|
+
fs27.readSync(fd, chunk, 0, chunk.length, offset);
|
|
58012
59334
|
offset = size;
|
|
58013
59335
|
buffer += chunk.toString("utf-8");
|
|
58014
59336
|
} finally {
|
|
58015
|
-
|
|
59337
|
+
fs27.closeSync(fd);
|
|
58016
59338
|
}
|
|
58017
59339
|
let newline = buffer.indexOf("\n");
|
|
58018
59340
|
while (newline !== -1) {
|
|
@@ -58052,7 +59374,7 @@ function formatReplayTable(report) {
|
|
|
58052
59374
|
function registerReplay(rules) {
|
|
58053
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) => {
|
|
58054
59376
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58055
|
-
const entries =
|
|
59377
|
+
const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
58056
59378
|
const sinceMs = resolveSinceMs(opts.since);
|
|
58057
59379
|
const filtered = filterRuleAudits(entries, {
|
|
58058
59380
|
sinceMs,
|
|
@@ -58172,7 +59494,7 @@ function registerSuggest(rules) {
|
|
|
58172
59494
|
for (const w2 of warnings) process.stderr.write(`warning: ${w2}
|
|
58173
59495
|
`);
|
|
58174
59496
|
if (opts.out) {
|
|
58175
|
-
|
|
59497
|
+
fs27.writeFileSync(opts.out, ruleYaml, "utf8");
|
|
58176
59498
|
if (!isJsonMode()) console.log(`\u2713 rule YAML written to ${opts.out}`);
|
|
58177
59499
|
} else if (isJsonMode()) {
|
|
58178
59500
|
printJson({ rule, rule_yaml: ruleYaml, warnings });
|
|
@@ -58247,7 +59569,7 @@ overall: ${overall ? "ok" : "issues found"}`);
|
|
|
58247
59569
|
function registerSummary(rules) {
|
|
58248
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) => {
|
|
58249
59571
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58250
|
-
const entries =
|
|
59572
|
+
const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
58251
59573
|
const sinceMs = resolveSinceMs(opts.since ?? "24h");
|
|
58252
59574
|
const filtered = filterRuleAudits(entries, { sinceMs, ruleName: opts.rule });
|
|
58253
59575
|
const report = aggregateRuleAudits(filtered);
|
|
@@ -58278,7 +59600,7 @@ function registerLastFired(rules) {
|
|
|
58278
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) => {
|
|
58279
59601
|
const file2 = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58280
59602
|
const n = opts.n ?? 10;
|
|
58281
|
-
const entries =
|
|
59603
|
+
const entries = fs27.existsSync(file2) ? readAudit(file2) : [];
|
|
58282
59604
|
const fires = filterRuleAudits(entries, {
|
|
58283
59605
|
ruleName: opts.rule,
|
|
58284
59606
|
kinds: ["rule-fire", "rule-fire-dry"]
|
|
@@ -58320,7 +59642,7 @@ function registerExplain(rules) {
|
|
|
58320
59642
|
return;
|
|
58321
59643
|
}
|
|
58322
59644
|
const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58323
|
-
const entries =
|
|
59645
|
+
const entries = fs27.existsSync(auditFile) ? readAudit(auditFile) : [];
|
|
58324
59646
|
const fires = filterRuleAudits(entries, { ruleName: name, kinds: ["rule-fire", "rule-fire-dry"] });
|
|
58325
59647
|
const lastFired = fires.length > 0 ? fires[fires.length - 1].t : null;
|
|
58326
59648
|
const detail = {
|
|
@@ -58361,7 +59683,7 @@ function registerTraceExplain(rules) {
|
|
|
58361
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(
|
|
58362
59684
|
(fireIdArg, opts) => {
|
|
58363
59685
|
const auditFile = opts.file ?? DEFAULT_AUDIT_PATH2;
|
|
58364
|
-
if (!
|
|
59686
|
+
if (!fs27.existsSync(auditFile)) {
|
|
58365
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).` });
|
|
58366
59688
|
return;
|
|
58367
59689
|
}
|
|
@@ -58396,14 +59718,14 @@ function registerSimulate(rules) {
|
|
|
58396
59718
|
async (ruleOrPolicy, opts) => {
|
|
58397
59719
|
let rule;
|
|
58398
59720
|
const auditLog = opts.auditLog ?? DEFAULT_AUDIT_PATH2;
|
|
58399
|
-
if (!
|
|
59721
|
+
if (!fs27.existsSync(ruleOrPolicy)) {
|
|
58400
59722
|
exitWithError({ code: 2, kind: "usage", message: `File not found: ${ruleOrPolicy}` });
|
|
58401
59723
|
return;
|
|
58402
59724
|
}
|
|
58403
59725
|
let parsed;
|
|
58404
59726
|
try {
|
|
58405
59727
|
const { parse: yamlParse5 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
|
|
58406
|
-
parsed = yamlParse5(
|
|
59728
|
+
parsed = yamlParse5(fs27.readFileSync(ruleOrPolicy, "utf-8"));
|
|
58407
59729
|
} catch {
|
|
58408
59730
|
exitWithError({ code: 2, kind: "usage", message: `Could not parse YAML file: ${ruleOrPolicy}` });
|
|
58409
59731
|
return;
|
|
@@ -58434,7 +59756,7 @@ function registerSimulate(rules) {
|
|
|
58434
59756
|
liveLlm: opts.liveLlm ?? false
|
|
58435
59757
|
});
|
|
58436
59758
|
if (opts.reportOut) {
|
|
58437
|
-
|
|
59759
|
+
fs27.writeFileSync(opts.reportOut, JSON.stringify(report, null, 2));
|
|
58438
59760
|
console.log(`Report written to ${opts.reportOut}`);
|
|
58439
59761
|
}
|
|
58440
59762
|
if (isJsonMode()) {
|
|
@@ -58525,10 +59847,10 @@ init_output();
|
|
|
58525
59847
|
init_arg_parsers();
|
|
58526
59848
|
init_request_context();
|
|
58527
59849
|
init_keychain();
|
|
58528
|
-
import
|
|
58529
|
-
import
|
|
58530
|
-
import
|
|
58531
|
-
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";
|
|
58532
59854
|
function activeProfile() {
|
|
58533
59855
|
return getActiveProfile() ?? "default";
|
|
58534
59856
|
}
|
|
@@ -58540,7 +59862,7 @@ function maskValue2(value) {
|
|
|
58540
59862
|
return `${head}${"*".repeat(Math.max(4, value.length - 4))}${tail}`;
|
|
58541
59863
|
}
|
|
58542
59864
|
async function promptSecret2(question) {
|
|
58543
|
-
const rl =
|
|
59865
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
58544
59866
|
return new Promise((resolve2) => {
|
|
58545
59867
|
process.stderr.write(question);
|
|
58546
59868
|
const stdin = process.stdin;
|
|
@@ -58573,7 +59895,7 @@ async function promptSecret2(question) {
|
|
|
58573
59895
|
});
|
|
58574
59896
|
}
|
|
58575
59897
|
function readStdinFile(filePath) {
|
|
58576
|
-
if (!
|
|
59898
|
+
if (!fs28.existsSync(filePath)) {
|
|
58577
59899
|
exitWithError({
|
|
58578
59900
|
code: 2,
|
|
58579
59901
|
kind: "usage",
|
|
@@ -58582,7 +59904,7 @@ function readStdinFile(filePath) {
|
|
|
58582
59904
|
}
|
|
58583
59905
|
let parsed;
|
|
58584
59906
|
try {
|
|
58585
|
-
parsed = JSON.parse(
|
|
59907
|
+
parsed = JSON.parse(fs28.readFileSync(filePath, "utf-8"));
|
|
58586
59908
|
} catch (err) {
|
|
58587
59909
|
exitWithError({
|
|
58588
59910
|
code: 2,
|
|
@@ -58612,10 +59934,10 @@ function cleanupMigratedSourceFile(sourceFile, parsed) {
|
|
|
58612
59934
|
delete next.token;
|
|
58613
59935
|
delete next.secret;
|
|
58614
59936
|
if (Object.keys(next).length === 0) {
|
|
58615
|
-
|
|
59937
|
+
fs28.unlinkSync(sourceFile);
|
|
58616
59938
|
return "deleted";
|
|
58617
59939
|
}
|
|
58618
|
-
|
|
59940
|
+
fs28.writeFileSync(sourceFile, JSON.stringify(next, null, 2), { mode: 384 });
|
|
58619
59941
|
return "scrubbed";
|
|
58620
59942
|
}
|
|
58621
59943
|
function registerAuthCommand(program3) {
|
|
@@ -58746,8 +60068,8 @@ function registerAuthCommand(program3) {
|
|
|
58746
60068
|
message: `backend "${store.name}" is not writable on this machine`
|
|
58747
60069
|
});
|
|
58748
60070
|
}
|
|
58749
|
-
const sourceFile = profile === "default" ?
|
|
58750
|
-
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)) {
|
|
58751
60073
|
exitWithError({
|
|
58752
60074
|
code: 2,
|
|
58753
60075
|
kind: "usage",
|
|
@@ -58757,7 +60079,7 @@ function registerAuthCommand(program3) {
|
|
|
58757
60079
|
}
|
|
58758
60080
|
let parsed;
|
|
58759
60081
|
try {
|
|
58760
|
-
const raw = JSON.parse(
|
|
60082
|
+
const raw = JSON.parse(fs28.readFileSync(sourceFile, "utf-8"));
|
|
58761
60083
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
58762
60084
|
throw new Error("expected a JSON object");
|
|
58763
60085
|
}
|
|
@@ -58819,8 +60141,8 @@ function registerAuthCommand(program3) {
|
|
|
58819
60141
|
init_cjs_shim();
|
|
58820
60142
|
init_esm();
|
|
58821
60143
|
init_load();
|
|
58822
|
-
import
|
|
58823
|
-
import
|
|
60144
|
+
import fs31 from "node:fs";
|
|
60145
|
+
import path27 from "node:path";
|
|
58824
60146
|
|
|
58825
60147
|
// src/install/steps.ts
|
|
58826
60148
|
init_cjs_shim();
|
|
@@ -58872,9 +60194,9 @@ init_cjs_shim();
|
|
|
58872
60194
|
init_load();
|
|
58873
60195
|
init_validate();
|
|
58874
60196
|
init_keychain();
|
|
58875
|
-
import
|
|
58876
|
-
import
|
|
58877
|
-
import
|
|
60197
|
+
import fs29 from "node:fs";
|
|
60198
|
+
import path25 from "node:path";
|
|
60199
|
+
import os23 from "node:os";
|
|
58878
60200
|
function parseMajor(version2) {
|
|
58879
60201
|
const m2 = /^v?(\d+)\./.exec(version2);
|
|
58880
60202
|
if (!m2) return null;
|
|
@@ -58964,10 +60286,10 @@ async function checkKeychain2() {
|
|
|
58964
60286
|
}
|
|
58965
60287
|
}
|
|
58966
60288
|
function checkHomeDirWritable() {
|
|
58967
|
-
const home =
|
|
58968
|
-
const switchbotDir =
|
|
60289
|
+
const home = os23.homedir();
|
|
60290
|
+
const switchbotDir = path25.join(home, ".switchbot");
|
|
58969
60291
|
try {
|
|
58970
|
-
const homeStat =
|
|
60292
|
+
const homeStat = fs29.statSync(home);
|
|
58971
60293
|
if (!homeStat.isDirectory()) {
|
|
58972
60294
|
return {
|
|
58973
60295
|
name: "home",
|
|
@@ -58976,8 +60298,8 @@ function checkHomeDirWritable() {
|
|
|
58976
60298
|
hint: "check your HOME/USERPROFILE environment configuration"
|
|
58977
60299
|
};
|
|
58978
60300
|
}
|
|
58979
|
-
if (
|
|
58980
|
-
const sbStat =
|
|
60301
|
+
if (fs29.existsSync(switchbotDir)) {
|
|
60302
|
+
const sbStat = fs29.statSync(switchbotDir);
|
|
58981
60303
|
if (!sbStat.isDirectory()) {
|
|
58982
60304
|
return {
|
|
58983
60305
|
name: "home",
|
|
@@ -58986,10 +60308,10 @@ function checkHomeDirWritable() {
|
|
|
58986
60308
|
hint: "move the file aside and re-run install"
|
|
58987
60309
|
};
|
|
58988
60310
|
}
|
|
58989
|
-
|
|
60311
|
+
fs29.accessSync(switchbotDir, fs29.constants.W_OK);
|
|
58990
60312
|
return { name: "home", status: "ok", message: `writable: ${switchbotDir}` };
|
|
58991
60313
|
}
|
|
58992
|
-
|
|
60314
|
+
fs29.accessSync(home, fs29.constants.W_OK);
|
|
58993
60315
|
return { name: "home", status: "ok", message: `writable: ${home}` };
|
|
58994
60316
|
} catch (err) {
|
|
58995
60317
|
return {
|
|
@@ -59003,8 +60325,8 @@ function checkHomeDirWritable() {
|
|
|
59003
60325
|
function nearestExistingPath(target) {
|
|
59004
60326
|
let cur = target;
|
|
59005
60327
|
while (true) {
|
|
59006
|
-
if (
|
|
59007
|
-
const parent =
|
|
60328
|
+
if (fs29.existsSync(cur)) return cur;
|
|
60329
|
+
const parent = path25.dirname(cur);
|
|
59008
60330
|
if (parent === cur) return null;
|
|
59009
60331
|
cur = parent;
|
|
59010
60332
|
}
|
|
@@ -59012,8 +60334,8 @@ function nearestExistingPath(target) {
|
|
|
59012
60334
|
function checkAgentSkillDirWritable(opts) {
|
|
59013
60335
|
const shouldCheck = opts.agent === "claude-code" && (opts.expectSkillLink ?? true);
|
|
59014
60336
|
if (!shouldCheck) return null;
|
|
59015
|
-
const home =
|
|
59016
|
-
const target =
|
|
60337
|
+
const home = os23.homedir();
|
|
60338
|
+
const target = path25.join(home, ".claude", "skills");
|
|
59017
60339
|
try {
|
|
59018
60340
|
const existing = nearestExistingPath(target);
|
|
59019
60341
|
if (!existing) {
|
|
@@ -59024,7 +60346,7 @@ function checkAgentSkillDirWritable(opts) {
|
|
|
59024
60346
|
hint: "check your home directory path and permissions"
|
|
59025
60347
|
};
|
|
59026
60348
|
}
|
|
59027
|
-
const stat =
|
|
60349
|
+
const stat = fs29.statSync(existing);
|
|
59028
60350
|
if (!stat.isDirectory()) {
|
|
59029
60351
|
return {
|
|
59030
60352
|
name: "agent-skills-dir",
|
|
@@ -59033,7 +60355,7 @@ function checkAgentSkillDirWritable(opts) {
|
|
|
59033
60355
|
hint: "move the blocking file aside and re-run install"
|
|
59034
60356
|
};
|
|
59035
60357
|
}
|
|
59036
|
-
|
|
60358
|
+
fs29.accessSync(existing, fs29.constants.W_OK);
|
|
59037
60359
|
return { name: "agent-skills-dir", status: "ok", message: `writable: ${target}` };
|
|
59038
60360
|
} catch (err) {
|
|
59039
60361
|
return {
|
|
@@ -59058,9 +60380,9 @@ async function runPreflight(options = {}) {
|
|
|
59058
60380
|
|
|
59059
60381
|
// src/install/default-steps.ts
|
|
59060
60382
|
init_cjs_shim();
|
|
59061
|
-
import
|
|
59062
|
-
import
|
|
59063
|
-
import
|
|
60383
|
+
import fs30 from "node:fs";
|
|
60384
|
+
import path26 from "node:path";
|
|
60385
|
+
import os24 from "node:os";
|
|
59064
60386
|
import { spawnSync } from "node:child_process";
|
|
59065
60387
|
init_keychain();
|
|
59066
60388
|
function stepPromptCredentials() {
|
|
@@ -59135,15 +60457,15 @@ function stepScaffoldPolicy() {
|
|
|
59135
60457
|
const r = ctx.policyScaffoldResult;
|
|
59136
60458
|
if (!r || r.skipped) return;
|
|
59137
60459
|
try {
|
|
59138
|
-
|
|
60460
|
+
fs30.unlinkSync(r.policyPath);
|
|
59139
60461
|
} catch {
|
|
59140
60462
|
}
|
|
59141
60463
|
}
|
|
59142
60464
|
};
|
|
59143
60465
|
}
|
|
59144
|
-
function skillLinkPathFor(agent, home =
|
|
60466
|
+
function skillLinkPathFor(agent, home = os24.homedir()) {
|
|
59145
60467
|
if (agent === "claude-code") {
|
|
59146
|
-
return
|
|
60468
|
+
return path26.join(home, ".claude", "skills", "switchbot");
|
|
59147
60469
|
}
|
|
59148
60470
|
return null;
|
|
59149
60471
|
}
|
|
@@ -59157,11 +60479,11 @@ function stepSymlinkSkill(opts = {}) {
|
|
|
59157
60479
|
ctx.skillRecipePrinted = true;
|
|
59158
60480
|
return;
|
|
59159
60481
|
}
|
|
59160
|
-
const target =
|
|
59161
|
-
if (!
|
|
60482
|
+
const target = path26.resolve(ctx.skillPath);
|
|
60483
|
+
if (!fs30.existsSync(target)) {
|
|
59162
60484
|
throw new Error(`--skill-path does not exist: ${target}`);
|
|
59163
60485
|
}
|
|
59164
|
-
const stat =
|
|
60486
|
+
const stat = fs30.statSync(target);
|
|
59165
60487
|
if (!stat.isDirectory()) {
|
|
59166
60488
|
throw new Error(`--skill-path is not a directory: ${target}`);
|
|
59167
60489
|
}
|
|
@@ -59170,17 +60492,17 @@ function stepSymlinkSkill(opts = {}) {
|
|
|
59170
60492
|
ctx.skillRecipePrinted = true;
|
|
59171
60493
|
return;
|
|
59172
60494
|
}
|
|
59173
|
-
if (!opts.force && !
|
|
60495
|
+
if (!opts.force && !fs30.existsSync(path26.join(target, "SKILL.md"))) {
|
|
59174
60496
|
throw new Error(
|
|
59175
60497
|
`${target} does not look like a skill (no SKILL.md at the root). Pass --force if you really mean to link this directory.`
|
|
59176
60498
|
);
|
|
59177
60499
|
}
|
|
59178
|
-
if (
|
|
59179
|
-
const st =
|
|
60500
|
+
if (fs30.existsSync(linkPath)) {
|
|
60501
|
+
const st = fs30.lstatSync(linkPath);
|
|
59180
60502
|
if (st.isSymbolicLink()) {
|
|
59181
60503
|
let existingTarget = null;
|
|
59182
60504
|
try {
|
|
59183
|
-
existingTarget =
|
|
60505
|
+
existingTarget = path26.resolve(path26.dirname(linkPath), fs30.readlinkSync(linkPath));
|
|
59184
60506
|
} catch {
|
|
59185
60507
|
existingTarget = null;
|
|
59186
60508
|
}
|
|
@@ -59194,23 +60516,23 @@ function stepSymlinkSkill(opts = {}) {
|
|
|
59194
60516
|
`${linkPath} already links to ${existingTarget ?? "(unreadable)"}; pass --force to replace it, or run \`switchbot uninstall\` first.`
|
|
59195
60517
|
);
|
|
59196
60518
|
}
|
|
59197
|
-
|
|
60519
|
+
fs30.unlinkSync(linkPath);
|
|
59198
60520
|
} else {
|
|
59199
60521
|
throw new Error(
|
|
59200
60522
|
`${linkPath} exists and is not a symlink; refusing to clobber (move it aside and re-run)`
|
|
59201
60523
|
);
|
|
59202
60524
|
}
|
|
59203
60525
|
}
|
|
59204
|
-
|
|
60526
|
+
fs30.mkdirSync(path26.dirname(linkPath), { recursive: true });
|
|
59205
60527
|
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
59206
|
-
|
|
60528
|
+
fs30.symlinkSync(target, linkPath, linkType);
|
|
59207
60529
|
ctx.skillLinkPath = linkPath;
|
|
59208
60530
|
ctx.skillLinkCreated = true;
|
|
59209
60531
|
},
|
|
59210
60532
|
undo(ctx) {
|
|
59211
60533
|
if (!ctx.skillLinkCreated || !ctx.skillLinkPath) return;
|
|
59212
60534
|
try {
|
|
59213
|
-
|
|
60535
|
+
fs30.unlinkSync(ctx.skillLinkPath);
|
|
59214
60536
|
} catch {
|
|
59215
60537
|
}
|
|
59216
60538
|
}
|
|
@@ -59312,18 +60634,18 @@ function printDryRun(steps, ctx) {
|
|
|
59312
60634
|
});
|
|
59313
60635
|
return;
|
|
59314
60636
|
}
|
|
59315
|
-
console.
|
|
59316
|
-
console.
|
|
59317
|
-
console.
|
|
59318
|
-
console.
|
|
59319
|
-
console.
|
|
59320
|
-
console.
|
|
59321
|
-
console.
|
|
60637
|
+
console.error(source_default.bold("switchbot install \u2014 dry run"));
|
|
60638
|
+
console.error(` profile: ${ctx.profile}`);
|
|
60639
|
+
console.error(` agent: ${ctx.agent}`);
|
|
60640
|
+
console.error(` skill: ${ctx.skillPath ?? "(none \u2014 recipe will be printed)"}`);
|
|
60641
|
+
console.error(` policy: ${ctx.policyPath}`);
|
|
60642
|
+
console.error("");
|
|
60643
|
+
console.error(source_default.bold("Steps that would run (in order):"));
|
|
59322
60644
|
for (const s2 of steps) {
|
|
59323
|
-
console.
|
|
60645
|
+
console.error(` \u2022 ${s2.name}${s2.description ? ` \u2014 ${s2.description}` : ""}`);
|
|
59324
60646
|
}
|
|
59325
|
-
console.
|
|
59326
|
-
console.
|
|
60647
|
+
console.error("");
|
|
60648
|
+
console.error(source_default.dim("No changes made. Re-run without --dry-run to apply."));
|
|
59327
60649
|
}
|
|
59328
60650
|
function registerInstallCommand(program3) {
|
|
59329
60651
|
program3.command("install").description("One-command bootstrap: credentials + policy + skill link (rolls back on failure)").option("--agent <name>", `target agent: ${AGENT_VALUES.join(" | ")} (default: claude-code)`).option("--skill-path <dir>", "local clone of openclaw-switchbot-skill (enables auto-link)").option("--token-file <path>", "two-line credential file (token, secret); read once and deleted on success").option("--skip <names>", 'comma-separated list of step names to skip (e.g. "scaffold-policy,symlink-skill")').option("--force", "replace an existing skill symlink pointing at a different path; allow link even without SKILL.md").option("--verify", "after a successful install, run `switchbot doctor --json` as a warn-only post-check").addHelpText(
|
|
@@ -59353,8 +60675,8 @@ Examples:
|
|
|
59353
60675
|
const agent = parseAgent(opts.agent);
|
|
59354
60676
|
const profile = getActiveProfile() ?? "default";
|
|
59355
60677
|
const skip = parseSkipList(opts.skip);
|
|
59356
|
-
const skillPath = opts.skillPath ?
|
|
59357
|
-
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;
|
|
59358
60680
|
const force = Boolean(opts.force);
|
|
59359
60681
|
const verify = Boolean(opts.verify);
|
|
59360
60682
|
const globalOpts = command.parent?.opts() ?? {};
|
|
@@ -59398,7 +60720,7 @@ Examples:
|
|
|
59398
60720
|
const report = await runInstall(steps, { context: ctx });
|
|
59399
60721
|
if (report.ok && tokenFile) {
|
|
59400
60722
|
try {
|
|
59401
|
-
|
|
60723
|
+
fs31.unlinkSync(tokenFile);
|
|
59402
60724
|
} catch {
|
|
59403
60725
|
}
|
|
59404
60726
|
}
|
|
@@ -59457,8 +60779,8 @@ Examples:
|
|
|
59457
60779
|
init_cjs_shim();
|
|
59458
60780
|
init_esm();
|
|
59459
60781
|
init_load();
|
|
59460
|
-
import
|
|
59461
|
-
import
|
|
60782
|
+
import fs32 from "node:fs";
|
|
60783
|
+
import readline7 from "node:readline";
|
|
59462
60784
|
init_keychain();
|
|
59463
60785
|
init_output();
|
|
59464
60786
|
init_request_context();
|
|
@@ -59473,7 +60795,7 @@ function parseAgent2(value) {
|
|
|
59473
60795
|
}
|
|
59474
60796
|
async function prompt(question, defaultYes) {
|
|
59475
60797
|
if (!process.stdin.isTTY) return defaultYes;
|
|
59476
|
-
const rl =
|
|
60798
|
+
const rl = readline7.createInterface({ input: process.stdin, output: process.stderr });
|
|
59477
60799
|
return new Promise((resolve2) => {
|
|
59478
60800
|
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
59479
60801
|
rl.question(question + suffix, (ans) => {
|
|
@@ -59522,10 +60844,10 @@ Examples:
|
|
|
59522
60844
|
action: "remove-skill-link",
|
|
59523
60845
|
detail: skillLink,
|
|
59524
60846
|
run: async () => {
|
|
59525
|
-
if (!
|
|
60847
|
+
if (!fs32.existsSync(skillLink)) {
|
|
59526
60848
|
return { action: "remove-skill-link", status: "absent", detail: skillLink };
|
|
59527
60849
|
}
|
|
59528
|
-
const stat =
|
|
60850
|
+
const stat = fs32.lstatSync(skillLink);
|
|
59529
60851
|
if (!stat.isSymbolicLink()) {
|
|
59530
60852
|
return {
|
|
59531
60853
|
action: "remove-skill-link",
|
|
@@ -59536,7 +60858,7 @@ Examples:
|
|
|
59536
60858
|
const ok = yes ? true : await prompt(`Remove skill link ${skillLink}?`, true);
|
|
59537
60859
|
if (!ok) return { action: "remove-skill-link", status: "skipped", detail: skillLink };
|
|
59538
60860
|
try {
|
|
59539
|
-
|
|
60861
|
+
fs32.unlinkSync(skillLink);
|
|
59540
60862
|
return { action: "remove-skill-link", status: "removed", detail: skillLink };
|
|
59541
60863
|
} catch (err) {
|
|
59542
60864
|
return {
|
|
@@ -59591,13 +60913,13 @@ Examples:
|
|
|
59591
60913
|
detail: "pass --remove-policy to delete policy.yaml"
|
|
59592
60914
|
};
|
|
59593
60915
|
}
|
|
59594
|
-
if (!
|
|
60916
|
+
if (!fs32.existsSync(policyPath)) {
|
|
59595
60917
|
return { action: "remove-policy", status: "absent", detail: policyPath };
|
|
59596
60918
|
}
|
|
59597
60919
|
const ok = yes ? true : await prompt(`Delete policy file ${policyPath}?`, false);
|
|
59598
60920
|
if (!ok) return { action: "remove-policy", status: "skipped", detail: policyPath };
|
|
59599
60921
|
try {
|
|
59600
|
-
|
|
60922
|
+
fs32.unlinkSync(policyPath);
|
|
59601
60923
|
return { action: "remove-policy", status: "removed", detail: policyPath };
|
|
59602
60924
|
} catch (err) {
|
|
59603
60925
|
return {
|
|
@@ -59618,14 +60940,14 @@ Examples:
|
|
|
59618
60940
|
plan: plan.map(({ action, detail }) => ({ action, detail }))
|
|
59619
60941
|
});
|
|
59620
60942
|
} else {
|
|
59621
|
-
console.
|
|
59622
|
-
console.
|
|
59623
|
-
console.
|
|
59624
|
-
console.
|
|
59625
|
-
console.
|
|
59626
|
-
for (const p2 of plan) console.
|
|
59627
|
-
console.
|
|
59628
|
-
console.
|
|
60943
|
+
console.error(source_default.bold("switchbot uninstall \u2014 dry run"));
|
|
60944
|
+
console.error(` profile: ${profile}`);
|
|
60945
|
+
console.error(` agent: ${agent}`);
|
|
60946
|
+
console.error("");
|
|
60947
|
+
console.error(source_default.bold("Would run:"));
|
|
60948
|
+
for (const p2 of plan) console.error(` \u2022 ${p2.action} \u2014 ${p2.detail}`);
|
|
60949
|
+
console.error("");
|
|
60950
|
+
console.error(source_default.dim("No changes made. Re-run without --dry-run (add --yes to skip prompts)."));
|
|
59629
60951
|
}
|
|
59630
60952
|
return;
|
|
59631
60953
|
}
|
|
@@ -59660,9 +60982,9 @@ init_request_context();
|
|
|
59660
60982
|
init_output();
|
|
59661
60983
|
init_flags();
|
|
59662
60984
|
import { spawn as spawn4, spawnSync as spawnSync2 } from "node:child_process";
|
|
59663
|
-
import
|
|
59664
|
-
import
|
|
59665
|
-
import
|
|
60985
|
+
import fs33 from "node:fs";
|
|
60986
|
+
import os25 from "node:os";
|
|
60987
|
+
import path28 from "node:path";
|
|
59666
60988
|
var DEFAULT_OPENCLAW_URL = "http://localhost:18789";
|
|
59667
60989
|
function resolveStatusSyncRuntime(options) {
|
|
59668
60990
|
if (!tryLoadConfig()) {
|
|
@@ -59796,14 +61118,14 @@ async function probeStatusSyncStart(options = {}) {
|
|
|
59796
61118
|
};
|
|
59797
61119
|
}
|
|
59798
61120
|
function resolveStatusSyncPaths(explicitStateDir) {
|
|
59799
|
-
const stateDir =
|
|
59800
|
-
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")
|
|
59801
61123
|
);
|
|
59802
61124
|
return {
|
|
59803
61125
|
stateDir,
|
|
59804
|
-
stateFile:
|
|
59805
|
-
stdoutLog:
|
|
59806
|
-
stderrLog:
|
|
61126
|
+
stateFile: path28.join(stateDir, "state.json"),
|
|
61127
|
+
stdoutLog: path28.join(stateDir, "stdout.log"),
|
|
61128
|
+
stderrLog: path28.join(stateDir, "stderr.log")
|
|
59807
61129
|
};
|
|
59808
61130
|
}
|
|
59809
61131
|
function buildStatusSyncChildArgs(options) {
|
|
@@ -59811,11 +61133,11 @@ function buildStatusSyncChildArgs(options) {
|
|
|
59811
61133
|
if (!scriptPath) {
|
|
59812
61134
|
throw new Error("Cannot determine the current CLI entrypoint path.");
|
|
59813
61135
|
}
|
|
59814
|
-
const args = [
|
|
61136
|
+
const args = [path28.resolve(scriptPath)];
|
|
59815
61137
|
const configPath = getConfigPath();
|
|
59816
61138
|
const profile = getActiveProfile();
|
|
59817
61139
|
if (configPath) {
|
|
59818
|
-
args.push("--config",
|
|
61140
|
+
args.push("--config", path28.resolve(configPath));
|
|
59819
61141
|
} else if (profile) {
|
|
59820
61142
|
args.push("--profile", profile);
|
|
59821
61143
|
}
|
|
@@ -59836,7 +61158,7 @@ function buildStatusSyncChildArgs(options) {
|
|
|
59836
61158
|
}
|
|
59837
61159
|
function safeUnlink(filePath) {
|
|
59838
61160
|
try {
|
|
59839
|
-
|
|
61161
|
+
fs33.unlinkSync(filePath);
|
|
59840
61162
|
} catch {
|
|
59841
61163
|
}
|
|
59842
61164
|
}
|
|
@@ -59851,9 +61173,9 @@ function isProcessRunning(pid) {
|
|
|
59851
61173
|
}
|
|
59852
61174
|
}
|
|
59853
61175
|
function readStateFile(paths) {
|
|
59854
|
-
if (!
|
|
61176
|
+
if (!fs33.existsSync(paths.stateFile)) return null;
|
|
59855
61177
|
try {
|
|
59856
|
-
const raw = JSON.parse(
|
|
61178
|
+
const raw = JSON.parse(fs33.readFileSync(paths.stateFile, "utf-8"));
|
|
59857
61179
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
59858
61180
|
safeUnlink(paths.stateFile);
|
|
59859
61181
|
return null;
|
|
@@ -59972,14 +61294,14 @@ function startStatusSync(options = {}) {
|
|
|
59972
61294
|
}
|
|
59973
61295
|
stopStatusSync({ stateDir: paths.stateDir });
|
|
59974
61296
|
}
|
|
59975
|
-
|
|
61297
|
+
fs33.mkdirSync(paths.stateDir, { recursive: true });
|
|
59976
61298
|
const configPath = getConfigPath();
|
|
59977
61299
|
const command = buildStatusSyncChildArgs(runtime);
|
|
59978
61300
|
let stdoutFd = null;
|
|
59979
61301
|
let stderrFd = null;
|
|
59980
61302
|
try {
|
|
59981
|
-
stdoutFd =
|
|
59982
|
-
stderrFd =
|
|
61303
|
+
stdoutFd = fs33.openSync(paths.stdoutLog, "a");
|
|
61304
|
+
stderrFd = fs33.openSync(paths.stderrLog, "a");
|
|
59983
61305
|
const child = spawn4(process.execPath, command, {
|
|
59984
61306
|
detached: true,
|
|
59985
61307
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
@@ -59997,16 +61319,16 @@ function startStatusSync(options = {}) {
|
|
|
59997
61319
|
openclawUrl: runtime.openclawUrl,
|
|
59998
61320
|
openclawModel: runtime.openclawModel,
|
|
59999
61321
|
topic: runtime.topic ?? null,
|
|
60000
|
-
configPath: configPath ?
|
|
61322
|
+
configPath: configPath ? path28.resolve(configPath) : null,
|
|
60001
61323
|
profile: configPath ? null : getActiveProfile() ?? null,
|
|
60002
61324
|
stdoutLog: paths.stdoutLog,
|
|
60003
61325
|
stderrLog: paths.stderrLog
|
|
60004
61326
|
};
|
|
60005
|
-
|
|
61327
|
+
fs33.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 384 });
|
|
60006
61328
|
return toStatus(paths, state, true);
|
|
60007
61329
|
} finally {
|
|
60008
|
-
if (stdoutFd !== null)
|
|
60009
|
-
if (stderrFd !== null)
|
|
61330
|
+
if (stdoutFd !== null) fs33.closeSync(stdoutFd);
|
|
61331
|
+
if (stderrFd !== null) fs33.closeSync(stderrFd);
|
|
60010
61332
|
}
|
|
60011
61333
|
}
|
|
60012
61334
|
async function runStatusSyncForeground(options = {}) {
|
|
@@ -60146,17 +61468,17 @@ Examples:
|
|
|
60146
61468
|
// src/commands/health.ts
|
|
60147
61469
|
init_cjs_shim();
|
|
60148
61470
|
init_output();
|
|
60149
|
-
import
|
|
61471
|
+
import http7 from "node:http";
|
|
60150
61472
|
|
|
60151
61473
|
// src/utils/health.ts
|
|
60152
61474
|
init_cjs_shim();
|
|
60153
61475
|
init_quota();
|
|
60154
61476
|
init_audit();
|
|
60155
61477
|
init_client();
|
|
60156
|
-
import
|
|
60157
|
-
import
|
|
60158
|
-
import
|
|
60159
|
-
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");
|
|
60160
61482
|
var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
60161
61483
|
var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set([161, 171, 190]);
|
|
60162
61484
|
function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
|
|
@@ -60178,7 +61500,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
|
|
|
60178
61500
|
status: pct >= 90 ? "critical" : pct >= 70 ? "warn" : "ok"
|
|
60179
61501
|
};
|
|
60180
61502
|
let auditHealth;
|
|
60181
|
-
if (!
|
|
61503
|
+
if (!fs34.existsSync(auditPath)) {
|
|
60182
61504
|
auditHealth = {
|
|
60183
61505
|
present: false,
|
|
60184
61506
|
recentErrors: 0,
|
|
@@ -60201,7 +61523,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
|
|
|
60201
61523
|
const breakdown = {};
|
|
60202
61524
|
let expectedErrors = 0;
|
|
60203
61525
|
for (const e of errorEntries) {
|
|
60204
|
-
const code = e.statusCode !== void 0 ? String(e.statusCode) : "
|
|
61526
|
+
const code = e.statusCode !== void 0 ? String(e.statusCode) : "client";
|
|
60205
61527
|
breakdown[code] = (breakdown[code] ?? 0) + 1;
|
|
60206
61528
|
if (e.statusCode !== void 0 && EXPECTED_ERROR_CODES.has(e.statusCode)) {
|
|
60207
61529
|
expectedErrors++;
|
|
@@ -60268,38 +61590,46 @@ function runHealthCheck(opts) {
|
|
|
60268
61590
|
process.stdout.write(toPrometheusText(report));
|
|
60269
61591
|
return;
|
|
60270
61592
|
}
|
|
60271
|
-
|
|
61593
|
+
const fmt = resolveFormat();
|
|
61594
|
+
if (fmt === "json" || isJsonMode()) {
|
|
60272
61595
|
printJson(report);
|
|
60273
61596
|
return;
|
|
60274
61597
|
}
|
|
61598
|
+
const headers = ["Component", "Status", "Detail"];
|
|
61599
|
+
const rows = [
|
|
61600
|
+
[
|
|
61601
|
+
"quota",
|
|
61602
|
+
report.quota.status,
|
|
61603
|
+
`${report.quota.used}/${report.quota.limit} (${report.quota.percentUsed}% used, ${report.quota.remaining} remaining)`
|
|
61604
|
+
],
|
|
61605
|
+
[
|
|
61606
|
+
"audit",
|
|
61607
|
+
report.audit.status,
|
|
61608
|
+
report.audit.present ? `${report.audit.recentErrors}/${report.audit.recentTotal} errors in 24h (${report.audit.errorRatePercent}%)` : "log not present"
|
|
61609
|
+
],
|
|
61610
|
+
[
|
|
61611
|
+
"circuit",
|
|
61612
|
+
report.circuit.status,
|
|
61613
|
+
`${report.circuit.name}: ${report.circuit.state} (failures: ${report.circuit.failures})`
|
|
61614
|
+
],
|
|
61615
|
+
[
|
|
61616
|
+
"process",
|
|
61617
|
+
"ok",
|
|
61618
|
+
`pid ${report.process.pid} \xB7 uptime ${report.process.uptimeSeconds}s \xB7 mem ${report.process.memoryMb}MB`
|
|
61619
|
+
]
|
|
61620
|
+
];
|
|
61621
|
+
if (fmt !== "table") {
|
|
61622
|
+
if (fmt === "id") {
|
|
61623
|
+
handleError(new UsageError("--format=id is not supported for health check (no deviceId column). Use --format json, yaml, tsv, jsonl, or markdown."));
|
|
61624
|
+
}
|
|
61625
|
+
renderRows(headers, rows, fmt);
|
|
61626
|
+
if (report.overall !== "ok") process.exit(1);
|
|
61627
|
+
return;
|
|
61628
|
+
}
|
|
60275
61629
|
const statusEmoji = report.overall === "ok" ? "\u2713" : report.overall === "degraded" ? "\u26A0" : "\u2717";
|
|
60276
61630
|
console.log(`${statusEmoji} overall: ${report.overall} (${report.generatedAt})`);
|
|
60277
61631
|
console.log("");
|
|
60278
|
-
printTable(
|
|
60279
|
-
["Component", "Status", "Detail"],
|
|
60280
|
-
[
|
|
60281
|
-
[
|
|
60282
|
-
"quota",
|
|
60283
|
-
report.quota.status,
|
|
60284
|
-
`${report.quota.used}/${report.quota.limit} (${report.quota.percentUsed}% used, ${report.quota.remaining} remaining)`
|
|
60285
|
-
],
|
|
60286
|
-
[
|
|
60287
|
-
"audit",
|
|
60288
|
-
report.audit.status,
|
|
60289
|
-
report.audit.present ? `${report.audit.recentErrors}/${report.audit.recentTotal} errors in 24h (${report.audit.errorRatePercent}%)` : "log not present"
|
|
60290
|
-
],
|
|
60291
|
-
[
|
|
60292
|
-
"circuit",
|
|
60293
|
-
report.circuit.status,
|
|
60294
|
-
`${report.circuit.name}: ${report.circuit.state} (failures: ${report.circuit.failures})`
|
|
60295
|
-
],
|
|
60296
|
-
[
|
|
60297
|
-
"process",
|
|
60298
|
-
"ok",
|
|
60299
|
-
`pid ${report.process.pid} \xB7 uptime ${report.process.uptimeSeconds}s \xB7 mem ${report.process.memoryMb}MB`
|
|
60300
|
-
]
|
|
60301
|
-
]
|
|
60302
|
-
);
|
|
61632
|
+
printTable(headers, rows);
|
|
60303
61633
|
if (report.overall !== "ok") process.exit(1);
|
|
60304
61634
|
}
|
|
60305
61635
|
function createHealthHandler(auditLogPath) {
|
|
@@ -60340,7 +61670,7 @@ Example:
|
|
|
60340
61670
|
const opts = cmd.optsWithGlobals();
|
|
60341
61671
|
const port = parseInt(opts.port, 10);
|
|
60342
61672
|
const handler = createHealthHandler(opts.auditLog);
|
|
60343
|
-
const server =
|
|
61673
|
+
const server = http7.createServer(handler);
|
|
60344
61674
|
server.on("error", (err) => {
|
|
60345
61675
|
if (err.code === "EADDRINUSE") {
|
|
60346
61676
|
handleError(Object.assign(new Error(`Port ${port} is already in use. Choose a different port with --port.`), { code: err.code }));
|
|
@@ -60371,13 +61701,13 @@ Example:
|
|
|
60371
61701
|
init_cjs_shim();
|
|
60372
61702
|
init_output();
|
|
60373
61703
|
init_source();
|
|
60374
|
-
import
|
|
61704
|
+
import https6 from "node:https";
|
|
60375
61705
|
var pkgName = "@switchbot/openapi-cli";
|
|
60376
61706
|
function fetchLatestVersion(packageName, timeoutMs = 8e3) {
|
|
60377
61707
|
const encoded = packageName.replace("/", "%2F");
|
|
60378
61708
|
const url2 = `https://registry.npmjs.org/${encoded}/latest`;
|
|
60379
61709
|
return new Promise((resolve2, reject) => {
|
|
60380
|
-
const req =
|
|
61710
|
+
const req = https6.get(url2, { timeout: timeoutMs }, (res) => {
|
|
60381
61711
|
const chunks = [];
|
|
60382
61712
|
res.on("data", (c) => chunks.push(c));
|
|
60383
61713
|
res.on("end", () => {
|
|
@@ -60474,8 +61804,8 @@ function registerUpgradeCheckCommand(program3) {
|
|
|
60474
61804
|
init_cjs_shim();
|
|
60475
61805
|
init_output();
|
|
60476
61806
|
import { spawn as spawn5 } from "node:child_process";
|
|
60477
|
-
import
|
|
60478
|
-
import
|
|
61807
|
+
import fs35 from "node:fs";
|
|
61808
|
+
import path30 from "node:path";
|
|
60479
61809
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
60480
61810
|
init_arg_parsers();
|
|
60481
61811
|
init_source();
|
|
@@ -60537,7 +61867,7 @@ function persistState(partial2) {
|
|
|
60537
61867
|
}
|
|
60538
61868
|
function readLastLines(filePath, n = 20) {
|
|
60539
61869
|
try {
|
|
60540
|
-
const content =
|
|
61870
|
+
const content = fs35.readFileSync(filePath, "utf-8");
|
|
60541
61871
|
const lines = content.split("\n");
|
|
60542
61872
|
return lines.slice(Math.max(0, lines.length - n)).join("\n").trim();
|
|
60543
61873
|
} catch {
|
|
@@ -60627,10 +61957,10 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60627
61957
|
}
|
|
60628
61958
|
}
|
|
60629
61959
|
const thisFile = fileURLToPath3(import.meta.url);
|
|
60630
|
-
const cliEntry =
|
|
61960
|
+
const cliEntry = path30.basename(thisFile) === "index.js" ? thisFile : path30.resolve(path30.dirname(thisFile), "..", "index.js");
|
|
60631
61961
|
const args = ["rules", "run"];
|
|
60632
61962
|
if (opts.policy) args.push(opts.policy);
|
|
60633
|
-
|
|
61963
|
+
fs35.mkdirSync(path30.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
|
|
60634
61964
|
persistState({
|
|
60635
61965
|
status: "starting",
|
|
60636
61966
|
pid: null,
|
|
@@ -60642,14 +61972,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60642
61972
|
healthzPid: null,
|
|
60643
61973
|
healthzPidFile: HEALTHZ_PID_FILE
|
|
60644
61974
|
});
|
|
60645
|
-
const logFd =
|
|
61975
|
+
const logFd = fs35.openSync(DAEMON_LOG_FILE, "a");
|
|
60646
61976
|
const child = spawn5(process.execPath, [cliEntry, ...args], {
|
|
60647
61977
|
detached: true,
|
|
60648
61978
|
stdio: ["ignore", logFd, logFd],
|
|
60649
61979
|
env: { ...process.env }
|
|
60650
61980
|
});
|
|
60651
61981
|
child.unref();
|
|
60652
|
-
|
|
61982
|
+
fs35.closeSync(logFd);
|
|
60653
61983
|
await probeLiveness({
|
|
60654
61984
|
child,
|
|
60655
61985
|
delayMs: 300,
|
|
@@ -60672,14 +62002,14 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60672
62002
|
let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
|
|
60673
62003
|
if (healthzPort !== null) {
|
|
60674
62004
|
const healthArgs = ["health", "serve", "--port", String(healthzPort)];
|
|
60675
|
-
const healthLogFd =
|
|
62005
|
+
const healthLogFd = fs35.openSync(DAEMON_LOG_FILE, "a");
|
|
60676
62006
|
const healthChild = spawn5(process.execPath, [cliEntry, ...healthArgs], {
|
|
60677
62007
|
detached: true,
|
|
60678
62008
|
stdio: ["ignore", healthLogFd, healthLogFd],
|
|
60679
62009
|
env: { ...process.env }
|
|
60680
62010
|
});
|
|
60681
62011
|
healthChild.unref();
|
|
60682
|
-
|
|
62012
|
+
fs35.closeSync(healthLogFd);
|
|
60683
62013
|
if (healthChild.pid) {
|
|
60684
62014
|
const healthAlive = await probeLiveness({ child: healthChild, delayMs: 200, fatal: false });
|
|
60685
62015
|
if (healthAlive) {
|
|
@@ -60742,11 +62072,11 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
60742
62072
|
});
|
|
60743
62073
|
}
|
|
60744
62074
|
try {
|
|
60745
|
-
|
|
62075
|
+
fs35.unlinkSync(DAEMON_PID_FILE);
|
|
60746
62076
|
} catch {
|
|
60747
62077
|
}
|
|
60748
62078
|
try {
|
|
60749
|
-
|
|
62079
|
+
fs35.unlinkSync(HEALTHZ_PID_FILE);
|
|
60750
62080
|
} catch {
|
|
60751
62081
|
}
|
|
60752
62082
|
persistState({
|
|
@@ -60853,6 +62183,7 @@ if (process.argv.includes("--no-color") || Boolean(process.env.NO_COLOR)) {
|
|
|
60853
62183
|
source_default.level = 0;
|
|
60854
62184
|
}
|
|
60855
62185
|
var program2 = new Command();
|
|
62186
|
+
program2.allowExcessArguments(false);
|
|
60856
62187
|
if (isJsonMode()) {
|
|
60857
62188
|
program2.configureOutput({ writeErr: () => {
|
|
60858
62189
|
} });
|