@switchbot/openapi-cli 3.3.3 → 3.4.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 CHANGED
@@ -45,44 +45,14 @@ Under the hood every surface shares the same catalog, cache, and HMAC client —
45
45
 
46
46
  ## Table of contents
47
47
 
48
- - [Features](#features)
49
- - [Requirements](#requirements)
50
- - [Installation](#installation)
48
+ - [Features](#features) · [Requirements](#requirements) · [Installation](#installation)
51
49
  - [Quick start](#quick-start)
52
50
  - [Credentials](#credentials)
53
- - [Policy](#policy)
51
+ - [Policy](#policy) · [Rules engine](#rules-engine)
54
52
  - [Global options](#global-options)
55
- - [Commands](#commands)
56
- - [`config`](#config--credential-management)
57
- - [`devices`](#devices--list-status-control)
58
- - [`devices batch`](#devices-batch--bulk-commands)
59
- - [`devices watch`](#devices-watch--poll-status)
60
- - [`scenes`](#scenes--run-manual-scenes)
61
- - [`webhook`](#webhook--receive-device-events-over-http)
62
- - [`events`](#events--receive-device-events)
63
- - [`status-sync`](#status-sync--mqttopenclaw-bridge)
64
- - [`plan`](#plan--declarative-batch-operations)
65
- - [`mcp`](#mcp--model-context-protocol-server)
66
- - [`doctor`](#doctor--self-check)
67
- - [`health`](#health--runtime-health-report)
68
- - [`upgrade-check`](#upgrade-check--version-check)
69
- - [`quota`](#quota--api-request-counter)
70
- - [`history`](#history--audit-log)
71
- - [`catalog`](#catalog--device-type-catalog)
72
- - [`schema`](#schema--export-catalog-as-json)
73
- - [`capabilities`](#capabilities--cli-manifest)
74
- - [`cache`](#cache--inspect-and-clear-local-cache)
75
- - [`policy`](#policy--validate-scaffold-and-migrate-policyyaml)
76
- - [`daemon`](#daemon--background-rules-engine-process)
77
- - [`completion`](#completion--shell-tab-completion)
78
- - [Output modes](#output-modes)
79
- - [Cache](#cache)
80
- - [Exit codes & error codes](#exit-codes--error-codes)
81
- - [Environment variables](#environment-variables)
82
- - [Scripting examples](#scripting-examples)
83
- - [Development](#development)
84
- - [License](#license)
85
- - [References](#references)
53
+ - [Commands](#commands): [config](#config--credential-management) · [devices](#devices--list-status-control) · [scenes](#scenes--run-manual-scenes) · [webhook](#webhook--receive-device-events-over-http) · [events](#events--receive-device-events) · [status-sync](#status-sync--mqttopenclaw-bridge) · [daemon](#daemon--background-rules-engine-process) · [plan](#plan--declarative-batch-operations) · [mcp](#mcp--model-context-protocol-server) · [doctor](#doctor--self-check) · [health](#health--runtime-health-report) · [upgrade-check](#upgrade-check--version-check) · [quota](#quota--api-request-counter) · [history](#history--audit-log) · [catalog](#catalog--device-type-catalog) · [schema](#schema--export-catalog-as-json) · [capabilities](#capabilities--cli-manifest) · [cache](#cache--inspect-and-clear-local-cache) · [policy cmd](#policy--validate-scaffold-and-migrate-policyyaml) · [completion](#completion--shell-tab-completion)
54
+ - [Output modes](#output-modes) · [Cache](#cache) · [Exit codes](#exit-codes--error-codes) · [Environment variables](#environment-variables)
55
+ - [Scripting examples](#scripting-examples) · [Development](#development) · [License](#license)
86
56
 
87
57
  ---
88
58
 
@@ -93,7 +63,7 @@ Under the hood every surface shares the same catalog, cache, and HMAC client —
93
63
  - 🎨 **Dual output modes** — colorized tables by default; `--json` passthrough for `jq` and scripting
94
64
  - 🔐 **Secure credentials** — HMAC-SHA256 signed requests; config file written with `0600`; env-var override for CI
95
65
  - 🔍 **Dry-run mode** — preview every mutating request before it hits the API
96
- - 🧪 **Fully tested** — 1959 Vitest tests, mocked axios, zero network in CI
66
+ - 🧪 **Fully tested** — 2225 Vitest tests, mocked axios, zero network in CI
97
67
  - ⚡ **Shell completion** — Bash / Zsh / Fish / PowerShell
98
68
 
99
69
  ## Requirements
@@ -273,9 +243,9 @@ Five annotated starter files covering common setups live in
273
243
  With a policy.yaml (v0.2) you can declare automations that the CLI
274
244
  executes for you. Supported triggers: **MQTT** (device events),
275
245
  **cron** (schedule-driven), and **webhook** (local HTTP POST).
276
- Supported conditions: `time_between` (quiet hours) and `device_state`
277
- (live API check with per-tick dedup). Every fire is recorded in
278
- `~/.switchbot/audit.log`. `rules run` is long-running; use
246
+ Supported conditions: `time_between` (quiet hours), `device_state`
247
+ (live API check with per-tick dedup), and `llm` (AI decision — see
248
+ below). Every fire is recorded in `~/.switchbot/audit.log`. `rules run` is long-running; use
279
249
  `daemon start` / `daemon reload` for the managed background mode.
280
250
 
281
251
  **Actions** — each rule's `then` array accepts two action types:
@@ -296,71 +266,44 @@ then:
296
266
  template: '{"rule":"{{ rule.name }}","fired":"{{ rule.fired_at }}"}'
297
267
  ```
298
268
 
299
- ```bash
300
- # 1. Author rules under `automation.rules`. See examples/policies/automation.yaml
301
- # for a walkthrough covering the three trigger sources.
302
-
303
- # 2. Static-check before running.
304
- switchbot rules lint # exit 0 valid, 1 error
305
- switchbot rules list --json | jq . # structured summary
269
+ **LLM condition** — add an AI judgement step before actions fire:
306
270
 
307
- # 3. Inspect a single rule in full detail (trigger, conditions, actions,
308
- # cooldown, hysteresis, maxFiringsPerHour, suppressIfAlreadyDesired, last fired).
309
- switchbot rules explain "motion on"
310
- switchbot rules explain "motion on" --json
271
+ ```yaml
272
+ conditions:
273
+ - llm:
274
+ prompt: "Is the temperature above normal comfort range?"
275
+ provider: auto # auto | openai | anthropic
276
+ cache_ttl: 5m
277
+ budget:
278
+ max_calls_per_hour: 20
279
+ on_error: pass # fail | pass | skip
280
+ ```
311
281
 
312
- # 4. Run the engine. --dry-run overrides every rule into audit-only mode;
313
- # --max-firings bounds a demo session.
314
- switchbot rules run --dry-run --max-firings 5
282
+ Set `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. `rules lint` flags misconfigured LLM conditions.
315
283
 
316
- # 5. Edit policy.yaml in another shell, then hot-reload without restart.
317
- switchbot daemon reload # managed daemon reload
284
+ **Decision trace** — set `automation.audit.evaluate_trace: sampled` (or `full`) in `policy.yaml` to record every evaluation decision.
318
285
 
319
- # 6. Review recorded fires.
320
- switchbot rules tail --follow # stream rule-* audit lines
321
- switchbot rules replay --since 1h --json # per-rule fires/dries/throttled/errors
322
- switchbot rules summary # aggregate fires/errors per rule (24h window)
323
- switchbot rules last-fired -n 20 # 20 most recent fire entries
286
+ ```bash
287
+ switchbot rules lint # static check: exit 0 valid, 1 error
288
+ switchbot rules list --json | jq . # structured rule summary
289
+ switchbot rules explain "motion on" # trigger, conditions, actions, last fired
290
+ switchbot rules run --dry-run --max-firings 5 # run engine; --dry-run = audit only
291
+ switchbot daemon reload # hot-reload policy without restart
324
292
 
325
- # 7. Conflict and health analysis.
326
- switchbot rules conflicts # opposing actions, high-frequency MQTT,
327
- # destructive commands, quiet-hours gaps
328
- switchbot rules doctor --json # lint + conflicts combined; exit 0 when clean
293
+ switchbot rules tail --follow # stream rule-* audit lines
294
+ switchbot rules replay --since 1h --json # per-rule fires/dries/throttled/errors
295
+ switchbot rules summary # aggregate fires/errors (24h)
296
+ switchbot rules conflicts # opposing actions, destructive cmds, quiet-hours gaps
297
+ switchbot rules doctor --json # lint + conflicts; exit 0 when clean
329
298
 
330
- # 8. Scaffold a new rule from natural language (heuristic or LLM-backed).
331
299
  switchbot rules suggest --intent "turn off AC at 11pm"
332
- switchbot rules suggest --intent "if door opens and temp below 20 turn on heater" \
333
- --llm auto # routes complex intents to LLM automatically
334
- switchbot rules suggest --intent "..." --llm openai # explicit backend
335
- # Set OPENAI_API_KEY or ANTHROPIC_API_KEY; auto mode falls back to heuristic on failure
336
- ```
337
-
338
- `rules suggest` enforces several guardrails on LLM output so a model can't quietly arm
339
- something unsafe:
340
-
341
- - **`dry_run` is forced to `true`** on every LLM-generated rule. Review the output and
342
- flip it yourself before running the engine without `--dry-run`.
343
- - **Explicit overrides always win.** If you pass `--trigger`, the LLM's answer must match;
344
- a mismatch fails fast. Within the same trigger, mismatched `--event` / `--schedule` /
345
- `--days` / `--webhook-path` are rewritten to your value with a warning.
346
- - **`--llm` is enum-validated at the CLI** (`auto | openai | anthropic`) — junk values
347
- exit non-zero instead of falling through.
348
- - **Notify URLs must be `http://` or `https://`.** `rules lint` and the runtime both
349
- reject `file://`, `ftp://`, etc., so a generated webhook can't smuggle in a non-HTTP
350
- scheme.
300
+ switchbot rules suggest --intent "..." --llm auto # LLM-backed (OPENAI_API_KEY or ANTHROPIC_API_KEY)
351
301
 
352
- When `quiet_hours` is configured in `policy.yaml`, `rules conflicts` additionally flags event-driven (MQTT / webhook) rules that lack a `time_between` condition — they would fire uninhibited during the quiet window. The hint in each finding includes a ready-to-paste `time_between` condition to add.
353
-
354
- Webhook trigger token management:
355
-
356
- ```bash
357
- switchbot rules webhook-rotate-token # rotate the bearer token for webhook triggers
358
- switchbot rules webhook-show-token # print current token (creates one if absent)
302
+ switchbot rules trace-explain --rule "motion on" --last # why a rule fired/was blocked
303
+ switchbot rules simulate "motion on" --since 7d --json # replay without running the engine
359
304
  ```
360
305
 
361
- See [`docs/design/phase4-rules.md`](./docs/design/phase4-rules.md) for
362
- the engine's pipeline (subscribe → classify → match → conditions →
363
- throttle → action → audit).
306
+ LLM-generated rules always have `dry_run: true` — flip it yourself after review. Notify URLs must be `http://` or `https://`. See [`docs/design/phase4-rules.md`](./docs/design/phase4-rules.md) for the full pipeline.
364
307
 
365
308
  ## Global options
366
309
 
@@ -385,23 +328,11 @@ throttle → action → audit).
385
328
  - `-V`, `--version`: Print the CLI version.
386
329
  - `-h`, `--help`: Show help for any command or subcommand.
387
330
 
388
- Every subcommand supports `--help`, and most include a parameter-format reference and examples.
389
-
390
- ```bash
391
- switchbot --help
392
- switchbot devices command --help
393
- ```
394
-
395
- > **Tip — required-value flags and subcommands.** Flags like `--profile`, `--timeout`, `--max`, and `--interval` take a value. If you omit it, Commander will happily consume the next token — including a subcommand name. Since v2.2.1 the CLI rejects that eagerly (exit 2 with a clear error), but if you ever hit `unknown command 'list'` after something like `switchbot --profile list`, use the `--flag=value` form: `switchbot --profile=home devices list`.
331
+ Every subcommand supports `--help`. Use `--flag=value` form when a flag takes a value and is followed by a subcommand (e.g. `switchbot --profile=home devices list`).
396
332
 
397
333
  ### `--dry-run`
398
334
 
399
- Intercepts every non-GET request: the CLI prints the URL/body it would have
400
- sent, then exits `0` without contacting the API. `GET` requests (list, status,
401
- query) are still executed so you can preview the state involved. Dry-run also
402
- validates command names against the device catalog and rejects unknown commands
403
- (exit 2) when the device type has a known catalog entry. Commands sent to
404
- read-only sensors (e.g. Meter) are likewise rejected.
335
+ Intercepts every non-GET request: prints the URL/body it would have sent, then exits `0`. GET requests still execute. Also validates command names against the device catalog (exit 2 on unknown commands or read-only sensors).
405
336
 
406
337
  ```bash
407
338
  switchbot devices command ABC123 turnOn --dry-run
@@ -417,127 +348,44 @@ switchbot devices command ABC123 turnOn --dry-run
417
348
  switchbot config set-token <token> <secret> # Save to ~/.switchbot/config.json
418
349
  switchbot config show # Print current source + masked secret
419
350
  switchbot config list-profiles # List saved profiles
420
-
421
- # Print (or write) the recommended AI-agent profile template
422
- switchbot config agent-profile # print to stdout
423
- switchbot config agent-profile --write # write to ~/.switchbot/profiles/agent.json (mode 0600)
424
- switchbot config agent-profile --write --force # overwrite if it already exists
425
- switchbot config agent-profile --json # structured JSON envelope
351
+ switchbot config agent-profile --write # write recommended AI-agent profile (mode 0600)
426
352
  ```
427
353
 
428
354
  ### `devices` — list, status, control
429
355
 
430
356
  ```bash
431
357
  # List all physical devices and IR remote devices
432
- # Default columns (4): deviceId, deviceName, type, category
433
- # Pass --wide for the full 10-column operator view
434
- switchbot devices list
435
- switchbot devices ls # short alias for 'list'
436
- switchbot devices list --wide
358
+ switchbot devices list # default 4 columns: deviceId, deviceName, type, category
359
+ switchbot devices list --wide # full 10-column operator view
437
360
  switchbot devices list --json | jq '.deviceList[].deviceId'
438
-
439
- # IR remotes: type = remoteType (e.g. "TV"), category = "ir"
440
- # Physical: category = "physical"
441
361
  switchbot devices list --format=tsv --fields=deviceId,type,category
442
362
 
443
- # Filter devices by type / name / category / room (server-side filter keys)
444
- switchbot devices list --filter category=physical
445
- switchbot devices list --filter type=Bot
446
- switchbot devices list --filter name=living,category=physical
447
-
448
- # Filter operators: = (substring; exact for `category`), ~ (substring),
449
- # =/regex/ (case-insensitive regex). Clauses are AND-ed.
450
- switchbot devices list --filter 'name~living'
451
- switchbot devices list --filter 'type=/Hub.*/'
452
- switchbot devices list --filter 'name~office,type=/Bulb|Strip/'
363
+ # Filter by type / name / category / room
364
+ # Operators: = (substring; exact for category), ~ (substring), =/regex/; clauses AND-ed
365
+ switchbot devices list --filter 'type=Bot'
366
+ switchbot devices list --filter 'name~living,type=/Bulb|Strip/'
367
+ switchbot devices list --filter 'category=physical'
453
368
 
454
- # Filter by family / room (family & room info requires the platform source
455
- # header, which this CLI sends on every request)
456
- switchbot devices list --json | jq '.deviceList[] | select(.familyName == "Home")'
457
- switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | group_by(.familyName)'
458
-
459
- # Query real-time status of a physical device
369
+ # Query real-time status
460
370
  switchbot devices status <deviceId>
461
- switchbot devices status <deviceId> --json
371
+ switchbot devices status --ids ABC,DEF,GHI # batch status
372
+ switchbot devices status --ids ABC,DEF --fields power,battery --format jsonl
462
373
 
463
374
  # Resolve device by fuzzy name instead of ID (status, command, describe, expand, watch)
464
375
  switchbot devices status --name "Living Room AC"
465
376
  switchbot devices command --name "Office Light" turnOn
466
- switchbot devices describe --name "Kitchen Bot"
467
-
468
- # Batch status across multiple devices
469
- switchbot devices status --ids ABC,DEF,GHI
470
- switchbot devices status --ids ABC,DEF --fields power,battery # only show specific fields
471
- switchbot devices status --ids ABC,DEF --format jsonl # one JSON line per device
472
377
 
473
378
  # Send a control command
474
379
  switchbot devices command <deviceId> <cmd> [parameter] [--type command|customize]
475
380
 
476
- # Describe a specific device (1 API call): metadata + supported commands + status fields
477
- switchbot devices describe <deviceId>
478
- switchbot devices describe <deviceId> --json
479
-
480
- # Discover what's supported (offline reference, no API call)
481
- switchbot devices types # List all device types + IR remote types (incl. role column)
482
- switchbot devices commands <type> # Show commands, parameter formats, and status fields
483
- switchbot devices commands Bot
484
- switchbot devices commands "Smart Lock"
485
- switchbot devices commands curtain # Case-insensitive, substring match
381
+ # Offline reference (no API call)
382
+ switchbot devices types # all device types
383
+ switchbot devices commands <type> # commands, parameter formats, status fields
486
384
  ```
487
385
 
488
- #### Filter expressionsper-command reference
489
-
490
- Three commands accept `--filter`. They share one four-operator grammar,
491
- but each exposes its own key set:
386
+ Parameters for `setAll`, `setPosition`, `setMode`, `setBrightness`, and `setColor` are validated client-side (exit 2 on bad input). `setColor` accepts `R:G:B`, `#RRGGBB`, `#RGB`, and CSS names all normalize to `R:G:B`. Pass `--skip-param-validation` to bypass. Unknown deviceIds exit 2 by default; pass `--allow-unknown-device` for scripted pass-through.
492
387
 
493
- - `devices list`
494
- Operators: `=` (substring; **exact** for `category`), `!=` (negated),
495
- `~` (substring), `=/regex/` (case-insensitive regex).
496
- Keys: `type`, `name`, `category`, `room`.
497
- - `devices batch`
498
- Operators: same as `devices list`.
499
- Keys: `type`, `family`, `room`, `category`.
500
- - `events tail` / `events mqtt-tail`
501
- Operators: same (tail only; mqtt-tail uses `--topic` instead).
502
- Keys: `deviceId`, `type`.
503
-
504
- Clauses are comma-separated and AND-ed. No OR across clauses — use regex
505
- alternation (`=/A|B/`) for that. `category` is the one key that stays exact
506
- under `=` / `!=` to preserve `category=physical` / `category!=ir` semantics.
507
- A clause with an empty value (e.g. `name~`, `type=`) is rejected with exit 2 —
508
- the parser refuses to guess whether an empty value means "no constraint" or
509
- "match empty string". Drop the clause outright to remove the constraint.
510
-
511
- #### Parameter formats
512
-
513
- `parameter` is optional — omit it for commands like `turnOn`/`turnOff` (auto-defaults to `"default"`).
514
- Numeric-only and JSON-object parameters are auto-parsed; strings with colons / commas / semicolons pass through as-is.
515
-
516
- For the exact commands and parameter formats a specific device supports, query the built-in catalog:
517
-
518
- ```bash
519
- switchbot devices commands <type> # e.g. Bot, Curtain, "Smart Lock", "Robot Vacuum Cleaner S10"
520
- ```
521
-
522
- Generic parameter shapes (which one applies is decided by the device — see the catalog):
523
-
524
- | Shape | Example |
525
- | ------------------- | -------------------------------------------------------- |
526
- | _(none)_ | `devices command <id> turnOn` |
527
- | `<integer>` | `devices command <id> setBrightness 75` |
528
- | `<R:G:B>` | `devices command <id> setColor "255:0:0"` |
529
- | `<direction;angle>` | `devices command <id> setPosition "up;60"` |
530
- | `<a,b,c,…>` | `devices command <id> setAll "26,1,3,on"` |
531
- | `<json object>` | `'{"action":"sweep","param":{"fanLevel":2,"times":1}}'` |
532
- | Custom IR button | `devices command <id> MyButton --type customize` |
533
-
534
- Parameters for `setAll` (Air Conditioner), `setPosition` (Curtain / Blind Tilt), `setMode` (Relay Switch), `setBrightness` (dimmable lights), and `setColor` (Color Bulb / Strip Light / Ceiling Light) are validated client-side before the request — malformed shapes, out-of-range values, and JSON for CSV fields all fail fast with exit 2. `setColor` accepts `R:G:B`, `R,G,B`, `#RRGGBB`, `#RGB`, and CSS named colors (`red`, `blue`, …); all normalize to `R:G:B` before hitting the API. Pass `--skip-param-validation` to bypass (escape hatch — prefer fixing the argument). Command names are also case-normalized against the catalog (e.g. `turnon` is auto-corrected to `turnOn` with a stderr warning); unknown names still exit 2 with the supported-commands list.
535
-
536
- Unknown deviceIds (not in the local cache) exit 2 by default so `--dry-run` is a reliable pre-flight gate. Unknown command names and commands on read-only sensors are also rejected during dry-run when the device type has a catalog entry. Run `switchbot devices list` first, or pass `--allow-unknown-device` for scripted pass-through.
537
-
538
- Negative numeric parameters (e.g. `setBrightness -1` for a probe) are passed through to the command validator instead of being swallowed by the flag parser as an unknown option.
539
-
540
- For the complete per-device command reference, see the [SwitchBot API docs](https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands).
388
+ For per-device command and parameter details: `switchbot devices commands <type>` or the [SwitchBot API docs](https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands).
541
389
 
542
390
  #### `devices expand` — named flags for packed parameters
543
391
 
@@ -557,61 +405,46 @@ switchbot devices expand <blindId> setPosition --direction up --angle 50
557
405
 
558
406
  # Relay Switch — setMode
559
407
  switchbot devices expand <relayId> setMode --channel 1 --mode edge
408
+
409
+ # Color Bulb / Strip Light / Floor Lamp / Ceiling Light — setBrightness / setColor / setColorTemperature
410
+ switchbot devices expand <bulbId> setBrightness --brightness 80
411
+ switchbot devices expand <bulbId> setColor --color "#FF0000"
412
+ switchbot devices expand <bulbId> setColorTemperature --color-temp 4000
560
413
  ```
561
414
 
562
- Run `switchbot devices expand <id> <command> --help` to see the available flags for any device command. `expand` is only meaningful for multi-parameter commands (the four above); single-parameter commands like `setBrightness 50` or `setColor "#FF0000"` are already flag-free at the CLI level.
415
+ Run `switchbot devices expand <id> <command> --help` to see the available flags for any device command.
563
416
 
564
417
  #### `devices explain` — one-shot device summary
565
418
 
566
419
  ```bash
567
- # Metadata + supported commands + live status in one call
568
- switchbot devices explain <deviceId>
569
-
570
- # Skip live status fetch (catalog-only output, no API call)
571
- switchbot devices explain <deviceId> --no-live
420
+ switchbot devices explain <deviceId> # metadata + commands + live status
421
+ switchbot devices explain <deviceId> --no-live # catalog-only, no API call
572
422
  ```
573
423
 
574
- Returns a combined view: static catalog info (commands, parameters, status fields) merged with the current live status. For Hub devices, also lists connected child devices. Prefer this over separate `status` + `describe` calls.
575
-
576
424
  #### `devices meta` — local device metadata
577
425
 
578
426
  ```bash
579
427
  switchbot devices meta set <deviceId> --alias "Office Light"
580
- switchbot devices meta set <deviceId> --hide # hide from `devices list`
428
+ switchbot devices meta set <deviceId> --hide # hide from `devices list`
581
429
  switchbot devices meta get <deviceId>
582
- switchbot devices meta list # show all saved metadata
430
+ switchbot devices meta list
583
431
  switchbot devices meta clear <deviceId>
584
432
  ```
585
433
 
586
- Stores local annotations (alias, hidden flag, notes) in `~/.switchbot/device-meta.json`. The alias is used as a display name; `--show-hidden` on `devices list` reveals hidden devices.
434
+ Stores local annotations in `~/.switchbot/device-meta.json`. `--show-hidden` on `devices list` reveals hidden devices.
587
435
 
588
436
  #### `devices batch` — bulk commands
589
437
 
590
438
  ```bash
591
- # Send the same command to every device matching a filter
439
+ # Same command to every matching device
592
440
  switchbot devices batch turnOff --filter 'type=Bot'
593
441
  switchbot devices batch setBrightness 50 --filter 'type~Light,family=Living'
594
-
595
- # Explicit device IDs (comma-separated)
596
442
  switchbot devices batch turnOn --ids ID1,ID2,ID3
597
-
598
- # Pipe device IDs from `devices list`
599
443
  switchbot devices list --format=id --filter 'type=Bot' | switchbot devices batch toggle -
600
-
601
- # Destructive commands require --yes
602
- switchbot devices batch unlock --filter 'type=Smart Lock' --yes
603
-
604
- # Skip devices whose cached status is offline (default: off)
605
- switchbot devices batch turnOn --ids ID1,ID2 --skip-offline
606
-
607
- # --idempotency-key is an alias for --idempotency-key-prefix; both append -<deviceId>
608
- switchbot devices batch turnOn --ids ID1,ID2 --idempotency-key morning-lights
444
+ switchbot devices batch unlock --filter 'type=Smart Lock' --yes # destructive: requires --yes
609
445
  ```
610
446
 
611
- Sends the same command to many devices in one run. Filter grammar matches `devices list` (`=` substring, `~` substring, `=/regex/` regex — clauses AND-ed); supported keys here are `type`, `family`, `room`, `category`. Destructive commands (Smart Lock unlock, Garage Door Opener, etc.) require `--yes` to prevent accidents.
612
-
613
- `--skip-offline` reads from the local status cache only (no new API calls);
614
- skipped devices appear under `summary.skipped` with `skippedReason:'offline'`.
447
+ Filter keys: `type`, `family`, `room`, `category`. Skipped-offline devices appear under `summary.skipped` when `--skip-offline` is passed.
615
448
 
616
449
  ### `scenes` — run manual scenes
617
450
 
@@ -646,140 +479,39 @@ The CLI validates that `<url>` is an absolute `http://` or `https://` URL before
646
479
 
647
480
  ### `events` — receive device events
648
481
 
649
- Two subcommands cover the two ways SwitchBot can push state changes to you.
650
-
651
482
  #### `events tail` — local webhook receiver
652
483
 
653
484
  ```bash
654
- # Listen on port 3000 and print every incoming webhook POST
655
- switchbot events tail
656
-
657
- # Filter to one device
658
- switchbot events tail --filter deviceId=ABC123
659
-
660
- # Stop after 5 matching events
661
- switchbot events tail --filter 'type=WoMeter' --max 5
662
-
663
- # Stop after 10 minutes regardless of event count
664
- switchbot events tail --for 10m
665
-
666
- # Custom port / path
485
+ switchbot events tail # listen on port 3000
486
+ switchbot events tail --filter deviceId=ABC123 # filter to one device
487
+ switchbot events tail --filter 'type=WoMeter' --max 5 --for 10m
667
488
  switchbot events tail --port 8080 --path /hook --json
668
489
  ```
669
490
 
670
- Run `switchbot webhook setup https://your.host/hook` first to tell SwitchBot where to send events, then expose the local port via ngrok/cloudflared and point the webhook URL at it. `events tail` only runs the local receiver — tunnelling is up to you.
671
-
672
- Output (one JSON line per matched event):
673
-
674
- ```json
675
- { "t": "2024-01-01T12:00:00.000Z", "remote": "1.2.3.4:54321", "path": "/", "body": {...}, "matched": true }
676
- ```
677
-
678
- Filter keys: `deviceId`, `type`. Operators: `=` (substring), `~` (substring), `=/regex/` (case-insensitive regex). Clauses comma-separated and AND-ed.
491
+ Run `switchbot webhook setup https://your.host/hook` first. `events tail` only runs the local receiver — tunnelling (ngrok/cloudflared) is up to you.
679
492
 
680
493
  #### `events mqtt-tail` — real-time MQTT stream
681
494
 
682
495
  ```bash
683
- # Stream all shadow-update events (runs in foreground until Ctrl-C)
684
- switchbot events mqtt-tail
685
-
686
- # Filter to a topic subtree
687
- switchbot events mqtt-tail --topic 'switchbot/#'
688
-
689
- # Stop after 10 events
690
- switchbot events mqtt-tail --max 10 --json
691
-
692
- # Stop after a fixed duration (emits __session_start under --json before connect)
693
- switchbot events mqtt-tail --for 30s --json
694
- ```
695
-
696
- Connects to the SwitchBot MQTT service automatically using the same credentials configured for the REST API (`SWITCHBOT_TOKEN` + `SWITCHBOT_SECRET`). No additional MQTT configuration is required — the client certificates are provisioned on first use.
697
-
698
- Output (one JSON line per message):
699
-
700
- ```json
701
- { "t": "2024-01-01T12:00:00.000Z", "topic": "switchbot/abc123/status", "payload": {...} }
702
- ```
703
-
704
- This command runs in the foreground and streams events until you press Ctrl-C. To run it persistently in the background, use a process manager:
705
-
706
- ```bash
707
- # pm2
708
- pm2 start "switchbot events mqtt-tail --json" --name switchbot-events
709
-
710
- # nohup
711
- nohup switchbot events mqtt-tail --json >> ~/switchbot-events.log 2>&1 &
496
+ switchbot events mqtt-tail # stream all shadow events (Ctrl-C to stop)
497
+ switchbot events mqtt-tail --topic 'switchbot/#' # filter to topic subtree
498
+ switchbot events mqtt-tail --max 10 --for 30s --json
712
499
  ```
713
500
 
714
- Run `switchbot doctor` to verify MQTT credentials are configured correctly before connecting.
501
+ Credentials are provisioned automatically from the REST API config. Use `--sink` to route events to external services (`file`, `webhook`, `telegram`, `homeassistant`, `openclaw`) — see `switchbot events mqtt-tail --help` for details.
715
502
 
716
503
  ### `status-sync` — MQTT/OpenClaw bridge
717
504
 
718
- Use this command family when you want the CLI itself to own the lifecycle of a
719
- long-running bridge that forwards SwitchBot MQTT shadow events into an OpenClaw
720
- gateway. Internally it reuses `events mqtt-tail --sink openclaw`, but adds a
721
- stable command surface for foreground execution, background startup, status
722
- inspection, and shutdown.
505
+ Forwards SwitchBot MQTT shadow events into an OpenClaw gateway with stable lifecycle management.
723
506
 
724
507
  ```bash
725
- # Foreground mode for supervisors / containers
726
- switchbot status-sync run --openclaw-model home-agent
727
-
728
- # Background mode for a normal shell session
729
- switchbot status-sync start --openclaw-model home-agent
730
-
731
- # Inspect the current bridge
508
+ switchbot status-sync run --openclaw-model home-agent # foreground (for supervisors)
509
+ switchbot status-sync start --openclaw-model home-agent # background
732
510
  switchbot status-sync status --json
733
-
734
- # Stop the running bridge
735
511
  switchbot status-sync stop
736
512
  ```
737
513
 
738
- Required input:
739
-
740
- - `OPENCLAW_MODEL` or `--openclaw-model <id>`
741
- - `OPENCLAW_TOKEN` or `--openclaw-token <token>`
742
-
743
- Optional input:
744
-
745
- - `OPENCLAW_URL` or `--openclaw-url <url>`
746
- - `--topic <pattern>` to narrow the MQTT subscription
747
- - `SWITCHBOT_STATUS_SYNC_HOME` or `--state-dir <path>` for custom runtime state
748
-
749
- Background mode writes these files under the state directory:
750
-
751
- - `state.json` — current pid, start time, effective command
752
- - `stdout.log` — child stdout
753
- - `stderr.log` — child stderr
754
-
755
- Foreground vs background:
756
-
757
- - `status-sync run` keeps the bridge attached to the current terminal
758
- - `status-sync start` detaches the bridge and returns immediately
759
- - `status-sync status` reports whether the bridge is alive plus paths/logs
760
- - `status-sync stop` terminates the managed bridge process tree
761
-
762
- #### `mqtt-tail` sinks — route events to external services
763
-
764
- By default `mqtt-tail` prints JSONL to stdout. Use `--sink` (repeatable) to route events to one or more destinations instead:
765
-
766
- | Sink | Required flags |
767
- | --- | --- |
768
- | `stdout` | (default when no `--sink` given) |
769
- | `file` | `--sink-file <path>` — append JSONL |
770
- | `webhook` | `--webhook-url <url>` — HTTP POST each event |
771
- | `telegram` | `--telegram-token` (or `$TELEGRAM_TOKEN`), `--telegram-chat <chatId>` |
772
- | `homeassistant` | `--ha-url <url>` + `--ha-webhook-id` (no auth) or `--ha-token` (REST event API) |
773
-
774
- ```bash
775
- # Generic webhook (n8n, Make, etc.)
776
- switchbot events mqtt-tail --sink webhook --webhook-url https://n8n.local/hook/abc
777
-
778
- # Forward to Home Assistant via webhook trigger
779
- switchbot events mqtt-tail --sink homeassistant --ha-url http://homeassistant.local:8123 --ha-webhook-id switchbot
780
- ```
781
-
782
- Device state is also persisted to `~/.switchbot/device-history/<deviceId>.json` (latest + 100-entry ring buffer) regardless of sink configuration. This enables the `get_device_history` MCP tool to answer state queries without an API call.
514
+ Required: `OPENCLAW_MODEL` (or `--openclaw-model`) and `OPENCLAW_TOKEN`. Optional: `OPENCLAW_URL`, `--topic`, `--state-dir`. Background mode writes `state.json`, `stdout.log`, and `stderr.log` under the state directory.
783
515
 
784
516
  ### `daemon` — background rules-engine process
785
517
 
@@ -878,15 +610,20 @@ switchbot mcp serve
878
610
  ```
879
611
 
880
612
  Exposes MCP tools (`list_devices`, `describe_device`, `get_device_status`,
613
+ `get_device_history`, `query_device_history`, `aggregate_device_history`,
881
614
  `send_command`, `list_scenes`, `run_scene`, `search_catalog`,
882
615
  `account_overview`, `plan_suggest`, `plan_run`, `audit_query`,
883
616
  `audit_stats`, `policy_diff`, `policy_validate`, `policy_new`,
884
- `policy_migrate`, `rules_suggest`, `rule_notifications`) plus a
617
+ `policy_migrate`, `policy_add_rule`, `rules_suggest`, `rule_notifications`,
618
+ `rules_explain`, `rules_simulate`) plus a
885
619
  `switchbot://events` resource for real-time shadow updates.
886
620
  `rules_suggest` accepts an optional `llm` parameter (`openai | anthropic | auto`)
887
621
  to generate YAML for complex intents via an LLM backend.
888
622
  `rule_notifications` returns `rule-notify` audit entries, filterable by rule
889
623
  name, time range, channel, and result.
624
+ `rules_explain` returns the decision trace for a specific evaluation (why a rule
625
+ fired or was blocked); `rules_simulate` replays historical events against a rule
626
+ and reports would-fire / blocked / throttled outcomes.
890
627
  See [`docs/agent-guide.md`](./docs/agent-guide.md) for the full tool reference and safety rules (destructive-command guard).
891
628
 
892
629
  ### `doctor` — self-check
@@ -896,7 +633,7 @@ switchbot doctor
896
633
  switchbot doctor --json
897
634
  ```
898
635
 
899
- Runs local checks (Node version, credentials, profiles, catalog, cache, quota, clock, MQTT, policy, MCP, notify-connectivity) and exits 1 if any check fails. `warn` results exit 0. The MQTT check reports `ok` when REST credentials are configured (auto-provisioned on first use). The `notify-connectivity` check probes webhook URLs declared in `type: notify` actions. Use this to diagnose connectivity or config issues before running automation.
636
+ Runs local checks (Node version, credentials, profiles, catalog, catalog-schema, cache, quota, clock, MQTT, policy, MCP, keychain, path, inventory, audit, daemon, health, notify-connectivity, release-notes) and exits 1 if any check fails. `warn` results exit 0. The MQTT check reports `ok` when REST credentials are configured (auto-provisioned on first use). The `notify-connectivity` check probes webhook URLs declared in `type: notify` actions. Use this to diagnose connectivity or config issues before running automation.
900
637
 
901
638
  `--json` output includes `maturityScore` (0–100) and `maturityLabel` (`production-ready` / `mostly-ready` / `needs-work` / `not-ready`) to give an at-a-glance readiness rating:
902
639
 
@@ -927,71 +664,44 @@ Port conflicts are reported immediately with a clear hint to choose a different
927
664
  ### `upgrade-check` — version check
928
665
 
929
666
  ```bash
930
- switchbot upgrade-check # human output; exits 1 when update available
931
- switchbot upgrade-check --json # structured JSON output
932
- switchbot upgrade-check --timeout 5000 # custom registry timeout (ms)
933
- ```
934
-
935
- Queries the npm registry for the latest published version and compares it against the running version. When the registry's `dist-tags.latest` is itself a prerelease (e.g. `4.0.0-rc.1`), the check is skipped and the current version is treated as up-to-date — accidental prerelease tags don't trigger spurious upgrade prompts.
936
- `--json` output:
937
-
938
- ```json
939
- {
940
- "current": "3.3.2",
941
- "latest": "4.0.0",
942
- "upToDate": false,
943
- "updateAvailable": true,
944
- "breakingChange": true,
945
- "installCommand": "npm install -g @switchbot/openapi-cli@4.0.0"
946
- }
667
+ switchbot upgrade-check # exits 1 when update available
668
+ switchbot upgrade-check --json # {current, latest, upToDate, updateAvailable, breakingChange, installCommand}
947
669
  ```
948
670
 
949
- `breakingChange` is `true` when the latest major version is higher than the current — useful for agents or CI that need to distinguish breaking upgrades from patch releases.
950
-
951
671
  ### `quota` — API request counter
952
672
 
953
673
  ```bash
954
- switchbot quota status # today's usage + last 7 days
955
- switchbot quota reset # delete the counter file
674
+ switchbot quota status # today's usage + last 7 days (10,000/day limit)
675
+ switchbot quota reset
956
676
  ```
957
677
 
958
- Tracks daily API calls against the 10,000/day account limit. The counter is stored in `~/.switchbot/quota.json` and incremented on every mutating request. Pass `--no-quota` to skip tracking for a single run.
959
-
960
678
  ### `history` — audit log
961
679
 
962
680
  ```bash
963
- switchbot history show # recent entries (newest first)
964
- switchbot history show --limit 20 # last 20 entries
965
- switchbot history replay 7 # re-run entry #7
681
+ switchbot history show --limit 20
682
+ switchbot history replay 7 # re-run entry #7
966
683
  switchbot --json history show --limit 50 | jq '.entries[] | select(.result=="error")'
967
684
  ```
968
685
 
969
- Reads the JSONL audit log (`~/.switchbot/audit.log` by default; override with `--audit-log --audit-log-path <path>`). Each entry records the timestamp, command, device ID, result, and dry-run flag. `replay` re-runs the original command with the original arguments.
970
-
971
686
  ### `catalog` — device type catalog
972
687
 
973
688
  ```bash
974
- switchbot catalog show # all 42 built-in types
975
- switchbot catalog list # alias for `show`
689
+ switchbot catalog show # all built-in types
976
690
  switchbot catalog show Bot # one type
977
- switchbot catalog search Hub # fuzzy match across type / aliases / commands
978
- switchbot catalog diff # what a local overlay changes vs built-in
979
- switchbot catalog path # location of the local overlay file
980
- switchbot catalog refresh # reload local overlay (clears in-process cache)
691
+ switchbot catalog search Hub # fuzzy match
692
+ switchbot catalog diff # local overlay vs built-in
981
693
  ```
982
694
 
983
- The built-in catalog ships with the package. Create `~/.switchbot/catalog-overlay.json` to add, extend, or override type definitions without modifying the package.
695
+ Create `~/.switchbot/catalog-overlay.json` to extend or override type definitions without modifying the package.
984
696
 
985
697
  ### `schema` — export catalog as JSON
986
698
 
987
699
  ```bash
988
- switchbot schema export # all types as structured JSON
989
- switchbot schema export --type 'Strip Light' # one type
990
- switchbot schema export --role sensor # filter by role
700
+ switchbot schema export # all types
701
+ switchbot schema export --type 'Strip Light'
702
+ switchbot schema export --role sensor
991
703
  ```
992
704
 
993
- Exports the effective catalog in a machine-readable format. Pipe the output into an agent's system prompt or tool schema to give it a complete picture of controllable devices.
994
-
995
705
  ### `capabilities` — CLI manifest
996
706
 
997
707
  ```bash
@@ -999,127 +709,58 @@ switchbot capabilities --json
999
709
  switchbot capabilities --used --json # only types seen in the local cache
1000
710
  ```
1001
711
 
1002
- Prints a versioned JSON manifest describing available surfaces (CLI, MCP, MQTT, plan runner), commands, and environment variables. Every subcommand leaf now carries a `{mutating, consumesQuota, idempotencySupported, agentSafetyTier, verifiability, typicalLatencyMs}` block, and the top-level payload publishes a flat `commandMeta` path-keyed lookup so agents don't have to walk the tree. `--used` filters the per-type summary to devices actually present in the local cache (same semantics as `schema export --used`).
712
+ Prints a versioned manifest of surfaces, commands, and environment variables. Each command leaf includes `{mutating, consumesQuota, agentSafetyTier, typicalLatencyMs}`.
1003
713
 
1004
714
  ### `cache` — inspect and clear local cache
1005
715
 
1006
716
  ```bash
1007
- # Show cache status (paths, age, entry counts)
1008
- switchbot cache show
1009
-
1010
- # Clear everything
1011
- switchbot cache clear
1012
-
1013
- # Clear only the device-list cache or only the status cache
1014
- switchbot cache clear --key list
1015
- switchbot cache clear --key status
717
+ switchbot cache show # paths, age, entry counts
718
+ switchbot cache clear # clear everything
719
+ switchbot cache clear --key list # list cache only
720
+ switchbot cache clear --key status # status cache only
1016
721
  ```
1017
722
 
1018
723
  ### `policy` — validate, scaffold, and migrate policy.yaml
1019
724
 
1020
- Companion to the separate SwitchBot skill repository for third-party agent hosts. The skill reads behaviour (aliases, confirmations, quiet hours, audit path) from `policy.yaml`. This command group checks that file before the skill ever sees it, turning what used to be silent failures into line-accurate errors.
1021
-
1022
725
  ```bash
1023
- # Write a starter policy at the default location
1024
- switchbot policy new # writes to the resolved default policy path
1025
- switchbot policy new ./custom/policy.yaml --force
1026
-
1027
- # Validate (compiler-style errors with line:col + caret + hints)
1028
- switchbot policy validate
1029
- switchbot policy validate ./custom/policy.yaml
726
+ switchbot policy new # write a starter policy
727
+ switchbot policy validate # compiler-style errors (line:col + caret)
1030
728
  switchbot policy validate --json | jq '.data.errors'
1031
- switchbot policy validate --no-snippet # plain error list, no source preview
1032
-
1033
- # Report the schema version the file declares
1034
- switchbot policy migrate
1035
-
1036
- # Snapshot and restore the active policy
1037
- switchbot policy backup # write timestamped backup alongside policy file
1038
- switchbot policy backup --out ./backups/ # custom destination directory
1039
- switchbot policy restore <backup-file> # overwrite active policy from backup (auto-backups first)
729
+ switchbot policy migrate # upgrade v0.1 v0.2 in-place
730
+ switchbot policy backup # timestamped backup
731
+ switchbot policy restore <backup-file>
1040
732
  ```
1041
733
 
1042
- Path resolution order: positional `[path]` > `SWITCHBOT_POLICY_PATH` env var > default policy path.
1043
-
1044
- **Exit codes:** `0` valid / `1` invalid / `2` file-not-found / `3` yaml-parse / `4` internal / `5` file already exists (on `new`, overridden with `--force`) / `6` unsupported schema version (on `migrate`).
1045
-
1046
- Example — editing an alias without quoting the deviceId:
1047
-
1048
- ```console
1049
- $ switchbot policy validate
1050
- <policy-path>:14:11
1051
- 14 | bedroom light: 01-abc-12345
1052
- ^^^^^^^^^^^^^
1053
- error: /aliases/bedroom light does not match pattern ^[A-Z0-9]{2,}-[A-Z0-9-]+$
1054
- hint: paste the deviceId from `switchbot devices list --format=tsv`, e.g. 01-202407090924-26354212
1055
-
1056
- ✗ 1 error in <policy-path> (schema v0.1)
1057
- ```
1058
-
1059
- The default policy schema shipped with the CLI (`src/policy/schema/v0.2.json`) is mirrored as `examples/policy.schema.json` in the companion skill repo; a CI job on every push diffs the two to prevent drift.
734
+ Path resolution: positional `[path]` > `SWITCHBOT_POLICY_PATH` > default. Exit codes: `0` valid / `1` invalid / `2` missing / `3` yaml-parse / `4` internal / `5` exists (use `--force`) / `6` unsupported version.
1060
735
 
1061
736
  ## Output modes
1062
737
 
1063
738
  - **Default** — ANSI-colored tables for `list`/`status`, key-value tables for details.
1064
- - **`--json`** — raw API payload passthrough. Output is the exact JSON the SwitchBot API returned, ideal for `jq` and scripting. Errors are also JSON on stderr: `{ "error": { "code", "kind", "message", "hint?" } }`.
1065
- - **`--format=json`** — projected row view. Same JSON structure but built from the CLI's column model (`--fields` applies). Use this when you only want specific fields.
1066
- - **`--format=tsv|yaml|jsonl|id`** — tabular text formats; `--fields` filters columns.
739
+ - **`--json`** — raw API payload passthrough. Errors are also JSON on **stdout**: `{ "schemaVersion": "1.2", "error": { "code", "kind", "message", "hint?" } }`.
740
+ - **`--format=json`** — projected row view; `--fields` applies.
741
+ - **`--format=tsv|yaml|jsonl|id`** — tabular text formats.
1067
742
 
1068
743
  ```bash
1069
- # Raw API payload (--json)
1070
744
  switchbot devices list --json | jq '.deviceList[] | {id: .deviceId, name: .deviceName}'
1071
-
1072
- # Projected rows with field filter (--format)
1073
745
  switchbot devices list --format tsv --fields deviceId,deviceName,type,cloud
1074
746
  switchbot devices list --format id # one deviceId per line
1075
- switchbot devices status <id> --format yaml
1076
747
  ```
1077
748
 
1078
749
  ## Cache
1079
750
 
1080
- The CLI maintains two local disk caches under `~/.switchbot/`:
1081
-
1082
- - `devices.json`: Device metadata (id, name, type, category, hub, room…).
1083
- Default TTL: 1 hour.
1084
- - `status.json`: Per-device status bodies.
1085
- Default TTL: off (0).
751
+ Two local disk caches under `~/.switchbot/`:
1086
752
 
1087
- The device-list cache powers offline validation (command name checks, destructive-command guard) and the MCP server's `send_command` tool. It is refreshed automatically on every `devices list` call.
1088
-
1089
- ### Cache control flags
753
+ | Cache | Default TTL | Purpose |
754
+ |---|---|---|
755
+ | `devices.json` | 1 hour | device metadata; powers offline validation |
756
+ | `status.json` | off | per-device status; GC'd after 24h |
1090
757
 
1091
758
  ```bash
1092
- # Turn off all cache reads for one invocation
1093
- switchbot devices list --no-cache
1094
-
1095
- # Set both list and status TTL to 5 minutes
1096
- switchbot devices status <id> --cache 5m
1097
-
1098
- # Set TTLs independently
759
+ switchbot devices list --no-cache # bypass for one invocation
760
+ switchbot devices status <id> --cache 5m # set list + status TTL
1099
761
  switchbot devices status <id> --cache-list 2h --cache-status 30s
1100
-
1101
- # Disable only the list cache (keep status cache at its current TTL)
1102
- switchbot devices list --cache-list 0
1103
762
  ```
1104
763
 
1105
- ### Cache management commands
1106
-
1107
- ```bash
1108
- # Show paths, age, and entry counts
1109
- switchbot cache show
1110
-
1111
- # Clear all cached data
1112
- switchbot cache clear
1113
-
1114
- # Scope the clear to one store
1115
- switchbot cache clear --key list
1116
- switchbot cache clear --key status
1117
- ```
1118
-
1119
- ### Status-cache GC
1120
-
1121
- `status.json` entries are automatically evicted after 24 hours (or 10× the configured status TTL, whichever is longer), so the file cannot grow without bound even when the status cache is left enabled long-term.
1122
-
1123
764
  ## Exit codes & error codes
1124
765
 
1125
766
  - `0`: Success (including `--dry-run` intercept when validation passes).
@@ -1166,111 +807,22 @@ npm install
1166
807
 
1167
808
  npm run dev -- <args> # Run from TypeScript sources via tsx
1168
809
  npm run build # Compile to dist/
1169
- npm test # Run the Vitest suite (1959 tests)
810
+ npm test # Run the Vitest suite (2225 tests)
1170
811
  npm run test:watch # Watch mode
1171
812
  npm run test:coverage # Coverage report (v8, HTML + text)
1172
813
  ```
1173
814
 
1174
- ### Project layout
1175
-
1176
- ```text
1177
- src/
1178
- ├── index.ts # Commander entry; mounts all subcommands; global flags
1179
- ├── auth.ts # HMAC-SHA256 signature (token + t + nonce → sign)
1180
- ├── config.ts # Credential load/save; env > keychain > file priority
1181
- ├── api/client.ts # axios instance + request/response interceptors;
1182
- │ # --verbose / --dry-run / --timeout wiring
1183
- ├── credentials/
1184
- │ ├── keychain.ts # Credential store interface + OS backend selection
1185
- │ └── backends/ # macos.ts / linux.ts / windows.ts / file.ts
1186
- ├── devices/
1187
- │ ├── catalog.ts # Static device catalog (commands, params, status fields)
1188
- │ └── cache.ts # Disk + in-memory cache for device list and status
1189
- ├── install/
1190
- │ ├── steps.ts # Generic step runner with rollback support
1191
- │ ├── preflight.ts # Pre-flight checks (Node, npm, network, agent)
1192
- │ └── default-steps.ts # Concrete steps: credentials, keychain, policy, skill, doctor
1193
- ├── policy/
1194
- │ ├── validate.ts # Schema version dispatch + JSON Schema validation
1195
- │ ├── migrate.ts # v0.1 → v0.2 migration
1196
- │ ├── load.ts # YAML file loading + error handling
1197
- │ ├── add-rule.ts # Rule injection into automation.rules[]
1198
- │ ├── diff.ts # Structural + line diff
1199
- │ └── schema/v0.2.json # Authoritative v0.2 JSON Schema
1200
- ├── rules/
1201
- │ ├── engine.ts # Main orchestrator (MQTT + cron + webhook)
1202
- │ ├── matcher.ts # Trigger + condition matchers
1203
- │ ├── action.ts # Command renderer + executor
1204
- │ ├── throttle.ts # Per-rule throttle gate
1205
- │ ├── cron-scheduler.ts # 5-field cron + days filter
1206
- │ ├── webhook-listener.ts # HTTP listener (bearer token, localhost-only)
1207
- │ ├── pid-file.ts # Hot-reload via SIGHUP or sentinel file
1208
- │ ├── audit-query.ts # Audit log filtering + aggregation
1209
- │ ├── conflict-analyzer.ts # Static conflict detection (opposing actions,
1210
- │ │ # high-freq MQTT, destructive cmds, quiet-hours gaps)
1211
- │ ├── suggest.ts # Heuristic + LLM-backed rule YAML generation
1212
- │ ├── notify.ts # notify action executor (webhook / file / openclaw)
1213
- │ └── types.ts # Shared rule/trigger/condition/action types (CommandAction | NotifyAction)
1214
- ├── llm/
1215
- │ ├── index.ts # createLLMProvider factory + LLM_AUTO_THRESHOLD
1216
- │ ├── complexity.ts # Intent complexity scorer (0–10) for auto-routing
1217
- │ ├── rule-prompt.ts # System prompt builder (embeds v0.2 schema snippet)
1218
- │ └── providers/
1219
- │ ├── openai.ts # OpenAI-compatible provider (uses Node.js https)
1220
- │ └── anthropic.ts # Anthropic provider
1221
- ├── status-sync/
1222
- │ └── manager.ts # Spawn/stop logic, state file, OpenClaw bridge
1223
- ├── lib/
1224
- │ └── devices.ts # Shared logic: listDevices, describeDevice, isDestructiveCommand
1225
- ├── commands/
1226
- │ ├── auth.ts # `auth keychain` subcommand group
1227
- │ ├── config.ts
1228
- │ ├── devices.ts
1229
- │ ├── expand.ts # `devices expand` — semantic flag builder
1230
- │ ├── explain.ts # `devices explain` — one-shot device summary
1231
- │ ├── device-meta.ts # `devices meta` — local aliases / hide flags
1232
- │ ├── install.ts # `switchbot install` / `uninstall`
1233
- │ ├── policy.ts # `policy validate/new/migrate/diff/add-rule/backup/restore`
1234
- │ ├── rules.ts # `rules suggest/lint/list/explain/run/reload/tail/replay/
1235
- │ │ # conflicts/doctor/summary/last-fired/webhook-*`
1236
- │ ├── scenes.ts
1237
- │ ├── health.ts # `health check/serve` — report + HTTP endpoints
1238
- │ ├── upgrade-check.ts # `upgrade-check` — npm registry version check
1239
- │ ├── status-sync.ts # `status-sync run/start/stop/status`
1240
- │ ├── webhook.ts
1241
- │ ├── watch.ts # `devices watch <deviceId>`
1242
- │ ├── events.ts # `events tail` / `events mqtt-tail`
1243
- │ ├── mcp.ts # `mcp serve` (MCP stdio/HTTP server)
1244
- │ ├── plan.ts # `plan run/validate/suggest`
1245
- │ ├── cache.ts # `cache show/clear`
1246
- │ ├── history.ts # `history show/replay`
1247
- │ ├── quota.ts # `quota status/reset`
1248
- │ ├── catalog.ts # `catalog show/diff/path`
1249
- │ ├── schema.ts # `schema export`
1250
- │ ├── doctor.ts # `doctor`
1251
- │ ├── capabilities.ts # `capabilities`
1252
- │ └── completion.ts # `completion bash|zsh|fish|powershell`
1253
- └── utils/
1254
- ├── flags.ts # Global flag readers (isVerbose / isDryRun / getCacheMode / …)
1255
- ├── output.ts # printTable / printKeyValue / printJson / handleError
1256
- ├── format.ts # renderRows / filterFields / output-format dispatch
1257
- ├── audit.ts # JSONL audit log writer
1258
- └── quota.ts # Local daily-quota counter
1259
- tests/ # Vitest suite (1959 tests, mocked axios, no network)
1260
- ```
815
+ Source layout: `src/commands/` (one file per command group), `src/devices/` (catalog + cache), `src/rules/` (engine, matcher, throttle, audit), `src/policy/` (validate, migrate, schema), `src/llm/` (providers), `src/utils/` (output, format, flags). Tests are in `tests/` and mirror the `src/` structure.
1261
816
 
1262
817
  ### Release flow
1263
818
 
1264
- Releases are cut on tag push and published to npm by GitHub Actions:
1265
-
1266
819
  ```bash
1267
- npm version patch # bump version + create git tag
820
+ npm version patch # bump + create git tag
1268
821
  git push --follow-tags
822
+ # then: GitHub → Releases → Draft → Publish
1269
823
  ```
1270
824
 
1271
- Then on GitHub → **Releases → Draft a new release → select tag → Publish**. The `publish.yml` workflow runs tests, verifies the tag matches `package.json`, and publishes `@switchbot/openapi-cli` to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements).
1272
-
1273
- See [`docs/release-pipeline.md`](./docs/release-pipeline.md) for the full pre-publish and post-publish verification flow (local hooks → CI → `publish.yml` → `npm-published-smoke.yml`).
825
+ See [`docs/release-pipeline.md`](./docs/release-pipeline.md) for the full CI / publish verification flow.
1274
826
 
1275
827
  ## License
1276
828