Skip to content

Latest commit

 

History

History
1597 lines (1278 loc) · 97.1 KB

File metadata and controls

1597 lines (1278 loc) · 97.1 KB

Changelog

All notable changes to @switchbot/openapi-cli are documented in this file.

The format is loosely based on Keep a Changelog. This project follows Semantic Versioning.

[3.8.0]

Added

  • AI MindClip support — new device catalog entry, CLI command group (switchbot mindclip recordings/recording/summary/todos/daily/weekly/urgent-todos), and 3 MCP tools: mindclip_recordings, mindclip_list_todos, mindclip_recall.

Changed

  • device_history MCP consolidationget_device_history / query_device_history / aggregate_device_history merged into a single device_history tool with a mode: "raw" | "query" | "aggregate" discriminator. Old names kept as deprecated aliases; CLI unchanged.
  • Plugin default profile — all three plugins now register switchbot mcp serve without --tools all; default profile exposes 17 tools. Add --tools all to your MCP config for all 28.
  • Profile counts: readonly 14, default 17, all 28.

Deprecated

  • get_device_history, query_device_history, aggregate_device_history — use device_history(mode=...) instead; aliases removed in 4.0.0.

Fixed

  • primeCredentials stale-write race condition on account switch
  • Idempotency cache now scoped per profile; clearForProfile on credential change
  • auth keychain set/delete/migrate now invalidates all caches via onCredentialChange()
  • clearCache / clearStatusCache EBUSY crash on Windows
  • MindClip URL path injection (encodeURIComponent on recording id)
  • MindClip MCP Zod schemas: reject empty strings and impossible calendar dates
  • ISO W53 validation for short years in CLI and MCP
  • MindClip MCP error envelope now preserves all structured fields via apiErrorToMcpError()
  • device_history(mode="raw") limit capped at 100 at runtime
  • capabilities --surface mcp derived from TOOL_PROFILES.all; deprecated aliases excluded
  • reset in-memory cleanup no longer double-calls unlinkSync
  • AI MindClip catalog aliases added (MindClip, Mind Clip, AIMindClip)

[3.7.9]

Added

  • Gemini CLI extension — new @switchbot/gemini-extension package (v0.1.0) wires Gemini CLI to the SwitchBot MCP server via the native Extension system. Includes 23 slash commands, GEMINI.md safety context, and credential injection via env mapping.
  • switchbot gemini setup — 6-step bootstrap pipeline: verify Gemini CLI, install switchbot CLI, register MCP server, authenticate, run doctor. Supports --yes, --skip, --dry-run, --json.
  • switchbot gemini doctor — health check covering node, path, credentials, MCP registration, and policy validation.
  • CI gemini-release-gates job — cross-platform (Ubuntu + Windows) static validation of extension manifest and command files.

Fixed

  • Credential prime cache TTL — added 5 s TTL to primed credentials so MCP hot-reload picks up rotated tokens without restart.
  • OAuth account nameconfig show now displays the account name from OAuth userinfo endpoint.

Changed

  • send_command tool description — explicitly lists mutation vs destructive commands so AI agents respect safety tiers even without GEMINI.md context.

[3.7.8]

Fixed

  • Codex marketplace name mismatchresolveMarketplaceName previously read .agents/plugins/marketplace.json (name=codex-plugin) before the root marketplace.json (name=switchbot), causing the plugin ID to resolve as switchbot@codex-plugin while Codex registered the marketplace as switchbot. Priority flipped: root manifest is now authoritative, matching what codex plugin marketplace add validates.
  • Route B git sparse checkout missing manifest at checkout root — Codex validates <checkout-root>/.agents/plugins/marketplace.json; the sparse set only included packages/codex-plugin, so the manifest was absent. Added repo-root .agents/plugins/marketplace.json and --sparse .agents/plugins to the Route B registration call.
  • checkCodexPluginRegistered false positive — the text and JSON fallback paths matched any output containing switchbot, including the Marketplace switchbot title line. Both paths now require the switchbot@ pattern so only actual plugin-ID lines are accepted.
  • Windows os error 32 stalls Route B for 17 s — when a git file-lock is detected and the local @switchbot/codex-plugin package directory already exists, the 2 s / 5 s / 10 s retry sequence is skipped and registerCodexPluginAuto falls back to Route A immediately. On a fresh machine where the package is absent, the retry sequence is preserved. An explicit stderr message names the fallback reason.
  • codex repair re-registers when already healthy — new preflight-plugin step runs checkCodexPluginRegistered before the destructive remove+re-register cycle. When the plugin is already installed, both remove-plugin and register-plugin are auto-skipped. Force re-registration with --skip preflight-plugin.
  • @switchbot/claude-code-plugin plugin.json missing skills fieldplugins/switchbot/.claude-plugin/plugin.json did not declare "skills": "./skills/", so Claude Code would not load SKILL.md and the skill instructions would be absent. Field added.

[3.7.7]

Added

  • switchbot claude-code setup command — 6-step pipeline that bootstraps Claude Code integration end-to-end: verify claude CLI on PATH, optionally install @switchbot/openapi-cli, register the MCP server via claude mcp add --scope user, authenticate, and run a health check. Supports --yes (non-interactive), --skip, --dry-run, --json. Paste npx @switchbot/openapi-cli claude-code setup into Claude Code chat to set up without opening a terminal.
  • Claude Code Plugin Marketplace support.claude-plugin/marketplace.json now correctly points to packages/claude-code-plugin/plugins/switchbot so /plugin marketplace add OpenWonderLabs/switchbot-openapi-cli followed by /plugin install switchbot@switchbot works end-to-end. smoke-codex-git-sparse.mjs extended to validate both the Claude Code and Codex marketplace entry points.

Fixed

  • switchbot auth login clears device/status cache — switching accounts no longer returns stale data from the previous account's cache. clearCache() and clearStatusCache() are called immediately after new credentials are saved.
  • codex setup --help / claude-code setup --help — both commands now list --dry-run and --json under a "Global flags that also apply to this command" section so users do not have to discover them from the README.

[3.7.5]

Fixed

  • EPERM guard in resolveMarketplaceSourceRoot — on Windows, junction creation at @-scoped npm paths fails with EPERM when the runner is not elevated. Both src/install/codex-checks.ts and packages/codex-plugin/bin/install.js now wrap every symlinkSync call in createAlias, which catches EPERM and throws an actionable error directing users to run the installer from an elevated terminal or choose a non-@-scoped install path.
  • CODEX_PLUGIN_DEFAULT_ID constant in repairStepRemovePlugin — the fallback plugin-remove call now uses the shared constant instead of a hardcoded string, eliminating the risk of divergence if the default ID changes.
  • Codex plugin manifest locationpackages/codex-plugin/.claude-plugin/marketplace.json is now at the correct path and format; plugins/switchbot/ structure added so codex plugin add switchbot@switchbot resolves the manifest reliably.
  • Codex marketplace registration — op order corrected, stale plugin DB cleared before re-registration, plugin path fixed; OS error 32 (file-in-use) retry improved for Windows.

Changed

  • OpenClaw plugin publish path@switchbot/openclaw-skill is back in the monorepo npm publish matrix. publish.yml now version-checks and publishes it alongside the CLI and Codex plugin, while npm-published-smoke.yml verifies the published tarball through the shared plugin smoke path.
  • OpenClaw package metadatapackages/openclaw-skill/README.md, .mcp.json, and package metadata now describe the actual runtime contract: OpenClaw bootstraps via bin/start.js, then delegates to switchbot mcp serve for the CLI-owned MCP tool surface.
  • Codex smoke testssmoke-codex-temp-prefix-route-a.mjs now uses lib/node_modules/ on Linux/macOS and node_modules/ on Windows for prefix installs; test scripts in both plugin packages changed to node --test for cross-platform glob-free discovery; plugin registration smokes added to release gate.

[3.7.3]

Added

  • Route B (git marketplace)codex setup and codex repair now try codex plugin marketplace add OpenWonderLabs/switchbot-openapi-cli --sparse packages/codex-plugin --ref main before falling back to the local npm path. Route B requires no locally-installed @switchbot/codex-plugin, making fresh installs and CI environments work out of the box. Configurable via CODEX_GIT_MARKETPLACE_REF (default main) and CODEX_MARKETPLACE_ADD_TIMEOUT (default 60 000 ms).
  • On-demand npm install — when both Route B and Route A fail, registerCodexPluginAuto installs @switchbot/codex-plugin@latest and retries Route A automatically. Covers fresh-machine setups where the npm package was never installed.

Changed

  • codex setup removes the install-codex-plugin step — on-demand install is now embedded inside register-plugin via registerCodexPluginAuto, so the explicit step is no longer needed. --skip install-codex-plugin is still accepted (silently ignored) for backward compatibility.
  • Legacy plugin ID cleanupcodex repair's remove-plugin step now removes both the current ID and the legacy ID switchbot@switchbot-skill before re-registering, matching the pre-clean behaviour already present in the registration helpers.

Fixed

  • Dangling symlink crashresolveMarketplaceSourceRoot called realpathSync without a guard; if the junction/symlink target was deleted (e.g. after nvm use or npm uninstall), it threw ENOENT and aborted registration. It now catches the error, removes the stale link, and recreates it pointing at the current package root.
  • Empty CODEX_GIT_MARKETPLACE_REF env var?? was used instead of ||, so setting the variable to an empty string passed --ref "" to the CLI instead of falling back to "main".
  • Misleading "installed" in error message — when @switchbot/codex-plugin was already present and Route A retry still failed, the error reported "installed @switchbot/codex-plugin but Route A still failed". The message now says "already present" when no install actually ran.
  • Platform-split error message — the alias-path error now reads "not a junction" on Windows and "not a symlink" on Linux/macOS instead of the ambiguous "not a symlink/junction".
  • switchbot-codex-install binary frozenresolveMarketplaceSourceRoot in packages/codex-plugin/bin/install.js is now marked frozen (not kept in sync with codex-checks.ts). The binary is deprecated; use switchbot codex setup instead.

[3.7.2]

Added

  • Monorepo absorption@switchbot/codex-plugin (formerly @cly-org/switchbot-codex-plugin) now ships from this repository under packages/codex-plugin/. A single GitHub Release publishes any package whose version was bumped since the previous release.
  • Codex command groupswitchbot codex setup, switchbot codex doctor, switchbot codex repair orchestrate Codex plugin install, the 7-check health summary, and end-to-end repair. switchbot install --agent codex is the register-only sibling.
  • codex setup 6-step flow — adds install-codex-plugin between install-switchbot-cli and register-plugin, so a single npx @switchbot/openapi-cli codex setup invocation can bootstrap a brand-new machine end-to-end without a separate npm install -g @switchbot/codex-plugin. Both install steps are skippable via --skip.

Changed

  • Plugin package name@cly-org/switchbot-codex-plugin@switchbot/codex-plugin. The old name was verification-stage with no published users; no npm deprecate notice is necessary.
  • Codex plugin onInstall hook — now best-effort: it always exits 0 so a missing or broken SwitchBot CLI never rolls back the Codex plugin install. When the CLI is present it runs switchbot codex setup --yes to fast-path setup; when absent it prints a hint pointing at npx @switchbot/openapi-cli codex setup.
  • Plugin version reset to 0.1.0 for first publish under the new scope.
  • Publish workflow.github/workflows/publish.yml gains a detect-versions step and per-package guards so plugin failures (or unbumped versions) do not block CLI promotion. Plugin steps run continue-on-error: true and surface failures as workflow annotations. openclaw-skill removed from publish scope (distributed via companion repo).
  • Smoke workflow.github/workflows/npm-published-smoke.yml is now a per-package matrix. CLI keeps offline + live smoke; plugins get tarball-shape checks (concrete peerDep, executable bin entries) without live smoke.
  • CIpolicy-schema-sync cross-repo job removed; the skill consumer is now in this monorepo.

Fixed

  • Codex register-plugin on Windows / npm scoped install dirscodex plugin marketplace add from <npm-root>/@switchbot/codex-plugin failed with --ref is only supported for git marketplace sources because codex CLI 0.133.0 misparses local paths containing @ as owner/repo@ref. Registration now bridges via a junction at %LOCALAPPDATA%\switchbot\codex-plugin-marketplace (fallback ~/.switchbot/codex-plugin-marketplace); divergent junctions are repaired in place, and any other state at the alias path surfaces an error instead of silently registering the broken @ path.
  • Codex plugin marketplace metadatapackages/codex-plugin/.agents/plugins/marketplace.json was missing from the published tarball (file untracked + omitted from files). Now committed and listed under files, with the local-source path corrected to ../../ so codex plugin add switchbot@codex-plugin resolves the plugin manifest at packageRoot/.codex-plugin/plugin.json.
  • Codex plugin add ACCESS_DENIED on Windowscodex plugin add attempts to back up an existing installation before replacing it; on junction-backed paths the backup rename() fails with os error 5. Registration now issues a best-effort codex plugin remove before codex plugin add to skip the backup path entirely.
  • OAuth callback port leak — when open() threw (e.g. no default browser), the callback HTTP server stayed bound until the 120 s timeout, causing EADDRINUSE on immediate retry. The server is now shut down in the open() catch block.

Removed

  • Sibling repository openclaw-switchbot-skill is archived; future development happens here.

[3.7.1]

Fixed

  • auth login — credentials now decoded as hex instead of UTF-8 after AES-128-CBC decryption. The Wonder API returns raw binary bytes; decoding as UTF-8 produced non-ASCII characters that caused "Invalid character in header content" HTTP errors on subsequent API calls.
  • auth login — OAuth callback server now calls closeAllConnections() in every termination path (success, OAuth error, timeout). Previously, browser keep-alive TCP sockets prevented the CLI process from exiting after login completed.
  • MCP list_deviceshubDeviceId schema relaxed to nullable().optional() in both deviceList and infraredRemoteList output schemas. Devices without a hub (standalone Wi-Fi devices, IR remotes) return null or omit the field; the strict z.string() schema caused tool call validation failures for these devices.

[3.7.0]

Added

  • auth login — browser-based OAuth 2.0 login; stores credentials in OS keychain
  • reset command — clear all local data (credentials, cache, quota, history, metadata)
  • SWITCHBOT_OAUTH_CLIENT_SECRET, SWITCHBOT_TOKEN_AES_KEY, SWITCHBOT_TOKEN_AES_IV env-var overrides
  • devices expand supports setBrightness, setColor, setColorTemperature

Fixed

  • reset --config: scoped data paths correctly; no longer deletes unrelated cache/ directory
  • devices list --json --fields: alias inputs resolve to canonical output keys
  • config: credential-missing hint preserves --config in recovery commands
  • Daemon start in bundled builds (BUG-001), rules run exit code (BUG-002), unknown subcommand exit code (BUG-005/008), mcp tools --json metadata (BUG-007), Pino stdout leak (BUG-009)

Changed (Breaking)

  • schemaVersion bumped to 1.2
  • catalog show <Type> --json / devices commands <Type> --json: data is always an array
  • _fetchedAt renamed to fetchedAt in devices status JSON output
  • MCP list_devices: roomID and controlType now accept null

[3.6.3]

Fixed

  • Webhook listener port race (webhook-listener.ts): read server.address() synchronously inside the listening callback to prevent a stale port on Windows IOCP.
  • Webhook 413 socket leak: changed req.destroy() to req.resume() so the 413 response flushes before the connection is torn down; socket is destroyed on res.finish.
  • plan suggest exit code: missing --device now exits 2 (usage error) instead of 1.
  • plan suggest --devices alias: added --devices as a repeatable alias for --device; mixed usage preserves argv insertion order.
  • devices status --strict: added --strict flag that exits 1 if any batch device fetch fails.
  • device-meta list hidden filter: human mode now hides hidden devices by default; --all shows everything; JSON always exports all records.
  • doctor MCP message: now reports the default-profile tool count and hints at --tools all.
  • quota status reset time: "Remaining budget" line now includes the local reset time.
  • policy/validate CJS interop: replaced createRequire hack with a static import for ajv-formats, fixing esbuild bundling.
  • @modelcontextprotocol/sdk bundled: moved from dependencies to devDependencies; esbuild now inlines it, reducing install footprint.

Added

  • Shell completions for policy, rules, auth keychain, status-sync, and daemon command groups (bash / zsh / fish / PowerShell).
  • TESTING.md: three coverage conventions (exit-path tests, option smoke tests, message keyword assertions).
  • Layered coverage thresholds in vitest.config.ts (global lines ≥ 81%, src/commands lines ≥ 75%).
  • Test backfill: 2581 tests (+116 vs 3.6.2) covering auth migrate, completion, config, device-meta, devices, doctor, plan, quota, rules, webhook-listener, status-sync manager, daemon socket path/state, and bundle self-containment.

[3.6.2]

Added

  • Catalog sync with upstream OpenAPI: added Weather Station (deviceType WeatherStation, sensor with atmosphericPressure field), Lock Vision and Lock Vision Pro (video smart locks with the same lock/unlock/deadbolt safety semantics as Smart Lock Pro), the Smart Lock Pro Wifi Matter alias on the existing Smart Lock entry, and uploadImage <imageUrl> on AI Art Frame. The upload command parameter is documented as a single https URL pending upstream parameter-shape clarification.

  • LLM condition USD and token budgets: per-rule and global llm_budget now accept max_tokens_per_hour (hourly window, aligned with max_calls_per_hour) and max_cost_per_day_usd (24h window). Costs are computed from per-model USD pricing in src/llm/pricing.ts and reported on DecideResult.usage. Audit entries for llm-condition now carry llmUsage, and llm-budget-exceeded records budgetDimension (calls | tokens | cost), budgetLimit, and budgetObserved. New lints: condition-llm-tokens-budget-zero (warns when token cap is 0) and condition-llm-cost-without-known-model (warns when a USD cap is set with provider: auto since the cost dimension silently skips models not in the pricing table).

  • Cross-event aggregation in conditions: new event_count condition counts how many events fired for a given device inside a rolling time window. Schema:

    conditions:
      - event_count:
          device: front-door         # deviceId or alias
          event: motion.detected     # optional canonical event filter
          window: "5m"               # duration: 100ms | 30s | 5m | 1h | 1d
          min: 3                     # required floor
          max: 10                    # optional ceiling

    Backed by the same per-device JSONL ring at ~/.switchbot/device-history/<deviceId>.jsonl used by events history, with rotation honored. The LLM-condition recent_events hook (declared in v0.2 schema since Track κ but unwired) now also pulls from this fetcher, populating context.recent_events with up to N most-recent matching events on the trigger device. Engine-level LlmConditionEvaluator is now wired into RulesEngine (previously only simulate had it). New lints: condition-event-count-bad-window and condition-event-count-max-below-min.

  • Local / non-tool-use LLM provider: new provider: local for llm conditions points at any OpenAI-compatible chat completions endpoint (Ollama, llama.cpp server, vLLM, LM Studio). Defaults to http://localhost:11434/v1; override with SWITCHBOT_LOCAL_LLM_URL, SWITCHBOT_LOCAL_LLM_MODEL. Because most local servers don't support OpenAI-style tool use, decide() falls back to a structured-output prompt that asks for a {"pass": bool, "reason": str} JSON object and runs one repair retry if the first response is not parseable. Operators on tool-use-capable local endpoints can opt in via YAML tool_use: true or SWITCHBOT_LOCAL_LLM_TOOL_USE=1. New LLMProvider.capabilities.toolUse flag exposes this to the rest of the system. New doctor check local-llm-reachable probes the configured endpoint when (and only when) policy uses provider: local.

  • Daemon JSON-RPC IPC transport: switchbot rules run now exposes a JSON-RPC 2.0 endpoint over a Unix domain socket on POSIX (~/.switchbot/daemon.sock, mode 0600) and a per-user named pipe on Windows (\\.\pipe\switchbot-daemon-<user>). v1 methods: daemon.status, daemon.ping, daemon.reload. New client at src/daemon/client.ts exposes IpcDaemonClient.call() and .ping(). Wire protocol: newline-delimited JSON-RPC. Sets the foundation for future mcp serve --via-daemon proxying so MCP clients can avoid per-call CLI cold-start. New doctor check daemon-ipc reports IPC reachability and round-trip latency when the daemon is running.

[3.4.0] - 2026-05-07

Added

  • Decision traceautomation.audit.evaluate_trace (full | sampled | off, default sampled) records every rule evaluation decision to the audit log with condition-level breakdown, LLM call results, and throttle state. evaluate_retention_days bounds how long records are kept.
  • rules trace-explain — inspect why a rule fired or was blocked. Reads trace records from the audit log; filter by rule name, fire ID, or time window. --last shows the most recent evaluation; --json outputs structured data for scripting.
  • LLM condition (llm:) — gates rule execution on an AI yes/no judgement. Supports provider: auto | openai | anthropic, cache_ttl (skip redundant calls for identical context), budget.max_calls_per_hour, and on_error: fail | pass | skip. Cache key is content-addressed so equivalent prompts + context always hit the cache. rules lint flags misconfigurations (missing provider key, TTL too high for trigger frequency, budget zero).
  • rules simulate — replay historical events from the audit log (or a --against <file> JSONL snapshot) against a rule and report would-fire / blocked-by-condition / throttled / error counts without starting the live engine. --since <duration> bounds the replay window.
  • MCP toolsrules_explain (decision trace lookup) and rules_simulate (offline rule replay) exposed via the MCP server alongside existing rule tools.

Changed

  • Test suite: 1959 → 2204 tests (+245, covering trace, explain, LLM condition, and simulate modules).

Added

  • type: notify action — rules can POST to webhooks, append JSONL to a file, or push to OpenClaw after firing. Supports {{ rule.name }} / {{ event.* }} templates.
  • rules suggest --llm <openai|anthropic|auto> — complex intents routed to LLM; auto mode falls back to heuristic on failure.
  • rules_suggest MCP tool gains llm parameter; new rule_notifications MCP tool queries notify delivery history.
  • doctor notify-connectivity check probes webhook URLs declared in notify actions.
  • Audit log gains rule-notify and llm-suggest kinds with latency fields.

Fixed

  • notify templates expand nested event fields ({{ event.context.deviceMac }}, {{ event.list.0 }}).
  • channel: file rejects relative paths at lint and runtime.
  • LLM prompt embeds triggerWebhook schema def; --trigger webhook no longer produces dangling $ref output.

[3.3.2] - 2026-04-26

Fixed

  • switchbot capabilities crashed on 3.3.1 with capabilities metadata coverage error: missing:mcp list-tools, missing:mcp tools. 3.3.0 added the mcp tools and mcp list-tools subcommands to mcp.ts but did not register them in COMMAND_META. The runtime coverage check inside capabilities fires against the fully-registered program, but the existing capabilities-meta unit test only registered the capabilities command in isolation — so the gap never surfaced in CI and only showed up in the post-publish tarball smoke. Entries are now present and the command prints its manifest again.

Added

  • Regression guard test capabilities-program-coverage.test.ts. Registers every register*Command from src/index.ts (mirrored order) against a fresh Command, then actually invokes capabilities --compact so validateCommandMetaCoverage walks the complete command tree. Any future leaf command added without a COMMAND_META entry now fails this test before publish instead of on users' machines.

[3.3.1] - 2026-04-26

Fixed

  • status-sync start --probe preflight was a false-negative. The probe hit the OpenClaw base URL with GET and accepted any response (including 401 / 404 / 5xx from upstream proxies) as success, so misconfigurations only surfaced after the detached daemon had already started and was silently dropping writes. The probe now POSTs to the same endpoint the sink uses at runtime (/v1/chat/completions), with a body the server actually parses, and separates network-error handling from HTTP-error handling. Status-specific hints (401/403 → token, 404 → URL path, 400/422 → model, 5xx → server) point at the misconfigured input.
  • switchbot schema --type / switchbot health --prometheus (bare fallback forms) silently ignored their flags. Commander v12 routes parsing of a parent/child dual-declared option to the parent command, leaving the subcommand's action with empty opts. Options are now declared on the root command only; subcommand actions pull them via cmd.optsWithGlobals(). Bare and explicit forms now behave identically.
  • Live-inventory policy validation could silently skip pathological rule device refs. validateLoadedPolicyAgainstInventory relies on the offline base pass catching undefined / empty-string / <id> device refs, because the live validator continues when resolveInventoryDeviceId() returns null. A parametric test now pins the contract so weakening either side is caught immediately (tests/policy/validate.test.ts).
  • writeAudit best-effort contract pinned by tests. Several callers (the src/lib/devices.ts dry-run path in particular) relied on audit log writes never throwing. The contract was enforced by the implementation but not by any test. ENOSPC / EACCES / mkdirSync failures are now asserted non-throwing (tests/utils/audit.test.ts).

Changed

  • test:release-smoke renamed to test:release-smoke:manual. The aggregated 25-file release suite runs manually, not from prepublishOnly or CI; the new name stops future maintainers from assuming it is wired in.

Removed

  • RELEASE_METADATA 3.3.0 entry. The {schemaVersion, data} JSON envelope actually shipped in v2.0.0 (commit 33d3825) and has been in every 3.x release; flagging 3.3.0 as the breaking boundary would fire upgrade-check warnings on every 3.2.x → 3.3.x upgrade for no reason. The registry is back to empty, and upgrade-check / doctor release-notes tests now pin that contract.

Added

  • Live-inventory policy validation tier. validateLoadedPolicyAgainstInventory(policy, devices) now runs after the always-on offline pass and raises alias-live-device-not-found and rule-live-unsupported-command with schema-pointed paths and actionable hint fields. Covered end-to-end in tests/policy/validate.test.ts.
  • Catalog fidelity fixture + regression test. tests/fixtures/catalog-fidelity.observed.json pins role and statusFields for 11 real device types; tests/devices/catalog-fidelity.test.ts fails loudly on silent catalog drift.
  • tests/helpers/contracts.ts — shared envelope-shape helpers (expectJsonEnvelopeShape, expectStreamHeaderShape) codify the {schemaVersion, data} contract across command tests. Stream variants pin schemaVersion: "1.1" for watch / events.

[3.3.0] - 2026-04-26

Fixed — P0 bundled-asset loader

  • switchbot policy new, switchbot policy validate, and the MCP policy_new tool no longer fail at runtime when installed from the packed tarball. Under esbuild bundling, import.meta.url points at dist/index.js instead of the original source file, so the three call-sites that loaded embedded assets via new URL('<relative>', import.meta.url) resolved to non-existent paths (dist/schema/v0.2.json instead of dist/policy/schema/v0.2.json, etc.). Fix: a new top-level src/embedded-assets.ts module, positioned at the source-tree counterpart of dist/index.js, now owns the two asset-loading functions (readPolicySchemaJson, readPolicyExampleYaml). Because embedded-assets.ts and the bundle entry sit at the same relative depth, ./policy/schema/... and ./policy/examples/... resolve identically under tsx (dev) and under the bundle (prod) — no runtime fallback needed. All three call-sites (src/policy/schema.ts, src/commands/policy.ts, src/commands/mcp.ts) now route through those two helpers.
  • scripts/smoke-pack-install.mjs now exercises the loader paths end-to-end against the installed tarball — in addition to the existing --version check, it runs switchbot policy new <tmp>/policy.yaml (asserts the template was written) and switchbot policy validate <path> --json (asserts the schema loads and validates). The exact bug class that slipped through 3.2.2 would now fail the smoke before publish.

Changed — UX polish

  • switchbot catalog search <keyword> now ranks hits in three tiers: exact type / exact alias matches first, role and command-name matches next, alias-substring-only matches last. Alias-only rows are explicitly labelled alias-only in the matched_on column (renamed from matched). A new --strict flag restricts hits to type-name matches only and prints a "(strict mode — try without --strict)" hint when nothing matches.
  • switchbot status-sync start now prints a multi-line hint when OPENCLAW_TOKEN or OPENCLAW_MODEL is missing — it names the flag, the env var, a short pointer to the admin-issued token, and the recommended verify step (switchbot status-sync status).
  • switchbot devices batch ... --skip-offline --dry-run now separates "Planned (dry-run)" from "Skipped (offline)" in the human-readable output and the summary line reports planned=N, skipped_offline=M alongside existing totals. No [dry-run] Would POST ... line is emitted for offline-skipped devices (JSON mode already separated these keys; no schema change).
  • switchbot devices watch --help clarifies that the default output is a human-readable table and that --json is the agent-friendly JSON-Lines form, with the seed-tick ("from": null) note surfaced near the top.

[3.2.2] - 2026-04-26

Changed — release pipeline

  • Release pipeline unified: npm run build is now the single source for the published tarball. It runs a 5-stage scripts/build.mjs orchestrator (clean → typecheck → bundle → copy-assets → ensure-binary). prepublishOnly, verify:pre-commit, verify:pre-push, publish.yml, and the bundle-smoke / pack-install-smoke CI jobs all call npm run build by name — no job re-implements the build steps and no other script writes to dist/.
  • Removed npm run build:prod and npm run clean — both are folded into scripts/build.mjs.
  • Added npm run typecheck (tsc --noEmit) as the local "does it still compile?" escape hatch.
  • Split scripts/copy-assets.mjs responsibility into two scripts with one failure mode each: copy-assets.mjs only copies policy assets, and the new scripts/ensure-binary.mjs asserts the shebang is present on dist/index.js and chmod 0755s it. ensure-binary.mjs is a regression guard — it fails loudly if the esbuild banner drops the shebang, rather than silently repairing it the way copy-assets.mjs used to.

[3.2.1] - 2026-04-25

Deprecated on npm. The initial 3.2.1 publish shipped a broken bin (missing shebang / exec bit after npm pack). It has been rolled back from main and relanded in 3.2.2; install @switchbot/openapi-cli@3.2.2 or later. The feature list below is retained as the historical record of what 3.2.1 intended to deliver and what 3.2.2 now ships.

Added — plan resource model, MCP risk profiles, rules safety primitives

  • switchbot plan save [file] — persist a validated plan to ~/.switchbot/plans/<planId>.json with status pending; prints the assigned planId.
  • switchbot plan list — table of saved plans with status, creation time, and step count.
  • switchbot plan review <planId> — show full step list and current status of a saved plan.
  • switchbot plan approve <planId> — transition a pending plan to approved; required before plan execute will run it.
  • switchbot plan execute <planId> — execute an approved plan; marks it executed on completion; all steps are recorded in the audit log with planId traceability.
  • MCP send_command now returns a riskProfile field per action: riskLevel, requiresConfirmation, supportsDryRun, idempotencyHint, and recommendedMode — computed from actual device type and command at request time.
  • Rules engine now enforces maxFiringsPerHour (sliding 3600 s count window), suppressIfAlreadyDesired (skip turnOn/turnOff when device's live state already matches), and hysteresis / requires_stable_for (fire only after trigger is continuously stable for the specified duration). All three are validated in rules lint.

Fixed

  • rules lint now validates hysteresis / requires_stable_for duration syntax and warns when hysteresis and requires_stable_for are both set.

Changed — release pipeline

  • Single publish source: publish.yml now runs npm run build:prod (esbuild) to match what prepublishOnly produces at npm publish time. The tarball validated by smoke:pack-install is now byte-identical to the tarball that actually ships to the registry — no artifact swap during publish.
  • Pre-publish smoke:pack-install runs in publish.yml before npm publish, and the same smoke runs locally via pre-push hook (verify:pre-push) and on every PR in CI (pack-install-smoke).
  • scripts/copy-assets.mjs now injects the #!/usr/bin/env node shebang into dist/index.js and chmods it to 0755 after every build, so the npm bin entry is always executable.
  • New npm-published-smoke.yml workflow verifies published tarballs on the npm registry, auto-promotes next → latest on success, and auto-deprecates on package-install/offline smoke failures only (never on live API flakes).
  • bundle-smoke CI job is now a blocking matrix across Node 18/20/22 (was single-node Node 20, advisory), so the esbuild bundle must start cleanly on every supported Node version before a PR can merge.
  • See docs/release-pipeline.md for the full gate sequence and invariants.

[3.2.0] - 2026-04-25

Added — daemon, upgrade-check, scenes validate/simulate, rules summary

  • switchbot daemon start/stop/status — runs rules run as a cross-platform detached background process; PID written to ~/.switchbot/daemon.pid; log at daemon.log.
  • switchbot upgrade-check — fetches latest version from npm registry, compares semver, and prints an upgrade command; exits 1 when a newer version is available.
  • switchbot scenes validate [sceneId...] — confirms scene IDs exist in your account; exits 1 if any are missing.
  • switchbot scenes simulate <sceneId> — shows exactly what scenes execute would POST without sending the request.
  • switchbot rules summary — aggregates rule-fire audit entries per rule over a configurable time window, printing a table of fires / throttled / errors.
  • switchbot rules last-fired — shows the N most recent rule-fire / rule-fire-dry audit entries, newest first; --rule filter supported.

Added — circuit breaker, health report, --explain, keychain hints

  • CircuitBreaker class in utils/retry.ts; module-level apiCircuitBreaker singleton wired in api/client.ts. Opens after 5 consecutive 5xx/network failures; auto-probes after 60 s. 4xx responses excluded from failure counting.
  • switchbot health — reports quota usage, audit error rate, circuit-breaker state, and process info in JSON or Prometheus text (--prometheus) format.
  • devices command --explain — prints risk level, device type, and safety reason before execution without sending the request.
  • config set-token now prints a tip to run switchbot auth keychain store when file-backend credentials are saved on a platform with a native keychain.
  • doctor now includes a keychain check that warns when file-backend credentials are in use and a native keychain is available.

Added — rules safety schema + conflict analyzer

  • Policy schema v0.2 extended with optional fields: throttle.dedupe_window, cooldown, requires_stable_for. All validated by rules lint.
  • ThrottleGate.check() now accepts dedupeWindowMs and returns dedupedBy field.
  • rules/conflict-analyzer.ts — static analysis detecting opposing-action pairs, high-frequency catch-all triggers without throttle, and destructive rule actions.
  • switchbot rules conflicts [path] and switchbot rules doctor [path] subcommands.

Added — risk metadata, plan traceability

  • agentSafetyTier in capabilities output maps to riskLevel (high/medium/low), idempotencyHint, and recommendedMode.
  • plan run now generates a UUID planId and stamps it on every audit entry produced during that run, enabling plan-to-audit traceability.

Added — stable error JSON, path diagnostics, doctor enhancements

  • resolutionHint and candidateMatches fields added to structured error output.
  • name-resolver.ts now rejects ambiguous device names when multiple candidates match.
  • doctor reports PATH discoverability and npm global bin reachability.

[3.0.0] - 2026-04-24

Major release — breaking changes, full feature parity across all branches.

Includes all features shipped in v2.10.0 through v2.15.0: one-command install/uninstall, L3 autonomous rule authoring, plan suggest + --require-approval, MCP policy_diff and audit tools, and rules engine enhancements (all/any/not conditions, cron day_of_week filter).

BREAKING — removed destructive: boolean output field

The destructive boolean field has been removed from all CLI and MCP JSON output surfaces:

  • switchbot schema export / schema export --compact
  • switchbot devices describe <id> command list
  • switchbot agent-bootstrap --compact
  • MCP catalog_search tool response
  • switchbot explain <deviceId>

Migration: replace entry.commands[].destructive checks with entry.commands[].safetyTier === 'destructive'. The safetyTier field has been present since v2.7.0 and carries the same information.

BREAKING — removed policy schema v0.1 support

policy.yaml files with version: "0.1" are no longer accepted. The validator now returns a clear error with migration instructions.

Migration: if you have a v0.1 policy file, run switchbot policy migrate with CLI ≤2.15 first, then upgrade to v3.0.

Changed — deriveSafetyTier no longer reads spec.destructive

The CommandSpec.destructive boolean and CommandSpec.destructiveReason fields have been removed from the catalog interface. Custom ~/.switchbot/catalog.json overlays that used destructive: true must switch to safetyTier: "destructive".

Quality release — v0.2 policy default + contract hardening + docs baseline cleanup.

Changed — policy schema defaults

  • CURRENT_POLICY_SCHEMA_VERSION now points to 0.2, so switchbot policy new scaffolds v0.2 files by default.
  • Embedded starter template updated to v0.2 and refreshed wording for third-party agent hosts.
  • Migration guidance updated to recommend explicit scalar version values using the current default (0.2) while keeping 0.1 compatibility.

Added — policy diff parity guardrail

  • Added a cross-surface contract test asserting MCP policy_diff structuredContent matches CLI switchbot --json policy diff output exactly for the same inputs.

Changed — docs quality baseline

  • Normalized markdown table/fence styles in roadmap, agent guide, and README to reduce lint noise and improve publish consistency.
  • Restored README Output modes anchor and fixed broken table-of-contents links.
  • Updated roadmap/README backlog text with an ordered execution queue and explicit acceptance-oriented wording.

[2.14.0] - 2026-04-24

Feature release — policy review parity for MCP + dry-run default alignment.

Added — MCP policy_diff

  • New read-only MCP tool policy_diff({ left_path, right_path }) returns the same contract as switchbot --json policy diff: { leftPath, rightPath, equal, changeCount, truncated, stats, changes, diff }.
  • Enables side-by-side policy review flows in MCP-only agent hosts.

Added — MCP review/execute parity for L2 workflows

  • plan_run executes Plan JSON directly in MCP (same destructive-step gating semantics as CLI: skipped unless approved).
  • audit_query filters audit log entries by time/device/rule/result.
  • audit_stats aggregates audit entries by kind/result/device/rule.
  • MCP tool count: 17 → 21.

Changed — dry_run defaults and docs consistency

  • Policy schema v0.2 now defaults rule dry_run to true.
  • Design/spec docs and quickstart examples now document explicit arming as dry_run: false (instead of “remove dry_run: true”).
  • Roadmap / agent guide / README capability statements updated to match implemented CLI + MCP surfaces.

[2.13.0] - 2026-04-24

Feature release — L3 fully autonomous rule authoring for agents.

Added — rules suggest

  • New subcommand switchbot rules suggest --intent <text> scaffolds a candidate automation rule YAML from natural language intent + optional device list. No LLM involved — uses keyword heuristics for trigger inference (mqtt/cron/webhook), schedule inference (am/pm/night/morning), and command inference (8 patterns). Always emits dry_run: true and throttle: { max_per: "10m" } for MQTT triggers.
  • Options: --trigger, --device (repeatable), --event, --schedule, --days, --webhook-path, --out.
  • Output: raw rule YAML on stdout; --json for structured output.
  • Exported suggestRule(opts) pure function in src/rules/suggest.ts.

Added — policy add-rule

  • New subcommand switchbot policy add-rule reads rule YAML from stdin and appends it to automation.rules[] in policy.yaml, preserving all existing comments and formatting.
  • --dry-run: print the unified diff without writing to disk.
  • --enable: set automation.enabled: true after inserting the rule.
  • --force: overwrite an existing rule with the same name.
  • Pipeline-friendly: switchbot rules suggest ... | switchbot policy add-rule.
  • Exported addRuleToPolicySource() / addRuleToPolicyFile() in src/policy/add-rule.ts.

Added — MCP tools rules_suggest + policy_add_rule

  • rules_suggest (tier read): MCP equivalent of rules suggest; agents call it without shell access to draft a rule YAML.
  • policy_add_rule (tier action): MCP equivalent of policy add-rule; agents inject a rule into policy.yaml and receive the diff for user confirmation. Always run with dry_run: true first.
  • MCP tool count: 15 → 17.

Changed

  • src/lib/command-keywords.ts: COMMAND_KEYWORDS array extracted from plan.ts into a shared module; imported by both plan.ts and rules/suggest.ts.
  • docs/agent-guide.md: new section "Autonomous rule authoring (L3)" covering the suggest → add-rule → lint → reload → dry-run → arm workflow.

[2.12.0] - 2026-04-23

Feature release — semi-autonomous L2 workflow for agents.

Added — plan suggest

  • New subcommand switchbot plan suggest --intent <text> --device <id>... scaffolds a candidate Plan JSON from natural language intent + device list. No LLM involved — uses keyword heuristics to match the intent against 8 command patterns (turnOn, turnOff, press, lock, unlock, open, close, pause). Defaults to turnOn with a warning when unrecognised. Output goes to stdout (or --out <file>); warnings go to stderr.
  • Exported suggestPlan(opts) pure function for programmatic use.

Added — plan run --require-approval

  • New flag --require-approval on plan run enables per-step TTY confirmation for destructive steps: the CLI prints the step details and prompts [y/N] before executing. Rejecting a step marks it skipped with decision: "rejected" in the JSON output.
  • Non-TTY environments (CI, pipes) auto-reject destructive steps.
  • --require-approval is mutually exclusive with --json (exits with an error if both are passed).
  • --yes takes precedence over --require-approval as blanket approval.

Added — MCP plan_suggest tool

  • New read-only MCP tool plan_suggest({ intent, device_ids }) wraps suggestPlan() for agents that prefer to stay in the MCP session. Returns { plan, warnings } structured content.

[2.11.0] - 2026-04-23

Feature release — install/uninstall UX polish, cross-OS keychain CI, and two rules engine enhancements.

Changed — switchbot install polish

  • --force replaces an existing skill symlink pointing at a different target, and bypasses the SKILL.md presence check (for in-development skill repos).
  • --verify runs switchbot doctor --json as a warn-only post-check after a successful install. The result is surfaced (text or --json) but never changes the exit code — a doctor failure after a good install does not trigger rollback.
  • stepSymlinkSkill now requires SKILL.md at the root of --skill-path before creating a link, so linking a random directory is caught at install time. Non-automating agents (cursor/copilot) are unaffected — they print a recipe before the check runs.
  • Existing symlinks pointing at a different target are now a hard error without --force; pointing at the same target remains idempotent.
  • Preflight gains an agent-skills-dir check for --agent claude-code that probes ~/.claude/skills/ writable before the step can fail.

Changed — switchbot uninstall polish

  • --purge is shorthand for --yes --remove-creds --remove-policy: removes everything in one flag without any prompts.

Added — CI

  • .github/workflows/keychain-matrix.yml — new workflow that runs the credential + install-step test suites on macOS (temp keychain), Linux (D-Bus + gnome-keyring), and Windows (Credential Manager). Triggers on changes under src/credentials/, src/install/, and their test counterparts.

Added — rules trigger.days (γ-lite)

  • Cron triggers now accept an optional days filter:

    when:
      source: cron
      schedule: "0 9 * * *"
      days: [mon, tue, wed, thu, fri]

    Values are matched case-insensitively; both 3-letter abbreviations (mon) and full names (monday) are accepted. Firings on unlisted weekdays are silently suppressed before dispatch — throttle counters and audit log entries are not written for suppressed firings.

Added — rules conditions composition (γ)

  • Rule conditions can now be composed with all (AND), any (OR), and not (negation):

    conditions:
      - any:
          - time_between: ["22:00", "06:00"]
          - device: lamp
            field: power
            op: "=="
            value: "on"
      - not:
          time_between: ["08:00", "20:00"]

    Nesting is unlimited. The top-level conditions[] array remains AND-joined so existing flat rules are unaffected.

Notes

  • 1676 tests pass (+22 vs 2.10.0: 6 polish + 9 day_of_week + 8 and/or/not + 1 purge).

[2.10.0] - 2026-04-23

Feature release — adds switchbot install / switchbot uninstall, collapsing the Phase 3 one-command bootstrap UX onto the Phase 3A orchestrator library that shipped in 2.9.0.

Added — switchbot install (Phase 3B, in-CLI)

  • switchbot install — one-command setup that composes four steps with rollback on failure: prompt credentials → write keychain → scaffold policy.yaml → symlink the Claude Code skill. The step library (src/install/default-steps.ts) is a thin layer over the generic runInstall() runner, so each factory is independently unit-tested and individually skippable via --skip.
  • --agent <name>claude-code (default: auto-links ~/.claude/skills/switchbot to --skill-path), cursor / copilot (prints a skill-install recipe for docs users to follow), none (skips the skill step entirely).
  • --skill-path <dir> — points at a local clone of the companion skill repo. No auto-clone — fork/offline/pin semantics stay in the user's court. On Windows the link uses an NTFS junction so it works without elevation.
  • --token-file <path> — two-line credential file for non-interactive installs. Deleted on success; left alone on failure so the user can retry.
  • --skip <names> — comma-separated step names to skip; useful for partial re-runs after fixing one failing step.
  • --dry-run — prints the step list (text) or a structured preview (--json) without mutation.
  • Exit codes: 0 ok · 2 preflight failed (nothing changed) · 3 step failed, rollback completed · 4 rollback had residue (the printed output tells the user what to clean up).
  • Deliberate non-decisions: doctor verification is NOT a step (treating its failure as rollback-worthy would destroy a freshly-installed good state); install prints next: switchbot doctor as a hint instead.

Added — switchbot uninstall

  • switchbot uninstall — reverse of install. Removes the skill symlink (default yes, confirm), credentials (opt-in via --remove-creds), and policy.yaml (opt-in via --remove-policy since user edits may live there). --yes assumes yes to every confirmation; --dry-run previews; --json emits structured outcomes.
  • Unlike install, uninstall is not rollback-safe — it keeps going on per-step failure and exits 3 if anything failed, so the user can read the per-action report and clean up manually.
  • The CLI binary is never uninstalled. Users remove it via npm rm -g @switchbot/openapi-cli.

Changed

  • src/commands/policy.ts exports scaffoldPolicyFile() so the install step can reuse the exact scaffolding logic policy new uses — no drift risk between the two code paths.
  • src/commands/config.ts exports promptTokenAndSecret() and readCredentialsFile() for the install step to reuse the same terminal-prompt behavior config set-token has.

Notes

  • No API/CLI surface breakage. Existing scripts continue to work.
  • Path for the skill symlink: ~/.claude/skills/switchbot on claude-code; other agents receive a recipe block instead of an auto-link.
  • 1654 tests pass (+30 vs 2.9.0: 20 step factory unit tests + 5 install smoke + 5 uninstall smoke).

[2.9.0] - 2026-04-23

Feature release — Policy v0.2, the Phase 4 rules engine, Phase 3A keychain support, and the install orchestrator library. This is the release that makes the CLI feel like a single integrated product instead of a collection of commands that happen to share a binary.

Added — Policy v0.2 schema (Phase 2 continuation)

  • policy new --version 0.2 emits a v0.2 starter file carrying the new automation.rules[] block. policy new still defaults to v0.1 so existing CLI builds keep parsing the output — the default flip is tracked on the Roadmap for v3.0.
  • policy validate dispatches the validator by schema version (0.1 or 0.2), so a mixed install can hold two policy files at different versions without false-positive errors.
  • policy migrate 0.1 → 0.2 walks the YAML with yaml@2's CST, rewrites the version: scalar, and appends an automation: block stub. Comments and key order are preserved byte-for-byte.
  • Destructive-command validator hook — the v0.2 schema rejects rules whose then.command would fire unlock, garage-door open, keypad createKey, or other destructive actions. The rejection is a schema error, not a runtime surprise.
  • doctor, agent-bootstrap --compact, MCP policy_validate now surface the detected policy schema version.

Added — Rules engine v0.2 (Phase 4)

  • switchbot rules lint — static checks against policy.yaml: schema, alias resolution, cron expression validity, duplicate rule names, destructive-command guard. Exit code 0/1/2/3.
  • switchbot rules list [--json] — prints every rule's name, trigger summary, dry_run state, and throttle window.
  • switchbot rules run [--dry-run] [--max-firings N] — the engine proper. Composes three triggers (mqtt / cron / webhook), two conditions (time_between / device_state), and the per-rule throttle and dry_run blocks.
  • switchbot rules reload — sends SIGHUP on Unix, writes a pid-file sentinel on Windows. The engine reloads the policy without restarting the process; in-flight firings complete on the old policy.
  • switchbot rules tail [--follow] — tails the audit log filtered to rule-* entries.
  • switchbot rules replay --since <duration> --dry-run — reads past MQTT shadow events from the cache and replays them against the current rule set without firing commands, for verifying rule changes before enabling them.
  • Audit log v2rule-fire, rule-fire-dry, rule-throttled, and rule-webhook-rejected record types. Format is documented in docs/audit-log.md.
  • MQTT trigger — subscribes to the cloud-issued broker, uses the same extractShadowEvent helper as events mqtt-tail.
  • Cron trigger — runs on local time, quiet-hours aware.
  • Webhook trigger — bearer-token HTTP ingest on a configurable port; tokens stored in the keychain, never in policy.yaml.
  • device_state condition — per-tick cached lookups so a single firing doesn't hit the API N times for N conditions.

Added — Phase 3A: keychain + install orchestrator

  • src/credentials/keychain.ts abstraction with four backends: macOS security, Windows cmdkey, Linux secret-tool (libsecret), and a 0600-permissioned JSON file fallback. The CLI picks the first backend that works on the running platform.
  • switchbot auth keychain describe | get | set | delete | migrate — explicit management of credentials in the chosen backend. migrate moves a value from the file fallback to the OS keychain and removes the file entry on success.
  • doctor and agent-bootstrap --compact report the active credential source in a field named credentialSource.
  • src/install/ — in-repo preflight + rollback-aware step runner library that the external plugin-manager install command (Phase 3B) can call into. Library only; no top-level install subcommand ships in this release (that is Track β).

Added — Docs

  • docs/design/roadmap.md — authoritative Phase 1-4 table with the skill repo's orthogonal autonomyLevel L1/L2/L3 mapping.
  • docs/ux-principles.md — 10 principles the CLI, MCP server, rules engine, and skill all obey.
  • docs/phase-1-manual-orchestration.md — frames Phase 1 as the complete manual-orchestration contract, not a transitional state.
  • examples/quickstart/ — 7-step walkthrough + policy.yaml.example, config.env.example, and a systemd unit template for running events mqtt-tail as a long-lived service.

Changed

  • README Roadmap section now points at docs/design/roadmap.md and lists reserved tracks β / γ / δ / ε alongside the existing long-term backlog.
  • README header — the skill-pointer blockquote now links directly to the sibling companion skill repo instead of saying "published separately".

Skill-side impact

  • The companion skill is bumped to 0.3.0 in the companion skill repo with authority.cli: ">=2.9.0 <3.0.0", policy.version: "0.2", and autonomyLevel: "L1". The skill's Authoritative command table adds the switchbot rules * and switchbot auth keychain * groups shipped in this release.

[2.8.0] - 2026-04-22

Feature release — switchbot policy command group.

The companion SwitchBot skill reads its behaviour from ~/.config/switchbot/policy.yaml (aliases, confirmations, quiet hours, audit path). Until this release, a typo in that file failed silently — the skill would load whatever YAML parsed and use defaults for anything it didn't understand, leaving the user to wonder why "bedroom light" didn't work. This release ships a dedicated command group that turns those silent failures into compiler-style errors with line numbers, carets, and fix hints, and eliminates the hand-crafted starter template step from the skill's Quickstart.

Added

  • switchbot policy validate [path] — validates the policy file against the embedded schema v0.1 (JSON Schema 2020-12). Reports each error with its path, YAML line:col, a source-line snippet + caret, and an action-specific hint (e.g. "paste the deviceId from switchbot devices list --format=tsv" on alias pattern mismatches; "destructive actions (lock/unlock/delete*/factoryReset) cannot be pre-approved in policy.yaml" on a forbidden never_confirm entry). Supports --json for programmatic consumers, --no-snippet to drop source preview, --no-color for piped output.
  • switchbot policy new [path] — writes a 99-line annotated starter template to the given path (or the default ~/.config/switchbot/policy.yaml). Refuses to overwrite an existing file unless --force is passed. Creates the parent directory if needed.
  • switchbot policy migrate [path] — reports the policy file's schema version against what this CLI supports. No-op today (only v0.1 exists); wired so future releases can run structural upgrades without breaking existing policies.
  • Path resolution precedence: [path] argument > SWITCHBOT_POLICY_PATH env var > default ~/.config/switchbot/policy.yaml. The same resolver is exported for the Phase 3 agent-bootstrap install flow to reuse.
  • Exit-code taxonomy (scriptable): 0 valid / 1 invalid / 2 file-not-found / 3 yaml-parse / 4 internal / 5 exists (on new without --force) / 6 unsupported-version (on migrate). --json mode emits the usual {schemaVersion, error} or {schemaVersion, data} envelope.
  • Embedded schema assetsrc/policy/schema/v0.1.json ships in the npm package via a post-build copy-assets.mjs step. The skill repository's examples/policy.schema.json is the mirror copy; a CI job diffs the two on every push to prevent drift.

Dependencies

  • Added yaml@^2 (source-map-preserving parser), ajv@^8 + ajv-formats@^3 (JSON Schema 2020-12 validator via ajv/dist/2020).

Skill-side impact

  • Companion SwitchBot skill v0.2.0 declares authority.cli: "@switchbot/openapi-cli@>=2.8.0 <3.0.0" and replaces the manual "edit this file by hand" Quickstart step with switchbot policy new + switchbot policy validate. See the skill repo's CHANGELOG.md for the matching entry.

[2.7.2] - 2026-04-21

Patch release — CI size-budget fix.

Fixed

  • schema export --compact — dropped the resources block from compact output. In v2.7.0 the resources catalog (scenes / webhooks / keys, ~12 KB) was added to the schema payload unconditionally, which pushed schema export --compact --used past the 15 KB agent-prompt budget enforced by CI. The resources block is still emitted under the full (non---compact) output, and is always available via capabilities --json, which is the canonical source for CLI resource metadata. No behaviour change for capabilities --json consumers.

[2.7.1] - 2026-04-21

AI-discoverability patch. Top-level --help / --help --json and every subcommand description now lead with the SwitchBot product category (smart home: lights, locks, curtains, sensors, plugs, IR appliances) so AI agents reading help text can identify scope without parsing the catalog. Identity is consolidated into a single module to prevent drift.

Changed

  • Top-level switchbot --help — program description rewritten to "SwitchBot smart home CLI — control lights, locks, curtains, sensors, plugs, and IR appliances (TV/AC/fan) via Cloud API v1.1; run scenes, stream real-time events, and integrate AI agents via MCP." (previously the terse "Command-line tool for SwitchBot API v1.1"). Both human and AI scanners now learn the product category on the first line.
  • switchbot --help --json (root) — now carries top-level product, domain, vendor, apiVersion, apiDocs, and productCategories[] fields for programmatic discovery. Subcommand --help --json output is unchanged (identity is root-only to keep per-command payloads tight).
  • Subcommand descriptionscatalog, schema, history, plan, doctor, capabilities now explicitly mention "SwitchBot" so each command self-describes in --help (the other 10 top-level commands already mentioned it).
  • README intro — rewritten to lead with the product category ("SwitchBot smart home CLI — control lights, locks, curtains, sensors, plugs, and IR appliances …") instead of the API version.

Refactored

  • Shared IDENTITY module — extracted the product-identity constant to src/commands/identity.ts; capabilities.ts, agent-bootstrap.ts, and utils/help-json.ts now import from a single source of truth to prevent field drift. The canonical IDENTITY adds productCategories: string[] (8 category keywords AI agents can scan) and clarifies constraints.transport = "Cloud API v1.1 (HTTPS)" — the CLI does not drive BLE radios directly; BLE-only devices are reached through a SwitchBot Hub, which the Cloud API handles transparently. agent-bootstrap --json gains additive identity fields (apiDocs, deviceCategories, productCategories, agentGuide) via the shared module; no fields removed.

[2.7.0] - 2026-04-21

AI-first maturity release. Broader field-alias coverage, richer capability metadata, and agent-discoverable resource surfaces (scenes, webhooks, keys).

Added

  • Field aliases — registry expanded from ~10 to ~51 canonical keys (~98% coverage of catalog statusFields + webhook payload fields), dispatched through devices status, devices watch, and --fields parsers. Phase 4 sweep adds ultra-niche sensor/webhook aliases: waterLeakDetect, pressure, moveCount, errorCode, buttonName, pressedAt, deviceMac, detectionState.
  • safetyTier enum (5 tiers) — catalog commands now carry safetyTier: 'read' | 'mutation' | 'ir-fire-forget' | 'destructive' | 'maintenance'; replaces the legacy destructive: boolean flag.
  • DeviceCatalogEntry.statusQueries — read-tier catalog entries exposing queryable status fields; derived from existing statusFields plus a curated STATUS_FIELD_DESCRIPTIONS map. Powers safetyTier: 'read' and lights up capabilities.catalog.readOnlyQueryCount.
  • capabilities.resources — new top-level resources block in capabilities --json and schema export, exposing scenes (list/execute/describe), webhooks (4 endpoints + 15 event specs + constraints), and keypad keys (4 types: permanent/timeLimit/disposable/urgent). Each endpoint/event declares its safety tier so agents can plan without trial-and-error.
  • Multi-format output--format=yaml and --format=tsv for all non-streaming commands (devices list, scenes list, catalog, etc.); id / markdown formats preserved. --json remains the alias for --format=json.
  • doctor upgrades — new --section, --list, --fix, --yes, --probe flags; new checks catalog-schema, audit, mcp (dry-run — instantiates MCP server and counts registered tools), plus live MQTT probe (guarded by --probe, 5 s timeout).
  • Streaming JSON contract — every streaming command (watch / events tail / events mqtt-tail) now emits a { schemaVersion, stream: true, eventKind, cadence } header as its first NDJSON line; documented in docs/json-contract.md.
  • Events envelope — unified { schemaVersion, t, source, deviceId, topic, type, payload } shape across events tail and events mqtt-tail.
  • MCP tool schema completeness — every tool input schema now carries .describe() annotations; new test suite enforces this.
  • Help-JSON contract test — table-driven coverage for all 16 top-level commands.
  • batch --emit-plan — new canonical flag alias for the deprecated --plan.

Changed

  • Error envelope — all error paths route through exitWithError() / handleError(); --json failure output always carries schemaVersion + structured error object.
  • Quota accounting — requests are recorded on attempt (request interceptor) instead of on success, so timeouts / 4xx / 5xx count against daily quota.
  • --json vs --format=json — both paths go through the same formatter; --json is now documented as the alias.

Deprecated

  • destructive: boolean on catalog entries — derived from safetyTier === 'destructive'. Removed in v3.0.
  • DeviceCatalogEntry.statusFields — superseded by statusQueries. Removed in v3.0.
  • batch --plan — renamed to --emit-plan. Old flag still works but prints a deprecation warning to stderr. Removed in v3.0.
  • Events legacy fields body / remote on events tail — superseded by the unified envelope. Removed in v3.0.

Reserved

  • safetyTier: 'maintenance' — enum value accepted by the type system but no catalog entry uses it today. Reserved for future SwitchBot API endpoints (factoryReset, firmwareUpdate, deepCalibrate).

Fixed

  • Quota counter no longer under-counts requests that fail at the transport or server layer.

[2.6.4] - 2026-04-21

Added

  • devices describe now shows a tip for device types that support devices expand (Air Conditioner, Curtain, Curtain 3, Blind Tilt, Relay Switch 2PM); --json output includes an expandHint field with command, flags, and a ready-to-run example string

Fixed

  • --filter controlType=X now works correctly; controlType was documented as a filterable key but was missing from the canonical key registry
  • field-aliases.ts: removed category from the controlType alias list to prevent collision with the physical/IR category filter key
  • MCP stdio path now handles SIGTERM and SIGINT with the same graceful shutdown as the HTTP path (30 s force-exit timeout, isShuttingDown guard)

Changed

  • Extracted exitWithError() helper in output.ts; deduplicated isJsonMode() across index.ts and config.ts

[2.6.3] - 2026-04-21

Fixed

  • MCP send_command dry-run now strictly rejects unknown command names when catalog has a definitive match (#55)
  • MCP send_command dry-run rejects commands sent to read-only sensors (e.g. Meter)
  • Previous v2.6.2 fix used lenient validateCommand which silently passed when catalog lookup was ambiguous

[2.6.2] - 2026-04-21

Fixed

  • scenes execute --dry-run now outputs structured result on stdout instead of silently exiting (#54)
  • MCP send_command dry-run validates command name against catalog before returning success (#55)
  • MCP run_scene dry-run validates sceneId against scene list before returning success (#56)

[2.6.1] - 2026-04-21

Follow-up to v2.6.0 from the external re-audit. Three real findings (R-2, R-3, R-4) plus a repo-wide English-only chore; R-1 rejected with reason.

Fixed

  • MCP send_command now runs the same client-side parameter validator as the CLI (setBrightness, setColor, AC setAll, Curtain / Blind Tilt setPosition, Relay setMode). Out-of-range values fail with isError:true and usage class before the API is called, in both the dryRun:true and live paths. setColor hex / named-colour / comma forms are normalised to the R:G:B wire format before dispatch, and the dry-run wouldSend.parameter reflects the normalised value. (R-2)

Documentation

  • --filter empty-value rejection is now called out in the README. A clause like name~ or type= with an empty RHS exits 2 — the parser refuses to guess between "no constraint" and "match empty string". Drop the clause outright to remove the constraint. (R-4)

Tests

  • Added coverage locking in plaintext scenes describe <unknown> rendering of Did you mean: <name> (<id>), …? when candidates are present (and suppressing it when the scene list is empty). The behaviour already worked in v2.6.0; the re-audit's counter-example was a 0-scene account. (R-3)

Chore

  • Stripped Chinese characters from README, source comments, help text, and test fixtures in favour of neutral English examples (Living Room AC, family=home, alias=bedroom-lamp, etc.). The Chinese left in tests/utils/string.test.ts is intentional — it exercises the CJK branch of normalizeDeviceName (\u4e00-\u9fff in src/utils/string.ts:20), which is a supported product feature.

Rejected

  • R-1 (MCP error envelope for Zod validation failures) — rejected by design. The MCP SDK wraps every tool handler error as CallToolResult.isError:true; only UrlElicitationRequired escapes as a JSON-RPC error. Faking a JSON-RPC error for Zod failures would require forking or monkey-patching the SDK. The text payload already carries "MCP error -32602: …", so agents can still extract the code from content[0].text if they need it.

[2.6.0] - 2026-04-21

Addresses 14 findings from the external v2.5.1 audit (B-1 … B-16, minus the two declined-as-misread items and four P3 items parked for v3.x). All in a single minor bump — no staged releases.

Added

  • --filter key!=value (negated clause) across devices list, devices batch, and events tail / mqtt-tail. neq is a negated substring by default; category stays exact. (B-5)
  • --allow-unknown-device and --skip-param-validation on devices command. Escape hatches for the stricter defaults below. (B-1, B-3)
  • capabilities --used filters the per-type summary to devices present in the local cache. Same semantics as schema export --used. (B-9)
  • catalog list (alias for show) and catalog search <keyword> (fuzzy match across type, aliases, role, commands). (B-8)
  • --retry-on-5xx <n> (default 2) transparently retries 502/503/ 504 and request timeouts on idempotent GETs with exponential backoff. Mutating calls still never auto-retry — use --idempotency-key for safe POST retries. (B-11)
  • commandMeta flat manifest in capabilities --json: each subcommand leaf now carries {mutating, consumesQuota, idempotencySupported, agentSafetyTier, verifiability, typicalLatencyMs}, and a path-keyed lookup table is published at the top level so agents don't have to walk the tree. (B-4)

Fixed

  • --flag=value equals form is now recognized everywhere. --format=json, --timeout=5000, --config=/path, --fields=a,b,c etc. previously dropped silently through getFlagValue; they now work identically to the space form. (B-6)
  • devices commands <type...> iterates truly variadic: devices commands Bot Curtain prints both sections instead of joining the tokens as one type name. Multi-word unquoted form still works. (B-7)
  • scenes describe <unknown> plaintext branch now renders Did you mean: …? candidates — previously only the --json branch did. (B-10)
  • setColor accepts R:G:B, R,G,B, #RRGGBB, #RGB, and 16 CSS named colors (red, blue, orange, …); all normalize to R:G:B before hitting the API. (B-12)
  • Ambiguous --name hint now explicitly lists --name-strategy=fuzzy / --name-strategy=first so users know how to break ties. (B-15)
  • Empty results under --table-style markdown render as _(empty)_ rather than a header-only skeleton. (B-16)
  • Error-path formatting is consistent with --format=json: every terminal error path now routes through emitJsonError / handleError, so piped consumers can always JSON.parse stderr (or stdout in JSON mode) without string parsing. (B-14)

Changed

  • setBrightness, setColor, and Curtain setPosition single-value form are now validated client-side. setBrightness requires integer 1-100; setColor validates each component 0-255; setPosition validates 0-100. Invalid input exits 2 before hitting the network. Soft-breaking: scripts that relied on the API silently rejecting bad input will now see exit 2 earlier. Pass --skip-param-validation as a narrow escape hatch. (B-1)
  • Unknown deviceId in devices command now exits 2 by default with a candidate list, rather than warning-and-passing-through. This makes --dry-run a reliable pre-flight gate for agents and plans. Soft-breaking: pass --allow-unknown-device to restore pass-through behavior for scripted workflows. (B-3)

Documentation

  • Roadmap section at the bottom of README for the v3.x track (daemon mode, standalone npx MCP package, self-test harness, record/replay) — audit items B-17 / B-18 / B-19 / B-21 are parked there rather than folded into this minor.
  • Clarified that devices expand is intentionally limited to multi-parameter commands (setAll, setPosition, setMode); single-parameter commands like setBrightness 50 or setColor #FF0000 don't need semantic flags. (B-13)

[2.5.1] - 2026-04-20

Round-2 + Round-3 smoke-test response: 24 bugs closed across three groups — Round-2 correctness (13), Round-2 leftovers (3), and Round-3 contract & DX (8). Sources: switchbot-cli-v2.5.0-round2-report.md and switchbot-cli-v2.5.0-round3-report.md.

The release was cut initially against the Round-2 report; the Round-3 report arrived shortly after and is folded into the same patch so consumers of 2.5.1 get the full fix set in one version bump. The two Round-3 🔴 items (#SYS-1, #SYS-3) are contract bugs that break agent pipelines and could not wait.

This version also contains one breaking change — the --filter grammar is now unified across devices list, devices batch, and events tail / mqtt-tail. devices batch and events tail keys that used to require exact matches are now substrings. See Changed (BREAKING) below for the migration.

Changed (BREAKING)

  • --filter grammar unified across three surfacesdevices list, devices batch, and events tail / mqtt-tail now share one DSL: key=value (case-insensitive substring; exact only for category), key~value (explicit case-insensitive substring), and key=/pattern/ (case-insensitive regex; invalid regex returns a usage error). Each command still exposes its own key set — see README §"Filter expressions — per-command reference". (bug #39)
    • Breaking: devices batch --filter 'type=Bot' previously required an exact match and now treats Bot as a substring (matches Bot Plus too). Pair = with a more specific value, or filter post-hoc, if exact match was load-bearing.
    • Breaking: devices batch --filter 'type~=...' (the ~= spelling) is removed. Use ~ instead: type~Light.
    • Breaking: events tail --filter 'deviceId=ABC' is now a substring match (previously exact).

Fixed (correctness & safety)

  • devices command --dry-run --json no longer emits empty stdout — the single-device write path was hitting handleError's silent DryRunSignal exit before the JSON serializer ran. Now mirrors the MCP send_command {dryRun:true} shape: {schemaVersion:"1.1", data:{dryRun:true, wouldSend:{deviceId,command,parameter,commandType}}}. Batch and plan dry-run paths were already correct. (bug #36)
  • MCP tool-call errors preserve structuresend_command / describe_device / run_scene were letting ApiErrors escape to the SDK's generic createToolError, collapsing {code, subKind, transient, hint, retryAfterMs, errorClass} to a plain-text string. Errors now return structuredContent.error alongside isError:true so agents can branch on subKind instead of parsing English. Also narrowed the mcpError() option types so subKind / errorClass are compile-time checked. (bug #38)
  • devices batch propagates verification + subKind for IR devices — a batch over IR remotes was emitting zero unverifiability signal, the exact contract 2.4.0 was released to establish. succeeded[] entries now include subKind:'ir-no-feedback' and the verification object for IR devices, plus summary.unverifiableCount. (bug #28)
  • Device & status cache scoped per profiledevices.json and status.json lived at a fixed disk path, so rotating credentials or switching profiles served the prior session's inventory. Cache files now live under ~/.switchbot/cache/<sha256(profile):8>/ when a profile is active; unnamed/default profile keeps the legacy ~/.switchbot/ path (backwards compatible). A follow-up fix also keys the in-memory hot cache (_listCache / _statusCache) by profile so mcp serve request-scoped profile switches do not leak either. (bug #37)
  • API code 190 reclassified device-internal-error — 190 fires for invalid deviceIds, unsupported parameters, AND non-device endpoints like webhook query with no webhook configured. The device-busy subKind and device-specific hint were misleading for webhook. Renamed subKind + rewrote hint to cover all three causes. (bug #27)
  • API code 3005 mapped to command-not-supported — 3005 "invalid value" is the API's catch-all for model-specific command rejections (e.g., Fan lowSpeed/middleSpeed/highSpeed on stock IR remotes that only work under --type customize). Now returns a useful subKind + hint pointing to devices commands <type> and --type customize. (bug #29)
  • scenes execute pre-validates sceneIdscenes execute <bogus> returned ok:true because the API does not validate sceneIds. scenes describe already guarded against this via scene_not_found — port the same check so agents do not silently burn quota. (bug #31)
  • devices meta set --alias enforces uniqueness — nothing stopped two devices from carrying the same alias; --name <dup-alias> behavior was undefined. Reject duplicate aliases with a clear error naming the existing holder; --force reassigns (clears the old holder's alias) with a log line. (bug #41)

Fixed (UX & docs)

  • --fields id / --fields name aliases restored on devices list — the 2.5.0 alias-map refactor dropped the short forms that 2.4.0 accepted, breaking scripts. id → deviceId is back alongside name → deviceName. (bug #22)
  • cache clear --status and --list shorthand aliases — the old --key status form still works, but the shorter flags no longer error with unknown option. Using them with --key or together raises UsageError. (bug #35)
  • history aggregate --metric marked requiredOption — help text said (default: []) implying optional; the command actually required at least one metric and threw a custom error. Now Commander enforces it and --help says required. (bug #42)
  • plan validate help text clarifies scope — now says "structural only; does not verify device or scene existence" and points to plan run --dry-run for semantic checks. (bug #32)
  • cache help text documents TTL behavior — the cache TTL is computed from the lastUpdated field inside the JSON, not file mtime. Operators who touched cache files to force a refresh were surprised. One-line note added to cache show --help. (bug #34)
  • devices meta surfaced in agent-bootstrap and capabilities — the local metadata system was completely undiscoverable in 2.5.0. meta set / get / list / clear now appear in capabilities with correct safety tiers, and agent-bootstrap's quickReference gains a meta entry. (bug #40)
  • ~/.switchbot/device-history/<id>.json companion file documented — the 100-entry ring buffer read by MCP get_device_history had no docs, while only the append-only .jsonl was mentioned. docs/agent-guide.md now describes both files and __control.jsonl. (bug #43)

Fixed (Round 3 contract bugs — 🔴)

  • --json errors now emit on stdout instead of stderr — piped consumers (cli --json ... | jq) could not decode failure envelopes because handleError wrote them to stderr. The JSON envelope {schemaVersion, error:{...}} now lands on stdout for both success and failure; TTY users still get a colored human-readable summary on stderr, non-TTY invocations get silence on stderr. 15+ bespoke JSON error sites across batch, config, devices, expand, history, mcp, and format were consolidated through a new emitJsonError helper. (bug #SYS-1)
  • MCP send_command { dryRun:true } validates deviceId against the local cache — dryRun previously accepted any string and echoed back a plausible-looking preview, defeating the whole point of a validation surface. Unknown IDs now return subKind:'device-not-found' with a hint to run list_devices first. Happy path unchanged for cached IDs. (bug #SYS-3)

Fixed (Round 2 leftovers)

  • devices batch --idempotency-key accepted as alias for --idempotency-key-prefix. Still uses prefix semantics internally (auto-appends -<deviceId> per step). (bug #30)

Added (Round 2/3 features)

  • devices batch --skip-offline (default off) skips devices whose cached status is offline, with each skip recorded under summary.skipped with skippedReason:'offline'. Reads the local status cache only — no new API calls. Off by default preserves 2.5.0 behavior. (bug #33)
  • --for <duration> alias on devices watch, events tail, and events mqtt-tail — stops after elapsed time instead of tick/event count. Accepts the same duration grammar as --since (ms/s/m/h/d/w). When both --for and --max are set, the first limit to hit wins. (bug #52)
  • Duration parser accepts d (days) and w (weeks) in addition to ms/s/m/h. Unsupported units like 1y / 1month now produce a usage error that lists the supported unit set. (bug #54)
  • events mqtt-tail --json emits a __session_start envelope immediately on invocation (before the broker connect), so downstream tools can distinguish "connecting" from "never connected" and get an eventId to correlate with subsequent __connect / __disconnect events. (bug #56)

Polish (Round 3 DX)

  • --name-strategy help + agent-bootstrap list all six strategiesexact, prefix, substring, fuzzy, first, require-unique. ALL_STRATEGIES in name-resolver.ts is the single source of truth; help text is generated from it. (bug #51)
  • MCP search_catalog rejects empty queries with a usage error pointing to list_catalog_types for enumeration. Silent "return everything" behavior was surprising and agent-hostile. (bug #57)
  • Negative positional parameters reach the validation layersetBrightness -1 was being swallowed by Commander as "unknown option -1". devices command now uses .passThroughOptions() so negative numeric positionals are forwarded to the command-specific validator, where they can be accepted or range-rejected as appropriate. (bug #53)

Not included (response to reports)

  • Report bug #19 (MCP strict schema not enforced) — false positive. All 11 MCP tools already have .strict() on their Zod input schemas and the SDK enforces it via safeParseAsync → JSON-RPC -32602. Could not reproduce the reported behavior; the existing test suite exercises the full JSON-RPC path.
  • Deferred to 2.6.0:
    • Report bug #58 (parallel devices status outlier) — needs profiling to separate CLI-side latency from API-side, and the fix likely involves a concurrency knob rather than a single flip.
    • Report bug #55 (devices watch --json rewording) — already works via the global --json flag; pure doc rewording scheduled with other doc sweeps.
    • MCP / CLI naming alignment (live vs includeStatus, metric vs metrics) flagged in Round-3 §4.
    • devices meta import/export (Round-2 #40 follow-up).

[2.5.0] - 2026-04-20

Added

  • history aggregate <deviceId> — on-demand bucketed statistics (count / min / max / avg / sum / p50 / p95) over the append-only JSONL device history. Flags: --since / --from / --to, repeatable --metric, --agg <csv>, --bucket <dur>, --max-bucket-samples <n>. Non-numeric samples are skipped; empty metrics are omitted from their bucket.
  • MCP aggregate_device_history — same contract as the CLI, exposed as a read-tier tool (_meta.agentSafetyTier: "read") with a strict Zod input schema (unknown keys reject with JSON-RPC -32602).
  • Capabilities manifest — new history aggregate entry in COMMAND_META; new aggregate_device_history entry in surfaces.mcp.tools.
  • scenes describe <sceneId> — returns {sceneId, sceneName, stepCount:null, note}; SwitchBot API v1.1 does not expose scene steps. Unknown sceneId returns structured scene_not_found with a candidate list. (bug #17)
  • --no-color flag + NO_COLOR env var — honors the standard no-color contract; disables chalk colors globally before any subcommand runs. (bug #12)
  • --format markdown — accepted as an alias for --format table with --table-style markdown forced at render time, independent of the user's --table-style flag. (bug #8)
  • cache status — alias for cache show, matching the quota subcommand's status/show parity. (bug #9)

Fixed (security & correctness — v2.4.0 report)

  • MCP strict input schemas on all 11 tools — unknown keys now reject with JSON-RPC -32602. Fixes the v2.4.0 hole where send_command {dryRun:true} silently fired the command anyway — particularly dangerous for Smart Lock / Garage. (bug #4)
  • MCP dryRun on mutating toolssend_command and run_scene accept dryRun:true; when set, no API call is made and the response is {ok:true, dryRun:true, wouldSend:{...}}. (bug #4)
  • MCP serverInfo.version — wired to package.json#version; was hardcoded "2.0.0" despite the CLI reporting the real version everywhere else. (bug #5)
  • MCP _meta.agentSafetyTier — every tool now emits its tier (read / action / destructive). Release notes already claimed this but no tool was actually emitting it. (bug #6)
  • --name require-unique + exact-match — exact-name short-circuit in name-resolver was returning the exact hit even when substring matches existed, defeating the write-path require-unique default. Exact hits now enter the candidate list under require-unique and go through the ambiguity check like any other match. (bug #1)
  • history verify on missing audit.log — exits 0 with status:"warn" and fileMissing:true rather than exit 1. Malformed/unversioned content still exits 1 as before. (bug #11)
  • events mqtt-tail control events__connect / __reconnect / __disconnect / __heartbeat now append to ~/.switchbot/device-history/__control.jsonl alongside per-device files, honoring the v2.4.0 "every event is persisted" claim. (bug #10)

Changed (docs)

  • --idempotency-key help text on devices command, devices batch, plan run, history replay now explicitly mentions the process-local 60s scope — independent CLI invocations do NOT share the cache. (bug #14)
  • mcp --help now says "eleven tools" and lists all 11 names. (bug #15)
  • New docs/verbose-redaction.md — documents the nine masked headers (authorization, token, sign, nonce, x-api-key, cookie, set-cookie, x-auth-token, t) and the --trace-unsafe opt-out. (bug #16)
  • plan schema now includes agentNotes.deviceNameStrategy declaring that plan steps using deviceName resolve with require-unique. (bug #18)
  • agent-bootstrap hints field carries JSDoc + schema export declares it in cliAddedFields — empty array means "no hints", never null. (bug #13)

Notes

  • Storage format unchanged. Aggregation streams the existing JSONL rotation files via readline — zero memory blow-up for large windows, with a hard ceiling of --max-bucket-samples × 8 bytes per (bucket × metric) for quantile computation.
  • Quantiles use nearest-rank on sorted per-bucket samples; if the cap is reached the result carries partial: true and a per-bucket notes[] entry. count / min / max / avg / sum remain exact.
  • All bug-fix items bundled into 2.5.0 rather than shipping a separate 2.4.1. Source of bug numbers: the v2.4.0 smoke-test report at D:/servicdata/workspace/switchbot-cli-v2.4.0-report.md.

Not included (deferred)

  • Cross-device aggregation (agents merge locally).
  • Trend / rate-of-change helpers (derivable from bucket series).
  • --fill-empty for missing buckets.
  • Disk-persisted idempotency cache for cross-invocation replay (report bug #2). Process-local is the documented 2.4.0 contract; the --help text now states this plainly — no code change. Revisit only if a concrete use case forces it.
  • capabilities --types / --fields / --used (report bug #7). schema export already offers these for the agent bootstrap path; capabilities --compact --surface <s> covers the payload-size story.

[2.4.0] - 2026-04-20

Large agent-experience overhaul driven by third-party agent + Claude integration feedback (19 items across P0/P1/P2/P3) plus a new device history aggregation subsystem. All schema changes are additive-only — existing agent integrations keep working without code changes and pick up the new fields when they upgrade.

P0 — Correctness & security

  • IR command verifiability tagdevices command responses for IR devices now carry verification: { verifiable: false, reason, suggestedFollowup }. Human output adds a stderr hint that IR transmissions cannot be acknowledged by the device. MCP send_command mirrors the same field.
  • config set-token secret scrubbing — positional invocations have their token/secret replaced in process.argv before any hook, audit log, or verbose trace can observe them. Interactive set-token (hidden-echo readline) is now the primary path; positional form prints a discouragement warning but still works for backwards compatibility.

P1 — Agent hardening

  • --name scope fixdevices status / devices command now accept --name directly on the subcommand (previously root-only). capabilities reflects the change.
  • Fuzzy name resolution contract — new src/devices/resolve-name.ts exports six strategies (exact | prefix | substring | fuzzy | first | require-unique). Reads default to fuzzy; writes default to require-unique and fail with exit code 2 + error: "ambiguous_name_match" and a candidate list when multiple devices match. Global filters --type, --room, --category, and --name-strategy compose with --name.
  • Smaller schema/capabilities payloads + pipe hygieneschema export and capabilities grew --compact, --types <csv>, --used, --fields <csv>, --surface cli|mcp|plan. Banners / tips / progress messages move to stderr; stdout is exactly one JSON document. Non-TTY no longer emits ANSI.
  • Semantic safety metadata — every leaf command in capabilities now carries { mutating, consumesQuota, idempotencySupported, agentSafetyTier: "read"|"action"|"destructive", verifiability, typicalLatencyMs }. MCP tools mirror the tier in meta.agentSafetyTier.

D — Device history (new subsystem)

  • JSONL storage — every events mqtt-tail event / MCP status refresh is appended to ~/.switchbot/device-history/<deviceId>.jsonl. The file rotates at 50 MB into .jsonl.1 → .jsonl.2 → .jsonl.3 with the oldest discarded. Writes are best-effort with 0o600 perms.
  • history range <deviceId> — time-windowed query with --since 7d / --from <iso> / --to <iso>, payload-field projection via repeatable --field <name>, --limit <n> (default 1000). Uses streaming readline so even 50 MB files never load into memory.
  • history stats <deviceId> — reports file count, total bytes, record count, earliest/newest timestamp.
  • MCP query_device_history — same contract as the CLI, exposed as a tool for agents with a 1000-record default safety cap.

P2 — DX & stability

  • doctor --json stable contract — locked shape { ok, generatedAt, checks[], summary }; each check is { name, status: ok|warn|fail, detail }. The clock check now probes the real API once and reports skewMs.
  • events mqtt-tail control events — synthesized JSONL records of __connect / __reconnect / __disconnect / __heartbeat. Every real event gets a UUIDv4 eventId when the broker doesn't supply one.
  • devices batch --stagger / --max-concurrent / --plan — throttled concurrent execution with per-step startedAt / finishedAt / durationMs / replayed telemetry and a planner (--dry-run --plan) that prints the plan JSON without executing.
  • Idempotency contract + replayed flag — cache hits now return replayed: true. A reused key with a different (command, parameter) shape within the 60 s window exits 2 with error: "idempotency_conflict" and the old/new shape in the payload. The cache is process-local, in-memory: keys live as SHA-256 fingerprints on the heap (never raw, so heap dumps / log captures don't leak the user-supplied key) and vanish when the process exits. Replay and conflict therefore apply within a single long-lived process — MCP server session, devices batch run, plan run, history replay — and do not carry across independent CLI invocations.
  • Profile label / description / daily cap / default flagsconfig set-token grew --label, --description, --daily-cap <N>, --default-flags "<csv>". The daily cap is enforced before any request leaves the CLI (pre-flight refusal, exit 2). config list-profiles / doctor / cache status surface the label.
  • --verbose header redactionAuthorization, token, sign, t, nonce, cookies, etc. are mid-masked in verbose output. --trace-unsafe opts in to raw output with a prominent one-time warning.

P3 — Polish

  • quota show alias for quota status.
  • showSuggestionAfterError across the full subcommand tree — typos like devices lst now suggest devices list.
  • schema export declares CLI-added fields — top-level cliAddedFields documents _fetchedAt, replayed, and verification so agents can distinguish CLI-synthesized data from upstream API fields.
  • switchbot agent-bootstrap [--compact] — single-command aggregate (identity, cached devices, catalog, quota, profile, safety tiers, quick reference) that stays under 20 KB in --compact mode. Offline-safe; no API calls.
  • --table-style <unicode|ascii|simple|markdown> + --format markdown — non-TTY now defaults to ascii; markdown emits fenced |col|col| tables for agent UI embedding.
  • Audit log versioning — every line now carries "auditVersion": 1. New docs/audit-log.md documents the format, crash-safety, and rotation guidance. New switchbot history verify reports parsed / malformed / version counts and exits non-zero on malformed content.

Migration notes

  • Fully backwards compatible. No fields changed or were removed; only added. Existing MCP and CLI integrations continue to work.
  • Agents that want the richer context can refresh their prompts by running switchbot agent-bootstrap --compact once per session instead of combining doctor + capabilities + schema + devices list.
  • Upgraders who manage profiles with sensitive daily budgets should run switchbot config set-token --profile <name> --label "..." --daily-cap N to take advantage of the pre-flight refusal guard.
  • Audit logs written by 2.3.0 coexist unchanged with 2.4.0 records; history verify reports them as unversioned.

[2.3.0] and earlier

See git history.