@switchbot/openapi-cli 3.1.1 → 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.
- package/README.md +3 -3
- package/dist/index.js +56945 -169
- package/dist/policy/schema/v0.2.json +1 -1
- package/package.json +3 -2
- package/dist/api/client.js +0 -235
- package/dist/auth.js +0 -20
- package/dist/commands/agent-bootstrap.js +0 -182
- package/dist/commands/auth.js +0 -354
- package/dist/commands/batch.js +0 -413
- package/dist/commands/cache.js +0 -126
- package/dist/commands/capabilities.js +0 -385
- package/dist/commands/catalog.js +0 -359
- package/dist/commands/completion.js +0 -385
- package/dist/commands/config.js +0 -376
- package/dist/commands/daemon.js +0 -410
- package/dist/commands/device-meta.js +0 -159
- package/dist/commands/devices.js +0 -948
- package/dist/commands/doctor.js +0 -1015
- package/dist/commands/events.js +0 -563
- package/dist/commands/expand.js +0 -130
- package/dist/commands/explain.js +0 -139
- package/dist/commands/health.js +0 -113
- package/dist/commands/history.js +0 -320
- package/dist/commands/identity.js +0 -59
- package/dist/commands/install.js +0 -246
- package/dist/commands/mcp.js +0 -2017
- package/dist/commands/plan.js +0 -653
- package/dist/commands/policy.js +0 -586
- package/dist/commands/quota.js +0 -78
- package/dist/commands/rules.js +0 -875
- package/dist/commands/scenes.js +0 -264
- package/dist/commands/schema.js +0 -177
- package/dist/commands/status-sync.js +0 -131
- package/dist/commands/uninstall.js +0 -237
- package/dist/commands/upgrade-check.js +0 -107
- package/dist/commands/watch.js +0 -194
- package/dist/commands/webhook.js +0 -182
- package/dist/config.js +0 -258
- package/dist/credentials/backends/file.js +0 -101
- package/dist/credentials/backends/linux.js +0 -129
- package/dist/credentials/backends/macos.js +0 -129
- package/dist/credentials/backends/windows.js +0 -215
- package/dist/credentials/keychain.js +0 -88
- package/dist/credentials/prime.js +0 -52
- package/dist/devices/cache.js +0 -293
- package/dist/devices/catalog.js +0 -767
- package/dist/devices/device-meta.js +0 -56
- package/dist/devices/history-agg.js +0 -138
- package/dist/devices/history-query.js +0 -181
- package/dist/devices/param-validator.js +0 -433
- package/dist/devices/resources.js +0 -270
- package/dist/install/default-steps.js +0 -257
- package/dist/install/preflight.js +0 -212
- package/dist/install/steps.js +0 -67
- package/dist/lib/command-keywords.js +0 -17
- package/dist/lib/daemon-state.js +0 -46
- package/dist/lib/destructive-mode.js +0 -12
- package/dist/lib/devices.js +0 -382
- package/dist/lib/idempotency.js +0 -106
- package/dist/lib/plan-store.js +0 -68
- package/dist/lib/request-context.js +0 -12
- package/dist/lib/scenes.js +0 -10
- package/dist/logger.js +0 -16
- package/dist/mcp/device-history.js +0 -145
- package/dist/mcp/events-subscription.js +0 -213
- package/dist/mqtt/client.js +0 -180
- package/dist/mqtt/credential.js +0 -30
- package/dist/policy/add-rule.js +0 -124
- package/dist/policy/diff.js +0 -91
- package/dist/policy/format.js +0 -57
- package/dist/policy/load.js +0 -61
- package/dist/policy/migrate.js +0 -67
- package/dist/policy/schema.js +0 -18
- package/dist/policy/validate.js +0 -262
- package/dist/rules/action.js +0 -216
- package/dist/rules/audit-query.js +0 -89
- package/dist/rules/conflict-analyzer.js +0 -214
- package/dist/rules/cron-scheduler.js +0 -186
- package/dist/rules/destructive.js +0 -52
- package/dist/rules/engine.js +0 -757
- package/dist/rules/matcher.js +0 -230
- package/dist/rules/pid-file.js +0 -95
- package/dist/rules/quiet-hours.js +0 -45
- package/dist/rules/suggest.js +0 -95
- package/dist/rules/throttle.js +0 -116
- package/dist/rules/types.js +0 -34
- package/dist/rules/webhook-listener.js +0 -223
- package/dist/rules/webhook-token.js +0 -90
- package/dist/schema/field-aliases.js +0 -131
- package/dist/sinks/dispatcher.js +0 -12
- package/dist/sinks/file.js +0 -19
- package/dist/sinks/format.js +0 -56
- package/dist/sinks/homeassistant.js +0 -44
- package/dist/sinks/openclaw.js +0 -33
- package/dist/sinks/stdout.js +0 -5
- package/dist/sinks/telegram.js +0 -28
- package/dist/sinks/types.js +0 -1
- package/dist/sinks/webhook.js +0 -22
- package/dist/status-sync/manager.js +0 -268
- package/dist/utils/arg-parsers.js +0 -66
- package/dist/utils/audit.js +0 -121
- package/dist/utils/filter.js +0 -189
- package/dist/utils/flags.js +0 -186
- package/dist/utils/format.js +0 -117
- package/dist/utils/health.js +0 -101
- package/dist/utils/help-json.js +0 -54
- package/dist/utils/name-resolver.js +0 -137
- package/dist/utils/output.js +0 -404
- package/dist/utils/quota.js +0 -227
- package/dist/utils/redact.js +0 -68
- package/dist/utils/retry.js +0 -140
- package/dist/utils/string.js +0 -22
- package/dist/version.js +0 -4
package/dist/commands/scenes.js
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { printJson, isJsonMode, handleError, StructuredUsageError } from '../utils/output.js';
|
|
2
|
-
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
3
|
-
import { fetchScenes, executeScene } from '../lib/scenes.js';
|
|
4
|
-
import { isDryRun } from '../utils/flags.js';
|
|
5
|
-
export function registerScenesCommand(program) {
|
|
6
|
-
const scenes = program
|
|
7
|
-
.command('scenes')
|
|
8
|
-
.description('Manage and execute SwitchBot scenes');
|
|
9
|
-
// switchbot scenes list
|
|
10
|
-
scenes
|
|
11
|
-
.command('list')
|
|
12
|
-
.description('List all manual scenes (scenes created in the SwitchBot app)')
|
|
13
|
-
.addHelpText('after', `
|
|
14
|
-
Output columns: sceneId, sceneName
|
|
15
|
-
--fields accepts any subset of these names (exit 2 on unknown names).
|
|
16
|
-
|
|
17
|
-
Examples:
|
|
18
|
-
$ switchbot scenes list
|
|
19
|
-
$ switchbot scenes list --format tsv --fields sceneId,sceneName
|
|
20
|
-
$ switchbot scenes list --format id
|
|
21
|
-
$ switchbot scenes list --json
|
|
22
|
-
`)
|
|
23
|
-
.action(async () => {
|
|
24
|
-
try {
|
|
25
|
-
const scenes = await fetchScenes();
|
|
26
|
-
const fmt = resolveFormat();
|
|
27
|
-
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
28
|
-
printJson(scenes);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
renderRows(['sceneId', 'sceneName'], scenes.map((s) => [s.sceneId, s.sceneName]), fmt, resolveFields(), { id: 'sceneId', name: 'sceneName' });
|
|
32
|
-
if (fmt === 'table' && scenes.length === 0) {
|
|
33
|
-
console.log('No scenes found');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
handleError(error);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
// switchbot scenes execute <sceneId>
|
|
41
|
-
scenes
|
|
42
|
-
.command('execute')
|
|
43
|
-
.description('Execute a manual scene by its ID')
|
|
44
|
-
.argument('<sceneId>', 'Scene ID from "scenes list"')
|
|
45
|
-
.addHelpText('after', `
|
|
46
|
-
Example:
|
|
47
|
-
$ switchbot scenes execute T12345678
|
|
48
|
-
`)
|
|
49
|
-
.action(async (sceneId) => {
|
|
50
|
-
try {
|
|
51
|
-
const sceneList = await fetchScenes();
|
|
52
|
-
const found = sceneList.find((s) => s.sceneId === sceneId);
|
|
53
|
-
if (!found) {
|
|
54
|
-
throw new StructuredUsageError(`scene not found: ${sceneId}`, {
|
|
55
|
-
error: 'scene_not_found',
|
|
56
|
-
sceneId,
|
|
57
|
-
candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (isDryRun()) {
|
|
61
|
-
const wouldSend = { method: 'POST', url: `/v1.1/scenes/${sceneId}/execute`, sceneId, sceneName: found.sceneName };
|
|
62
|
-
if (isJsonMode()) {
|
|
63
|
-
printJson({ dryRun: true, wouldSend });
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
console.log(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
|
|
67
|
-
}
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
await executeScene(sceneId);
|
|
71
|
-
if (isJsonMode()) {
|
|
72
|
-
printJson({ ok: true, sceneId });
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
console.log(`✓ Scene executed: ${sceneId}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
handleError(error);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
// switchbot scenes describe <sceneId>
|
|
83
|
-
scenes
|
|
84
|
-
.command('describe')
|
|
85
|
-
.description('Show metadata for a scene by its ID (SwitchBot API v1.1 does not expose step detail)')
|
|
86
|
-
.argument('<sceneId>', 'Scene ID from "scenes list"')
|
|
87
|
-
.addHelpText('after', `
|
|
88
|
-
Note: SwitchBot API v1.1 does not return scene step detail. Only the scene name is available.
|
|
89
|
-
|
|
90
|
-
Example:
|
|
91
|
-
$ switchbot scenes describe T12345678
|
|
92
|
-
`)
|
|
93
|
-
.action(async (sceneId) => {
|
|
94
|
-
try {
|
|
95
|
-
const sceneList = await fetchScenes();
|
|
96
|
-
const found = sceneList.find((s) => s.sceneId === sceneId);
|
|
97
|
-
if (!found) {
|
|
98
|
-
throw new StructuredUsageError(`scene not found: ${sceneId}`, {
|
|
99
|
-
error: 'scene_not_found',
|
|
100
|
-
sceneId,
|
|
101
|
-
candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
const result = {
|
|
105
|
-
sceneId: found.sceneId,
|
|
106
|
-
sceneName: found.sceneName,
|
|
107
|
-
stepCount: null,
|
|
108
|
-
note: 'SwitchBot API v1.1 does not expose scene steps — displayed name only',
|
|
109
|
-
};
|
|
110
|
-
if (isJsonMode()) {
|
|
111
|
-
printJson(result);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
console.log(`sceneId: ${result.sceneId}`);
|
|
115
|
-
console.log(`sceneName: ${result.sceneName}`);
|
|
116
|
-
console.log(`stepCount: (not available)`);
|
|
117
|
-
console.log(`note: ${result.note}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
handleError(error);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
// switchbot scenes validate [sceneId...]
|
|
125
|
-
scenes
|
|
126
|
-
.command('validate')
|
|
127
|
-
.description('Verify that one or more scenes exist. If no IDs are given, validates all scenes are reachable.')
|
|
128
|
-
.argument('[sceneId...]', 'Scene IDs to validate (default: all scenes)')
|
|
129
|
-
.addHelpText('after', `
|
|
130
|
-
Note: SwitchBot API v1.1 does not expose scene steps; validation only confirms
|
|
131
|
-
the scene IDs exist in your account.
|
|
132
|
-
|
|
133
|
-
Examples:
|
|
134
|
-
$ switchbot scenes validate
|
|
135
|
-
$ switchbot scenes validate T12345678 T87654321
|
|
136
|
-
`)
|
|
137
|
-
.action(async (sceneIds) => {
|
|
138
|
-
try {
|
|
139
|
-
const sceneList = await fetchScenes();
|
|
140
|
-
const sceneMap = new Map(sceneList.map((s) => [s.sceneId, s.sceneName]));
|
|
141
|
-
const targets = sceneIds.length > 0 ? sceneIds : sceneList.map((s) => s.sceneId);
|
|
142
|
-
const results = targets.map((id) => ({
|
|
143
|
-
sceneId: id,
|
|
144
|
-
sceneName: sceneMap.get(id) ?? null,
|
|
145
|
-
valid: sceneMap.has(id),
|
|
146
|
-
}));
|
|
147
|
-
const allValid = results.every((r) => r.valid);
|
|
148
|
-
if (isJsonMode()) {
|
|
149
|
-
printJson({ ok: allValid, results });
|
|
150
|
-
if (!allValid)
|
|
151
|
-
process.exit(1);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
for (const r of results) {
|
|
155
|
-
const icon = r.valid ? '✓' : '✗';
|
|
156
|
-
const label = r.valid ? r.sceneName : '(not found)';
|
|
157
|
-
console.log(`${icon} ${r.sceneId} ${label}`);
|
|
158
|
-
}
|
|
159
|
-
if (!allValid)
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
handleError(error);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
// switchbot scenes simulate <sceneId>
|
|
167
|
-
scenes
|
|
168
|
-
.command('simulate')
|
|
169
|
-
.description('Show what `scenes execute` would do without actually executing the scene.')
|
|
170
|
-
.argument('<sceneId>', 'Scene ID from "scenes list"')
|
|
171
|
-
.addHelpText('after', `
|
|
172
|
-
Note: SwitchBot API v1.1 does not expose scene step details. Simulation reports
|
|
173
|
-
the scene name, confirms it exists, and shows the POST that would be issued.
|
|
174
|
-
|
|
175
|
-
Example:
|
|
176
|
-
$ switchbot scenes simulate T12345678
|
|
177
|
-
`)
|
|
178
|
-
.action(async (sceneId) => {
|
|
179
|
-
try {
|
|
180
|
-
const sceneList = await fetchScenes();
|
|
181
|
-
const found = sceneList.find((s) => s.sceneId === sceneId);
|
|
182
|
-
if (!found) {
|
|
183
|
-
throw new StructuredUsageError(`scene not found: ${sceneId}`, {
|
|
184
|
-
error: 'scene_not_found',
|
|
185
|
-
sceneId,
|
|
186
|
-
candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
const simulation = {
|
|
190
|
-
sceneId: found.sceneId,
|
|
191
|
-
sceneName: found.sceneName,
|
|
192
|
-
wouldSend: { method: 'POST', url: `/v1.1/scenes/${sceneId}/execute` },
|
|
193
|
-
note: 'SwitchBot API v1.1 does not expose individual scene steps.',
|
|
194
|
-
};
|
|
195
|
-
if (isJsonMode()) {
|
|
196
|
-
printJson({ simulated: true, ...simulation });
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
console.log(`sceneId: ${simulation.sceneId}`);
|
|
200
|
-
console.log(`sceneName: ${simulation.sceneName}`);
|
|
201
|
-
console.log(`wouldSend: ${simulation.wouldSend.method} ${simulation.wouldSend.url}`);
|
|
202
|
-
console.log(`note: ${simulation.note}`);
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
handleError(error);
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
// switchbot scenes explain <sceneId>
|
|
209
|
-
scenes
|
|
210
|
-
.command('explain')
|
|
211
|
-
.description('Explain in plain language what a scene does and how to execute it safely.')
|
|
212
|
-
.argument('<sceneId>', 'Scene ID from "scenes list"')
|
|
213
|
-
.addHelpText('after', `
|
|
214
|
-
Shows the scene name, action description, risk level, and the exact command to
|
|
215
|
-
run. Unlike "simulate" (which shows raw HTTP detail), "explain" is aimed at a
|
|
216
|
-
human or agent deciding whether to proceed.
|
|
217
|
-
|
|
218
|
-
Note: SwitchBot API v1.1 does not expose scene step details; risk is reported
|
|
219
|
-
as "low" because scenes only trigger pre-configured automations in the app.
|
|
220
|
-
|
|
221
|
-
Example:
|
|
222
|
-
$ switchbot scenes explain T12345678
|
|
223
|
-
`)
|
|
224
|
-
.action(async (sceneId) => {
|
|
225
|
-
try {
|
|
226
|
-
const sceneList = await fetchScenes();
|
|
227
|
-
const found = sceneList.find((s) => s.sceneId === sceneId);
|
|
228
|
-
if (!found) {
|
|
229
|
-
throw new StructuredUsageError(`scene not found: ${sceneId}`, {
|
|
230
|
-
error: 'scene_not_found',
|
|
231
|
-
sceneId,
|
|
232
|
-
candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
const explanation = {
|
|
236
|
-
sceneId: found.sceneId,
|
|
237
|
-
sceneName: found.sceneName,
|
|
238
|
-
action: `Trigger scene (POST /v1.1/scenes/${found.sceneId}/execute)`,
|
|
239
|
-
riskLevel: 'low',
|
|
240
|
-
idempotent: null,
|
|
241
|
-
toExecute: `switchbot scenes execute ${found.sceneId}`,
|
|
242
|
-
dryRun: isDryRun(),
|
|
243
|
-
note: 'SwitchBot API v1.1 does not expose individual scene steps.',
|
|
244
|
-
};
|
|
245
|
-
if (isJsonMode()) {
|
|
246
|
-
printJson(explanation);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
console.log(`sceneId: ${explanation.sceneId}`);
|
|
250
|
-
console.log(`sceneName: ${explanation.sceneName}`);
|
|
251
|
-
console.log(`action: ${explanation.action}`);
|
|
252
|
-
console.log(`riskLevel: ${explanation.riskLevel}`);
|
|
253
|
-
console.log(`idempotent: unknown (scene steps not exposed by API)`);
|
|
254
|
-
console.log(`toExecute: ${explanation.toExecute}`);
|
|
255
|
-
if (explanation.dryRun) {
|
|
256
|
-
console.log(`dryRun: true (pass --dry-run to execute would be a no-op)`);
|
|
257
|
-
}
|
|
258
|
-
console.log(`note: ${explanation.note}`);
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
handleError(error);
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
}
|
package/dist/commands/schema.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
2
|
-
import { printJson } from '../utils/output.js';
|
|
3
|
-
import { getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
|
|
4
|
-
import { RESOURCE_CATALOG } from '../devices/resources.js';
|
|
5
|
-
import { loadCache } from '../devices/cache.js';
|
|
6
|
-
function toSchemaEntry(e) {
|
|
7
|
-
return {
|
|
8
|
-
type: e.type,
|
|
9
|
-
description: e.description ?? '',
|
|
10
|
-
category: e.category,
|
|
11
|
-
aliases: e.aliases ?? [],
|
|
12
|
-
role: e.role ?? null,
|
|
13
|
-
readOnly: e.readOnly ?? false,
|
|
14
|
-
commands: e.commands.map((c) => toSchemaCommand(c, e)),
|
|
15
|
-
statusFields: e.statusFields ?? [],
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
function toSchemaCommand(c, entry) {
|
|
19
|
-
const tier = deriveSafetyTier(c, entry);
|
|
20
|
-
const reason = getCommandSafetyReason(c);
|
|
21
|
-
return {
|
|
22
|
-
command: c.command,
|
|
23
|
-
parameter: c.parameter,
|
|
24
|
-
description: c.description,
|
|
25
|
-
commandType: (c.commandType ?? 'command'),
|
|
26
|
-
idempotent: Boolean(c.idempotent),
|
|
27
|
-
safetyTier: tier,
|
|
28
|
-
...(reason ? { safetyReason: reason } : {}),
|
|
29
|
-
...(c.exampleParams ? { exampleParams: c.exampleParams } : {}),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function toCompactEntry(e) {
|
|
33
|
-
return {
|
|
34
|
-
type: e.type,
|
|
35
|
-
category: e.category,
|
|
36
|
-
role: e.role ?? null,
|
|
37
|
-
readOnly: e.readOnly ?? false,
|
|
38
|
-
commands: e.commands.map((c) => {
|
|
39
|
-
const tier = deriveSafetyTier(c, e);
|
|
40
|
-
return {
|
|
41
|
-
command: c.command,
|
|
42
|
-
parameter: c.parameter,
|
|
43
|
-
commandType: (c.commandType ?? 'command'),
|
|
44
|
-
idempotent: Boolean(c.idempotent),
|
|
45
|
-
safetyTier: tier,
|
|
46
|
-
};
|
|
47
|
-
}),
|
|
48
|
-
statusFields: e.statusFields ?? [],
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function projectFields(entry, fields) {
|
|
52
|
-
const out = {};
|
|
53
|
-
for (const f of fields) {
|
|
54
|
-
if (f in entry)
|
|
55
|
-
out[f] = entry[f];
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
export function registerSchemaCommand(program) {
|
|
60
|
-
const ROLES = ['lighting', 'security', 'sensor', 'climate', 'media', 'cleaning', 'curtain', 'fan', 'power', 'hub', 'other'];
|
|
61
|
-
const CATEGORIES = ['physical', 'ir'];
|
|
62
|
-
const schema = program
|
|
63
|
-
.command('schema')
|
|
64
|
-
.description('Export the SwitchBot device catalog as structured JSON (for AI agent prompts / tooling)');
|
|
65
|
-
schema
|
|
66
|
-
.command('export')
|
|
67
|
-
.description('Print the catalog as structured JSON (one object per type)')
|
|
68
|
-
.option('--type <type>', 'Restrict to a single device type (e.g. "Strip Light")', stringArg('--type'))
|
|
69
|
-
.option('--types <csv>', 'Restrict to multiple device types (comma-separated)', stringArg('--types'))
|
|
70
|
-
.option('--role <role>', 'Restrict to a functional role: lighting, security, sensor, climate, media, cleaning, curtain, fan, power, hub, other', enumArg('--role', ROLES))
|
|
71
|
-
.option('--category <cat>', 'Restrict to "physical" or "ir"', enumArg('--category', CATEGORIES))
|
|
72
|
-
.option('--compact', 'Drop descriptions/aliases/example params — emit ~60% smaller payload. Useful for agent prompts.')
|
|
73
|
-
.option('--used', 'Restrict to device types present in the local devices cache (run "devices list" first)')
|
|
74
|
-
.option('--project <csv>', 'Project per-type fields (e.g. --project type,commands,statusFields)', stringArg('--project'))
|
|
75
|
-
.addHelpText('after', `
|
|
76
|
-
Output is always JSON (this command ignores --format). The output is a
|
|
77
|
-
catalog export — not a formal JSON Schema standard document — suitable for
|
|
78
|
-
pre-baking LLM prompts or regenerating docs when the catalog changes.
|
|
79
|
-
|
|
80
|
-
Size tips:
|
|
81
|
-
--compact --used Smallest realistic payload for a given account
|
|
82
|
-
(< 15 KB on most accounts).
|
|
83
|
-
--fields type,commands Strip statusFields / role / etc. when only
|
|
84
|
-
commands are needed.
|
|
85
|
-
--type + --compact Inspect one type with minimum footprint.
|
|
86
|
-
|
|
87
|
-
Common top-level fields:
|
|
88
|
-
schemaVersion CLI schema version (stable for agent contracts)
|
|
89
|
-
data.version Catalog schema version
|
|
90
|
-
data.types Array of SchemaEntry (or CompactSchemaEntry with --compact)
|
|
91
|
-
data._fetchedAt CLI-added; present on live-query responses ('devices status'),
|
|
92
|
-
not on this offline export.
|
|
93
|
-
|
|
94
|
-
Examples:
|
|
95
|
-
$ switchbot schema export > catalog.json
|
|
96
|
-
$ switchbot schema export --compact --used | wc -c # small prompt-ready payload
|
|
97
|
-
$ switchbot schema export --type Bot | jq '.data.types[0].commands'
|
|
98
|
-
$ switchbot schema export --types "Bot,Curtain,Color Bulb"
|
|
99
|
-
$ switchbot schema export --role lighting | jq '[.data.types[].type]'
|
|
100
|
-
$ switchbot schema export --role security --category physical
|
|
101
|
-
$ switchbot schema export --project type,commands,statusFields
|
|
102
|
-
`)
|
|
103
|
-
.action((options) => {
|
|
104
|
-
const catalog = getEffectiveCatalog();
|
|
105
|
-
let filtered = catalog;
|
|
106
|
-
if (options.type) {
|
|
107
|
-
const q = options.type.toLowerCase();
|
|
108
|
-
filtered = filtered.filter((e) => e.type.toLowerCase() === q ||
|
|
109
|
-
(e.aliases ?? []).some((a) => a.toLowerCase() === q));
|
|
110
|
-
}
|
|
111
|
-
if (options.types) {
|
|
112
|
-
const set = new Set(options.types.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean));
|
|
113
|
-
filtered = filtered.filter((e) => set.has(e.type.toLowerCase()) ||
|
|
114
|
-
(e.aliases ?? []).some((a) => set.has(a.toLowerCase())));
|
|
115
|
-
}
|
|
116
|
-
if (options.role) {
|
|
117
|
-
const q = options.role.toLowerCase();
|
|
118
|
-
filtered = filtered.filter((e) => (e.role ?? 'other') === q);
|
|
119
|
-
}
|
|
120
|
-
if (options.category) {
|
|
121
|
-
const q = options.category.toLowerCase();
|
|
122
|
-
filtered = filtered.filter((e) => e.category === q);
|
|
123
|
-
}
|
|
124
|
-
if (options.used) {
|
|
125
|
-
const cache = loadCache();
|
|
126
|
-
if (cache) {
|
|
127
|
-
const usedTypes = new Set(Object.values(cache.devices).map((d) => d.type.toLowerCase()));
|
|
128
|
-
filtered = filtered.filter((e) => usedTypes.has(e.type.toLowerCase()) ||
|
|
129
|
-
(e.aliases ?? []).some((a) => usedTypes.has(a.toLowerCase())));
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
filtered = [];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const mapped = options.compact
|
|
136
|
-
? filtered.map(toCompactEntry)
|
|
137
|
-
: filtered.map(toSchemaEntry);
|
|
138
|
-
const projected = options.project
|
|
139
|
-
? mapped.map((e) => projectFields(e, options.project.split(',').map((s) => s.trim()).filter(Boolean)))
|
|
140
|
-
: mapped;
|
|
141
|
-
const payload = {
|
|
142
|
-
version: '1.0',
|
|
143
|
-
types: projected,
|
|
144
|
-
};
|
|
145
|
-
if (!options.compact) {
|
|
146
|
-
payload.generatedAt = new Date().toISOString();
|
|
147
|
-
payload.resources = RESOURCE_CATALOG;
|
|
148
|
-
payload.cliAddedFields = [
|
|
149
|
-
{
|
|
150
|
-
field: '_fetchedAt',
|
|
151
|
-
appliesTo: ['devices status', 'devices describe'],
|
|
152
|
-
type: 'string (ISO-8601)',
|
|
153
|
-
description: 'CLI-synthesized timestamp indicating when this status response was fetched or served from the cache. Not part of the upstream SwitchBot API.',
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
field: 'replayed',
|
|
157
|
-
appliesTo: ['devices command (with --idempotency-key)'],
|
|
158
|
-
type: 'boolean',
|
|
159
|
-
description: 'CLI-synthesized flag — true when the response was served from the idempotency cache instead of re-executing the command.',
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
field: 'verification',
|
|
163
|
-
appliesTo: ['devices command'],
|
|
164
|
-
type: 'object',
|
|
165
|
-
description: 'CLI-synthesized receipt-acknowledgment metadata. For IR devices, verifiable:false signals that no device-side confirmation is possible.',
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
field: 'hints',
|
|
169
|
-
appliesTo: ['agent-bootstrap'],
|
|
170
|
-
type: 'string[]',
|
|
171
|
-
description: 'CLI-synthesized advisory messages for the calling agent. Always emitted; empty array ([]) means no hints to report — never null and not a disabled-field signal.',
|
|
172
|
-
},
|
|
173
|
-
];
|
|
174
|
-
}
|
|
175
|
-
printJson(payload);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { stringArg } from '../utils/arg-parsers.js';
|
|
2
|
-
import { handleError, isJsonMode, printJson } from '../utils/output.js';
|
|
3
|
-
import { getStatusSyncStatus, runStatusSyncForeground, startStatusSync, stopStatusSync, } from '../status-sync/manager.js';
|
|
4
|
-
function printHumanStatus(status) {
|
|
5
|
-
if (!status.running) {
|
|
6
|
-
console.log('status-sync is not running');
|
|
7
|
-
console.log(`state: ${status.stateDir}`);
|
|
8
|
-
console.log(`stdout: ${status.stdoutLog}`);
|
|
9
|
-
console.log(`stderr: ${status.stderrLog}`);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
console.log(`status-sync is running (PID ${status.pid})`);
|
|
13
|
-
console.log(`started: ${status.startedAt}`);
|
|
14
|
-
console.log(`state: ${status.stateDir}`);
|
|
15
|
-
console.log(`stdout: ${status.stdoutLog}`);
|
|
16
|
-
console.log(`stderr: ${status.stderrLog}`);
|
|
17
|
-
}
|
|
18
|
-
export function registerStatusSyncCommand(program) {
|
|
19
|
-
const statusSync = program
|
|
20
|
-
.command('status-sync')
|
|
21
|
-
.description('Manage a background MQTT -> OpenClaw status-sync bridge powered by events mqtt-tail');
|
|
22
|
-
statusSync
|
|
23
|
-
.command('run')
|
|
24
|
-
.description('Run the status-sync bridge in the foreground for a supervisor or terminal session')
|
|
25
|
-
.option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)', stringArg('--openclaw-url'))
|
|
26
|
-
.option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)', stringArg('--openclaw-token'))
|
|
27
|
-
.option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to (or env OPENCLAW_MODEL)', stringArg('--openclaw-model'))
|
|
28
|
-
.option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)', stringArg('--topic'))
|
|
29
|
-
.addHelpText('after', `
|
|
30
|
-
Runs the same MQTT -> OpenClaw bridge logic as \'status-sync start\',
|
|
31
|
-
but keeps the process attached to the current terminal. This is the best fit
|
|
32
|
-
for agent supervisors, service managers, or container entrypoints that want
|
|
33
|
-
foreground process semantics.
|
|
34
|
-
|
|
35
|
-
Examples:
|
|
36
|
-
$ switchbot status-sync run --openclaw-model home-agent
|
|
37
|
-
$ OPENCLAW_TOKEN=abc OPENCLAW_MODEL=home-agent switchbot status-sync run
|
|
38
|
-
`)
|
|
39
|
-
.action(async (options) => {
|
|
40
|
-
try {
|
|
41
|
-
const exitCode = await runStatusSyncForeground(options);
|
|
42
|
-
if (exitCode !== 0) {
|
|
43
|
-
process.exit(exitCode);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
handleError(error);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
statusSync
|
|
51
|
-
.command('start')
|
|
52
|
-
.description('Start the background status-sync bridge')
|
|
53
|
-
.option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)', stringArg('--openclaw-url'))
|
|
54
|
-
.option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)', stringArg('--openclaw-token'))
|
|
55
|
-
.option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to (or env OPENCLAW_MODEL)', stringArg('--openclaw-model'))
|
|
56
|
-
.option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)', stringArg('--topic'))
|
|
57
|
-
.option('--state-dir <path>', 'Override the status-sync state directory (or env SWITCHBOT_STATUS_SYNC_HOME)', stringArg('--state-dir'))
|
|
58
|
-
.option('--force', 'Stop any existing status-sync bridge before starting a new one')
|
|
59
|
-
.addHelpText('after', `
|
|
60
|
-
Starts a detached child process that runs:
|
|
61
|
-
switchbot status-sync run ...
|
|
62
|
-
|
|
63
|
-
State files:
|
|
64
|
-
state.json process metadata (pid, startedAt, command)
|
|
65
|
-
stdout.log redirected stdout from the child process
|
|
66
|
-
stderr.log redirected stderr from the child process
|
|
67
|
-
|
|
68
|
-
Examples:
|
|
69
|
-
$ switchbot status-sync start --openclaw-model home-agent
|
|
70
|
-
$ OPENCLAW_TOKEN=abc OPENCLAW_MODEL=home-agent switchbot status-sync start
|
|
71
|
-
$ switchbot status-sync start --state-dir ~/.switchbot/custom-status-sync --force
|
|
72
|
-
`)
|
|
73
|
-
.action((options) => {
|
|
74
|
-
try {
|
|
75
|
-
const status = startStatusSync(options);
|
|
76
|
-
if (isJsonMode()) {
|
|
77
|
-
printJson(status);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
console.log(`Started status-sync (PID ${status.pid}).`);
|
|
81
|
-
console.log(`state: ${status.stateDir}`);
|
|
82
|
-
console.log(`stdout: ${status.stdoutLog}`);
|
|
83
|
-
console.log(`stderr: ${status.stderrLog}`);
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
handleError(error);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
statusSync
|
|
90
|
-
.command('stop')
|
|
91
|
-
.description('Stop the background status-sync bridge')
|
|
92
|
-
.option('--state-dir <path>', 'Override the status-sync state directory (or env SWITCHBOT_STATUS_SYNC_HOME)', stringArg('--state-dir'))
|
|
93
|
-
.action((options) => {
|
|
94
|
-
try {
|
|
95
|
-
const result = stopStatusSync(options);
|
|
96
|
-
if (isJsonMode()) {
|
|
97
|
-
printJson(result);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (result.stopped) {
|
|
101
|
-
console.log(`Stopped status-sync (PID ${result.pid}).`);
|
|
102
|
-
}
|
|
103
|
-
else if (result.stale) {
|
|
104
|
-
console.log(`Removed stale status-sync state for PID ${result.pid}.`);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
console.log('status-sync is not running');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
handleError(error);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
statusSync
|
|
115
|
-
.command('status')
|
|
116
|
-
.description('Inspect the current status-sync bridge state')
|
|
117
|
-
.option('--state-dir <path>', 'Override the status-sync state directory (or env SWITCHBOT_STATUS_SYNC_HOME)', stringArg('--state-dir'))
|
|
118
|
-
.action((options) => {
|
|
119
|
-
try {
|
|
120
|
-
const status = getStatusSyncStatus(options);
|
|
121
|
-
if (isJsonMode()) {
|
|
122
|
-
printJson(status);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
printHumanStatus(status);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
handleError(error);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|