@switchbot/openapi-cli 1.3.2 → 2.0.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 (122) hide show
  1. package/README.md +30 -1
  2. package/dist/api/client.js +22 -5
  3. package/dist/auth.js +0 -1
  4. package/dist/commands/batch.js +12 -6
  5. package/dist/commands/cache.js +0 -1
  6. package/dist/commands/capabilities.js +5 -3
  7. package/dist/commands/catalog.js +0 -1
  8. package/dist/commands/completion.js +0 -1
  9. package/dist/commands/config.js +0 -1
  10. package/dist/commands/device-meta.js +0 -1
  11. package/dist/commands/devices.js +2 -2
  12. package/dist/commands/doctor.js +0 -1
  13. package/dist/commands/events.js +0 -1
  14. package/dist/commands/expand.js +0 -1
  15. package/dist/commands/explain.js +0 -1
  16. package/dist/commands/history.js +0 -1
  17. package/dist/commands/mcp.js +334 -18
  18. package/dist/commands/plan.js +0 -1
  19. package/dist/commands/quota.js +0 -1
  20. package/dist/commands/scenes.js +0 -1
  21. package/dist/commands/schema.js +2 -9
  22. package/dist/commands/watch.js +0 -1
  23. package/dist/commands/webhook.js +0 -1
  24. package/dist/config.js +5 -5
  25. package/dist/devices/cache.js +0 -1
  26. package/dist/devices/catalog.js +0 -1
  27. package/dist/devices/device-meta.js +0 -1
  28. package/dist/index.js +0 -1
  29. package/dist/lib/devices.js +22 -18
  30. package/dist/lib/idempotency.js +72 -0
  31. package/dist/lib/request-context.js +12 -0
  32. package/dist/lib/scenes.js +0 -1
  33. package/dist/logger.js +16 -0
  34. package/dist/mcp/events-subscription.js +210 -0
  35. package/dist/mqtt/client.js +184 -0
  36. package/dist/mqtt/credential.js +12 -0
  37. package/dist/utils/audit.js +0 -1
  38. package/dist/utils/filter.js +0 -1
  39. package/dist/utils/flags.js +0 -1
  40. package/dist/utils/format.js +0 -1
  41. package/dist/utils/name-resolver.js +0 -1
  42. package/dist/utils/output.js +30 -6
  43. package/dist/utils/quota.js +0 -1
  44. package/dist/utils/retry.js +0 -1
  45. package/dist/utils/string.js +0 -1
  46. package/package.json +6 -2
  47. package/dist/api/client.d.ts +0 -18
  48. package/dist/api/client.js.map +0 -1
  49. package/dist/auth.d.ts +0 -1
  50. package/dist/auth.js.map +0 -1
  51. package/dist/commands/batch.d.ts +0 -2
  52. package/dist/commands/batch.js.map +0 -1
  53. package/dist/commands/cache.d.ts +0 -2
  54. package/dist/commands/cache.js.map +0 -1
  55. package/dist/commands/capabilities.d.ts +0 -2
  56. package/dist/commands/capabilities.js.map +0 -1
  57. package/dist/commands/catalog.d.ts +0 -2
  58. package/dist/commands/catalog.js.map +0 -1
  59. package/dist/commands/completion.d.ts +0 -2
  60. package/dist/commands/completion.js.map +0 -1
  61. package/dist/commands/config.d.ts +0 -2
  62. package/dist/commands/config.js.map +0 -1
  63. package/dist/commands/device-meta.d.ts +0 -2
  64. package/dist/commands/device-meta.js.map +0 -1
  65. package/dist/commands/devices.d.ts +0 -2
  66. package/dist/commands/devices.js.map +0 -1
  67. package/dist/commands/doctor.d.ts +0 -2
  68. package/dist/commands/doctor.js.map +0 -1
  69. package/dist/commands/events.d.ts +0 -15
  70. package/dist/commands/events.js.map +0 -1
  71. package/dist/commands/expand.d.ts +0 -2
  72. package/dist/commands/expand.js.map +0 -1
  73. package/dist/commands/explain.d.ts +0 -2
  74. package/dist/commands/explain.js.map +0 -1
  75. package/dist/commands/history.d.ts +0 -2
  76. package/dist/commands/history.js.map +0 -1
  77. package/dist/commands/mcp.d.ts +0 -4
  78. package/dist/commands/mcp.js.map +0 -1
  79. package/dist/commands/plan.d.ts +0 -38
  80. package/dist/commands/plan.js.map +0 -1
  81. package/dist/commands/quota.d.ts +0 -2
  82. package/dist/commands/quota.js.map +0 -1
  83. package/dist/commands/scenes.d.ts +0 -2
  84. package/dist/commands/scenes.js.map +0 -1
  85. package/dist/commands/schema.d.ts +0 -2
  86. package/dist/commands/schema.js.map +0 -1
  87. package/dist/commands/watch.d.ts +0 -2
  88. package/dist/commands/watch.js.map +0 -1
  89. package/dist/commands/webhook.d.ts +0 -2
  90. package/dist/commands/webhook.js.map +0 -1
  91. package/dist/config.d.ts +0 -18
  92. package/dist/config.js.map +0 -1
  93. package/dist/devices/cache.d.ts +0 -79
  94. package/dist/devices/cache.js.map +0 -1
  95. package/dist/devices/catalog.d.ts +0 -70
  96. package/dist/devices/catalog.js.map +0 -1
  97. package/dist/devices/device-meta.d.ts +0 -15
  98. package/dist/devices/device-meta.js.map +0 -1
  99. package/dist/index.d.ts +0 -2
  100. package/dist/index.js.map +0 -1
  101. package/dist/lib/devices.d.ts +0 -144
  102. package/dist/lib/devices.js.map +0 -1
  103. package/dist/lib/scenes.d.ts +0 -7
  104. package/dist/lib/scenes.js.map +0 -1
  105. package/dist/utils/audit.d.ts +0 -13
  106. package/dist/utils/audit.js.map +0 -1
  107. package/dist/utils/filter.d.ts +0 -45
  108. package/dist/utils/filter.js.map +0 -1
  109. package/dist/utils/flags.d.ts +0 -52
  110. package/dist/utils/flags.js.map +0 -1
  111. package/dist/utils/format.d.ts +0 -9
  112. package/dist/utils/format.js.map +0 -1
  113. package/dist/utils/name-resolver.d.ts +0 -17
  114. package/dist/utils/name-resolver.js.map +0 -1
  115. package/dist/utils/output.d.ts +0 -23
  116. package/dist/utils/output.js.map +0 -1
  117. package/dist/utils/quota.d.ts +0 -50
  118. package/dist/utils/quota.js.map +0 -1
  119. package/dist/utils/retry.d.ts +0 -23
  120. package/dist/utils/retry.js.map +0 -1
  121. package/dist/utils/string.d.ts +0 -2
  122. package/dist/utils/string.js.map +0 -1
package/README.md CHANGED
@@ -11,6 +11,7 @@ List devices, query live status, send control commands, run scenes, and manage w
11
11
 
12
12
  - **npm package:** [`@switchbot/openapi-cli`](https://www.npmjs.com/package/@switchbot/openapi-cli)
13
13
  - **Source code:** [github.com/OpenWonderLabs/switchbot-openapi-cli](https://github.com/OpenWonderLabs/switchbot-openapi-cli)
14
+ - **Releases / changelog:** [GitHub Releases](https://github.com/OpenWonderLabs/switchbot-openapi-cli/releases)
14
15
  - **Issues / feature requests:** [GitHub Issues](https://github.com/OpenWonderLabs/switchbot-openapi-cli/issues)
15
16
 
16
17
  ---
@@ -251,6 +252,34 @@ Generic parameter shapes (which one applies is decided by the device — see the
251
252
 
252
253
  For the complete per-device command reference, see the [SwitchBot API docs](https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands).
253
254
 
255
+ #### `devices expand` — named flags for packed parameters
256
+
257
+ Some commands require a packed string like `"26,2,2,on"`. `devices expand` builds it from readable flags:
258
+
259
+ ```bash
260
+ # Air Conditioner — setAll
261
+ switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
262
+
263
+ # Curtain / Roller Shade — setPosition
264
+ switchbot devices expand <curtainId> setPosition --position 50 --mode silent
265
+
266
+ # Blind Tilt — setPosition
267
+ switchbot devices expand <blindId> setPosition --direction up --angle 50
268
+
269
+ # Relay Switch — setMode
270
+ switchbot devices expand <relayId> setMode --channel 1 --mode edge
271
+ ```
272
+
273
+ Run `switchbot devices expand <id> <command> --help` to see the available flags for any device command.
274
+
275
+ #### `devices explain` — plain-language command description
276
+
277
+ ```bash
278
+ switchbot devices explain <deviceId> <command> # e.g. "explain ABC123 setAll"
279
+ ```
280
+
281
+ Returns a human-readable description of what the command does and what each parameter means.
282
+
254
283
  ### `scenes` — run manual scenes
255
284
 
256
285
  ```bash
@@ -326,7 +355,7 @@ Output is a stream of JSON status objects (with `--json`) or a refreshed table.
326
355
  switchbot mcp serve
327
356
  ```
328
357
 
329
- Exposes 7 MCP tools: `list_devices`, `describe_device`, `get_device_status`, `send_command`, `list_scenes`, `run_scene`, `search_catalog`.
358
+ Exposes 8 MCP tools (`list_devices`, `describe_device`, `get_device_status`, `send_command`, `list_scenes`, `run_scene`, `search_catalog`, `account_overview`) plus a `switchbot://events` resource for real-time shadow updates.
330
359
  See [`docs/agent-guide.md`](./docs/agent-guide.md) for the full tool reference and safety rules (destructive-command guard).
331
360
 
332
361
  ### `cache` — inspect and clear local cache
@@ -79,7 +79,7 @@ export function createClient() {
79
79
  throw error;
80
80
  if (axios.isAxiosError(error)) {
81
81
  if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
82
- throw new ApiError(`Request timed out after ${getTimeout()}ms (override with --timeout <ms>)`, 0, { retryable: false });
82
+ throw new ApiError(`Request timed out after ${getTimeout()}ms (override with --timeout <ms>)`, 0, { transient: true, retryable: false });
83
83
  }
84
84
  const status = error.response?.status;
85
85
  const config = error.config;
@@ -105,12 +105,26 @@ export function createClient() {
105
105
  recordRequest(method, url);
106
106
  }
107
107
  if (status === 401) {
108
- throw new ApiError('Authentication failed: invalid token or daily 10,000-request quota exceeded', 401, { retryable: false, hint: 'Run `switchbot config set-token <token> <secret>` to re-enter credentials, or `switchbot quota status` to check today\'s local count.' });
108
+ throw new ApiError('Authentication failed: invalid token or daily 10,000-request quota exceeded', 401, {
109
+ transient: false,
110
+ retryable: false,
111
+ hint: 'Run `switchbot config set-token <token> <secret>` to re-enter credentials, or `switchbot quota status` to check today\'s local count.'
112
+ });
109
113
  }
110
114
  if (status === 429) {
111
- throw new ApiError('Request rate too high: daily 10,000-request quota exceeded (retries exhausted)', 429, { retryable: true, hint: 'Use `switchbot quota status` to see today\'s usage; raise `--retry-on-429 <n>` for more retries.' });
115
+ const retryAfter = error.response?.headers?.['retry-after'];
116
+ const retryAfterMs = nextRetryDelayMs(maxRetries - 1, backoff, retryAfter);
117
+ throw new ApiError('Request rate too high: daily 10,000-request quota exceeded (retries exhausted)', 429, {
118
+ retryable: true,
119
+ transient: true,
120
+ retryAfterMs,
121
+ hint: 'Use `switchbot quota status` to see today\'s usage; raise `--retry-on-429 <n>` for more retries.'
122
+ });
112
123
  }
113
- throw new ApiError(`HTTP ${status ?? '?'}: ${error.message}`, status ?? 0, { retryable: status !== undefined && status >= 500 });
124
+ throw new ApiError(`HTTP ${status ?? '?'}: ${error.message}`, status ?? 0, {
125
+ retryable: status !== undefined && status >= 500,
126
+ transient: status !== undefined && (status >= 500 || status === 0) // 5xx, 0 = connection error
127
+ });
114
128
  }
115
129
  throw error;
116
130
  });
@@ -120,12 +134,15 @@ export class ApiError extends Error {
120
134
  code;
121
135
  retryable;
122
136
  hint;
137
+ retryAfterMs;
138
+ transient;
123
139
  constructor(message, code, meta = {}) {
124
140
  super(message);
125
141
  this.code = code;
126
142
  this.name = 'ApiError';
127
143
  this.retryable = meta.retryable ?? false;
128
144
  this.hint = meta.hint;
145
+ this.retryAfterMs = meta.retryAfterMs;
146
+ this.transient = meta.transient ?? false;
129
147
  }
130
148
  }
131
- //# sourceMappingURL=client.js.map
package/dist/auth.js CHANGED
@@ -18,4 +18,3 @@ export function buildAuthHeaders(token, secret) {
18
18
  'Content-Type': 'application/json',
19
19
  };
20
20
  }
21
- //# sourceMappingURL=auth.js.map
@@ -1,4 +1,4 @@
1
- import { printJson, isJsonMode, handleError } from '../utils/output.js';
1
+ import { printJson, isJsonMode, handleError, buildErrorPayload } from '../utils/output.js';
2
2
  import { fetchDeviceList, executeCommand, isDestructiveCommand, buildHubLocationMap, } from '../lib/devices.js';
3
3
  import { createClient } from '../api/client.js';
4
4
  import { parseFilter, applyFilter, FilterSyntaxError } from '../utils/filter.js';
@@ -90,6 +90,7 @@ export function registerBatchCommand(devices) {
90
90
  .option('--yes', 'Allow destructive commands (Smart Lock unlock, garage open, ...)')
91
91
  .option('--type <commandType>', '"command" (default) or "customize" for user-defined IR buttons', 'command')
92
92
  .option('--stdin', 'Read deviceIds from stdin, one per line (same as trailing "-")')
93
+ .option('--idempotency-key-prefix <prefix>', 'Prefix for idempotency keys (key per device: <prefix>-<deviceId>)')
93
94
  .addHelpText('after', `
94
95
  Targets are resolved in this priority order:
95
96
  1. --ids when present (explicit deviceIds)
@@ -214,7 +215,12 @@ Examples:
214
215
  const startedAt = Date.now();
215
216
  const outcomes = await runPool(resolved.ids, concurrency, async (id) => {
216
217
  try {
217
- const result = await executeCommand(id, cmd, parsedParam, effectiveType, getClient());
218
+ const idempotencyKey = options.idempotencyKeyPrefix
219
+ ? `${options.idempotencyKeyPrefix}-${id}`
220
+ : undefined;
221
+ const result = await executeCommand(id, cmd, parsedParam, effectiveType, getClient(), {
222
+ idempotencyKey,
223
+ });
218
224
  if (!isJsonMode()) {
219
225
  console.log(`✓ ${id}: ${cmd}`);
220
226
  }
@@ -226,11 +232,11 @@ Examples:
226
232
  if (err instanceof DryRunSignal) {
227
233
  return { ok: 'dry-run', deviceId: id };
228
234
  }
229
- const message = err instanceof Error ? err.message : String(err);
235
+ const errorPayload = buildErrorPayload(err);
230
236
  if (!isJsonMode()) {
231
- console.error(`✗ ${id}: ${message}`);
237
+ console.error(`✗ ${id}: ${errorPayload.message}`);
232
238
  }
233
- return { ok: false, deviceId: id, error: message };
239
+ return { ok: false, deviceId: id, error: errorPayload };
234
240
  }
235
241
  });
236
242
  const succeeded = outcomes.filter((o) => o.ok === true);
@@ -245,6 +251,7 @@ Examples:
245
251
  failed: failed.length,
246
252
  skipped: dryRunned.length,
247
253
  durationMs: Date.now() - startedAt,
254
+ schemaVersion: '1.1',
248
255
  ...(dryRun ? { dryRun: true } : {}),
249
256
  },
250
257
  };
@@ -259,4 +266,3 @@ Examples:
259
266
  process.exit(1);
260
267
  });
261
268
  }
262
- //# sourceMappingURL=batch.js.map
@@ -105,4 +105,3 @@ Examples:
105
105
  }
106
106
  });
107
107
  }
108
- //# sourceMappingURL=cache.js.map
@@ -1,4 +1,5 @@
1
1
  import { getEffectiveCatalog } from '../devices/catalog.js';
2
+ import { printJson } from '../utils/output.js';
2
3
  const IDENTITY = {
3
4
  product: 'SwitchBot',
4
5
  domain: 'IoT smart home device control',
@@ -24,6 +25,7 @@ const MCP_TOOLS = [
24
25
  'list_scenes',
25
26
  'run_scene',
26
27
  'search_catalog',
28
+ 'account_overview',
27
29
  ];
28
30
  export function registerCapabilitiesCommand(program) {
29
31
  program
@@ -55,7 +57,7 @@ export function registerCapabilitiesCommand(program) {
55
57
  description: opt.description,
56
58
  }));
57
59
  const roles = [...new Set(catalog.map((e) => e.role ?? 'other'))].sort();
58
- console.log(JSON.stringify({
60
+ printJson({
59
61
  version: program.version(),
60
62
  generatedAt: new Date().toISOString(),
61
63
  identity: IDENTITY,
@@ -64,6 +66,7 @@ export function registerCapabilitiesCommand(program) {
64
66
  entry: 'mcp serve',
65
67
  protocol: 'stdio (default) or --port <n> for HTTP',
66
68
  tools: MCP_TOOLS,
69
+ resources: ['switchbot://events'],
67
70
  },
68
71
  plan: {
69
72
  schemaCmd: 'plan schema',
@@ -85,7 +88,6 @@ export function registerCapabilitiesCommand(program) {
85
88
  destructiveCommandCount: catalog.reduce((n, e) => n + e.commands.filter((c) => c.destructive).length, 0),
86
89
  readOnlyTypeCount: catalog.filter((e) => e.readOnly).length,
87
90
  },
88
- }, null, 2));
91
+ });
89
92
  });
90
93
  }
91
- //# sourceMappingURL=capabilities.js.map
@@ -288,4 +288,3 @@ function renderEntry(entry) {
288
288
  console.log(' ' + entry.statusFields.join(', '));
289
289
  }
290
290
  }
291
- //# sourceMappingURL=catalog.js.map
@@ -256,4 +256,3 @@ source it directly:
256
256
  }
257
257
  });
258
258
  }
259
- //# sourceMappingURL=completion.js.map
@@ -147,4 +147,3 @@ Files are written with mode 0600. Profiles live under ~/.switchbot/profiles/<nam
147
147
  console.log(p);
148
148
  });
149
149
  }
150
- //# sourceMappingURL=config.js.map
@@ -139,4 +139,3 @@ export function registerDevicesMetaCommand(devices) {
139
139
  }
140
140
  });
141
141
  }
142
- //# sourceMappingURL=device-meta.js.map
@@ -189,6 +189,7 @@ Examples:
189
189
  .option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
190
190
  .option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', 'command')
191
191
  .option('--yes', 'Confirm a destructive command (Smart Lock unlock, Garage open, …). --dry-run is always allowed without --yes.')
192
+ .option('--idempotency-key <key>', 'Idempotency key for deduplication (60s window; same key replays cached result)')
192
193
  .addHelpText('after', `
193
194
  ────────────────────────────────────────────────────────────────────────
194
195
  For the full list of commands a specific device supports — and their
@@ -298,7 +299,7 @@ Examples:
298
299
  // keep as string
299
300
  }
300
301
  }
301
- const body = await executeCommand(deviceId, cmd, parsedParam, options.type);
302
+ const body = await executeCommand(deviceId, cmd, parsedParam, options.type, undefined, { idempotencyKey: options.idempotencyKey });
302
303
  const isIr = getCachedDevice(deviceId)?.category === 'ir';
303
304
  if (isJsonMode()) {
304
305
  const result = { ok: true, command: cmd, deviceId };
@@ -554,4 +555,3 @@ function renderCatalogEntry(entry) {
554
555
  console.log(' ' + entry.statusFields.join(', '));
555
556
  }
556
557
  }
557
- //# sourceMappingURL=devices.js.map
@@ -144,4 +144,3 @@ Examples:
144
144
  process.exit(1);
145
145
  });
146
146
  }
147
- //# sourceMappingURL=doctor.js.map
@@ -185,4 +185,3 @@ Examples:
185
185
  }
186
186
  });
187
187
  }
188
- //# sourceMappingURL=events.js.map
@@ -192,4 +192,3 @@ Examples:
192
192
  }
193
193
  });
194
194
  }
195
- //# sourceMappingURL=expand.js.map
@@ -134,4 +134,3 @@ function printHuman(r) {
134
134
  }
135
135
  }
136
136
  }
137
- //# sourceMappingURL=explain.js.map
@@ -101,4 +101,3 @@ Examples:
101
101
  }
102
102
  });
103
103
  }
104
- //# sourceMappingURL=history.js.map