@switchbot/openapi-cli 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/dist/api/client.d.ts +31 -0
  2. package/dist/api/client.js +236 -0
  3. package/dist/api/client.js.map +1 -0
  4. package/dist/auth.d.ts +1 -0
  5. package/dist/auth.js +21 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/commands/agent-bootstrap.d.ts +10 -0
  8. package/dist/commands/agent-bootstrap.js +200 -0
  9. package/dist/commands/agent-bootstrap.js.map +1 -0
  10. package/dist/commands/auth.d.ts +18 -0
  11. package/dist/commands/auth.js +355 -0
  12. package/dist/commands/auth.js.map +1 -0
  13. package/dist/commands/batch.d.ts +2 -0
  14. package/dist/commands/batch.js +414 -0
  15. package/dist/commands/batch.js.map +1 -0
  16. package/dist/commands/cache.d.ts +2 -0
  17. package/dist/commands/cache.js +127 -0
  18. package/dist/commands/cache.js.map +1 -0
  19. package/dist/commands/capabilities.d.ts +31 -0
  20. package/dist/commands/capabilities.js +383 -0
  21. package/dist/commands/capabilities.js.map +1 -0
  22. package/dist/commands/catalog.d.ts +2 -0
  23. package/dist/commands/catalog.js +360 -0
  24. package/dist/commands/catalog.js.map +1 -0
  25. package/dist/commands/completion.d.ts +2 -0
  26. package/dist/commands/completion.js +386 -0
  27. package/dist/commands/completion.js.map +1 -0
  28. package/dist/commands/config.d.ts +21 -0
  29. package/dist/commands/config.js +377 -0
  30. package/dist/commands/config.js.map +1 -0
  31. package/dist/commands/daemon.d.ts +2 -0
  32. package/dist/commands/daemon.js +411 -0
  33. package/dist/commands/daemon.js.map +1 -0
  34. package/dist/commands/device-meta.d.ts +2 -0
  35. package/dist/commands/device-meta.js +160 -0
  36. package/dist/commands/device-meta.js.map +1 -0
  37. package/dist/commands/devices.d.ts +2 -0
  38. package/dist/commands/devices.js +949 -0
  39. package/dist/commands/devices.js.map +1 -0
  40. package/dist/commands/doctor.d.ts +3 -0
  41. package/dist/commands/doctor.js +1016 -0
  42. package/dist/commands/doctor.js.map +1 -0
  43. package/dist/commands/events.d.ts +31 -0
  44. package/dist/commands/events.js +564 -0
  45. package/dist/commands/events.js.map +1 -0
  46. package/dist/commands/expand.d.ts +2 -0
  47. package/dist/commands/expand.js +131 -0
  48. package/dist/commands/expand.js.map +1 -0
  49. package/dist/commands/explain.d.ts +2 -0
  50. package/dist/commands/explain.js +140 -0
  51. package/dist/commands/explain.js.map +1 -0
  52. package/dist/commands/health.d.ts +8 -0
  53. package/dist/commands/health.js +114 -0
  54. package/dist/commands/health.js.map +1 -0
  55. package/dist/commands/history.d.ts +2 -0
  56. package/dist/commands/history.js +321 -0
  57. package/dist/commands/history.js.map +1 -0
  58. package/dist/commands/identity.d.ts +45 -0
  59. package/dist/commands/identity.js +60 -0
  60. package/dist/commands/identity.js.map +1 -0
  61. package/dist/commands/install.d.ts +20 -0
  62. package/dist/commands/install.js +247 -0
  63. package/dist/commands/install.js.map +1 -0
  64. package/dist/commands/mcp.d.ts +14 -0
  65. package/dist/commands/mcp.js +2018 -0
  66. package/dist/commands/mcp.js.map +1 -0
  67. package/dist/commands/plan.d.ts +51 -0
  68. package/dist/commands/plan.js +654 -0
  69. package/dist/commands/plan.js.map +1 -0
  70. package/dist/commands/policy.d.ts +24 -0
  71. package/dist/commands/policy.js +587 -0
  72. package/dist/commands/policy.js.map +1 -0
  73. package/dist/commands/quota.d.ts +2 -0
  74. package/dist/commands/quota.js +79 -0
  75. package/dist/commands/quota.js.map +1 -0
  76. package/dist/commands/rules.d.ts +2 -0
  77. package/dist/commands/rules.js +876 -0
  78. package/dist/commands/rules.js.map +1 -0
  79. package/dist/commands/scenes.d.ts +2 -0
  80. package/dist/commands/scenes.js +265 -0
  81. package/dist/commands/scenes.js.map +1 -0
  82. package/dist/commands/schema.d.ts +2 -0
  83. package/dist/commands/schema.js +185 -0
  84. package/dist/commands/schema.js.map +1 -0
  85. package/dist/commands/status-sync.d.ts +2 -0
  86. package/dist/commands/status-sync.js +132 -0
  87. package/dist/commands/status-sync.js.map +1 -0
  88. package/dist/commands/uninstall.d.ts +20 -0
  89. package/dist/commands/uninstall.js +238 -0
  90. package/dist/commands/uninstall.js.map +1 -0
  91. package/dist/commands/upgrade-check.d.ts +2 -0
  92. package/dist/commands/upgrade-check.js +107 -0
  93. package/dist/commands/upgrade-check.js.map +1 -0
  94. package/dist/commands/watch.d.ts +2 -0
  95. package/dist/commands/watch.js +195 -0
  96. package/dist/commands/watch.js.map +1 -0
  97. package/dist/commands/webhook.d.ts +2 -0
  98. package/dist/commands/webhook.js +183 -0
  99. package/dist/commands/webhook.js.map +1 -0
  100. package/dist/config.d.ts +57 -0
  101. package/dist/config.js +259 -0
  102. package/dist/config.js.map +1 -0
  103. package/dist/credentials/backends/file.d.ts +18 -0
  104. package/dist/credentials/backends/file.js +102 -0
  105. package/dist/credentials/backends/file.js.map +1 -0
  106. package/dist/credentials/backends/linux.d.ts +16 -0
  107. package/dist/credentials/backends/linux.js +130 -0
  108. package/dist/credentials/backends/linux.js.map +1 -0
  109. package/dist/credentials/backends/macos.d.ts +18 -0
  110. package/dist/credentials/backends/macos.js +130 -0
  111. package/dist/credentials/backends/macos.js.map +1 -0
  112. package/dist/credentials/backends/windows.d.ts +23 -0
  113. package/dist/credentials/backends/windows.js +216 -0
  114. package/dist/credentials/backends/windows.js.map +1 -0
  115. package/dist/credentials/keychain.d.ts +83 -0
  116. package/dist/credentials/keychain.js +89 -0
  117. package/dist/credentials/keychain.js.map +1 -0
  118. package/dist/credentials/prime.d.ts +32 -0
  119. package/dist/credentials/prime.js +53 -0
  120. package/dist/credentials/prime.js.map +1 -0
  121. package/dist/devices/cache.d.ts +79 -0
  122. package/dist/devices/cache.js +294 -0
  123. package/dist/devices/cache.js.map +1 -0
  124. package/dist/devices/catalog.d.ts +138 -0
  125. package/dist/devices/catalog.js +768 -0
  126. package/dist/devices/catalog.js.map +1 -0
  127. package/dist/devices/device-meta.d.ts +15 -0
  128. package/dist/devices/device-meta.js +57 -0
  129. package/dist/devices/device-meta.js.map +1 -0
  130. package/dist/devices/history-agg.d.ts +37 -0
  131. package/dist/devices/history-agg.js +139 -0
  132. package/dist/devices/history-agg.js.map +1 -0
  133. package/dist/devices/history-query.d.ts +45 -0
  134. package/dist/devices/history-query.js +182 -0
  135. package/dist/devices/history-query.js.map +1 -0
  136. package/dist/devices/param-validator.d.ts +40 -0
  137. package/dist/devices/param-validator.js +434 -0
  138. package/dist/devices/param-validator.js.map +1 -0
  139. package/dist/devices/resources.d.ts +74 -0
  140. package/dist/devices/resources.js +271 -0
  141. package/dist/devices/resources.js.map +1 -0
  142. package/dist/index.d.ts +1 -0
  143. package/dist/index.js +170 -56946
  144. package/dist/index.js.map +1 -0
  145. package/dist/install/default-steps.d.ts +66 -0
  146. package/dist/install/default-steps.js +258 -0
  147. package/dist/install/default-steps.js.map +1 -0
  148. package/dist/install/preflight.d.ts +60 -0
  149. package/dist/install/preflight.js +213 -0
  150. package/dist/install/preflight.js.map +1 -0
  151. package/dist/install/steps.d.ts +61 -0
  152. package/dist/install/steps.js +68 -0
  153. package/dist/install/steps.js.map +1 -0
  154. package/dist/lib/command-keywords.d.ts +5 -0
  155. package/dist/lib/command-keywords.js +18 -0
  156. package/dist/lib/command-keywords.js.map +1 -0
  157. package/dist/lib/daemon-state.d.ts +24 -0
  158. package/dist/lib/daemon-state.js +47 -0
  159. package/dist/lib/daemon-state.js.map +1 -0
  160. package/dist/lib/destructive-mode.d.ts +2 -0
  161. package/dist/lib/destructive-mode.js +13 -0
  162. package/dist/lib/destructive-mode.js.map +1 -0
  163. package/dist/lib/devices.d.ts +151 -0
  164. package/dist/lib/devices.js +383 -0
  165. package/dist/lib/devices.js.map +1 -0
  166. package/dist/lib/idempotency.d.ts +46 -0
  167. package/dist/lib/idempotency.js +107 -0
  168. package/dist/lib/idempotency.js.map +1 -0
  169. package/dist/lib/plan-store.d.ts +19 -0
  170. package/dist/lib/plan-store.js +69 -0
  171. package/dist/lib/plan-store.js.map +1 -0
  172. package/dist/lib/request-context.d.ts +7 -0
  173. package/dist/lib/request-context.js +13 -0
  174. package/dist/lib/request-context.js.map +1 -0
  175. package/dist/lib/scenes.d.ts +7 -0
  176. package/dist/lib/scenes.js +11 -0
  177. package/dist/lib/scenes.js.map +1 -0
  178. package/dist/logger.d.ts +4 -0
  179. package/dist/logger.js +17 -0
  180. package/dist/logger.js.map +1 -0
  181. package/dist/mcp/device-history.d.ts +36 -0
  182. package/dist/mcp/device-history.js +146 -0
  183. package/dist/mcp/device-history.js.map +1 -0
  184. package/dist/mcp/events-subscription.d.ts +45 -0
  185. package/dist/mcp/events-subscription.js +214 -0
  186. package/dist/mcp/events-subscription.js.map +1 -0
  187. package/dist/mqtt/client.d.ts +25 -0
  188. package/dist/mqtt/client.js +181 -0
  189. package/dist/mqtt/client.js.map +1 -0
  190. package/dist/mqtt/credential.d.ts +16 -0
  191. package/dist/mqtt/credential.js +31 -0
  192. package/dist/mqtt/credential.js.map +1 -0
  193. package/dist/policy/add-rule.d.ts +21 -0
  194. package/dist/policy/add-rule.js +125 -0
  195. package/dist/policy/add-rule.js.map +1 -0
  196. package/dist/policy/diff.d.ts +21 -0
  197. package/dist/policy/diff.js +92 -0
  198. package/dist/policy/diff.js.map +1 -0
  199. package/dist/policy/format.d.ts +6 -0
  200. package/dist/policy/format.js +58 -0
  201. package/dist/policy/format.js.map +1 -0
  202. package/dist/policy/load.d.ts +32 -0
  203. package/dist/policy/load.js +62 -0
  204. package/dist/policy/load.js.map +1 -0
  205. package/dist/policy/migrate.d.ts +21 -0
  206. package/dist/policy/migrate.js +68 -0
  207. package/dist/policy/migrate.js.map +1 -0
  208. package/dist/policy/schema.d.ts +5 -0
  209. package/dist/policy/schema.js +19 -0
  210. package/dist/policy/schema.js.map +1 -0
  211. package/dist/policy/validate.d.ts +19 -0
  212. package/dist/policy/validate.js +263 -0
  213. package/dist/policy/validate.js.map +1 -0
  214. package/dist/rules/action.d.ts +65 -0
  215. package/dist/rules/action.js +217 -0
  216. package/dist/rules/action.js.map +1 -0
  217. package/dist/rules/audit-query.d.ts +51 -0
  218. package/dist/rules/audit-query.js +90 -0
  219. package/dist/rules/audit-query.js.map +1 -0
  220. package/dist/rules/conflict-analyzer.d.ts +57 -0
  221. package/dist/rules/conflict-analyzer.js +215 -0
  222. package/dist/rules/conflict-analyzer.js.map +1 -0
  223. package/dist/rules/cron-scheduler.d.ts +62 -0
  224. package/dist/rules/cron-scheduler.js +187 -0
  225. package/dist/rules/cron-scheduler.js.map +1 -0
  226. package/dist/rules/destructive.d.ts +20 -0
  227. package/dist/rules/destructive.js +53 -0
  228. package/dist/rules/destructive.js.map +1 -0
  229. package/dist/rules/engine.d.ts +193 -0
  230. package/dist/rules/engine.js +758 -0
  231. package/dist/rules/engine.js.map +1 -0
  232. package/dist/rules/matcher.d.ts +56 -0
  233. package/dist/rules/matcher.js +231 -0
  234. package/dist/rules/matcher.js.map +1 -0
  235. package/dist/rules/pid-file.d.ts +43 -0
  236. package/dist/rules/pid-file.js +96 -0
  237. package/dist/rules/pid-file.js.map +1 -0
  238. package/dist/rules/quiet-hours.d.ts +26 -0
  239. package/dist/rules/quiet-hours.js +46 -0
  240. package/dist/rules/quiet-hours.js.map +1 -0
  241. package/dist/rules/suggest.d.ts +20 -0
  242. package/dist/rules/suggest.js +96 -0
  243. package/dist/rules/suggest.js.map +1 -0
  244. package/dist/rules/throttle.d.ts +61 -0
  245. package/dist/rules/throttle.js +117 -0
  246. package/dist/rules/throttle.js.map +1 -0
  247. package/dist/rules/types.d.ts +117 -0
  248. package/dist/rules/types.js +35 -0
  249. package/dist/rules/types.js.map +1 -0
  250. package/dist/rules/webhook-listener.d.ts +63 -0
  251. package/dist/rules/webhook-listener.js +224 -0
  252. package/dist/rules/webhook-listener.js.map +1 -0
  253. package/dist/rules/webhook-token.d.ts +50 -0
  254. package/dist/rules/webhook-token.js +91 -0
  255. package/dist/rules/webhook-token.js.map +1 -0
  256. package/dist/schema/field-aliases.d.ts +34 -0
  257. package/dist/schema/field-aliases.js +132 -0
  258. package/dist/schema/field-aliases.js.map +1 -0
  259. package/dist/sinks/dispatcher.d.ts +7 -0
  260. package/dist/sinks/dispatcher.js +13 -0
  261. package/dist/sinks/dispatcher.js.map +1 -0
  262. package/dist/sinks/file.d.ts +6 -0
  263. package/dist/sinks/file.js +20 -0
  264. package/dist/sinks/file.js.map +1 -0
  265. package/dist/sinks/format.d.ts +20 -0
  266. package/dist/sinks/format.js +57 -0
  267. package/dist/sinks/format.js.map +1 -0
  268. package/dist/sinks/homeassistant.d.ts +18 -0
  269. package/dist/sinks/homeassistant.js +45 -0
  270. package/dist/sinks/homeassistant.js.map +1 -0
  271. package/dist/sinks/openclaw.d.ts +13 -0
  272. package/dist/sinks/openclaw.js +34 -0
  273. package/dist/sinks/openclaw.js.map +1 -0
  274. package/dist/sinks/stdout.d.ts +4 -0
  275. package/dist/sinks/stdout.js +6 -0
  276. package/dist/sinks/stdout.js.map +1 -0
  277. package/dist/sinks/telegram.d.ts +11 -0
  278. package/dist/sinks/telegram.js +29 -0
  279. package/dist/sinks/telegram.js.map +1 -0
  280. package/dist/sinks/types.d.ts +13 -0
  281. package/dist/sinks/types.js +2 -0
  282. package/dist/sinks/types.js.map +1 -0
  283. package/dist/sinks/webhook.d.ts +6 -0
  284. package/dist/sinks/webhook.js +23 -0
  285. package/dist/sinks/webhook.js.map +1 -0
  286. package/dist/status-sync/manager.d.ts +48 -0
  287. package/dist/status-sync/manager.js +269 -0
  288. package/dist/status-sync/manager.js.map +1 -0
  289. package/dist/utils/arg-parsers.d.ts +16 -0
  290. package/dist/utils/arg-parsers.js +67 -0
  291. package/dist/utils/arg-parsers.js.map +1 -0
  292. package/dist/utils/audit.d.ts +69 -0
  293. package/dist/utils/audit.js +122 -0
  294. package/dist/utils/audit.js.map +1 -0
  295. package/dist/utils/filter.d.ts +81 -0
  296. package/dist/utils/filter.js +190 -0
  297. package/dist/utils/filter.js.map +1 -0
  298. package/dist/utils/flags.d.ts +72 -0
  299. package/dist/utils/flags.js +187 -0
  300. package/dist/utils/flags.js.map +1 -0
  301. package/dist/utils/format.d.ts +9 -0
  302. package/dist/utils/format.js +118 -0
  303. package/dist/utils/format.js.map +1 -0
  304. package/dist/utils/health.d.ts +48 -0
  305. package/dist/utils/health.js +102 -0
  306. package/dist/utils/health.js.map +1 -0
  307. package/dist/utils/help-json.d.ts +39 -0
  308. package/dist/utils/help-json.js +55 -0
  309. package/dist/utils/help-json.js.map +1 -0
  310. package/dist/utils/name-resolver.d.ts +26 -0
  311. package/dist/utils/name-resolver.js +138 -0
  312. package/dist/utils/name-resolver.js.map +1 -0
  313. package/dist/utils/output.d.ts +73 -0
  314. package/dist/utils/output.js +405 -0
  315. package/dist/utils/output.js.map +1 -0
  316. package/dist/utils/quota.d.ts +61 -0
  317. package/dist/utils/quota.js +228 -0
  318. package/dist/utils/quota.js.map +1 -0
  319. package/dist/utils/redact.d.ts +23 -0
  320. package/dist/utils/redact.js +69 -0
  321. package/dist/utils/redact.js.map +1 -0
  322. package/dist/utils/retry.d.ts +72 -0
  323. package/dist/utils/retry.js +141 -0
  324. package/dist/utils/retry.js.map +1 -0
  325. package/dist/utils/string.d.ts +2 -0
  326. package/dist/utils/string.js +23 -0
  327. package/dist/utils/string.js.map +1 -0
  328. package/dist/version.d.ts +2 -0
  329. package/dist/version.js +5 -0
  330. package/dist/version.js.map +1 -0
  331. package/package.json +2 -2
@@ -0,0 +1,383 @@
1
+ import { createClient } from '../api/client.js';
2
+ import { idempotencyCache } from './idempotency.js';
3
+ import { findCatalogEntry, suggestedActions, getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
4
+ import { getCachedDevice, updateCacheFromDeviceList, loadCache, isListCacheFresh, getCachedStatus, setCachedStatus, } from '../devices/cache.js';
5
+ import { getCacheMode } from '../utils/flags.js';
6
+ import { writeAudit } from '../utils/audit.js';
7
+ import { isDryRun } from '../utils/flags.js';
8
+ export class DeviceNotFoundError extends Error {
9
+ deviceId;
10
+ constructor(deviceId) {
11
+ super(`No device with id "${deviceId}" found on this account.`);
12
+ this.deviceId = deviceId;
13
+ this.name = 'DeviceNotFoundError';
14
+ }
15
+ }
16
+ export class CommandValidationError extends Error {
17
+ kind;
18
+ hint;
19
+ constructor(message, kind, hint) {
20
+ super(message);
21
+ this.kind = kind;
22
+ this.hint = hint;
23
+ this.name = 'CommandValidationError';
24
+ }
25
+ }
26
+ /** Fetch the full device + IR remote inventory and refresh the local cache. */
27
+ export async function fetchDeviceList(client) {
28
+ // TTL-gated read: when the on-disk cache is younger than the configured
29
+ // list TTL, skip the API call and synthesize a DeviceListBody from the
30
+ // metadata cache.
31
+ const mode = getCacheMode();
32
+ if (mode.listTtlMs > 0 && isListCacheFresh(mode.listTtlMs)) {
33
+ const cached = loadCache();
34
+ if (cached) {
35
+ const deviceList = [];
36
+ const infraredRemoteList = [];
37
+ for (const [deviceId, entry] of Object.entries(cached.devices)) {
38
+ if (entry.category === 'physical') {
39
+ deviceList.push({
40
+ deviceId,
41
+ deviceName: entry.name,
42
+ deviceType: entry.type,
43
+ enableCloudService: entry.enableCloudService ?? true,
44
+ hubDeviceId: entry.hubDeviceId ?? '',
45
+ roomID: entry.roomID,
46
+ roomName: entry.roomName,
47
+ familyName: entry.familyName,
48
+ controlType: entry.controlType,
49
+ });
50
+ }
51
+ else {
52
+ infraredRemoteList.push({
53
+ deviceId,
54
+ deviceName: entry.name,
55
+ remoteType: entry.type,
56
+ hubDeviceId: entry.hubDeviceId ?? '',
57
+ controlType: entry.controlType,
58
+ });
59
+ }
60
+ }
61
+ return { deviceList, infraredRemoteList };
62
+ }
63
+ }
64
+ const c = client ?? createClient();
65
+ const res = await c.get('/v1.1/devices');
66
+ updateCacheFromDeviceList(res.data.body);
67
+ return res.data.body;
68
+ }
69
+ /** Fetch live status for a single physical device. IR remotes have no status channel. */
70
+ export async function fetchDeviceStatus(deviceId, client) {
71
+ const mode = getCacheMode();
72
+ if (mode.statusTtlMs > 0) {
73
+ const cached = getCachedStatus(deviceId, mode.statusTtlMs);
74
+ if (cached)
75
+ return cached;
76
+ }
77
+ const c = client ?? createClient();
78
+ const res = await c.get(`/v1.1/devices/${deviceId}/status`);
79
+ if (mode.statusTtlMs > 0) {
80
+ setCachedStatus(deviceId, res.data.body, new Date(), mode.statusTtlMs);
81
+ }
82
+ return res.data.body;
83
+ }
84
+ /**
85
+ * Execute a command on a device. `parameter` is the fully-parsed value already
86
+ * (JSON-object when applicable), not a raw CLI string — callers should parse
87
+ * upstream if needed.
88
+ */
89
+ export async function executeCommand(deviceId, cmd, parameter, commandType, client, options) {
90
+ const c = client ?? createClient();
91
+ const body = {
92
+ command: cmd,
93
+ parameter: parameter ?? 'default',
94
+ commandType,
95
+ };
96
+ const baseAudit = {
97
+ t: new Date().toISOString(),
98
+ kind: 'command',
99
+ deviceId,
100
+ command: cmd,
101
+ parameter,
102
+ commandType,
103
+ dryRun: isDryRun(),
104
+ ...(options?.planId ? { planId: options.planId } : {}),
105
+ };
106
+ // Wrap in idempotency cache if key is provided
107
+ const execute = async () => {
108
+ try {
109
+ const res = await c.post(`/v1.1/devices/${deviceId}/commands`, body);
110
+ writeAudit({ ...baseAudit, result: 'ok' });
111
+ return res.data.body;
112
+ }
113
+ catch (err) {
114
+ // Dry-run intercepts throw DryRunSignal — still log the intent.
115
+ if (err instanceof Error && err.name === 'DryRunSignal') {
116
+ writeAudit({ ...baseAudit, result: 'ok' });
117
+ }
118
+ else {
119
+ writeAudit({
120
+ ...baseAudit,
121
+ result: 'error',
122
+ error: err instanceof Error ? err.message : String(err),
123
+ });
124
+ }
125
+ throw err;
126
+ }
127
+ };
128
+ const { result, replayed } = await idempotencyCache.run(options?.idempotencyKey, execute, { command: cmd, parameter });
129
+ if (!replayed)
130
+ return result;
131
+ // Cached hit — attach replayed marker without mutating the original.
132
+ if (result && typeof result === 'object') {
133
+ return { ...result, replayed: true };
134
+ }
135
+ return { value: result, replayed: true };
136
+ }
137
+ /**
138
+ * Validate a command against the locally-cached device → catalog mapping.
139
+ * Returns `{ ok: true }` when validation passes or is skipped (unknown device,
140
+ * custom IR button, etc.). On a case-only mismatch the canonical command name
141
+ * is returned via `normalized` along with a `caseNormalizedFrom` field so the
142
+ * caller can emit a warning and continue with the canonical name.
143
+ * Returns `{ ok: false, error }` only when the caller should refuse the call.
144
+ */
145
+ export function validateCommand(deviceId, cmd, parameter, commandType) {
146
+ if (commandType === 'customize')
147
+ return { ok: true };
148
+ const cached = getCachedDevice(deviceId);
149
+ if (!cached)
150
+ return { ok: true };
151
+ const match = findCatalogEntry(cached.type);
152
+ if (!match || Array.isArray(match))
153
+ return { ok: true };
154
+ const builtinCommands = match.commands.filter((c) => c.commandType !== 'customize');
155
+ if (match.readOnly || builtinCommands.length === 0) {
156
+ return {
157
+ ok: false,
158
+ error: new CommandValidationError(`${cached.name} (${cached.type}) is a read-only sensor — it has no control commands.`, 'read-only-device', `Use 'switchbot devices status ${deviceId}' to read this device instead.`),
159
+ };
160
+ }
161
+ let spec = builtinCommands.find((c) => c.command === cmd);
162
+ let caseNormalizedFrom;
163
+ let normalizedCmd = cmd;
164
+ if (!spec) {
165
+ const unique = [...new Set(builtinCommands.map((c) => c.command))];
166
+ const caseMatch = unique.find((c) => c.toLowerCase() === cmd.toLowerCase());
167
+ if (caseMatch) {
168
+ // Case-only mismatch: normalize and continue.
169
+ caseNormalizedFrom = cmd;
170
+ normalizedCmd = caseMatch;
171
+ spec = builtinCommands.find((c) => c.command === caseMatch);
172
+ }
173
+ else {
174
+ const hint = `Supported commands: ${unique.join(', ')}`;
175
+ return {
176
+ ok: false,
177
+ error: new CommandValidationError(`"${cmd}" is not a supported command for ${cached.name} (${cached.type}).`, 'unknown-command', hint),
178
+ };
179
+ }
180
+ }
181
+ if (!spec)
182
+ return { ok: true, normalized: normalizedCmd, caseNormalizedFrom };
183
+ const noParamExpected = spec.parameter === '—';
184
+ const userProvidedParam = parameter !== undefined && parameter !== 'default';
185
+ if (noParamExpected && userProvidedParam) {
186
+ return {
187
+ ok: false,
188
+ error: new CommandValidationError(`"${normalizedCmd}" takes no parameter, but one was provided: "${parameter}".`, 'unexpected-parameter', `Try: switchbot devices command ${deviceId} ${normalizedCmd}`),
189
+ };
190
+ }
191
+ // Warn when a parameter is required but the user omitted it
192
+ const paramRequired = !noParamExpected && spec.parameter !== 'default';
193
+ if (paramRequired && !userProvidedParam) {
194
+ const example = spec.exampleParams?.[0];
195
+ return {
196
+ ok: false,
197
+ error: new CommandValidationError(`"${normalizedCmd}" requires a parameter (${spec.parameter}).`, 'missing-parameter', example
198
+ ? `Example: switchbot devices command <deviceId> ${normalizedCmd} "${example}"`
199
+ : `See: switchbot devices commands ${cached.type}`),
200
+ };
201
+ }
202
+ return { ok: true, normalized: normalizedCmd, caseNormalizedFrom };
203
+ }
204
+ /**
205
+ * Inspect catalog annotations to decide whether a command is destructive,
206
+ * i.e. has hard-to-reverse real-world effects and should require an explicit
207
+ * confirmation from an agent / operator before execution. Customize commands
208
+ * are considered non-destructive here — they're user-defined IR buttons
209
+ * whose behavior the catalog can't know about.
210
+ */
211
+ export function isDestructiveCommand(deviceType, cmd, commandType) {
212
+ if (commandType === 'customize')
213
+ return false;
214
+ if (!deviceType)
215
+ return false;
216
+ const match = findCatalogEntry(deviceType);
217
+ if (!match || Array.isArray(match))
218
+ return false;
219
+ const spec = match.commands.find((c) => c.command === cmd);
220
+ if (!spec)
221
+ return false;
222
+ return deriveSafetyTier(spec, match) === 'destructive';
223
+ }
224
+ /** Return the safetyReason for a command, or null if not destructive / not found. */
225
+ export function getDestructiveReason(deviceType, cmd, commandType) {
226
+ if (commandType === 'customize')
227
+ return null;
228
+ if (!deviceType)
229
+ return null;
230
+ const match = findCatalogEntry(deviceType);
231
+ if (!match || Array.isArray(match))
232
+ return null;
233
+ const spec = match.commands.find((c) => c.command === cmd);
234
+ return spec ? getCommandSafetyReason(spec) : null;
235
+ }
236
+ /**
237
+ * Describe a device by id: metadata + catalog entry (if known) +
238
+ * optional live status. Throws `DeviceNotFoundError` when the id is unknown.
239
+ */
240
+ export async function describeDevice(deviceId, options = {}, client) {
241
+ const body = await fetchDeviceList(client);
242
+ const { deviceList, infraredRemoteList } = body;
243
+ const physical = deviceList.find((d) => d.deviceId === deviceId);
244
+ const ir = infraredRemoteList.find((d) => d.deviceId === deviceId);
245
+ if (!physical && !ir)
246
+ throw new DeviceNotFoundError(deviceId);
247
+ const typeName = physical ? (physical.deviceType ?? '') : ir.remoteType;
248
+ const match = typeName ? findCatalogEntry(typeName) : null;
249
+ const catalogEntry = !match || Array.isArray(match) ? null : match;
250
+ let liveStatus;
251
+ if (options.live && physical) {
252
+ try {
253
+ liveStatus = await fetchDeviceStatus(deviceId, client);
254
+ }
255
+ catch (err) {
256
+ liveStatus = { error: err instanceof Error ? err.message : String(err) };
257
+ }
258
+ }
259
+ const source = catalogEntry
260
+ ? liveStatus
261
+ ? 'catalog+live'
262
+ : 'catalog'
263
+ : liveStatus
264
+ ? 'live'
265
+ : 'none';
266
+ const capabilities = catalogEntry
267
+ ? {
268
+ role: catalogEntry.role ?? null,
269
+ readOnly: catalogEntry.readOnly ?? false,
270
+ commands: catalogEntry.commands.map((c) => {
271
+ const tier = deriveSafetyTier(c, catalogEntry);
272
+ const reason = getCommandSafetyReason(c);
273
+ return {
274
+ ...c,
275
+ safetyTier: tier,
276
+ ...(reason ? { safetyReason: reason } : {}),
277
+ };
278
+ }),
279
+ statusFields: catalogEntry.statusFields ?? [],
280
+ ...(liveStatus !== undefined ? { liveStatus } : {}),
281
+ }
282
+ : liveStatus !== undefined
283
+ ? { liveStatus }
284
+ : null;
285
+ return {
286
+ device: (physical ?? ir),
287
+ isPhysical: Boolean(physical),
288
+ typeName,
289
+ controlType: physical?.controlType ?? ir?.controlType ?? null,
290
+ catalog: catalogEntry,
291
+ capabilities,
292
+ source,
293
+ suggestedActions: catalogEntry ? suggestedActions(catalogEntry) : [],
294
+ inheritedLocation: ir ? buildHubLocationMap(deviceList).get(ir.hubDeviceId) : undefined,
295
+ };
296
+ }
297
+ /** Build a map from hubDeviceId → room/family/roomID for IR-remote inheritance. */
298
+ export function buildHubLocationMap(deviceList) {
299
+ const map = new Map();
300
+ for (const d of deviceList) {
301
+ if (!d.deviceId)
302
+ continue;
303
+ map.set(d.deviceId, {
304
+ family: d.familyName ?? undefined,
305
+ room: d.roomName ?? undefined,
306
+ roomID: d.roomID ?? undefined,
307
+ });
308
+ }
309
+ return map;
310
+ }
311
+ /**
312
+ * Search the local catalog by type name / alias. Returns up to `limit`
313
+ * entries whose type or alias contains the query (case-insensitive).
314
+ * Intended for MCP's `search_catalog` tool — not for dispatching commands.
315
+ */
316
+ export function searchCatalog(query, limit = 20) {
317
+ const catalog = getEffectiveCatalog();
318
+ const q = query.trim().toLowerCase();
319
+ if (!q)
320
+ return catalog.slice(0, limit);
321
+ const hits = [];
322
+ for (const entry of catalog) {
323
+ const haystack = [entry.type, ...(entry.aliases ?? [])].map((s) => s.toLowerCase());
324
+ if (haystack.some((h) => h.includes(q))) {
325
+ hits.push(entry);
326
+ if (hits.length >= limit)
327
+ break;
328
+ }
329
+ }
330
+ return hits;
331
+ }
332
+ /** Convert a DescribeResult to the shape validated by the MCP outputSchema. */
333
+ export function toMcpDescribeShape(r) {
334
+ const d = r.device;
335
+ return {
336
+ device: {
337
+ deviceId: d.deviceId,
338
+ deviceName: d.deviceName,
339
+ ...(d.deviceType !== undefined ? { deviceType: d.deviceType } : {}),
340
+ ...('enableCloudService' in d ? { enableCloudService: d.enableCloudService } : {}),
341
+ ...(d.hubDeviceId !== undefined ? { hubDeviceId: d.hubDeviceId } : {}),
342
+ ...(d.roomID !== undefined ? { roomID: d.roomID } : {}),
343
+ ...(d.roomName !== undefined ? { roomName: d.roomName } : {}),
344
+ ...(d.familyName !== undefined ? { familyName: d.familyName } : {}),
345
+ ...(d.controlType !== undefined ? { controlType: d.controlType } : {}),
346
+ ...('remoteType' in d && d.remoteType !== undefined ? { remoteType: d.remoteType } : {}),
347
+ },
348
+ isPhysical: r.isPhysical,
349
+ typeName: r.typeName,
350
+ controlType: r.controlType,
351
+ source: r.source,
352
+ capabilities: r.capabilities,
353
+ suggestedActions: r.suggestedActions,
354
+ ...(r.inheritedLocation !== undefined
355
+ ? { inheritedLocation: { family: r.inheritedLocation.family, room: r.inheritedLocation.room } }
356
+ : {}),
357
+ };
358
+ }
359
+ /** Shapes the device list body for the MCP list_devices outputSchema. */
360
+ export function toMcpDeviceListShape(d) {
361
+ return {
362
+ deviceId: d.deviceId,
363
+ deviceName: d.deviceName,
364
+ deviceType: d.deviceType,
365
+ enableCloudService: d.enableCloudService,
366
+ hubDeviceId: d.hubDeviceId,
367
+ roomID: d.roomID,
368
+ roomName: d.roomName,
369
+ familyName: d.familyName,
370
+ controlType: d.controlType,
371
+ };
372
+ }
373
+ /** Shapes an infrared remote for the MCP list_devices outputSchema. */
374
+ export function toMcpIrDeviceShape(d) {
375
+ return {
376
+ deviceId: d.deviceId,
377
+ deviceName: d.deviceName,
378
+ remoteType: d.remoteType,
379
+ hubDeviceId: d.hubDeviceId,
380
+ controlType: d.controlType,
381
+ };
382
+ }
383
+ //# sourceMappingURL=devices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/lib/devices.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * In-memory LRU cache for idempotent request deduplication.
3
+ * Caches the outcome of a keyed operation for 60 seconds;
4
+ * duplicate keys within the window return the cached result (with a
5
+ * `replayed: true` marker). Duplicate keys within the window for a DIFFERENT
6
+ * (command, parameter) shape raise {@link IdempotencyConflictError}.
7
+ *
8
+ * Keys are stored in-memory as a SHA-256 fingerprint of the user-provided
9
+ * key — the original string never touches the Map keys, so a later heap dump
10
+ * or inadvertent log capture does not leak the raw token.
11
+ *
12
+ * Process-local only — not shared across replicas.
13
+ */
14
+ export declare class IdempotencyConflictError extends Error {
15
+ readonly key: string;
16
+ readonly existingShape: string;
17
+ readonly newShape: string;
18
+ constructor(message: string, key: string, existingShape: string, newShape: string);
19
+ }
20
+ export declare class IdempotencyCache {
21
+ private cache;
22
+ private readonly ttlMs;
23
+ private readonly maxEntries;
24
+ constructor(ttlMs?: number, maxEntries?: number);
25
+ /**
26
+ * Execute fn if the key is not cached, or return the cached result if it is.
27
+ * On new execution, caches the result for ttlMs.
28
+ *
29
+ * When `shape` is provided, a cached hit is validated against the original
30
+ * (command, parameter) fingerprint; mismatched shape raises
31
+ * {@link IdempotencyConflictError}.
32
+ *
33
+ * Returns a tuple-esque object with `replayed: true` when the cached
34
+ * result is served. The `result` field is the original cached value.
35
+ */
36
+ run<T>(key: string | undefined, fn: () => Promise<T>, shape?: {
37
+ command: string;
38
+ parameter: unknown;
39
+ }): Promise<{
40
+ result: T;
41
+ replayed: boolean;
42
+ }>;
43
+ clear(): void;
44
+ size(): number;
45
+ }
46
+ export declare const idempotencyCache: IdempotencyCache;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * In-memory LRU cache for idempotent request deduplication.
3
+ * Caches the outcome of a keyed operation for 60 seconds;
4
+ * duplicate keys within the window return the cached result (with a
5
+ * `replayed: true` marker). Duplicate keys within the window for a DIFFERENT
6
+ * (command, parameter) shape raise {@link IdempotencyConflictError}.
7
+ *
8
+ * Keys are stored in-memory as a SHA-256 fingerprint of the user-provided
9
+ * key — the original string never touches the Map keys, so a later heap dump
10
+ * or inadvertent log capture does not leak the raw token.
11
+ *
12
+ * Process-local only — not shared across replicas.
13
+ */
14
+ import crypto from 'node:crypto';
15
+ const DEFAULT_TTL_MS = 60000; // 60 seconds
16
+ const DEFAULT_MAX_ENTRIES = 1024;
17
+ export class IdempotencyConflictError extends Error {
18
+ key;
19
+ existingShape;
20
+ newShape;
21
+ constructor(message, key, existingShape, newShape) {
22
+ super(message);
23
+ this.key = key;
24
+ this.existingShape = existingShape;
25
+ this.newShape = newShape;
26
+ this.name = 'IdempotencyConflictError';
27
+ }
28
+ }
29
+ function hashKey(key) {
30
+ return crypto.createHash('sha256').update(key).digest('hex');
31
+ }
32
+ function shapeSignature(command, parameter) {
33
+ // Canonical-ish JSON — stable enough for object equality with no nested sort
34
+ // (callers can pass primitives or small objects).
35
+ let parm;
36
+ try {
37
+ parm = JSON.stringify(parameter ?? 'default');
38
+ }
39
+ catch {
40
+ parm = String(parameter);
41
+ }
42
+ return `${command}::${parm}`;
43
+ }
44
+ export class IdempotencyCache {
45
+ cache = new Map();
46
+ ttlMs;
47
+ maxEntries;
48
+ constructor(ttlMs, maxEntries) {
49
+ this.ttlMs = ttlMs ?? DEFAULT_TTL_MS;
50
+ this.maxEntries = maxEntries ?? DEFAULT_MAX_ENTRIES;
51
+ }
52
+ /**
53
+ * Execute fn if the key is not cached, or return the cached result if it is.
54
+ * On new execution, caches the result for ttlMs.
55
+ *
56
+ * When `shape` is provided, a cached hit is validated against the original
57
+ * (command, parameter) fingerprint; mismatched shape raises
58
+ * {@link IdempotencyConflictError}.
59
+ *
60
+ * Returns a tuple-esque object with `replayed: true` when the cached
61
+ * result is served. The `result` field is the original cached value.
62
+ */
63
+ async run(key, fn, shape) {
64
+ if (!key) {
65
+ const result = await fn();
66
+ return { result, replayed: false };
67
+ }
68
+ const hashed = hashKey(key);
69
+ const now = Date.now();
70
+ const cached = this.cache.get(hashed);
71
+ const currentShape = shape ? shapeSignature(shape.command, shape.parameter) : '*';
72
+ if (cached && cached.expiresAt > now) {
73
+ if (shape && cached.shape !== '*' && cached.shape !== currentShape) {
74
+ throw new IdempotencyConflictError(`idempotency_conflict: key was first used for ${cached.shape.replace('::', ' ')}; refusing new shape ${currentShape.replace('::', ' ')}`, '<redacted>', cached.shape, currentShape);
75
+ }
76
+ return { result: cached.result, replayed: true };
77
+ }
78
+ const result = await fn();
79
+ if (this.cache.size >= this.maxEntries) {
80
+ const toRemove = Math.ceil(this.maxEntries * 0.1);
81
+ let removed = 0;
82
+ for (const [k, v] of this.cache.entries()) {
83
+ if (removed >= toRemove)
84
+ break;
85
+ if (v.expiresAt <= now) {
86
+ this.cache.delete(k);
87
+ removed++;
88
+ }
89
+ }
90
+ if (this.cache.size >= this.maxEntries) {
91
+ const firstKey = this.cache.keys().next().value;
92
+ if (firstKey)
93
+ this.cache.delete(firstKey);
94
+ }
95
+ }
96
+ this.cache.set(hashed, { result, expiresAt: now + this.ttlMs, shape: currentShape });
97
+ return { result, replayed: false };
98
+ }
99
+ clear() {
100
+ this.cache.clear();
101
+ }
102
+ size() {
103
+ return this.cache.size;
104
+ }
105
+ }
106
+ export const idempotencyCache = new IdempotencyCache();
107
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../src/lib/idempotency.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { Plan } from '../commands/plan.js';
2
+ export declare const PLANS_DIR: string;
3
+ export type PlanStatus = 'pending' | 'approved' | 'rejected' | 'executed' | 'failed';
4
+ export interface PlanRecord {
5
+ planId: string;
6
+ createdAt: string;
7
+ status: PlanStatus;
8
+ approvedAt?: string;
9
+ executedAt?: string;
10
+ /** Set when status transitions to 'failed'. */
11
+ failedAt?: string;
12
+ /** Summary of why the plan failed (e.g. "1 error, 2 skipped"). */
13
+ failureReason?: string;
14
+ plan: Plan;
15
+ }
16
+ export declare function savePlanRecord(plan: Plan): PlanRecord;
17
+ export declare function loadPlanRecord(planId: string): PlanRecord | null;
18
+ export declare function updatePlanRecord(planId: string, updates: Partial<PlanRecord>): PlanRecord;
19
+ export declare function listPlanRecords(): PlanRecord[];
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ export const PLANS_DIR = path.join(os.homedir(), '.switchbot', 'plans');
6
+ function ensurePlansDir() {
7
+ fs.mkdirSync(PLANS_DIR, { recursive: true, mode: 0o700 });
8
+ }
9
+ function planPath(planId) {
10
+ return path.join(PLANS_DIR, `${planId}.json`);
11
+ }
12
+ const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13
+ function assertValidPlanId(planId) {
14
+ if (!UUID_V4_RE.test(planId)) {
15
+ throw new Error(`invalid planId: ${planId}`);
16
+ }
17
+ }
18
+ export function savePlanRecord(plan) {
19
+ ensurePlansDir();
20
+ const record = {
21
+ planId: randomUUID(),
22
+ createdAt: new Date().toISOString(),
23
+ status: 'pending',
24
+ plan,
25
+ };
26
+ fs.writeFileSync(planPath(record.planId), JSON.stringify(record, null, 2), { mode: 0o600 });
27
+ return record;
28
+ }
29
+ export function loadPlanRecord(planId) {
30
+ assertValidPlanId(planId);
31
+ try {
32
+ const raw = fs.readFileSync(planPath(planId), 'utf-8');
33
+ return JSON.parse(raw);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function updatePlanRecord(planId, updates) {
40
+ assertValidPlanId(planId);
41
+ const record = loadPlanRecord(planId);
42
+ if (!record)
43
+ throw new Error(`Plan ${planId} not found in ${PLANS_DIR}`);
44
+ const updated = { ...record, ...updates };
45
+ fs.writeFileSync(planPath(planId), JSON.stringify(updated, null, 2), { mode: 0o600 });
46
+ return updated;
47
+ }
48
+ export function listPlanRecords() {
49
+ try {
50
+ if (!fs.existsSync(PLANS_DIR))
51
+ return [];
52
+ return fs
53
+ .readdirSync(PLANS_DIR)
54
+ .filter((f) => f.endsWith('.json'))
55
+ .flatMap((f) => {
56
+ try {
57
+ return [JSON.parse(fs.readFileSync(path.join(PLANS_DIR, f), 'utf-8'))];
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ })
63
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ }
69
+ //# sourceMappingURL=plan-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-store.js","sourceRoot":"","sources":["../../src/lib/plan-store.ts"],"names":[],"mappings":"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"}