@switchbot/openapi-cli 3.4.0 → 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.
Files changed (3) hide show
  1. package/README.md +110 -602
  2. package/dist/index.js +177 -40
  3. package/package.json +1 -1
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** — 2204 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
@@ -296,110 +266,44 @@ then:
296
266
  template: '{"rule":"{{ rule.name }}","fired":"{{ rule.fired_at }}"}'
297
267
  ```
298
268
 
299
- **LLM condition** — add an AI judgement step before actions fire. The engine calls the
300
- configured LLM provider, passes the prompt plus recent event context, and gates execution
301
- on the model's yes/no answer:
269
+ **LLM condition** — add an AI judgement step before actions fire:
302
270
 
303
271
  ```yaml
304
272
  conditions:
305
273
  - llm:
306
274
  prompt: "Is the temperature above normal comfort range?"
307
275
  provider: auto # auto | openai | anthropic
308
- cache_ttl: 5m # skip redundant calls for identical context
276
+ cache_ttl: 5m
309
277
  budget:
310
278
  max_calls_per_hour: 20
311
279
  on_error: pass # fail | pass | skip
312
280
  ```
313
281
 
314
- Set `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` (provider `auto` tries Anthropic first).
315
- `rules lint` flags misconfigured LLM conditions (no provider key, cache TTL too high for
316
- the trigger frequency, budget zero). Evaluation decisions are recorded in the trace log.
282
+ Set `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. `rules lint` flags misconfigured LLM conditions.
317
283
 
318
- **Decision trace** — enable `automation.audit.evaluate_trace` in `policy.yaml` to record
319
- every evaluation decision (why a rule fired or was blocked):
320
-
321
- ```yaml
322
- automation:
323
- audit:
324
- evaluate_trace: sampled # full | sampled | off (default: sampled)
325
- evaluate_retention_days: 7
326
- ```
284
+ **Decision trace** — set `automation.audit.evaluate_trace: sampled` (or `full`) in `policy.yaml` to record every evaluation decision.
327
285
 
328
286
  ```bash
329
- # 1. Author rules under `automation.rules`. See examples/policies/automation.yaml
330
- # for a walkthrough covering the three trigger sources.
331
-
332
- # 2. Static-check before running.
333
- switchbot rules lint # exit 0 valid, 1 error
334
- switchbot rules list --json | jq . # structured summary
335
-
336
- # 3. Inspect a single rule in full detail (trigger, conditions, actions,
337
- # cooldown, hysteresis, maxFiringsPerHour, suppressIfAlreadyDesired, last fired).
338
- switchbot rules explain "motion on"
339
- switchbot rules explain "motion on" --json
340
-
341
- # 4. Run the engine. --dry-run overrides every rule into audit-only mode;
342
- # --max-firings bounds a demo session.
343
- switchbot rules run --dry-run --max-firings 5
344
-
345
- # 5. Edit policy.yaml in another shell, then hot-reload without restart.
346
- switchbot daemon reload # managed daemon reload
347
-
348
- # 6. Review recorded fires.
349
- switchbot rules tail --follow # stream rule-* audit lines
350
- switchbot rules replay --since 1h --json # per-rule fires/dries/throttled/errors
351
- switchbot rules summary # aggregate fires/errors per rule (24h window)
352
- switchbot rules last-fired -n 20 # 20 most recent fire entries
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
353
292
 
354
- # 7. Conflict and health analysis.
355
- switchbot rules conflicts # opposing actions, high-frequency MQTT,
356
- # destructive commands, quiet-hours gaps
357
- 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
358
298
 
359
- # 8. Scaffold a new rule from natural language (heuristic or LLM-backed).
360
299
  switchbot rules suggest --intent "turn off AC at 11pm"
361
- switchbot rules suggest --intent "if door opens and temp below 20 turn on heater" \
362
- --llm auto # routes complex intents to LLM automatically
363
- switchbot rules suggest --intent "..." --llm openai # explicit backend
364
- # Set OPENAI_API_KEY or ANTHROPIC_API_KEY; auto mode falls back to heuristic on failure
365
-
366
- # 9. Explain why a specific evaluation fired or was blocked (requires evaluate_trace).
367
- switchbot rules trace-explain --rule "motion on" --last
368
- switchbot rules trace-explain --rule "motion on" --since 1h --json
369
- switchbot rules trace-explain <fireId> # single evaluation by ID
370
-
371
- # 10. Simulate a rule against historical events without running the engine.
372
- switchbot rules simulate "motion on" # replay last 24h from audit log
373
- switchbot rules simulate "motion on" --since 7d --json
374
- switchbot rules simulate policy.yaml --rule "night AC" --against events.jsonl
375
- ```
376
-
377
- `rules suggest` enforces several guardrails on LLM output so a model can't quietly arm
378
- something unsafe:
379
-
380
- - **`dry_run` is forced to `true`** on every LLM-generated rule. Review the output and
381
- flip it yourself before running the engine without `--dry-run`.
382
- - **Explicit overrides always win.** If you pass `--trigger`, the LLM's answer must match;
383
- a mismatch fails fast. Within the same trigger, mismatched `--event` / `--schedule` /
384
- `--days` / `--webhook-path` are rewritten to your value with a warning.
385
- - **`--llm` is enum-validated at the CLI** (`auto | openai | anthropic`) — junk values
386
- exit non-zero instead of falling through.
387
- - **Notify URLs must be `http://` or `https://`.** `rules lint` and the runtime both
388
- reject `file://`, `ftp://`, etc., so a generated webhook can't smuggle in a non-HTTP
389
- scheme.
390
-
391
- 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.
300
+ switchbot rules suggest --intent "..." --llm auto # LLM-backed (OPENAI_API_KEY or ANTHROPIC_API_KEY)
392
301
 
393
- Webhook trigger token management:
394
-
395
- ```bash
396
- switchbot rules webhook-rotate-token # rotate the bearer token for webhook triggers
397
- 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
398
304
  ```
399
305
 
400
- See [`docs/design/phase4-rules.md`](./docs/design/phase4-rules.md) for
401
- the engine's pipeline (subscribe → classify → match → conditions →
402
- 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.
403
307
 
404
308
  ## Global options
405
309
 
@@ -424,23 +328,11 @@ throttle → action → audit).
424
328
  - `-V`, `--version`: Print the CLI version.
425
329
  - `-h`, `--help`: Show help for any command or subcommand.
426
330
 
427
- Every subcommand supports `--help`, and most include a parameter-format reference and examples.
428
-
429
- ```bash
430
- switchbot --help
431
- switchbot devices command --help
432
- ```
433
-
434
- > **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`).
435
332
 
436
333
  ### `--dry-run`
437
334
 
438
- Intercepts every non-GET request: the CLI prints the URL/body it would have
439
- sent, then exits `0` without contacting the API. `GET` requests (list, status,
440
- query) are still executed so you can preview the state involved. Dry-run also
441
- validates command names against the device catalog and rejects unknown commands
442
- (exit 2) when the device type has a known catalog entry. Commands sent to
443
- 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).
444
336
 
445
337
  ```bash
446
338
  switchbot devices command ABC123 turnOn --dry-run
@@ -456,127 +348,44 @@ switchbot devices command ABC123 turnOn --dry-run
456
348
  switchbot config set-token <token> <secret> # Save to ~/.switchbot/config.json
457
349
  switchbot config show # Print current source + masked secret
458
350
  switchbot config list-profiles # List saved profiles
459
-
460
- # Print (or write) the recommended AI-agent profile template
461
- switchbot config agent-profile # print to stdout
462
- switchbot config agent-profile --write # write to ~/.switchbot/profiles/agent.json (mode 0600)
463
- switchbot config agent-profile --write --force # overwrite if it already exists
464
- switchbot config agent-profile --json # structured JSON envelope
351
+ switchbot config agent-profile --write # write recommended AI-agent profile (mode 0600)
465
352
  ```
466
353
 
467
354
  ### `devices` — list, status, control
468
355
 
469
356
  ```bash
470
357
  # List all physical devices and IR remote devices
471
- # Default columns (4): deviceId, deviceName, type, category
472
- # Pass --wide for the full 10-column operator view
473
- switchbot devices list
474
- switchbot devices ls # short alias for 'list'
475
- 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
476
360
  switchbot devices list --json | jq '.deviceList[].deviceId'
477
-
478
- # IR remotes: type = remoteType (e.g. "TV"), category = "ir"
479
- # Physical: category = "physical"
480
361
  switchbot devices list --format=tsv --fields=deviceId,type,category
481
362
 
482
- # Filter devices by type / name / category / room (server-side filter keys)
483
- switchbot devices list --filter category=physical
484
- switchbot devices list --filter type=Bot
485
- switchbot devices list --filter name=living,category=physical
486
-
487
- # Filter operators: = (substring; exact for `category`), ~ (substring),
488
- # =/regex/ (case-insensitive regex). Clauses are AND-ed.
489
- switchbot devices list --filter 'name~living'
490
- switchbot devices list --filter 'type=/Hub.*/'
491
- switchbot devices list --filter 'name~office,type=/Bulb|Strip/'
492
-
493
- # Filter by family / room (family & room info requires the platform source
494
- # header, which this CLI sends on every request)
495
- switchbot devices list --json | jq '.deviceList[] | select(.familyName == "Home")'
496
- switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | group_by(.familyName)'
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'
497
368
 
498
- # Query real-time status of a physical device
369
+ # Query real-time status
499
370
  switchbot devices status <deviceId>
500
- 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
501
373
 
502
374
  # Resolve device by fuzzy name instead of ID (status, command, describe, expand, watch)
503
375
  switchbot devices status --name "Living Room AC"
504
376
  switchbot devices command --name "Office Light" turnOn
505
- switchbot devices describe --name "Kitchen Bot"
506
-
507
- # Batch status across multiple devices
508
- switchbot devices status --ids ABC,DEF,GHI
509
- switchbot devices status --ids ABC,DEF --fields power,battery # only show specific fields
510
- switchbot devices status --ids ABC,DEF --format jsonl # one JSON line per device
511
377
 
512
378
  # Send a control command
513
379
  switchbot devices command <deviceId> <cmd> [parameter] [--type command|customize]
514
380
 
515
- # Describe a specific device (1 API call): metadata + supported commands + status fields
516
- switchbot devices describe <deviceId>
517
- switchbot devices describe <deviceId> --json
518
-
519
- # Discover what's supported (offline reference, no API call)
520
- switchbot devices types # List all device types + IR remote types (incl. role column)
521
- switchbot devices commands <type> # Show commands, parameter formats, and status fields
522
- switchbot devices commands Bot
523
- switchbot devices commands "Smart Lock"
524
- 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
525
384
  ```
526
385
 
527
- #### Filter expressionsper-command reference
528
-
529
- Three commands accept `--filter`. They share one four-operator grammar,
530
- but each exposes its own key set:
531
-
532
- - `devices list`
533
- Operators: `=` (substring; **exact** for `category`), `!=` (negated),
534
- `~` (substring), `=/regex/` (case-insensitive regex).
535
- Keys: `type`, `name`, `category`, `room`.
536
- - `devices batch`
537
- Operators: same as `devices list`.
538
- Keys: `type`, `family`, `room`, `category`.
539
- - `events tail` / `events mqtt-tail`
540
- Operators: same (tail only; mqtt-tail uses `--topic` instead).
541
- Keys: `deviceId`, `type`.
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.
542
387
 
543
- Clauses are comma-separated and AND-ed. No OR across clauses use regex
544
- alternation (`=/A|B/`) for that. `category` is the one key that stays exact
545
- under `=` / `!=` to preserve `category=physical` / `category!=ir` semantics.
546
- A clause with an empty value (e.g. `name~`, `type=`) is rejected with exit 2 —
547
- the parser refuses to guess whether an empty value means "no constraint" or
548
- "match empty string". Drop the clause outright to remove the constraint.
549
-
550
- #### Parameter formats
551
-
552
- `parameter` is optional — omit it for commands like `turnOn`/`turnOff` (auto-defaults to `"default"`).
553
- Numeric-only and JSON-object parameters are auto-parsed; strings with colons / commas / semicolons pass through as-is.
554
-
555
- For the exact commands and parameter formats a specific device supports, query the built-in catalog:
556
-
557
- ```bash
558
- switchbot devices commands <type> # e.g. Bot, Curtain, "Smart Lock", "Robot Vacuum Cleaner S10"
559
- ```
560
-
561
- Generic parameter shapes (which one applies is decided by the device — see the catalog):
562
-
563
- | Shape | Example |
564
- | ------------------- | -------------------------------------------------------- |
565
- | _(none)_ | `devices command <id> turnOn` |
566
- | `<integer>` | `devices command <id> setBrightness 75` |
567
- | `<R:G:B>` | `devices command <id> setColor "255:0:0"` |
568
- | `<direction;angle>` | `devices command <id> setPosition "up;60"` |
569
- | `<a,b,c,…>` | `devices command <id> setAll "26,1,3,on"` |
570
- | `<json object>` | `'{"action":"sweep","param":{"fanLevel":2,"times":1}}'` |
571
- | Custom IR button | `devices command <id> MyButton --type customize` |
572
-
573
- 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.
574
-
575
- 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.
576
-
577
- 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.
578
-
579
- 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).
580
389
 
581
390
  #### `devices expand` — named flags for packed parameters
582
391
 
@@ -596,61 +405,46 @@ switchbot devices expand <blindId> setPosition --direction up --angle 50
596
405
 
597
406
  # Relay Switch — setMode
598
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
599
413
  ```
600
414
 
601
- 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.
602
416
 
603
417
  #### `devices explain` — one-shot device summary
604
418
 
605
419
  ```bash
606
- # Metadata + supported commands + live status in one call
607
- switchbot devices explain <deviceId>
608
-
609
- # Skip live status fetch (catalog-only output, no API call)
610
- 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
611
422
  ```
612
423
 
613
- 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.
614
-
615
424
  #### `devices meta` — local device metadata
616
425
 
617
426
  ```bash
618
427
  switchbot devices meta set <deviceId> --alias "Office Light"
619
- switchbot devices meta set <deviceId> --hide # hide from `devices list`
428
+ switchbot devices meta set <deviceId> --hide # hide from `devices list`
620
429
  switchbot devices meta get <deviceId>
621
- switchbot devices meta list # show all saved metadata
430
+ switchbot devices meta list
622
431
  switchbot devices meta clear <deviceId>
623
432
  ```
624
433
 
625
- 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.
626
435
 
627
436
  #### `devices batch` — bulk commands
628
437
 
629
438
  ```bash
630
- # Send the same command to every device matching a filter
439
+ # Same command to every matching device
631
440
  switchbot devices batch turnOff --filter 'type=Bot'
632
441
  switchbot devices batch setBrightness 50 --filter 'type~Light,family=Living'
633
-
634
- # Explicit device IDs (comma-separated)
635
442
  switchbot devices batch turnOn --ids ID1,ID2,ID3
636
-
637
- # Pipe device IDs from `devices list`
638
443
  switchbot devices list --format=id --filter 'type=Bot' | switchbot devices batch toggle -
639
-
640
- # Destructive commands require --yes
641
- switchbot devices batch unlock --filter 'type=Smart Lock' --yes
642
-
643
- # Skip devices whose cached status is offline (default: off)
644
- switchbot devices batch turnOn --ids ID1,ID2 --skip-offline
645
-
646
- # --idempotency-key is an alias for --idempotency-key-prefix; both append -<deviceId>
647
- switchbot devices batch turnOn --ids ID1,ID2 --idempotency-key morning-lights
444
+ switchbot devices batch unlock --filter 'type=Smart Lock' --yes # destructive: requires --yes
648
445
  ```
649
446
 
650
- 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.
651
-
652
- `--skip-offline` reads from the local status cache only (no new API calls);
653
- 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.
654
448
 
655
449
  ### `scenes` — run manual scenes
656
450
 
@@ -685,140 +479,39 @@ The CLI validates that `<url>` is an absolute `http://` or `https://` URL before
685
479
 
686
480
  ### `events` — receive device events
687
481
 
688
- Two subcommands cover the two ways SwitchBot can push state changes to you.
689
-
690
482
  #### `events tail` — local webhook receiver
691
483
 
692
484
  ```bash
693
- # Listen on port 3000 and print every incoming webhook POST
694
- switchbot events tail
695
-
696
- # Filter to one device
697
- switchbot events tail --filter deviceId=ABC123
698
-
699
- # Stop after 5 matching events
700
- switchbot events tail --filter 'type=WoMeter' --max 5
701
-
702
- # Stop after 10 minutes regardless of event count
703
- switchbot events tail --for 10m
704
-
705
- # 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
706
488
  switchbot events tail --port 8080 --path /hook --json
707
489
  ```
708
490
 
709
- 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.
710
-
711
- Output (one JSON line per matched event):
712
-
713
- ```json
714
- { "t": "2024-01-01T12:00:00.000Z", "remote": "1.2.3.4:54321", "path": "/", "body": {...}, "matched": true }
715
- ```
716
-
717
- 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.
718
492
 
719
493
  #### `events mqtt-tail` — real-time MQTT stream
720
494
 
721
495
  ```bash
722
- # Stream all shadow-update events (runs in foreground until Ctrl-C)
723
- switchbot events mqtt-tail
724
-
725
- # Filter to a topic subtree
726
- switchbot events mqtt-tail --topic 'switchbot/#'
727
-
728
- # Stop after 10 events
729
- switchbot events mqtt-tail --max 10 --json
730
-
731
- # Stop after a fixed duration (emits __session_start under --json before connect)
732
- switchbot events mqtt-tail --for 30s --json
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
733
499
  ```
734
500
 
735
- 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.
736
-
737
- Output (one JSON line per message):
738
-
739
- ```json
740
- { "t": "2024-01-01T12:00:00.000Z", "topic": "switchbot/abc123/status", "payload": {...} }
741
- ```
742
-
743
- 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:
744
-
745
- ```bash
746
- # pm2
747
- pm2 start "switchbot events mqtt-tail --json" --name switchbot-events
748
-
749
- # nohup
750
- nohup switchbot events mqtt-tail --json >> ~/switchbot-events.log 2>&1 &
751
- ```
752
-
753
- 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.
754
502
 
755
503
  ### `status-sync` — MQTT/OpenClaw bridge
756
504
 
757
- Use this command family when you want the CLI itself to own the lifecycle of a
758
- long-running bridge that forwards SwitchBot MQTT shadow events into an OpenClaw
759
- gateway. Internally it reuses `events mqtt-tail --sink openclaw`, but adds a
760
- stable command surface for foreground execution, background startup, status
761
- inspection, and shutdown.
505
+ Forwards SwitchBot MQTT shadow events into an OpenClaw gateway with stable lifecycle management.
762
506
 
763
507
  ```bash
764
- # Foreground mode for supervisors / containers
765
- switchbot status-sync run --openclaw-model home-agent
766
-
767
- # Background mode for a normal shell session
768
- switchbot status-sync start --openclaw-model home-agent
769
-
770
- # 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
771
510
  switchbot status-sync status --json
772
-
773
- # Stop the running bridge
774
511
  switchbot status-sync stop
775
512
  ```
776
513
 
777
- Required input:
778
-
779
- - `OPENCLAW_MODEL` or `--openclaw-model <id>`
780
- - `OPENCLAW_TOKEN` or `--openclaw-token <token>`
781
-
782
- Optional input:
783
-
784
- - `OPENCLAW_URL` or `--openclaw-url <url>`
785
- - `--topic <pattern>` to narrow the MQTT subscription
786
- - `SWITCHBOT_STATUS_SYNC_HOME` or `--state-dir <path>` for custom runtime state
787
-
788
- Background mode writes these files under the state directory:
789
-
790
- - `state.json` — current pid, start time, effective command
791
- - `stdout.log` — child stdout
792
- - `stderr.log` — child stderr
793
-
794
- Foreground vs background:
795
-
796
- - `status-sync run` keeps the bridge attached to the current terminal
797
- - `status-sync start` detaches the bridge and returns immediately
798
- - `status-sync status` reports whether the bridge is alive plus paths/logs
799
- - `status-sync stop` terminates the managed bridge process tree
800
-
801
- #### `mqtt-tail` sinks — route events to external services
802
-
803
- By default `mqtt-tail` prints JSONL to stdout. Use `--sink` (repeatable) to route events to one or more destinations instead:
804
-
805
- | Sink | Required flags |
806
- | --- | --- |
807
- | `stdout` | (default when no `--sink` given) |
808
- | `file` | `--sink-file <path>` — append JSONL |
809
- | `webhook` | `--webhook-url <url>` — HTTP POST each event |
810
- | `telegram` | `--telegram-token` (or `$TELEGRAM_TOKEN`), `--telegram-chat <chatId>` |
811
- | `homeassistant` | `--ha-url <url>` + `--ha-webhook-id` (no auth) or `--ha-token` (REST event API) |
812
-
813
- ```bash
814
- # Generic webhook (n8n, Make, etc.)
815
- switchbot events mqtt-tail --sink webhook --webhook-url https://n8n.local/hook/abc
816
-
817
- # Forward to Home Assistant via webhook trigger
818
- switchbot events mqtt-tail --sink homeassistant --ha-url http://homeassistant.local:8123 --ha-webhook-id switchbot
819
- ```
820
-
821
- 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.
822
515
 
823
516
  ### `daemon` — background rules-engine process
824
517
 
@@ -917,10 +610,11 @@ switchbot mcp serve
917
610
  ```
918
611
 
919
612
  Exposes MCP tools (`list_devices`, `describe_device`, `get_device_status`,
613
+ `get_device_history`, `query_device_history`, `aggregate_device_history`,
920
614
  `send_command`, `list_scenes`, `run_scene`, `search_catalog`,
921
615
  `account_overview`, `plan_suggest`, `plan_run`, `audit_query`,
922
616
  `audit_stats`, `policy_diff`, `policy_validate`, `policy_new`,
923
- `policy_migrate`, `rules_suggest`, `rule_notifications`,
617
+ `policy_migrate`, `policy_add_rule`, `rules_suggest`, `rule_notifications`,
924
618
  `rules_explain`, `rules_simulate`) plus a
925
619
  `switchbot://events` resource for real-time shadow updates.
926
620
  `rules_suggest` accepts an optional `llm` parameter (`openai | anthropic | auto`)
@@ -939,7 +633,7 @@ switchbot doctor
939
633
  switchbot doctor --json
940
634
  ```
941
635
 
942
- 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.
943
637
 
944
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:
945
639
 
@@ -970,71 +664,44 @@ Port conflicts are reported immediately with a clear hint to choose a different
970
664
  ### `upgrade-check` — version check
971
665
 
972
666
  ```bash
973
- switchbot upgrade-check # human output; exits 1 when update available
974
- switchbot upgrade-check --json # structured JSON output
975
- switchbot upgrade-check --timeout 5000 # custom registry timeout (ms)
976
- ```
977
-
978
- 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.
979
- `--json` output:
980
-
981
- ```json
982
- {
983
- "current": "3.3.2",
984
- "latest": "4.0.0",
985
- "upToDate": false,
986
- "updateAvailable": true,
987
- "breakingChange": true,
988
- "installCommand": "npm install -g @switchbot/openapi-cli@4.0.0"
989
- }
667
+ switchbot upgrade-check # exits 1 when update available
668
+ switchbot upgrade-check --json # {current, latest, upToDate, updateAvailable, breakingChange, installCommand}
990
669
  ```
991
670
 
992
- `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.
993
-
994
671
  ### `quota` — API request counter
995
672
 
996
673
  ```bash
997
- switchbot quota status # today's usage + last 7 days
998
- 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
999
676
  ```
1000
677
 
1001
- 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.
1002
-
1003
678
  ### `history` — audit log
1004
679
 
1005
680
  ```bash
1006
- switchbot history show # recent entries (newest first)
1007
- switchbot history show --limit 20 # last 20 entries
1008
- switchbot history replay 7 # re-run entry #7
681
+ switchbot history show --limit 20
682
+ switchbot history replay 7 # re-run entry #7
1009
683
  switchbot --json history show --limit 50 | jq '.entries[] | select(.result=="error")'
1010
684
  ```
1011
685
 
1012
- 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.
1013
-
1014
686
  ### `catalog` — device type catalog
1015
687
 
1016
688
  ```bash
1017
- switchbot catalog show # all 42 built-in types
1018
- switchbot catalog list # alias for `show`
689
+ switchbot catalog show # all built-in types
1019
690
  switchbot catalog show Bot # one type
1020
- switchbot catalog search Hub # fuzzy match across type / aliases / commands
1021
- switchbot catalog diff # what a local overlay changes vs built-in
1022
- switchbot catalog path # location of the local overlay file
1023
- 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
1024
693
  ```
1025
694
 
1026
- 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.
1027
696
 
1028
697
  ### `schema` — export catalog as JSON
1029
698
 
1030
699
  ```bash
1031
- switchbot schema export # all types as structured JSON
1032
- switchbot schema export --type 'Strip Light' # one type
1033
- 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
1034
703
  ```
1035
704
 
1036
- 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.
1037
-
1038
705
  ### `capabilities` — CLI manifest
1039
706
 
1040
707
  ```bash
@@ -1042,127 +709,58 @@ switchbot capabilities --json
1042
709
  switchbot capabilities --used --json # only types seen in the local cache
1043
710
  ```
1044
711
 
1045
- 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}`.
1046
713
 
1047
714
  ### `cache` — inspect and clear local cache
1048
715
 
1049
716
  ```bash
1050
- # Show cache status (paths, age, entry counts)
1051
- switchbot cache show
1052
-
1053
- # Clear everything
1054
- switchbot cache clear
1055
-
1056
- # Clear only the device-list cache or only the status cache
1057
- switchbot cache clear --key list
1058
- 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
1059
721
  ```
1060
722
 
1061
723
  ### `policy` — validate, scaffold, and migrate policy.yaml
1062
724
 
1063
- 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.
1064
-
1065
725
  ```bash
1066
- # Write a starter policy at the default location
1067
- switchbot policy new # writes to the resolved default policy path
1068
- switchbot policy new ./custom/policy.yaml --force
1069
-
1070
- # Validate (compiler-style errors with line:col + caret + hints)
1071
- switchbot policy validate
1072
- switchbot policy validate ./custom/policy.yaml
726
+ switchbot policy new # write a starter policy
727
+ switchbot policy validate # compiler-style errors (line:col + caret)
1073
728
  switchbot policy validate --json | jq '.data.errors'
1074
- switchbot policy validate --no-snippet # plain error list, no source preview
1075
-
1076
- # Report the schema version the file declares
1077
- switchbot policy migrate
1078
-
1079
- # Snapshot and restore the active policy
1080
- switchbot policy backup # write timestamped backup alongside policy file
1081
- switchbot policy backup --out ./backups/ # custom destination directory
1082
- 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>
1083
732
  ```
1084
733
 
1085
- Path resolution order: positional `[path]` > `SWITCHBOT_POLICY_PATH` env var > default policy path.
1086
-
1087
- **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`).
1088
-
1089
- Example — editing an alias without quoting the deviceId:
1090
-
1091
- ```console
1092
- $ switchbot policy validate
1093
- <policy-path>:14:11
1094
- 14 | bedroom light: 01-abc-12345
1095
- ^^^^^^^^^^^^^
1096
- error: /aliases/bedroom light does not match pattern ^[A-Z0-9]{2,}-[A-Z0-9-]+$
1097
- hint: paste the deviceId from `switchbot devices list --format=tsv`, e.g. 01-202407090924-26354212
1098
-
1099
- ✗ 1 error in <policy-path> (schema v0.1)
1100
- ```
1101
-
1102
- 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.
1103
735
 
1104
736
  ## Output modes
1105
737
 
1106
738
  - **Default** — ANSI-colored tables for `list`/`status`, key-value tables for details.
1107
- - **`--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?" } }`.
1108
- - **`--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.
1109
- - **`--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.
1110
742
 
1111
743
  ```bash
1112
- # Raw API payload (--json)
1113
744
  switchbot devices list --json | jq '.deviceList[] | {id: .deviceId, name: .deviceName}'
1114
-
1115
- # Projected rows with field filter (--format)
1116
745
  switchbot devices list --format tsv --fields deviceId,deviceName,type,cloud
1117
746
  switchbot devices list --format id # one deviceId per line
1118
- switchbot devices status <id> --format yaml
1119
747
  ```
1120
748
 
1121
749
  ## Cache
1122
750
 
1123
- The CLI maintains two local disk caches under `~/.switchbot/`:
1124
-
1125
- - `devices.json`: Device metadata (id, name, type, category, hub, room…).
1126
- Default TTL: 1 hour.
1127
- - `status.json`: Per-device status bodies.
1128
- Default TTL: off (0).
751
+ Two local disk caches under `~/.switchbot/`:
1129
752
 
1130
- 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.
1131
-
1132
- ### 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 |
1133
757
 
1134
758
  ```bash
1135
- # Turn off all cache reads for one invocation
1136
- switchbot devices list --no-cache
1137
-
1138
- # Set both list and status TTL to 5 minutes
1139
- switchbot devices status <id> --cache 5m
1140
-
1141
- # Set TTLs independently
759
+ switchbot devices list --no-cache # bypass for one invocation
760
+ switchbot devices status <id> --cache 5m # set list + status TTL
1142
761
  switchbot devices status <id> --cache-list 2h --cache-status 30s
1143
-
1144
- # Disable only the list cache (keep status cache at its current TTL)
1145
- switchbot devices list --cache-list 0
1146
762
  ```
1147
763
 
1148
- ### Cache management commands
1149
-
1150
- ```bash
1151
- # Show paths, age, and entry counts
1152
- switchbot cache show
1153
-
1154
- # Clear all cached data
1155
- switchbot cache clear
1156
-
1157
- # Scope the clear to one store
1158
- switchbot cache clear --key list
1159
- switchbot cache clear --key status
1160
- ```
1161
-
1162
- ### Status-cache GC
1163
-
1164
- `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.
1165
-
1166
764
  ## Exit codes & error codes
1167
765
 
1168
766
  - `0`: Success (including `--dry-run` intercept when validation passes).
@@ -1209,112 +807,22 @@ npm install
1209
807
 
1210
808
  npm run dev -- <args> # Run from TypeScript sources via tsx
1211
809
  npm run build # Compile to dist/
1212
- npm test # Run the Vitest suite (2204 tests)
810
+ npm test # Run the Vitest suite (2225 tests)
1213
811
  npm run test:watch # Watch mode
1214
812
  npm run test:coverage # Coverage report (v8, HTML + text)
1215
813
  ```
1216
814
 
1217
- ### Project layout
1218
-
1219
- ```text
1220
- src/
1221
- ├── index.ts # Commander entry; mounts all subcommands; global flags
1222
- ├── auth.ts # HMAC-SHA256 signature (token + t + nonce → sign)
1223
- ├── config.ts # Credential load/save; env > keychain > file priority
1224
- ├── api/client.ts # axios instance + request/response interceptors;
1225
- │ # --verbose / --dry-run / --timeout wiring
1226
- ├── credentials/
1227
- │ ├── keychain.ts # Credential store interface + OS backend selection
1228
- │ └── backends/ # macos.ts / linux.ts / windows.ts / file.ts
1229
- ├── devices/
1230
- │ ├── catalog.ts # Static device catalog (commands, params, status fields)
1231
- │ └── cache.ts # Disk + in-memory cache for device list and status
1232
- ├── install/
1233
- │ ├── steps.ts # Generic step runner with rollback support
1234
- │ ├── preflight.ts # Pre-flight checks (Node, npm, network, agent)
1235
- │ └── default-steps.ts # Concrete steps: credentials, keychain, policy, skill, doctor
1236
- ├── policy/
1237
- │ ├── validate.ts # Schema version dispatch + JSON Schema validation
1238
- │ ├── migrate.ts # v0.1 → v0.2 migration
1239
- │ ├── load.ts # YAML file loading + error handling
1240
- │ ├── add-rule.ts # Rule injection into automation.rules[]
1241
- │ ├── diff.ts # Structural + line diff
1242
- │ └── schema/v0.2.json # Authoritative v0.2 JSON Schema
1243
- ├── rules/
1244
- │ ├── engine.ts # Main orchestrator (MQTT + cron + webhook)
1245
- │ ├── matcher.ts # Trigger + condition matchers
1246
- │ ├── action.ts # Command renderer + executor
1247
- │ ├── throttle.ts # Per-rule throttle gate
1248
- │ ├── cron-scheduler.ts # 5-field cron + days filter
1249
- │ ├── webhook-listener.ts # HTTP listener (bearer token, localhost-only)
1250
- │ ├── pid-file.ts # Hot-reload via SIGHUP or sentinel file
1251
- │ ├── audit-query.ts # Audit log filtering + aggregation
1252
- │ ├── conflict-analyzer.ts # Static conflict detection (opposing actions,
1253
- │ │ # high-freq MQTT, destructive cmds, quiet-hours gaps)
1254
- │ ├── suggest.ts # Heuristic + LLM-backed rule YAML generation
1255
- │ ├── notify.ts # notify action executor (webhook / file / openclaw)
1256
- │ └── types.ts # Shared rule/trigger/condition/action types (CommandAction | NotifyAction)
1257
- ├── llm/
1258
- │ ├── index.ts # createLLMProvider factory + LLM_AUTO_THRESHOLD
1259
- │ ├── complexity.ts # Intent complexity scorer (0–10) for auto-routing
1260
- │ ├── rule-prompt.ts # System prompt builder (embeds v0.2 schema snippet)
1261
- │ └── providers/
1262
- │ ├── openai.ts # OpenAI-compatible provider (uses Node.js https)
1263
- │ └── anthropic.ts # Anthropic provider
1264
- ├── status-sync/
1265
- │ └── manager.ts # Spawn/stop logic, state file, OpenClaw bridge
1266
- ├── lib/
1267
- │ └── devices.ts # Shared logic: listDevices, describeDevice, isDestructiveCommand
1268
- ├── commands/
1269
- │ ├── auth.ts # `auth keychain` subcommand group
1270
- │ ├── config.ts
1271
- │ ├── devices.ts
1272
- │ ├── expand.ts # `devices expand` — semantic flag builder
1273
- │ ├── explain.ts # `devices explain` — one-shot device summary
1274
- │ ├── device-meta.ts # `devices meta` — local aliases / hide flags
1275
- │ ├── install.ts # `switchbot install` / `uninstall`
1276
- │ ├── policy.ts # `policy validate/new/migrate/diff/add-rule/backup/restore`
1277
- │ ├── rules.ts # `rules suggest/lint/list/explain/run/reload/tail/replay/
1278
- │ │ # conflicts/doctor/summary/last-fired/webhook-*/
1279
- │ │ # trace-explain/simulate`
1280
- │ ├── scenes.ts
1281
- │ ├── health.ts # `health check/serve` — report + HTTP endpoints
1282
- │ ├── upgrade-check.ts # `upgrade-check` — npm registry version check
1283
- │ ├── status-sync.ts # `status-sync run/start/stop/status`
1284
- │ ├── webhook.ts
1285
- │ ├── watch.ts # `devices watch <deviceId>`
1286
- │ ├── events.ts # `events tail` / `events mqtt-tail`
1287
- │ ├── mcp.ts # `mcp serve` (MCP stdio/HTTP server)
1288
- │ ├── plan.ts # `plan run/validate/suggest`
1289
- │ ├── cache.ts # `cache show/clear`
1290
- │ ├── history.ts # `history show/replay`
1291
- │ ├── quota.ts # `quota status/reset`
1292
- │ ├── catalog.ts # `catalog show/diff/path`
1293
- │ ├── schema.ts # `schema export`
1294
- │ ├── doctor.ts # `doctor`
1295
- │ ├── capabilities.ts # `capabilities`
1296
- │ └── completion.ts # `completion bash|zsh|fish|powershell`
1297
- └── utils/
1298
- ├── flags.ts # Global flag readers (isVerbose / isDryRun / getCacheMode / …)
1299
- ├── output.ts # printTable / printKeyValue / printJson / handleError
1300
- ├── format.ts # renderRows / filterFields / output-format dispatch
1301
- ├── audit.ts # JSONL audit log writer
1302
- └── quota.ts # Local daily-quota counter
1303
- tests/ # Vitest suite (2204 tests, mocked axios, no network)
1304
- ```
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.
1305
816
 
1306
817
  ### Release flow
1307
818
 
1308
- Releases are cut on tag push and published to npm by GitHub Actions:
1309
-
1310
819
  ```bash
1311
- npm version patch # bump version + create git tag
820
+ npm version patch # bump + create git tag
1312
821
  git push --follow-tags
822
+ # then: GitHub → Releases → Draft → Publish
1313
823
  ```
1314
824
 
1315
- 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).
1316
-
1317
- 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.
1318
826
 
1319
827
  ## License
1320
828