@switchbot/openapi-cli 2.7.2 → 3.0.0

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 (55) hide show
  1. package/README.md +383 -101
  2. package/dist/commands/agent-bootstrap.js +47 -2
  3. package/dist/commands/auth.js +354 -0
  4. package/dist/commands/config.js +30 -0
  5. package/dist/commands/devices.js +0 -1
  6. package/dist/commands/doctor.js +184 -7
  7. package/dist/commands/events.js +3 -3
  8. package/dist/commands/explain.js +1 -2
  9. package/dist/commands/install.js +246 -0
  10. package/dist/commands/mcp.js +796 -3
  11. package/dist/commands/plan.js +110 -14
  12. package/dist/commands/policy.js +469 -0
  13. package/dist/commands/rules.js +657 -0
  14. package/dist/commands/schema.js +0 -2
  15. package/dist/commands/status-sync.js +131 -0
  16. package/dist/commands/uninstall.js +237 -0
  17. package/dist/config.js +14 -0
  18. package/dist/credentials/backends/file.js +101 -0
  19. package/dist/credentials/backends/linux.js +129 -0
  20. package/dist/credentials/backends/macos.js +129 -0
  21. package/dist/credentials/backends/windows.js +215 -0
  22. package/dist/credentials/keychain.js +88 -0
  23. package/dist/credentials/prime.js +52 -0
  24. package/dist/devices/catalog.js +4 -10
  25. package/dist/index.js +23 -1
  26. package/dist/install/default-steps.js +257 -0
  27. package/dist/install/preflight.js +212 -0
  28. package/dist/install/steps.js +67 -0
  29. package/dist/lib/command-keywords.js +17 -0
  30. package/dist/lib/devices.js +0 -1
  31. package/dist/policy/add-rule.js +124 -0
  32. package/dist/policy/diff.js +91 -0
  33. package/dist/policy/examples/policy.example.yaml +99 -0
  34. package/dist/policy/format.js +57 -0
  35. package/dist/policy/load.js +61 -0
  36. package/dist/policy/migrate.js +67 -0
  37. package/dist/policy/schema/v0.2.json +302 -0
  38. package/dist/policy/schema.js +18 -0
  39. package/dist/policy/validate.js +262 -0
  40. package/dist/rules/action.js +205 -0
  41. package/dist/rules/audit-query.js +89 -0
  42. package/dist/rules/cron-scheduler.js +186 -0
  43. package/dist/rules/destructive.js +52 -0
  44. package/dist/rules/engine.js +567 -0
  45. package/dist/rules/matcher.js +230 -0
  46. package/dist/rules/pid-file.js +95 -0
  47. package/dist/rules/quiet-hours.js +45 -0
  48. package/dist/rules/suggest.js +95 -0
  49. package/dist/rules/throttle.js +78 -0
  50. package/dist/rules/types.js +34 -0
  51. package/dist/rules/webhook-listener.js +223 -0
  52. package/dist/rules/webhook-token.js +90 -0
  53. package/dist/status-sync/manager.js +268 -0
  54. package/dist/utils/audit.js +12 -2
  55. package/package.json +12 -4
@@ -0,0 +1,129 @@
1
+ /**
2
+ * macOS Keychain backend.
3
+ *
4
+ * Wraps the built-in `security(1)` CLI so `npm install` stays free of
5
+ * native compile steps. Service name is shared with the Linux and
6
+ * Windows backends (`com.openclaw.switchbot`), so a user migrating a
7
+ * config between machines sees the same lookup shape.
8
+ *
9
+ * Errors never leak credential material — `add-generic-password`
10
+ * receives the password via `-w <value>` on argv, which is visible in
11
+ * `ps` to the current user but not persisted anywhere, and any stderr
12
+ * we surface back up is bounded to the library's own messages
13
+ * (`password not found`, `could not be added`, etc.) rather than our
14
+ * input values.
15
+ */
16
+ import { spawn } from 'node:child_process';
17
+ import { accountFor, CREDENTIAL_SERVICE, KeychainError, } from '../keychain.js';
18
+ function run(cmd, args, stdin) {
19
+ return new Promise((resolve) => {
20
+ const proc = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
21
+ let stdout = '';
22
+ let stderr = '';
23
+ proc.stdout.on('data', (buf) => {
24
+ stdout += buf.toString('utf-8');
25
+ });
26
+ proc.stderr.on('data', (buf) => {
27
+ stderr += buf.toString('utf-8');
28
+ });
29
+ proc.on('error', () => resolve({ code: 127, stdout, stderr }));
30
+ proc.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
31
+ if (stdin !== undefined) {
32
+ proc.stdin.write(stdin);
33
+ }
34
+ proc.stdin.end();
35
+ });
36
+ }
37
+ export async function macOsAvailable() {
38
+ if (process.platform !== 'darwin')
39
+ return false;
40
+ const res = await run('which', ['security']);
41
+ return res.code === 0 && res.stdout.trim().length > 0;
42
+ }
43
+ async function readField(profile, field) {
44
+ const account = accountFor(profile, field);
45
+ const res = await run('security', [
46
+ 'find-generic-password',
47
+ '-s', CREDENTIAL_SERVICE,
48
+ '-a', account,
49
+ '-w',
50
+ ]);
51
+ if (res.code !== 0)
52
+ return null;
53
+ const value = res.stdout.replace(/\n$/, '');
54
+ return value.length > 0 ? value : null;
55
+ }
56
+ async function writeField(profile, field, value) {
57
+ const account = accountFor(profile, field);
58
+ const res = await run('security', [
59
+ 'add-generic-password',
60
+ '-U', // update if exists
61
+ '-s', CREDENTIAL_SERVICE,
62
+ '-a', account,
63
+ '-w', value,
64
+ ]);
65
+ if (res.code !== 0) {
66
+ throw new KeychainError('keychain', 'set', `security(1) exit ${res.code}`);
67
+ }
68
+ }
69
+ async function deleteField(profile, field) {
70
+ const account = accountFor(profile, field);
71
+ const res = await run('security', [
72
+ 'delete-generic-password',
73
+ '-s', CREDENTIAL_SERVICE,
74
+ '-a', account,
75
+ ]);
76
+ // exit 44 = "The specified item could not be found" — tolerate as idempotent delete.
77
+ if (res.code !== 0 && res.code !== 44) {
78
+ throw new KeychainError('keychain', 'delete', `security(1) exit ${res.code}`);
79
+ }
80
+ }
81
+ async function restoreField(profile, field, value) {
82
+ try {
83
+ if (value === null) {
84
+ await deleteField(profile, field);
85
+ return;
86
+ }
87
+ await writeField(profile, field, value);
88
+ }
89
+ catch {
90
+ // Best effort only. Preserve the original write failure.
91
+ }
92
+ }
93
+ export function createMacOsBackend() {
94
+ return {
95
+ name: 'keychain',
96
+ async get(profile) {
97
+ const token = await readField(profile, 'token');
98
+ const secret = await readField(profile, 'secret');
99
+ if (!token || !secret)
100
+ return null;
101
+ return { token, secret };
102
+ },
103
+ async set(profile, creds) {
104
+ const previousToken = await readField(profile, 'token');
105
+ const previousSecret = await readField(profile, 'secret');
106
+ try {
107
+ await writeField(profile, 'token', creds.token);
108
+ await writeField(profile, 'secret', creds.secret);
109
+ }
110
+ catch (err) {
111
+ await restoreField(profile, 'token', previousToken);
112
+ await restoreField(profile, 'secret', previousSecret);
113
+ throw err;
114
+ }
115
+ },
116
+ async delete(profile) {
117
+ await deleteField(profile, 'token');
118
+ await deleteField(profile, 'secret');
119
+ },
120
+ describe() {
121
+ return {
122
+ backend: 'macOS Keychain',
123
+ tag: 'keychain',
124
+ writable: true,
125
+ notes: `Stored under service "${CREDENTIAL_SERVICE}" via security(1).`,
126
+ };
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Windows Credential Manager backend.
3
+ *
4
+ * Uses PowerShell + Win32 P/Invoke (`CredReadW` / `CredWriteW` /
5
+ * `CredDeleteW`) instead of a native binding so `npm install` stays
6
+ * toolchain-free on Windows runners. `cmdkey.exe` could create and
7
+ * delete credentials but can't read the password back — reading is the
8
+ * whole point, so PowerShell is mandatory.
9
+ *
10
+ * Target-name shape is `com.openclaw.switchbot:<profile>:<field>` so
11
+ * `rundll32.exe keymgr.dll,KRShowKeyMgr` displays our entries in a
12
+ * clear, groupable list.
13
+ *
14
+ * Credential values are passed to the child process via environment
15
+ * variables, not argv — this keeps them out of any process listing
16
+ * and out of the PowerShell command history. Env blocks on Windows
17
+ * are only visible to the current user (and admins), so this is a
18
+ * reasonable trade versus the alternatives (stdin requires a second
19
+ * round-trip; temp files leave disk residue).
20
+ */
21
+ import { spawn } from 'node:child_process';
22
+ import { accountFor, CREDENTIAL_SERVICE, KeychainError, } from '../keychain.js';
23
+ const PS_HEADER = `$ErrorActionPreference = 'Stop'
24
+ Add-Type -MemberDefinition @'
25
+ [DllImport("Advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
26
+ public static extern bool CredReadW(string target, int type, int flags, out System.IntPtr credentialPtr);
27
+
28
+ [DllImport("Advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
29
+ public static extern bool CredWriteW(ref CREDENTIAL cred, int flags);
30
+
31
+ [DllImport("Advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
32
+ public static extern bool CredDeleteW(string target, int type, int flags);
33
+
34
+ [DllImport("Advapi32.dll", SetLastError=true)]
35
+ public static extern void CredFree(System.IntPtr buffer);
36
+
37
+ [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
38
+ public struct CREDENTIAL {
39
+ public int Flags;
40
+ public int Type;
41
+ public System.IntPtr TargetName;
42
+ public System.IntPtr Comment;
43
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
44
+ public int CredentialBlobSize;
45
+ public System.IntPtr CredentialBlob;
46
+ public int Persist;
47
+ public int AttributeCount;
48
+ public System.IntPtr Attributes;
49
+ public System.IntPtr TargetAlias;
50
+ public System.IntPtr UserName;
51
+ }
52
+ '@ -Name CredApi -Namespace Win32 | Out-Null
53
+ `;
54
+ const PS_GET = `${PS_HEADER}
55
+ $target = $env:SWITCHBOT_CRED_TARGET
56
+ $ptr = [System.IntPtr]::Zero
57
+ $ok = [Win32.CredApi]::CredReadW($target, 1, 0, [ref]$ptr)
58
+ if (-not $ok) { exit 2 }
59
+ $cred = [System.Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][Win32.CredApi+CREDENTIAL])
60
+ $bytes = New-Object byte[] $cred.CredentialBlobSize
61
+ [System.Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $bytes, 0, $cred.CredentialBlobSize)
62
+ [Win32.CredApi]::CredFree($ptr) | Out-Null
63
+ $password = [System.Text.Encoding]::Unicode.GetString($bytes)
64
+ [Console]::Out.Write([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($password)))
65
+ `;
66
+ const PS_SET = `${PS_HEADER}
67
+ $target = $env:SWITCHBOT_CRED_TARGET
68
+ $user = $env:SWITCHBOT_CRED_USER
69
+ $value = $env:SWITCHBOT_CRED_VALUE
70
+ $bytes = [System.Text.Encoding]::Unicode.GetBytes($value)
71
+ $blob = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
72
+ [System.Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
73
+ $cred = New-Object Win32.CredApi+CREDENTIAL
74
+ $cred.Flags = 0
75
+ $cred.Type = 1
76
+ $cred.TargetName = [System.Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($target)
77
+ $cred.UserName = [System.Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($user)
78
+ $cred.CredentialBlob = $blob
79
+ $cred.CredentialBlobSize = $bytes.Length
80
+ $cred.Persist = 2
81
+ $cred.AttributeCount = 0
82
+ $ok = [Win32.CredApi]::CredWriteW([ref]$cred, 0)
83
+ [System.Runtime.InteropServices.Marshal]::FreeCoTaskMem($cred.TargetName)
84
+ [System.Runtime.InteropServices.Marshal]::FreeCoTaskMem($cred.UserName)
85
+ [System.Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
86
+ if (-not $ok) { exit 3 }
87
+ `;
88
+ const PS_DELETE = `${PS_HEADER}
89
+ $target = $env:SWITCHBOT_CRED_TARGET
90
+ $ok = [Win32.CredApi]::CredDeleteW($target, 1, 0)
91
+ if (-not $ok) {
92
+ $errno = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
93
+ # 1168 = ERROR_NOT_FOUND — tolerate as idempotent delete.
94
+ if ($errno -ne 1168) { exit 4 }
95
+ }
96
+ `;
97
+ function encodePs(script) {
98
+ return Buffer.from(script, 'utf16le').toString('base64');
99
+ }
100
+ function runPowerShell(script, env) {
101
+ return new Promise((resolve) => {
102
+ const proc = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encodePs(script)], {
103
+ stdio: ['ignore', 'pipe', 'pipe'],
104
+ env: { ...process.env, ...env },
105
+ });
106
+ let stdout = '';
107
+ let stderr = '';
108
+ proc.stdout.on('data', (buf) => {
109
+ stdout += buf.toString('utf-8');
110
+ });
111
+ proc.stderr.on('data', (buf) => {
112
+ stderr += buf.toString('utf-8');
113
+ });
114
+ proc.on('error', () => resolve({ code: 127, stdout, stderr }));
115
+ proc.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
116
+ });
117
+ }
118
+ function targetFor(profile, field) {
119
+ return `${CREDENTIAL_SERVICE}:${accountFor(profile, field)}`;
120
+ }
121
+ export async function windowsAvailable() {
122
+ if (process.platform !== 'win32')
123
+ return false;
124
+ return new Promise((resolve) => {
125
+ const proc = spawn('where', ['powershell.exe'], { stdio: ['ignore', 'pipe', 'pipe'] });
126
+ let ok = false;
127
+ proc.stdout.on('data', (buf) => {
128
+ if (buf.toString().trim().length > 0)
129
+ ok = true;
130
+ });
131
+ proc.on('error', () => resolve(false));
132
+ proc.on('close', (code) => resolve(ok && (code ?? 0) === 0));
133
+ });
134
+ }
135
+ async function readField(profile, field) {
136
+ const res = await runPowerShell(PS_GET, {
137
+ SWITCHBOT_CRED_TARGET: targetFor(profile, field),
138
+ });
139
+ if (res.code !== 0)
140
+ return null;
141
+ try {
142
+ const decoded = Buffer.from(res.stdout, 'base64').toString('utf-8');
143
+ return decoded.length > 0 ? decoded : null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ async function writeField(profile, field, value) {
150
+ const res = await runPowerShell(PS_SET, {
151
+ SWITCHBOT_CRED_TARGET: targetFor(profile, field),
152
+ SWITCHBOT_CRED_USER: accountFor(profile, field),
153
+ SWITCHBOT_CRED_VALUE: value,
154
+ });
155
+ if (res.code !== 0) {
156
+ throw new KeychainError('credman', 'set', `CredWrite exit ${res.code}`);
157
+ }
158
+ }
159
+ async function deleteField(profile, field) {
160
+ const res = await runPowerShell(PS_DELETE, {
161
+ SWITCHBOT_CRED_TARGET: targetFor(profile, field),
162
+ });
163
+ if (res.code !== 0) {
164
+ throw new KeychainError('credman', 'delete', `CredDelete exit ${res.code}`);
165
+ }
166
+ }
167
+ async function restoreField(profile, field, value) {
168
+ try {
169
+ if (value === null) {
170
+ await deleteField(profile, field);
171
+ return;
172
+ }
173
+ await writeField(profile, field, value);
174
+ }
175
+ catch {
176
+ // Best effort only. Preserve the original write failure.
177
+ }
178
+ }
179
+ export function createWindowsBackend() {
180
+ return {
181
+ name: 'credman',
182
+ async get(profile) {
183
+ const token = await readField(profile, 'token');
184
+ const secret = await readField(profile, 'secret');
185
+ if (!token || !secret)
186
+ return null;
187
+ return { token, secret };
188
+ },
189
+ async set(profile, creds) {
190
+ const previousToken = await readField(profile, 'token');
191
+ const previousSecret = await readField(profile, 'secret');
192
+ try {
193
+ await writeField(profile, 'token', creds.token);
194
+ await writeField(profile, 'secret', creds.secret);
195
+ }
196
+ catch (err) {
197
+ await restoreField(profile, 'token', previousToken);
198
+ await restoreField(profile, 'secret', previousSecret);
199
+ throw err;
200
+ }
201
+ },
202
+ async delete(profile) {
203
+ await deleteField(profile, 'token');
204
+ await deleteField(profile, 'secret');
205
+ },
206
+ describe() {
207
+ return {
208
+ backend: 'Credential Manager (Windows)',
209
+ tag: 'credman',
210
+ writable: true,
211
+ notes: `Stored under target "${CREDENTIAL_SERVICE}:*" via Win32 CredRead/CredWrite.`,
212
+ };
213
+ },
214
+ };
215
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * OS-keychain credential store abstraction.
3
+ *
4
+ * F1 scope (plan: `feat/v2.8-policy-tooling`):
5
+ * - Defines the `CredentialStore` contract the rest of the CLI can
6
+ * depend on (token/secret per profile, auditable describe(), best-
7
+ * effort delete()).
8
+ * - Ships four backends: `macos` (security(1)), `linux`
9
+ * (secret-tool), `windows` (PowerShell + Win32 CredRead/CredWrite)
10
+ * and `file` (the existing `~/.switchbot/config.json` shape as
11
+ * last-resort fallback).
12
+ * - `selectCredentialStore()` picks the OS-native backend first and
13
+ * silently degrades to `file` whenever a backend is absent or
14
+ * non-writable — so a fresh Linux box without libsecret installed
15
+ * still Just Works.
16
+ *
17
+ * Out of scope here: migrating existing users off `~/.switchbot/config.json`
18
+ * into the keychain. F3's `switchbot auth keychain migrate` subcommand
19
+ * handles the explicit opt-in; F2 only wires the *read* path.
20
+ *
21
+ * Design choices:
22
+ * - No native bindings. Every native backend shells out to an
23
+ * OS-provided CLI / interpreter, which keeps `npm install` free of
24
+ * compile steps on CI machines.
25
+ * - Errors never leak credential material to logs or stderr. On any
26
+ * subprocess failure backends return `null` (read) or throw a
27
+ * `KeychainError` without the input token/secret in the message.
28
+ * - Service / account namespacing is identical across backends
29
+ * (`com.openclaw.switchbot` / `<profile>:<field>`) so a user can
30
+ * move between machines and expect `switchbot auth keychain get`
31
+ * to produce the same lookup shape.
32
+ */
33
+ export const CREDENTIAL_SERVICE = 'com.openclaw.switchbot';
34
+ export const CREDENTIAL_FIELDS = ['token', 'secret'];
35
+ /**
36
+ * Thrown when a backend cannot service a `set`/`delete` request even
37
+ * though it reported itself as writable. Never includes the
38
+ * credential material in the message.
39
+ */
40
+ export class KeychainError extends Error {
41
+ backend;
42
+ operation;
43
+ constructor(backend, operation, message) {
44
+ super(`[${backend}] ${operation} failed: ${message}`);
45
+ this.backend = backend;
46
+ this.operation = operation;
47
+ this.name = 'KeychainError';
48
+ }
49
+ }
50
+ /** Encode the account string used by every native backend. Kept public
51
+ * so F3's CLI can show what the underlying keychain will see. */
52
+ export function accountFor(profile, field) {
53
+ return `${profile}:${field}`;
54
+ }
55
+ /**
56
+ * Select the best backend for the current platform. The caller does
57
+ * not need to handle "no keychain available" — this function always
58
+ * returns a store, falling back to the file backend if necessary.
59
+ *
60
+ * Detection is done eagerly at call time (cheap `which` probe) so a
61
+ * long-running process reflects environment changes (e.g. user
62
+ * installs secret-tool after first run). Selection does NOT mutate
63
+ * any state; calling it twice returns fresh instances.
64
+ */
65
+ export async function selectCredentialStore(opts = {}) {
66
+ if (opts.preferFile) {
67
+ const { createFileBackend } = await import('./backends/file.js');
68
+ return createFileBackend();
69
+ }
70
+ const platform = process.platform;
71
+ if (platform === 'darwin') {
72
+ const { createMacOsBackend, macOsAvailable } = await import('./backends/macos.js');
73
+ if (await macOsAvailable())
74
+ return createMacOsBackend();
75
+ }
76
+ else if (platform === 'linux') {
77
+ const { createLinuxBackend, linuxAvailable } = await import('./backends/linux.js');
78
+ if (await linuxAvailable())
79
+ return createLinuxBackend();
80
+ }
81
+ else if (platform === 'win32') {
82
+ const { createWindowsBackend, windowsAvailable } = await import('./backends/windows.js');
83
+ if (await windowsAvailable())
84
+ return createWindowsBackend();
85
+ }
86
+ const { createFileBackend } = await import('./backends/file.js');
87
+ return createFileBackend();
88
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Credential priming cache.
3
+ *
4
+ * `loadConfig()` runs synchronously, but every OS keychain backend is
5
+ * async (subprocess-based). We bridge the two by priming credentials
6
+ * once per command, early in the `preAction` hook, and keeping the
7
+ * result in a tiny in-process cache keyed by profile name.
8
+ *
9
+ * After priming, sync callers can consult `getPrimedCredentials()` to
10
+ * pick up keychain-stored token/secret without any await.
11
+ *
12
+ * This module intentionally swallows errors — a flaky keychain
13
+ * probe must never block the CLI from running. When the probe fails
14
+ * we behave as "nothing primed" and the existing file path is used.
15
+ */
16
+ import { selectCredentialStore } from './keychain.js';
17
+ let cache = null;
18
+ /**
19
+ * Look up the given profile in the active credential store and cache
20
+ * the result. Safe to call multiple times — subsequent calls with the
21
+ * same profile short-circuit against the cache. Swallows all errors.
22
+ */
23
+ export async function primeCredentials(profile) {
24
+ if (cache?.profile === profile)
25
+ return;
26
+ try {
27
+ const store = await selectCredentialStore();
28
+ const creds = await store.get(profile);
29
+ cache = { profile, creds };
30
+ }
31
+ catch {
32
+ cache = { profile, creds: null };
33
+ }
34
+ }
35
+ /**
36
+ * Sync accessor for code paths that cannot be made async. Returns
37
+ * null when the cache is empty or keyed against a different profile,
38
+ * so existing file-based fallback stays the authoritative source.
39
+ */
40
+ export function getPrimedCredentials(profile) {
41
+ if (!cache)
42
+ return null;
43
+ if (cache.profile !== profile)
44
+ return null;
45
+ return cache.creds;
46
+ }
47
+ /**
48
+ * Test helper. Not used by production code.
49
+ */
50
+ export function __resetPrimedCredentials() {
51
+ cache = null;
52
+ }
@@ -10,9 +10,6 @@
10
10
  * - CommandSpec.safetyTier: explicit action safety classification. See
11
11
  * SafetyTier for the 5-tier enum. Built-in entries set this on the
12
12
  * destructive tier; other tiers are derived (see deriveSafetyTier).
13
- * - CommandSpec.destructive (deprecated, v3.0 removal): legacy boolean
14
- * that maps to safetyTier === 'destructive'. Still accepted in
15
- * ~/.switchbot/catalog.json overlays and derived into safetyTier.
16
13
  * - DeviceCatalogEntry.role: functional grouping for filter/search
17
14
  * ("all lighting", "all security"). Does not affect API behavior.
18
15
  * - DeviceCatalogEntry.readOnly: the device has no control commands; it
@@ -629,25 +626,22 @@ export function findCatalogEntry(query) {
629
626
  *
630
627
  * The inference order is:
631
628
  * 1. Explicit `spec.safetyTier`.
632
- * 2. Legacy `spec.destructive: true` → `'destructive'` (overlay compat).
633
- * 3. IR context (customize command OR entry.category === 'ir')
629
+ * 2. IR context (customize command OR entry.category === 'ir')
634
630
  * → `'ir-fire-forget'`.
635
- * 4. Default → `'mutation'`.
631
+ * 3. Default → `'mutation'`.
636
632
  */
637
633
  export function deriveSafetyTier(spec, entry) {
638
634
  if (spec.safetyTier)
639
635
  return spec.safetyTier;
640
- if (spec.destructive)
641
- return 'destructive';
642
636
  if (spec.commandType === 'customize')
643
637
  return 'ir-fire-forget';
644
638
  if (entry?.category === 'ir')
645
639
  return 'ir-fire-forget';
646
640
  return 'mutation';
647
641
  }
648
- /** Read the safety reason for a command, with fallback to the legacy field. */
642
+ /** Read the safety reason for a command. */
649
643
  export function getCommandSafetyReason(spec) {
650
- return spec.safetyReason ?? spec.destructiveReason ?? null;
644
+ return spec.safetyReason ?? null;
651
645
  }
652
646
  /**
653
647
  * Pick up to 3 non-destructive, idempotent commands an agent can safely invoke
package/dist/index.js CHANGED
@@ -23,6 +23,14 @@ import { registerHistoryCommand } from './commands/history.js';
23
23
  import { registerPlanCommand } from './commands/plan.js';
24
24
  import { registerCapabilitiesCommand } from './commands/capabilities.js';
25
25
  import { registerAgentBootstrapCommand } from './commands/agent-bootstrap.js';
26
+ import { registerPolicyCommand } from './commands/policy.js';
27
+ import { registerRulesCommand } from './commands/rules.js';
28
+ import { registerAuthCommand } from './commands/auth.js';
29
+ import { registerInstallCommand } from './commands/install.js';
30
+ import { registerUninstallCommand } from './commands/uninstall.js';
31
+ import { registerStatusSyncCommand } from './commands/status-sync.js';
32
+ import { primeCredentials } from './credentials/prime.js';
33
+ import { getActiveProfile } from './lib/request-context.js';
26
34
  const require = createRequire(import.meta.url);
27
35
  const { version: pkgVersion } = require('../package.json');
28
36
  // Early initialization: check for --no-color flag or NO_COLOR env var and disable chalk.
@@ -41,7 +49,7 @@ if (isJsonMode()) {
41
49
  const TOP_LEVEL_COMMANDS = [
42
50
  'config', 'devices', 'scenes', 'webhook', 'completion', 'mcp',
43
51
  'quota', 'catalog', 'cache', 'events', 'doctor', 'schema',
44
- 'history', 'plan', 'capabilities', 'agent-bootstrap',
52
+ 'history', 'plan', 'capabilities', 'agent-bootstrap', 'install', 'uninstall', 'status-sync',
45
53
  ];
46
54
  const cacheModeArg = (value) => {
47
55
  if (value.startsWith('-')) {
@@ -94,6 +102,19 @@ registerHistoryCommand(program);
94
102
  registerPlanCommand(program);
95
103
  registerCapabilitiesCommand(program);
96
104
  registerAgentBootstrapCommand(program);
105
+ registerPolicyCommand(program);
106
+ registerRulesCommand(program);
107
+ registerAuthCommand(program);
108
+ registerInstallCommand(program);
109
+ registerUninstallCommand(program);
110
+ registerStatusSyncCommand(program);
111
+ // Prime keychain-stored credentials before any command runs. This is a
112
+ // best-effort probe: failures are silently swallowed inside primeCredentials,
113
+ // so the existing file-based path remains the safety net. We probe once per
114
+ // invocation (even for --help and --version, which is harmless).
115
+ program.hook('preAction', async () => {
116
+ await primeCredentials(getActiveProfile() ?? 'default');
117
+ });
97
118
  program.addHelpText('after', `
98
119
  Credentials:
99
120
  Provide SwitchBot API v1.1 credentials via either:
@@ -122,6 +143,7 @@ Examples:
122
143
  $ switchbot devices command <deviceId> turnOn --dry-run
123
144
  $ switchbot scenes execute <sceneId> --verbose
124
145
  $ switchbot webhook setup https://your.host/hook
146
+ $ switchbot status-sync start --openclaw-model home-agent
125
147
 
126
148
  Discovery:
127
149
  Don't know a device ID / what it supports?