@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.
- package/README.md +30 -1
- package/dist/api/client.js +22 -5
- package/dist/auth.js +0 -1
- package/dist/commands/batch.js +12 -6
- package/dist/commands/cache.js +0 -1
- package/dist/commands/capabilities.js +5 -3
- package/dist/commands/catalog.js +0 -1
- package/dist/commands/completion.js +0 -1
- package/dist/commands/config.js +0 -1
- package/dist/commands/device-meta.js +0 -1
- package/dist/commands/devices.js +2 -2
- package/dist/commands/doctor.js +0 -1
- package/dist/commands/events.js +0 -1
- package/dist/commands/expand.js +0 -1
- package/dist/commands/explain.js +0 -1
- package/dist/commands/history.js +0 -1
- package/dist/commands/mcp.js +334 -18
- package/dist/commands/plan.js +0 -1
- package/dist/commands/quota.js +0 -1
- package/dist/commands/scenes.js +0 -1
- package/dist/commands/schema.js +2 -9
- package/dist/commands/watch.js +0 -1
- package/dist/commands/webhook.js +0 -1
- package/dist/config.js +5 -5
- package/dist/devices/cache.js +0 -1
- package/dist/devices/catalog.js +0 -1
- package/dist/devices/device-meta.js +0 -1
- package/dist/index.js +0 -1
- package/dist/lib/devices.js +22 -18
- package/dist/lib/idempotency.js +72 -0
- package/dist/lib/request-context.js +12 -0
- package/dist/lib/scenes.js +0 -1
- package/dist/logger.js +16 -0
- package/dist/mcp/events-subscription.js +210 -0
- package/dist/mqtt/client.js +184 -0
- package/dist/mqtt/credential.js +12 -0
- package/dist/utils/audit.js +0 -1
- package/dist/utils/filter.js +0 -1
- package/dist/utils/flags.js +0 -1
- package/dist/utils/format.js +0 -1
- package/dist/utils/name-resolver.js +0 -1
- package/dist/utils/output.js +30 -6
- package/dist/utils/quota.js +0 -1
- package/dist/utils/retry.js +0 -1
- package/dist/utils/string.js +0 -1
- package/package.json +6 -2
- package/dist/api/client.d.ts +0 -18
- package/dist/api/client.js.map +0 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -2
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/cache.d.ts +0 -2
- package/dist/commands/cache.js.map +0 -1
- package/dist/commands/capabilities.d.ts +0 -2
- package/dist/commands/capabilities.js.map +0 -1
- package/dist/commands/catalog.d.ts +0 -2
- package/dist/commands/catalog.js.map +0 -1
- package/dist/commands/completion.d.ts +0 -2
- package/dist/commands/completion.js.map +0 -1
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/device-meta.d.ts +0 -2
- package/dist/commands/device-meta.js.map +0 -1
- package/dist/commands/devices.d.ts +0 -2
- package/dist/commands/devices.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/events.d.ts +0 -15
- package/dist/commands/events.js.map +0 -1
- package/dist/commands/expand.d.ts +0 -2
- package/dist/commands/expand.js.map +0 -1
- package/dist/commands/explain.d.ts +0 -2
- package/dist/commands/explain.js.map +0 -1
- package/dist/commands/history.d.ts +0 -2
- package/dist/commands/history.js.map +0 -1
- package/dist/commands/mcp.d.ts +0 -4
- package/dist/commands/mcp.js.map +0 -1
- package/dist/commands/plan.d.ts +0 -38
- package/dist/commands/plan.js.map +0 -1
- package/dist/commands/quota.d.ts +0 -2
- package/dist/commands/quota.js.map +0 -1
- package/dist/commands/scenes.d.ts +0 -2
- package/dist/commands/scenes.js.map +0 -1
- package/dist/commands/schema.d.ts +0 -2
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -2
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/webhook.d.ts +0 -2
- package/dist/commands/webhook.js.map +0 -1
- package/dist/config.d.ts +0 -18
- package/dist/config.js.map +0 -1
- package/dist/devices/cache.d.ts +0 -79
- package/dist/devices/cache.js.map +0 -1
- package/dist/devices/catalog.d.ts +0 -70
- package/dist/devices/catalog.js.map +0 -1
- package/dist/devices/device-meta.d.ts +0 -15
- package/dist/devices/device-meta.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/lib/devices.d.ts +0 -144
- package/dist/lib/devices.js.map +0 -1
- package/dist/lib/scenes.d.ts +0 -7
- package/dist/lib/scenes.js.map +0 -1
- package/dist/utils/audit.d.ts +0 -13
- package/dist/utils/audit.js.map +0 -1
- package/dist/utils/filter.d.ts +0 -45
- package/dist/utils/filter.js.map +0 -1
- package/dist/utils/flags.d.ts +0 -52
- package/dist/utils/flags.js.map +0 -1
- package/dist/utils/format.d.ts +0 -9
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/name-resolver.d.ts +0 -17
- package/dist/utils/name-resolver.js.map +0 -1
- package/dist/utils/output.d.ts +0 -23
- package/dist/utils/output.js.map +0 -1
- package/dist/utils/quota.d.ts +0 -50
- package/dist/utils/quota.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -23
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/string.d.ts +0 -2
- 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
|
|
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
|
package/dist/api/client.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
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, {
|
|
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
package/dist/commands/batch.js
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
package/dist/commands/cache.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|
|
91
|
+
});
|
|
89
92
|
});
|
|
90
93
|
}
|
|
91
|
-
//# sourceMappingURL=capabilities.js.map
|
package/dist/commands/catalog.js
CHANGED
package/dist/commands/config.js
CHANGED
package/dist/commands/devices.js
CHANGED
|
@@ -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
|
package/dist/commands/doctor.js
CHANGED
package/dist/commands/events.js
CHANGED
package/dist/commands/expand.js
CHANGED
package/dist/commands/explain.js
CHANGED
package/dist/commands/history.js
CHANGED