@switchbot/openapi-cli 3.1.0 → 3.2.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 (113) hide show
  1. package/README.md +34 -42
  2. package/dist/index.js +56945 -169
  3. package/dist/policy/schema/v0.2.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/api/client.js +0 -235
  6. package/dist/auth.js +0 -20
  7. package/dist/commands/agent-bootstrap.js +0 -182
  8. package/dist/commands/auth.js +0 -354
  9. package/dist/commands/batch.js +0 -413
  10. package/dist/commands/cache.js +0 -126
  11. package/dist/commands/capabilities.js +0 -385
  12. package/dist/commands/catalog.js +0 -359
  13. package/dist/commands/completion.js +0 -385
  14. package/dist/commands/config.js +0 -376
  15. package/dist/commands/daemon.js +0 -367
  16. package/dist/commands/device-meta.js +0 -159
  17. package/dist/commands/devices.js +0 -948
  18. package/dist/commands/doctor.js +0 -1015
  19. package/dist/commands/events.js +0 -563
  20. package/dist/commands/expand.js +0 -130
  21. package/dist/commands/explain.js +0 -139
  22. package/dist/commands/health.js +0 -113
  23. package/dist/commands/history.js +0 -320
  24. package/dist/commands/identity.js +0 -59
  25. package/dist/commands/install.js +0 -246
  26. package/dist/commands/mcp.js +0 -2017
  27. package/dist/commands/plan.js +0 -653
  28. package/dist/commands/policy.js +0 -586
  29. package/dist/commands/quota.js +0 -78
  30. package/dist/commands/rules.js +0 -875
  31. package/dist/commands/scenes.js +0 -264
  32. package/dist/commands/schema.js +0 -177
  33. package/dist/commands/status-sync.js +0 -131
  34. package/dist/commands/uninstall.js +0 -237
  35. package/dist/commands/upgrade-check.js +0 -88
  36. package/dist/commands/watch.js +0 -194
  37. package/dist/commands/webhook.js +0 -182
  38. package/dist/config.js +0 -258
  39. package/dist/credentials/backends/file.js +0 -101
  40. package/dist/credentials/backends/linux.js +0 -129
  41. package/dist/credentials/backends/macos.js +0 -129
  42. package/dist/credentials/backends/windows.js +0 -215
  43. package/dist/credentials/keychain.js +0 -88
  44. package/dist/credentials/prime.js +0 -52
  45. package/dist/devices/cache.js +0 -293
  46. package/dist/devices/catalog.js +0 -767
  47. package/dist/devices/device-meta.js +0 -56
  48. package/dist/devices/history-agg.js +0 -138
  49. package/dist/devices/history-query.js +0 -181
  50. package/dist/devices/param-validator.js +0 -433
  51. package/dist/devices/resources.js +0 -270
  52. package/dist/install/default-steps.js +0 -257
  53. package/dist/install/preflight.js +0 -212
  54. package/dist/install/steps.js +0 -67
  55. package/dist/lib/command-keywords.js +0 -17
  56. package/dist/lib/daemon-state.js +0 -46
  57. package/dist/lib/destructive-mode.js +0 -12
  58. package/dist/lib/devices.js +0 -382
  59. package/dist/lib/idempotency.js +0 -106
  60. package/dist/lib/plan-store.js +0 -68
  61. package/dist/lib/request-context.js +0 -12
  62. package/dist/lib/scenes.js +0 -10
  63. package/dist/logger.js +0 -16
  64. package/dist/mcp/device-history.js +0 -145
  65. package/dist/mcp/events-subscription.js +0 -213
  66. package/dist/mqtt/client.js +0 -180
  67. package/dist/mqtt/credential.js +0 -30
  68. package/dist/policy/add-rule.js +0 -124
  69. package/dist/policy/diff.js +0 -91
  70. package/dist/policy/format.js +0 -57
  71. package/dist/policy/load.js +0 -61
  72. package/dist/policy/migrate.js +0 -67
  73. package/dist/policy/schema.js +0 -18
  74. package/dist/policy/validate.js +0 -262
  75. package/dist/rules/action.js +0 -205
  76. package/dist/rules/audit-query.js +0 -89
  77. package/dist/rules/conflict-analyzer.js +0 -203
  78. package/dist/rules/cron-scheduler.js +0 -186
  79. package/dist/rules/destructive.js +0 -52
  80. package/dist/rules/engine.js +0 -757
  81. package/dist/rules/matcher.js +0 -230
  82. package/dist/rules/pid-file.js +0 -95
  83. package/dist/rules/quiet-hours.js +0 -45
  84. package/dist/rules/suggest.js +0 -95
  85. package/dist/rules/throttle.js +0 -116
  86. package/dist/rules/types.js +0 -34
  87. package/dist/rules/webhook-listener.js +0 -223
  88. package/dist/rules/webhook-token.js +0 -90
  89. package/dist/schema/field-aliases.js +0 -131
  90. package/dist/sinks/dispatcher.js +0 -12
  91. package/dist/sinks/file.js +0 -19
  92. package/dist/sinks/format.js +0 -56
  93. package/dist/sinks/homeassistant.js +0 -44
  94. package/dist/sinks/openclaw.js +0 -33
  95. package/dist/sinks/stdout.js +0 -5
  96. package/dist/sinks/telegram.js +0 -28
  97. package/dist/sinks/types.js +0 -1
  98. package/dist/sinks/webhook.js +0 -22
  99. package/dist/status-sync/manager.js +0 -268
  100. package/dist/utils/arg-parsers.js +0 -66
  101. package/dist/utils/audit.js +0 -117
  102. package/dist/utils/filter.js +0 -189
  103. package/dist/utils/flags.js +0 -186
  104. package/dist/utils/format.js +0 -117
  105. package/dist/utils/health.js +0 -101
  106. package/dist/utils/help-json.js +0 -54
  107. package/dist/utils/name-resolver.js +0 -137
  108. package/dist/utils/output.js +0 -404
  109. package/dist/utils/quota.js +0 -227
  110. package/dist/utils/redact.js +0 -68
  111. package/dist/utils/retry.js +0 -140
  112. package/dist/utils/string.js +0 -22
  113. package/dist/version.js +0 -4
@@ -18,7 +18,7 @@
18
18
  "description": "Unchanged from v0.1.",
19
19
  "additionalProperties": {
20
20
  "type": "string",
21
- "pattern": "^[A-Z0-9]{2,}-[A-Z0-9-]+$"
21
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$"
22
22
  }
23
23
  },
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "SwitchBot smart home CLI — control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
5
5
  "keywords": [
6
6
  "switchbot",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsc && node scripts/copy-assets.mjs",
40
- "build:prod": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
40
+ "build:prod": "node scripts/bundle.mjs && node scripts/copy-assets.mjs",
41
41
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
42
42
  "dev": "tsx src/index.ts",
43
43
  "lint:md": "markdownlint \"**/*.md\"",
@@ -69,6 +69,7 @@
69
69
  "@types/node": "^22.10.7",
70
70
  "@types/uuid": "^10.0.0",
71
71
  "@vitest/coverage-v8": "^2.1.9",
72
+ "esbuild": "^0.28.0",
72
73
  "markdownlint-cli": "^0.48.0",
73
74
  "tsx": "^4.19.2",
74
75
  "typescript": "^5.7.3",
@@ -1,235 +0,0 @@
1
- import axios from 'axios';
2
- import chalk from 'chalk';
3
- import { buildAuthHeaders } from '../auth.js';
4
- import { loadConfig } from '../config.js';
5
- import { isVerbose, isDryRun, getTimeout, getRetryOn429, getRetryOn5xx, getBackoffStrategy, isQuotaDisabled, } from '../utils/flags.js';
6
- import { nextRetryDelayMs, sleep, CircuitBreaker, CircuitOpenError } from '../utils/retry.js';
7
- import { recordRequest, checkDailyCap } from '../utils/quota.js';
8
- import { readProfileMeta } from '../config.js';
9
- import { getActiveProfile } from '../lib/request-context.js';
10
- import { redactHeaders, warnOnceIfUnsafe } from '../utils/redact.js';
11
- class DailyCapExceededError extends Error {
12
- cap;
13
- total;
14
- profile;
15
- constructor(cap, total, profile) {
16
- super(`Local daily cap reached: ${total}/${cap} SwitchBot API calls used today${profile ? ` for profile "${profile}"` : ''}. ` +
17
- `Raise with: switchbot ${profile ? `--profile ${profile} ` : ''}config set-token --daily-cap <N>`);
18
- this.cap = cap;
19
- this.total = total;
20
- this.profile = profile;
21
- this.name = 'DailyCapExceededError';
22
- }
23
- }
24
- const API_ERROR_MESSAGES = {
25
- 151: 'Device type does not support this command',
26
- 152: 'Device ID does not exist',
27
- 160: 'This device does not support this command',
28
- 161: 'Device offline (check Wi-Fi / Bluetooth connection)',
29
- 171: 'Hub device offline (BLE devices require a Hub to communicate)',
30
- 190: 'Device internal error — often an invalid deviceId, unsupported parameter, or device busy',
31
- };
32
- /** Thrown by the request interceptor when --dry-run intercepts a mutating call. */
33
- export class DryRunSignal extends Error {
34
- method;
35
- url;
36
- constructor(method, url) {
37
- super('dry-run');
38
- this.method = method;
39
- this.url = url;
40
- this.name = 'DryRunSignal';
41
- }
42
- }
43
- /**
44
- * Module-level circuit breaker for the SwitchBot API. Shared across all
45
- * client instances in the process. Opens after 5 consecutive 5xx / network
46
- * errors; resets to half-open after 60 s.
47
- * Exported for health-check inspection.
48
- */
49
- export const apiCircuitBreaker = new CircuitBreaker('switchbot-api', {
50
- failureThreshold: 5,
51
- resetTimeoutMs: 60_000,
52
- });
53
- export { CircuitOpenError };
54
- export function createClient() {
55
- const { token, secret } = loadConfig();
56
- const verbose = isVerbose();
57
- const dryRun = isDryRun();
58
- const maxRetries = getRetryOn429();
59
- const max5xxRetries = getRetryOn5xx();
60
- const backoff = getBackoffStrategy();
61
- const quotaEnabled = !isQuotaDisabled();
62
- const profile = getActiveProfile();
63
- const profileMeta = readProfileMeta(profile);
64
- const dailyCap = profileMeta?.limits?.dailyCap;
65
- const client = axios.create({
66
- baseURL: 'https://api.switch-bot.com',
67
- timeout: getTimeout(),
68
- });
69
- // Inject auth headers; optionally log the request; short-circuit on --dry-run.
70
- client.interceptors.request.use((config) => {
71
- // Circuit breaker check — fail fast when the API is consistently down.
72
- apiCircuitBreaker.checkAndAllow();
73
- // Pre-flight cap check: refuse the call before it touches the network.
74
- if (dailyCap) {
75
- const check = checkDailyCap(dailyCap);
76
- if (check.over) {
77
- throw new DailyCapExceededError(dailyCap, check.total, profile);
78
- }
79
- }
80
- const authHeaders = buildAuthHeaders(token, secret);
81
- Object.assign(config.headers, authHeaders);
82
- const method = (config.method ?? 'get').toUpperCase();
83
- const url = `${config.baseURL ?? ''}${config.url ?? ''}`;
84
- if (verbose) {
85
- warnOnceIfUnsafe();
86
- process.stderr.write(chalk.grey(`[verbose] ${method} ${url}\n`));
87
- const { safe, redactedCount } = redactHeaders(config.headers);
88
- process.stderr.write(chalk.grey(`[verbose] headers: ${JSON.stringify(safe)}\n`));
89
- if (redactedCount > 0) {
90
- process.stderr.write(chalk.grey(`[verbose] 🔒 ${redactedCount} sensitive header(s) redacted.\n`));
91
- }
92
- if (config.data !== undefined) {
93
- process.stderr.write(chalk.grey(`[verbose] body: ${JSON.stringify(config.data)}\n`));
94
- }
95
- }
96
- if (dryRun && method !== 'GET') {
97
- process.stderr.write(chalk.yellow(`[dry-run] Would ${method} ${url}\n`));
98
- if (config.data !== undefined) {
99
- process.stderr.write(chalk.yellow(`[dry-run] body: ${JSON.stringify(config.data)}\n`));
100
- }
101
- throw new DryRunSignal(method, url);
102
- }
103
- // P8: record the quota attempt BEFORE the request is dispatched so
104
- // failures (timeouts / DNS errors / 5xx / aborted) also count. Only
105
- // pre-flight refusals (daily-cap, --dry-run) above skip recording
106
- // since they never touch the network. Retries re-enter this
107
- // interceptor and record again, which matches the SwitchBot API
108
- // billing model (every dispatched HTTP request consumes quota).
109
- if (quotaEnabled) {
110
- recordRequest(method, url);
111
- }
112
- return config;
113
- });
114
- // Handle API-level errors (HTTP 200 but statusCode !== 100)
115
- client.interceptors.response.use((response) => {
116
- if (verbose) {
117
- process.stderr.write(chalk.grey(`[verbose] ${response.status} ${response.statusText}\n`));
118
- }
119
- const data = response.data;
120
- if (data.statusCode !== undefined && data.statusCode !== 100) {
121
- const msg = API_ERROR_MESSAGES[data.statusCode] ??
122
- data.message ??
123
- `API error code: ${data.statusCode}`;
124
- throw new ApiError(msg, data.statusCode);
125
- }
126
- // Successful HTTP response — record for circuit breaker.
127
- apiCircuitBreaker.recordSuccess();
128
- return response;
129
- }, (error) => {
130
- if (error instanceof DryRunSignal)
131
- throw error;
132
- if (axios.isAxiosError(error)) {
133
- const config = error.config;
134
- const method = (config?.method ?? 'get').toUpperCase();
135
- const isIdempotentRead = method === 'GET';
136
- if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
137
- // Retry idempotent GETs on timeout up to `max5xxRetries` times.
138
- if (isIdempotentRead && config && max5xxRetries > 0) {
139
- const attempt = config.__retryCount ?? 0;
140
- if (attempt < max5xxRetries) {
141
- config.__retryCount = attempt + 1;
142
- const delay = nextRetryDelayMs(attempt, backoff, undefined);
143
- if (verbose) {
144
- process.stderr.write(chalk.grey(`[verbose] timeout — retry ${attempt + 1}/${max5xxRetries} in ${delay}ms\n`));
145
- }
146
- return sleep(delay).then(() => client.request(config));
147
- }
148
- }
149
- // Network-level failure — record for circuit breaker.
150
- apiCircuitBreaker.recordFailure();
151
- throw new ApiError(`Request timed out after ${getTimeout()}ms (override with --timeout <ms>)`, 0, { transient: true, retryable: isIdempotentRead });
152
- }
153
- const status = error.response?.status;
154
- // 429 → transparent retry with Retry-After / exponential backoff.
155
- // Skipped when: no config (shouldn't happen for real axios errors),
156
- // retries disabled, or we've already used our budget.
157
- if (status === 429 && config && maxRetries > 0) {
158
- const attempt = config.__retryCount ?? 0;
159
- if (attempt < maxRetries) {
160
- config.__retryCount = attempt + 1;
161
- const delay = nextRetryDelayMs(attempt, backoff, error.response?.headers?.['retry-after']);
162
- if (verbose) {
163
- process.stderr.write(chalk.grey(`[verbose] 429 received — retry ${attempt + 1}/${maxRetries} in ${delay}ms\n`));
164
- }
165
- return sleep(delay).then(() => client.request(config));
166
- }
167
- }
168
- // 502/503/504 on idempotent GETs → transparent retry. Mutating calls
169
- // never auto-retry; use --idempotency-key for safe POST retries.
170
- if (isIdempotentRead &&
171
- status !== undefined &&
172
- (status === 502 || status === 503 || status === 504) &&
173
- config &&
174
- max5xxRetries > 0) {
175
- const attempt = config.__retryCount ?? 0;
176
- if (attempt < max5xxRetries) {
177
- config.__retryCount = attempt + 1;
178
- const delay = nextRetryDelayMs(attempt, backoff, error.response?.headers?.['retry-after']);
179
- if (verbose) {
180
- process.stderr.write(chalk.grey(`[verbose] ${status} received — retry ${attempt + 1}/${max5xxRetries} in ${delay}ms\n`));
181
- }
182
- return sleep(delay).then(() => client.request(config));
183
- }
184
- }
185
- // Record 5xx and network errors for circuit breaker. 4xx errors are
186
- // expected business responses — don't count toward circuit threshold.
187
- if (status === undefined || status >= 500) {
188
- apiCircuitBreaker.recordFailure();
189
- }
190
- // P8: quota already recorded in the request interceptor before
191
- // dispatch — no extra bookkeeping needed here on the error path.
192
- // Timeouts, DNS failures, 5xx, and exhausted retries all counted
193
- // when the attempt was first made.
194
- if (status === 401) {
195
- throw new ApiError('Authentication failed: invalid token or daily 10,000-request quota exceeded', 401, {
196
- transient: false,
197
- retryable: false,
198
- hint: 'Run `switchbot config set-token <token> <secret>` to re-enter credentials, or `switchbot quota status` to check today\'s local count.'
199
- });
200
- }
201
- if (status === 429) {
202
- const retryAfter = error.response?.headers?.['retry-after'];
203
- const retryAfterMs = nextRetryDelayMs(maxRetries - 1, backoff, retryAfter);
204
- throw new ApiError('Request rate too high: daily 10,000-request quota exceeded (retries exhausted)', 429, {
205
- retryable: true,
206
- transient: true,
207
- retryAfterMs,
208
- hint: 'Use `switchbot quota status` to see today\'s usage; raise `--retry-on-429 <n>` for more retries.'
209
- });
210
- }
211
- throw new ApiError(`HTTP ${status ?? '?'}: ${error.message}`, status ?? 0, {
212
- retryable: status !== undefined && status >= 500,
213
- transient: status !== undefined && (status >= 500 || status === 0) // 5xx, 0 = connection error
214
- });
215
- }
216
- throw error;
217
- });
218
- return client;
219
- }
220
- export class ApiError extends Error {
221
- code;
222
- retryable;
223
- hint;
224
- retryAfterMs;
225
- transient;
226
- constructor(message, code, meta = {}) {
227
- super(message);
228
- this.code = code;
229
- this.name = 'ApiError';
230
- this.retryable = meta.retryable ?? false;
231
- this.hint = meta.hint;
232
- this.retryAfterMs = meta.retryAfterMs;
233
- this.transient = meta.transient ?? false;
234
- }
235
- }
package/dist/auth.js DELETED
@@ -1,20 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import { v4 as uuidv4 } from 'uuid';
3
- export function buildAuthHeaders(token, secret) {
4
- const t = String(Date.now()); // 13-digit millisecond timestamp
5
- const nonce = uuidv4(); // unique per request
6
- const data = token + t + nonce;
7
- const sign = crypto
8
- .createHmac('sha256', secret)
9
- .update(data)
10
- .digest('base64')
11
- .toUpperCase(); // API requires uppercase
12
- return {
13
- Authorization: token,
14
- t,
15
- sign,
16
- nonce,
17
- src: 'OpenClaw',
18
- 'Content-Type': 'application/json',
19
- };
20
- }
@@ -1,182 +0,0 @@
1
- import { printJson } from '../utils/output.js';
2
- import { loadCache } from '../devices/cache.js';
3
- import { getEffectiveCatalog, deriveSafetyTier, CATALOG_SCHEMA_VERSION, } from '../devices/catalog.js';
4
- import { readProfileMeta } from '../config.js';
5
- import { todayUsage, DAILY_QUOTA } from '../utils/quota.js';
6
- import { ALL_STRATEGIES } from '../utils/name-resolver.js';
7
- import { IDENTITY } from './identity.js';
8
- import { resolvePolicyPath, loadPolicyFile, PolicyFileNotFoundError, PolicyYamlParseError, } from '../policy/load.js';
9
- import { validateLoadedPolicy } from '../policy/validate.js';
10
- import { selectCredentialStore } from '../credentials/keychain.js';
11
- import { createRequire } from 'node:module';
12
- const require = createRequire(import.meta.url);
13
- const { version: pkgVersion } = require('../../package.json');
14
- /**
15
- * Schema version of the agent-bootstrap payload. Must stay in lockstep
16
- * with the catalog schema — bootstrap consumers (AI agents) reason about
17
- * catalog-derived fields (safetyTier, destructive flag), so a drift
18
- * between the two would silently break their assumptions. `doctor`
19
- * fails the `catalog-schema` check when these differ.
20
- */
21
- export const AGENT_BOOTSTRAP_SCHEMA_VERSION = CATALOG_SCHEMA_VERSION;
22
- const SAFETY_TIERS = {
23
- read: 'No state mutation; safe to call freely.',
24
- action: 'Mutates device/cloud state but reversible (turnOn, setColor).',
25
- destructive: 'Hard to reverse / physical-world side effects (unlock). Requires confirmation.',
26
- };
27
- const QUICK_REFERENCE = {
28
- discovery: ['devices list', 'devices describe <id>', 'devices status <id>'],
29
- action: ['devices command <id> <cmd>', 'devices command --name <q> <cmd>', 'scenes execute <id>'],
30
- safety: ['--dry-run', '--idempotency-key <k>', '--audit-log', '--no-quota'],
31
- observability: ['doctor --json', 'quota status', 'cache status', 'events mqtt-tail'],
32
- history: ['history range <id> --since 7d', 'history stats <id>'],
33
- meta: ['devices meta set <id> --alias <name>', 'devices meta list', 'devices meta get <id>'],
34
- policy: ['policy validate', 'policy new', 'policy migrate'],
35
- auth: ['auth keychain describe', 'auth keychain migrate', 'auth keychain get'],
36
- };
37
- function readPolicyStatus() {
38
- // Lightweight read — used by the bootstrap payload so agents know whether
39
- // a policy file exists and is healthy without shelling out to
40
- // `switchbot policy validate`. Parallel to `checkPolicy` in doctor but
41
- // returns a more compact shape (no first-error drill-down; agents who
42
- // want that run the dedicated command).
43
- const policyPath = resolvePolicyPath();
44
- try {
45
- const loaded = loadPolicyFile(policyPath);
46
- const result = validateLoadedPolicy(loaded);
47
- return {
48
- present: true,
49
- valid: result.valid,
50
- path: policyPath,
51
- schemaVersion: result.schemaVersion,
52
- errorCount: result.valid ? 0 : result.errors.length,
53
- };
54
- }
55
- catch (err) {
56
- if (err instanceof PolicyFileNotFoundError) {
57
- return { present: false, valid: null, path: policyPath };
58
- }
59
- if (err instanceof PolicyYamlParseError) {
60
- return { present: true, valid: false, path: policyPath, errorCount: 1 };
61
- }
62
- return { present: false, valid: null, path: policyPath };
63
- }
64
- }
65
- async function readCredentialsBackend() {
66
- try {
67
- const store = await selectCredentialStore();
68
- const desc = store.describe();
69
- return { name: store.name, label: desc.backend, writable: desc.writable };
70
- }
71
- catch {
72
- return { name: 'file', label: 'File (~/.switchbot/config.json)', writable: true };
73
- }
74
- }
75
- export function registerAgentBootstrapCommand(program) {
76
- program
77
- .command('agent-bootstrap')
78
- .description('Print a compact, aggregate JSON snapshot for agent onboarding — combines identity, cached devices, catalog summary, quota usage, and profile in a single call. Offline-safe; does not hit the API.')
79
- .option('--compact', 'Emit an even smaller payload by dropping catalog descriptions and non-essential fields (target: <20 KB).')
80
- .addHelpText('after', `
81
- Output is always JSON (this command ignores --format). It is a one-shot
82
- orientation document for an agent/LLM to understand what's available without
83
- spending quota. It reads from local cache (devices + quota + profile) and the
84
- bundled catalog — no network calls.
85
-
86
- For fresher device state, have the agent follow up with:
87
- $ switchbot devices list --json # refreshes cache
88
- $ switchbot devices status <id> --json
89
-
90
- Examples:
91
- $ switchbot agent-bootstrap --compact | wc -c # fit in agent context window
92
- $ switchbot agent-bootstrap | jq '.devices | length'
93
- $ switchbot agent-bootstrap --compact | jq '.quickReference'
94
- `)
95
- .action(async (opts) => {
96
- const compact = Boolean(opts.compact);
97
- const cache = loadCache();
98
- const catalog = getEffectiveCatalog();
99
- const usage = todayUsage();
100
- const meta = readProfileMeta(undefined);
101
- const credentialsBackend = await readCredentialsBackend();
102
- const cachedDevices = cache
103
- ? Object.entries(cache.devices).map(([id, d]) => ({
104
- deviceId: id,
105
- type: d.type,
106
- name: d.name,
107
- category: d.category,
108
- roomName: d.roomName ?? null,
109
- }))
110
- : [];
111
- const usedTypes = new Set(cachedDevices.map((d) => d.type.toLowerCase()));
112
- const relevantCatalog = cachedDevices.length > 0
113
- ? catalog.filter((e) => usedTypes.has(e.type.toLowerCase()) ||
114
- (e.aliases ?? []).some((a) => usedTypes.has(a.toLowerCase())))
115
- : catalog;
116
- const catalogTypes = relevantCatalog.map((e) => {
117
- if (compact) {
118
- return {
119
- type: e.type,
120
- category: e.category,
121
- role: e.role ?? null,
122
- readOnly: e.readOnly ?? false,
123
- commands: e.commands.map((c) => c.command),
124
- statusFields: e.statusFields ?? [],
125
- };
126
- }
127
- return {
128
- type: e.type,
129
- category: e.category,
130
- role: e.role ?? null,
131
- readOnly: e.readOnly ?? false,
132
- commands: e.commands.map((c) => {
133
- const tier = deriveSafetyTier(c, e);
134
- return {
135
- command: c.command,
136
- parameter: c.parameter,
137
- safetyTier: tier,
138
- idempotent: Boolean(c.idempotent),
139
- };
140
- }),
141
- statusFields: e.statusFields ?? [],
142
- };
143
- });
144
- const payload = {
145
- schemaVersion: AGENT_BOOTSTRAP_SCHEMA_VERSION,
146
- generatedAt: new Date().toISOString(),
147
- cliVersion: pkgVersion,
148
- identity: IDENTITY,
149
- quickReference: QUICK_REFERENCE,
150
- safetyTiers: SAFETY_TIERS,
151
- nameStrategies: [...ALL_STRATEGIES],
152
- profile: meta
153
- ? {
154
- label: meta.label ?? null,
155
- description: meta.description ?? null,
156
- dailyCap: meta.limits?.dailyCap ?? null,
157
- defaultFlags: meta.defaults?.flags ?? null,
158
- }
159
- : null,
160
- quota: {
161
- date: usage.date,
162
- total: usage.total,
163
- remaining: usage.remaining,
164
- dailyLimit: DAILY_QUOTA,
165
- },
166
- policyStatus: readPolicyStatus(),
167
- credentialsBackend,
168
- devices: cachedDevices,
169
- catalog: {
170
- scope: cachedDevices.length > 0 ? 'used' : 'all',
171
- types: catalogTypes,
172
- },
173
- // hints: empty array means no hints to report; always emitted, never null.
174
- // An empty array signals "nothing to act on" — agents should not treat
175
- // it as a disabled or missing field.
176
- hints: cachedDevices.length === 0
177
- ? ['Run `switchbot devices list` once to populate the device cache for richer bootstrap output.']
178
- : [],
179
- };
180
- printJson(payload);
181
- });
182
- }