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.
- 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.
device_historyMCP consolidation —get_device_history/query_device_history/aggregate_device_historymerged into a singledevice_historytool with amode: "raw" | "query" | "aggregate"discriminator. Old names kept as deprecated aliases; CLI unchanged.- Plugin default profile — all three plugins now register
switchbot mcp servewithout--tools all; default profile exposes 17 tools. Add--tools allto your MCP config for all 28. - Profile counts:
readonly14,default17,all28.
get_device_history,query_device_history,aggregate_device_history— usedevice_history(mode=...)instead; aliases removed in 4.0.0.
primeCredentialsstale-write race condition on account switch- Idempotency cache now scoped per profile;
clearForProfileon credential change auth keychain set/delete/migratenow invalidates all caches viaonCredentialChange()clearCache/clearStatusCacheEBUSY crash on Windows- MindClip URL path injection (
encodeURIComponenton 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 runtimecapabilities --surface mcpderived fromTOOL_PROFILES.all; deprecated aliases excludedresetin-memory cleanup no longer double-callsunlinkSync- AI MindClip catalog aliases added (
MindClip,Mind Clip,AIMindClip)
- Gemini CLI extension — new
@switchbot/gemini-extensionpackage (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 viaenvmapping. 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-gatesjob — cross-platform (Ubuntu + Windows) static validation of extension manifest and command files.
- Credential prime cache TTL — added 5 s TTL to primed credentials so MCP hot-reload picks up rotated tokens without restart.
- OAuth account name —
config shownow displays the account name from OAuth userinfo endpoint.
send_commandtool description — explicitly lists mutation vs destructive commands so AI agents respect safety tiers even without GEMINI.md context.
- Codex marketplace name mismatch —
resolveMarketplaceNamepreviously read.agents/plugins/marketplace.json(name=codex-plugin) before the rootmarketplace.json(name=switchbot), causing the plugin ID to resolve asswitchbot@codex-pluginwhile Codex registered the marketplace asswitchbot. Priority flipped: root manifest is now authoritative, matching whatcodex plugin marketplace addvalidates. - Route B git sparse checkout missing manifest at checkout root — Codex validates
<checkout-root>/.agents/plugins/marketplace.json; the sparse set only includedpackages/codex-plugin, so the manifest was absent. Added repo-root.agents/plugins/marketplace.jsonand--sparse .agents/pluginsto the Route B registration call. checkCodexPluginRegisteredfalse positive — the text and JSON fallback paths matched any output containingswitchbot, including theMarketplace switchbottitle line. Both paths now require theswitchbot@pattern so only actual plugin-ID lines are accepted.- Windows
os error 32stalls Route B for 17 s — when a git file-lock is detected and the local@switchbot/codex-pluginpackage directory already exists, the 2 s / 5 s / 10 s retry sequence is skipped andregisterCodexPluginAutofalls 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 repairre-registers when already healthy — newpreflight-pluginstep runscheckCodexPluginRegisteredbefore the destructive remove+re-register cycle. When the plugin is already installed, bothremove-pluginandregister-pluginare auto-skipped. Force re-registration with--skip preflight-plugin.@switchbot/claude-code-pluginplugin.jsonmissingskillsfield —plugins/switchbot/.claude-plugin/plugin.jsondid not declare"skills": "./skills/", so Claude Code would not loadSKILL.mdand the skill instructions would be absent. Field added.
switchbot claude-code setupcommand — 6-step pipeline that bootstraps Claude Code integration end-to-end: verifyclaudeCLI on PATH, optionally install@switchbot/openapi-cli, register the MCP server viaclaude mcp add --scope user, authenticate, and run a health check. Supports--yes(non-interactive),--skip,--dry-run,--json. Pastenpx @switchbot/openapi-cli claude-code setupinto Claude Code chat to set up without opening a terminal.- Claude Code Plugin Marketplace support —
.claude-plugin/marketplace.jsonnow correctly points topackages/claude-code-plugin/plugins/switchbotso/plugin marketplace add OpenWonderLabs/switchbot-openapi-clifollowed by/plugin install switchbot@switchbotworks end-to-end.smoke-codex-git-sparse.mjsextended to validate both the Claude Code and Codex marketplace entry points.
switchbot auth loginclears device/status cache — switching accounts no longer returns stale data from the previous account's cache.clearCache()andclearStatusCache()are called immediately after new credentials are saved.codex setup --help/claude-code setup --help— both commands now list--dry-runand--jsonunder a "Global flags that also apply to this command" section so users do not have to discover them from the README.
- EPERM guard in
resolveMarketplaceSourceRoot— on Windows, junction creation at@-scoped npm paths fails withEPERMwhen the runner is not elevated. Bothsrc/install/codex-checks.tsandpackages/codex-plugin/bin/install.jsnow wrap everysymlinkSynccall increateAlias, which catchesEPERMand throws an actionable error directing users to run the installer from an elevated terminal or choose a non-@-scoped install path. CODEX_PLUGIN_DEFAULT_IDconstant inrepairStepRemovePlugin— 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 location —
packages/codex-plugin/.claude-plugin/marketplace.jsonis now at the correct path and format;plugins/switchbot/structure added socodex plugin add switchbot@switchbotresolves 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.
- OpenClaw plugin publish path —
@switchbot/openclaw-skillis back in the monorepo npm publish matrix.publish.ymlnow version-checks and publishes it alongside the CLI and Codex plugin, whilenpm-published-smoke.ymlverifies the published tarball through the shared plugin smoke path. - OpenClaw package metadata —
packages/openclaw-skill/README.md,.mcp.json, and package metadata now describe the actual runtime contract: OpenClaw bootstraps viabin/start.js, then delegates toswitchbot mcp servefor the CLI-owned MCP tool surface. - Codex smoke tests —
smoke-codex-temp-prefix-route-a.mjsnow useslib/node_modules/on Linux/macOS andnode_modules/on Windows for prefix installs; test scripts in both plugin packages changed tonode --testfor cross-platform glob-free discovery; plugin registration smokes added to release gate.
- Route B (git marketplace) —
codex setupandcodex repairnow trycodex plugin marketplace add OpenWonderLabs/switchbot-openapi-cli --sparse packages/codex-plugin --ref mainbefore 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 viaCODEX_GIT_MARKETPLACE_REF(defaultmain) andCODEX_MARKETPLACE_ADD_TIMEOUT(default 60 000 ms). - On-demand npm install — when both Route B and Route A fail,
registerCodexPluginAutoinstalls@switchbot/codex-plugin@latestand retries Route A automatically. Covers fresh-machine setups where the npm package was never installed.
codex setupremoves theinstall-codex-pluginstep — on-demand install is now embedded insideregister-pluginviaregisterCodexPluginAuto, so the explicit step is no longer needed.--skip install-codex-pluginis still accepted (silently ignored) for backward compatibility.- Legacy plugin ID cleanup —
codex repair'sremove-pluginstep now removes both the current ID and the legacy IDswitchbot@switchbot-skillbefore re-registering, matching the pre-clean behaviour already present in the registration helpers.
- Dangling symlink crash —
resolveMarketplaceSourceRootcalledrealpathSyncwithout a guard; if the junction/symlink target was deleted (e.g. afternvm useornpm 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_REFenv 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-pluginwas 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-installbinary frozen —resolveMarketplaceSourceRootinpackages/codex-plugin/bin/install.jsis now marked frozen (not kept in sync withcodex-checks.ts). The binary is deprecated; useswitchbot codex setupinstead.
- Monorepo absorption —
@switchbot/codex-plugin(formerly@cly-org/switchbot-codex-plugin) now ships from this repository underpackages/codex-plugin/. A single GitHub Release publishes any package whose version was bumped since the previous release. - Codex command group —
switchbot codex setup,switchbot codex doctor,switchbot codex repairorchestrate Codex plugin install, the 7-check health summary, and end-to-end repair.switchbot install --agent codexis the register-only sibling. codex setup6-step flow — addsinstall-codex-pluginbetweeninstall-switchbot-cliandregister-plugin, so a singlenpx @switchbot/openapi-cli codex setupinvocation can bootstrap a brand-new machine end-to-end without a separatenpm install -g @switchbot/codex-plugin. Both install steps are skippable via--skip.
- Plugin package name —
@cly-org/switchbot-codex-plugin→@switchbot/codex-plugin. The old name was verification-stage with no published users; nonpm deprecatenotice is necessary. - Codex plugin
onInstallhook — 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 runsswitchbot codex setup --yesto fast-path setup; when absent it prints a hint pointing atnpx @switchbot/openapi-cli codex setup. - Plugin version reset to
0.1.0for first publish under the new scope. - Publish workflow —
.github/workflows/publish.ymlgains adetect-versionsstep and per-package guards so plugin failures (or unbumped versions) do not block CLI promotion. Plugin steps runcontinue-on-error: trueand surface failures as workflow annotations.openclaw-skillremoved from publish scope (distributed via companion repo). - Smoke workflow —
.github/workflows/npm-published-smoke.ymlis now a per-package matrix. CLI keeps offline + live smoke; plugins get tarball-shape checks (concrete peerDep, executable bin entries) without live smoke. - CI —
policy-schema-synccross-repo job removed; the skill consumer is now in this monorepo.
- Codex
register-pluginon Windows / npm scoped install dirs —codex plugin marketplace addfrom<npm-root>/@switchbot/codex-pluginfailed with--ref is only supported for git marketplace sourcesbecause codex CLI 0.133.0 misparses local paths containing@asowner/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 metadata —
packages/codex-plugin/.agents/plugins/marketplace.jsonwas missing from the published tarball (file untracked + omitted fromfiles). Now committed and listed underfiles, with the local-source path corrected to../../socodex plugin add switchbot@codex-pluginresolves the plugin manifest atpackageRoot/.codex-plugin/plugin.json. - Codex
plugin addACCESS_DENIED on Windows —codex plugin addattempts to back up an existing installation before replacing it; on junction-backed paths the backuprename()fails withos error 5. Registration now issues a best-effortcodex plugin removebeforecodex plugin addto 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, causingEADDRINUSEon immediate retry. The server is now shut down in theopen()catch block.
- Sibling repository
openclaw-switchbot-skillis archived; future development happens here.
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 callscloseAllConnections()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_devices—hubDeviceIdschema relaxed tonullable().optional()in bothdeviceListandinfraredRemoteListoutput schemas. Devices without a hub (standalone Wi-Fi devices, IR remotes) returnnullor omit the field; the strictz.string()schema caused tool call validation failures for these devices.
auth login— browser-based OAuth 2.0 login; stores credentials in OS keychainresetcommand — clear all local data (credentials, cache, quota, history, metadata)SWITCHBOT_OAUTH_CLIENT_SECRET,SWITCHBOT_TOKEN_AES_KEY,SWITCHBOT_TOKEN_AES_IVenv-var overridesdevices expandsupportssetBrightness,setColor,setColorTemperature
reset --config: scoped data paths correctly; no longer deletes unrelatedcache/directorydevices list --json --fields: alias inputs resolve to canonical output keysconfig: credential-missing hint preserves--configin recovery commands- Daemon start in bundled builds (BUG-001),
rules runexit code (BUG-002), unknown subcommand exit code (BUG-005/008),mcp tools --jsonmetadata (BUG-007), Pino stdout leak (BUG-009)
schemaVersionbumped to1.2catalog show <Type> --json/devices commands <Type> --json:datais always an array_fetchedAtrenamed tofetchedAtindevices statusJSON output- MCP
list_devices:roomIDandcontrolTypenow acceptnull
- Webhook listener port race (
webhook-listener.ts): readserver.address()synchronously inside thelisteningcallback to prevent a stale port on Windows IOCP. - Webhook 413 socket leak: changed
req.destroy()toreq.resume()so the 413 response flushes before the connection is torn down; socket is destroyed onres.finish. plan suggestexit code: missing--devicenow exits 2 (usage error) instead of 1.plan suggest --devicesalias: added--devicesas a repeatable alias for--device; mixed usage preserves argv insertion order.devices status --strict: added--strictflag that exits 1 if any batch device fetch fails.device-meta listhidden filter: human mode now hides hidden devices by default;--allshows everything; JSON always exports all records.doctorMCP message: now reports the default-profile tool count and hints at--tools all.quota statusreset time: "Remaining budget" line now includes the local reset time.policy/validateCJS interop: replacedcreateRequirehack with a staticimportforajv-formats, fixing esbuild bundling.@modelcontextprotocol/sdkbundled: moved fromdependenciestodevDependencies; esbuild now inlines it, reducing install footprint.
- Shell completions for
policy,rules,auth keychain,status-sync, anddaemoncommand 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/commandslines ≥ 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.
-
Catalog sync with upstream OpenAPI: added
Weather Station(deviceTypeWeatherStation, sensor withatmosphericPressurefield),Lock VisionandLock Vision Pro(video smart locks with the same lock/unlock/deadbolt safety semantics as Smart Lock Pro), theSmart Lock Pro WifiMatter alias on the existing Smart Lock entry, anduploadImage <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_budgetnow acceptmax_tokens_per_hour(hourly window, aligned withmax_calls_per_hour) andmax_cost_per_day_usd(24h window). Costs are computed from per-model USD pricing insrc/llm/pricing.tsand reported onDecideResult.usage. Audit entries forllm-conditionnow carryllmUsage, andllm-budget-exceededrecordsbudgetDimension(calls | tokens | cost),budgetLimit, andbudgetObserved. New lints:condition-llm-tokens-budget-zero(warns when token cap is 0) andcondition-llm-cost-without-known-model(warns when a USD cap is set withprovider: autosince the cost dimension silently skips models not in the pricing table). -
Cross-event aggregation in conditions: new
event_countcondition 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>.jsonlused byevents history, with rotation honored. The LLM-conditionrecent_eventshook (declared in v0.2 schema since Track κ but unwired) now also pulls from this fetcher, populatingcontext.recent_eventswith up to N most-recent matching events on the trigger device. Engine-levelLlmConditionEvaluatoris now wired intoRulesEngine(previously onlysimulatehad it). New lints:condition-event-count-bad-windowandcondition-event-count-max-below-min. -
Local / non-tool-use LLM provider: new
provider: localforllmconditions points at any OpenAI-compatible chat completions endpoint (Ollama, llama.cpp server, vLLM, LM Studio). Defaults tohttp://localhost:11434/v1; override withSWITCHBOT_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 YAMLtool_use: trueorSWITCHBOT_LOCAL_LLM_TOOL_USE=1. NewLLMProvider.capabilities.toolUseflag exposes this to the rest of the system. Newdoctorchecklocal-llm-reachableprobes the configured endpoint when (and only when) policy usesprovider: local. -
Daemon JSON-RPC IPC transport:
switchbot rules runnow 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 atsrc/daemon/client.tsexposesIpcDaemonClient.call()and.ping(). Wire protocol: newline-delimited JSON-RPC. Sets the foundation for futuremcp serve --via-daemonproxying so MCP clients can avoid per-call CLI cold-start. Newdoctorcheckdaemon-ipcreports IPC reachability and round-trip latency when the daemon is running.
- Decision trace —
automation.audit.evaluate_trace(full | sampled | off, defaultsampled) records every rule evaluation decision to the audit log with condition-level breakdown, LLM call results, and throttle state.evaluate_retention_daysbounds 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.--lastshows the most recent evaluation;--jsonoutputs structured data for scripting.- LLM condition (
llm:) — gates rule execution on an AI yes/no judgement. Supportsprovider: auto | openai | anthropic,cache_ttl(skip redundant calls for identical context),budget.max_calls_per_hour, andon_error: fail | pass | skip. Cache key is content-addressed so equivalent prompts + context always hit the cache.rules lintflags 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 tools —
rules_explain(decision trace lookup) andrules_simulate(offline rule replay) exposed via the MCP server alongside existing rule tools.
- Test suite: 1959 → 2204 tests (+245, covering trace, explain, LLM condition, and simulate modules).
type: notifyaction — 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_suggestMCP tool gainsllmparameter; newrule_notificationsMCP tool queries notify delivery history.doctor notify-connectivitycheck probes webhook URLs declared in notify actions.- Audit log gains
rule-notifyandllm-suggestkinds with latency fields.
- notify templates expand nested event fields (
{{ event.context.deviceMac }},{{ event.list.0 }}). channel: filerejects relative paths at lint and runtime.- LLM prompt embeds
triggerWebhookschema def;--trigger webhookno longer produces dangling$refoutput.
switchbot capabilitiescrashed on 3.3.1 withcapabilities metadata coverage error: missing:mcp list-tools, missing:mcp tools. 3.3.0 added themcp toolsandmcp list-toolssubcommands tomcp.tsbut did not register them inCOMMAND_META. The runtime coverage check insidecapabilitiesfires against the fully-registered program, but the existingcapabilities-metaunit test only registered thecapabilitiescommand 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.
- Regression guard test
capabilities-program-coverage.test.ts. Registers everyregister*Commandfromsrc/index.ts(mirrored order) against a freshCommand, then actually invokescapabilities --compactsovalidateCommandMetaCoveragewalks the complete command tree. Any future leaf command added without aCOMMAND_METAentry now fails this test before publish instead of on users' machines.
status-sync start --probepreflight 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 viacmd.optsWithGlobals(). Bare and explicit forms now behave identically.- Live-inventory policy validation could silently skip pathological
rule device refs.
validateLoadedPolicyAgainstInventoryrelies on the offline base pass catchingundefined/ empty-string /<id>device refs, because the live validatorcontinues whenresolveInventoryDeviceId()returns null. A parametric test now pins the contract so weakening either side is caught immediately (tests/policy/validate.test.ts). writeAuditbest-effort contract pinned by tests. Several callers (thesrc/lib/devices.tsdry-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).
test:release-smokerenamed totest:release-smoke:manual. The aggregated 25-file release suite runs manually, not fromprepublishOnlyor CI; the new name stops future maintainers from assuming it is wired in.
RELEASE_METADATA3.3.0 entry. The{schemaVersion, data}JSON envelope actually shipped inv2.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, andupgrade-check/doctor release-notestests now pin that contract.
- Live-inventory policy validation tier.
validateLoadedPolicyAgainstInventory(policy, devices)now runs after the always-on offline pass and raisesalias-live-device-not-foundandrule-live-unsupported-commandwith schema-pointed paths and actionablehintfields. Covered end-to-end intests/policy/validate.test.ts. - Catalog fidelity fixture + regression test.
tests/fixtures/catalog-fidelity.observed.jsonpins role andstatusFieldsfor 11 real device types;tests/devices/catalog-fidelity.test.tsfails 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 pinschemaVersion: "1.1"for watch / events.
switchbot policy new,switchbot policy validate, and the MCPpolicy_newtool no longer fail at runtime when installed from the packed tarball. Under esbuild bundling,import.meta.urlpoints atdist/index.jsinstead of the original source file, so the three call-sites that loaded embedded assets vianew URL('<relative>', import.meta.url)resolved to non-existent paths (dist/schema/v0.2.jsoninstead ofdist/policy/schema/v0.2.json, etc.). Fix: a new top-levelsrc/embedded-assets.tsmodule, positioned at the source-tree counterpart ofdist/index.js, now owns the two asset-loading functions (readPolicySchemaJson,readPolicyExampleYaml). Becauseembedded-assets.tsand 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.mjsnow exercises the loader paths end-to-end against the installed tarball — in addition to the existing--versioncheck, it runsswitchbot policy new <tmp>/policy.yaml(asserts the template was written) andswitchbot 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.
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 labelledalias-onlyin thematched_oncolumn (renamed frommatched). A new--strictflag restricts hits to type-name matches only and prints a "(strict mode — try without --strict)" hint when nothing matches.switchbot status-sync startnow prints a multi-line hint whenOPENCLAW_TOKENorOPENCLAW_MODELis 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-runnow separates "Planned (dry-run)" from "Skipped (offline)" in the human-readable output and the summary line reportsplanned=N, skipped_offline=Malongside 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 --helpclarifies that the default output is a human-readable table and that--jsonis the agent-friendly JSON-Lines form, with the seed-tick ("from": null) note surfaced near the top.
- Release pipeline unified:
npm run buildis now the single source for the published tarball. It runs a 5-stagescripts/build.mjsorchestrator (clean → typecheck → bundle → copy-assets → ensure-binary).prepublishOnly,verify:pre-commit,verify:pre-push,publish.yml, and thebundle-smoke/pack-install-smokeCI jobs all callnpm run buildby name — no job re-implements the build steps and no other script writes todist/. - Removed
npm run build:prodandnpm run clean— both are folded intoscripts/build.mjs. - Added
npm run typecheck(tsc --noEmit) as the local "does it still compile?" escape hatch. - Split
scripts/copy-assets.mjsresponsibility into two scripts with one failure mode each:copy-assets.mjsonly copies policy assets, and the newscripts/ensure-binary.mjsasserts the shebang is present ondist/index.jsandchmod 0755s it.ensure-binary.mjsis a regression guard — it fails loudly if the esbuild banner drops the shebang, rather than silently repairing it the waycopy-assets.mjsused to.
Deprecated on npm. The initial
3.2.1publish shipped a broken bin (missing shebang / exec bit afternpm pack). It has been rolled back frommainand relanded in3.2.2; install@switchbot/openapi-cli@3.2.2or later. The feature list below is retained as the historical record of what3.2.1intended to deliver and what3.2.2now ships.
switchbot plan save [file]— persist a validated plan to~/.switchbot/plans/<planId>.jsonwith statuspending; prints the assignedplanId.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 apendingplan toapproved; required beforeplan executewill run it.switchbot plan execute <planId>— execute anapprovedplan; marks itexecutedon completion; all steps are recorded in the audit log withplanIdtraceability.- MCP
send_commandnow returns ariskProfilefield per action:riskLevel,requiresConfirmation,supportsDryRun,idempotencyHint, andrecommendedMode— computed from actual device type and command at request time. - Rules engine now enforces
maxFiringsPerHour(sliding 3600 s count window),suppressIfAlreadyDesired(skipturnOn/turnOffwhen device's live state already matches), andhysteresis/requires_stable_for(fire only after trigger is continuously stable for the specified duration). All three are validated inrules lint.
rules lintnow validateshysteresis/requires_stable_forduration syntax and warns whenhysteresisandrequires_stable_forare both set.
- Single publish source:
publish.ymlnow runsnpm run build:prod(esbuild) to match whatprepublishOnlyproduces atnpm publishtime. The tarball validated bysmoke:pack-installis now byte-identical to the tarball that actually ships to the registry — no artifact swap during publish. - Pre-publish
smoke:pack-installruns inpublish.ymlbeforenpm publish, and the same smoke runs locally viapre-pushhook (verify:pre-push) and on every PR in CI (pack-install-smoke). scripts/copy-assets.mjsnow injects the#!/usr/bin/env nodeshebang intodist/index.jsand chmods it to0755after every build, so the npm bin entry is always executable.- New
npm-published-smoke.ymlworkflow verifies published tarballs on the npm registry, auto-promotesnext → lateston success, and auto-deprecates on package-install/offline smoke failures only (never on live API flakes). bundle-smokeCI 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.mdfor the full gate sequence and invariants.
switchbot daemon start/stop/status— runsrules runas a cross-platform detached background process; PID written to~/.switchbot/daemon.pid; log atdaemon.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 whatscenes executewould POST without sending the request.switchbot rules summary— aggregatesrule-fireaudit entries per rule over a configurable time window, printing a table of fires / throttled / errors.switchbot rules last-fired— shows the N most recentrule-fire/rule-fire-dryaudit entries, newest first;--rulefilter supported.
CircuitBreakerclass inutils/retry.ts; module-levelapiCircuitBreakersingleton wired inapi/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-tokennow prints a tip to runswitchbot auth keychain storewhen file-backend credentials are saved on a platform with a native keychain.doctornow includes akeychaincheck that warns when file-backend credentials are in use and a native keychain is available.
- Policy schema v0.2 extended with optional fields:
throttle.dedupe_window,cooldown,requires_stable_for. All validated byrules lint. ThrottleGate.check()now acceptsdedupeWindowMsand returnsdedupedByfield.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]andswitchbot rules doctor [path]subcommands.
agentSafetyTierin capabilities output maps toriskLevel(high/medium/low),idempotencyHint, andrecommendedMode.plan runnow generates a UUIDplanIdand stamps it on every audit entry produced during that run, enabling plan-to-audit traceability.
resolutionHintandcandidateMatchesfields added to structured error output.name-resolver.tsnow rejects ambiguous device names when multiple candidates match.doctorreports PATH discoverability and npm global bin reachability.
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).
The destructive boolean field has been removed from all CLI and MCP JSON output surfaces:
switchbot schema export/schema export --compactswitchbot devices describe <id>command listswitchbot agent-bootstrap --compact- MCP
catalog_searchtool 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.
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.
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.
CURRENT_POLICY_SCHEMA_VERSIONnow points to0.2, soswitchbot policy newscaffolds 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
versionvalues using the current default (0.2) while keeping0.1compatibility.
- Added a cross-surface contract test asserting MCP
policy_diffstructuredContentmatches CLIswitchbot --json policy diffoutput exactly for the same inputs.
- Normalized markdown table/fence styles in roadmap, agent guide, and README to reduce lint noise and improve publish consistency.
- Restored README
Output modesanchor and fixed broken table-of-contents links. - Updated roadmap/README backlog text with an ordered execution queue and explicit acceptance-oriented wording.
Feature release — policy review parity for MCP + dry-run default alignment.
- New read-only MCP tool
policy_diff({ left_path, right_path })returns the same contract asswitchbot --json policy diff:{ leftPath, rightPath, equal, changeCount, truncated, stats, changes, diff }. - Enables side-by-side policy review flows in MCP-only agent hosts.
plan_runexecutes Plan JSON directly in MCP (same destructive-step gating semantics as CLI: skipped unless approved).audit_queryfilters audit log entries by time/device/rule/result.audit_statsaggregates audit entries by kind/result/device/rule.- MCP tool count: 17 → 21.
- Policy schema v0.2 now defaults rule
dry_runtotrue. - 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.
Feature release — L3 fully autonomous rule authoring for agents.
- 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 emitsdry_run: trueandthrottle: { max_per: "10m" }for MQTT triggers. - Options:
--trigger,--device(repeatable),--event,--schedule,--days,--webhook-path,--out. - Output: raw rule YAML on stdout;
--jsonfor structured output. - Exported
suggestRule(opts)pure function insrc/rules/suggest.ts.
- New subcommand
switchbot policy add-rulereads rule YAML from stdin and appends it toautomation.rules[]in policy.yaml, preserving all existing comments and formatting. --dry-run: print the unified diff without writing to disk.--enable: setautomation.enabled: trueafter inserting the rule.--force: overwrite an existing rule with the same name.- Pipeline-friendly:
switchbot rules suggest ... | switchbot policy add-rule. - Exported
addRuleToPolicySource()/addRuleToPolicyFile()insrc/policy/add-rule.ts.
rules_suggest(tierread): MCP equivalent ofrules suggest; agents call it without shell access to draft a rule YAML.policy_add_rule(tieraction): MCP equivalent ofpolicy add-rule; agents inject a rule into policy.yaml and receive the diff for user confirmation. Always run withdry_run: truefirst.- MCP tool count: 15 → 17.
src/lib/command-keywords.ts:COMMAND_KEYWORDSarray extracted fromplan.tsinto a shared module; imported by bothplan.tsandrules/suggest.ts.docs/agent-guide.md: new section "Autonomous rule authoring (L3)" covering the suggest → add-rule → lint → reload → dry-run → arm workflow.
Feature release — semi-autonomous L2 workflow for agents.
- 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 toturnOnwith a warning when unrecognised. Output goes to stdout (or--out <file>); warnings go to stderr. - Exported
suggestPlan(opts)pure function for programmatic use.
- New flag
--require-approvalonplan runenables per-step TTY confirmation for destructive steps: the CLI prints the step details and prompts[y/N]before executing. Rejecting a step marks itskippedwithdecision: "rejected"in the JSON output. - Non-TTY environments (CI, pipes) auto-reject destructive steps.
--require-approvalis mutually exclusive with--json(exits with an error if both are passed).--yestakes precedence over--require-approvalas blanket approval.
- New read-only MCP tool
plan_suggest({ intent, device_ids })wrapssuggestPlan()for agents that prefer to stay in the MCP session. Returns{ plan, warnings }structured content.
Feature release — install/uninstall UX polish, cross-OS keychain CI, and two rules engine enhancements.
--forcereplaces an existing skill symlink pointing at a different target, and bypasses theSKILL.mdpresence check (for in-development skill repos).--verifyrunsswitchbot doctor --jsonas 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.stepSymlinkSkillnow requiresSKILL.mdat the root of--skill-pathbefore 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-dircheck for--agent claude-codethat probes~/.claude/skills/writable before the step can fail.
--purgeis shorthand for--yes --remove-creds --remove-policy: removes everything in one flag without any prompts.
.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 undersrc/credentials/,src/install/, and their test counterparts.
-
Cron triggers now accept an optional
daysfilter: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.
-
Rule conditions can now be composed with
all(AND),any(OR), andnot(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.
- 1676 tests pass (+22 vs 2.10.0: 6 polish + 9 day_of_week + 8 and/or/not + 1 purge).
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.
switchbot install— one-command setup that composes four steps with rollback on failure: prompt credentials → write keychain → scaffoldpolicy.yaml→ symlink the Claude Code skill. The step library (src/install/default-steps.ts) is a thin layer over the genericrunInstall()runner, so each factory is independently unit-tested and individually skippable via--skip.--agent <name>—claude-code(default: auto-links~/.claude/skills/switchbotto--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:
0ok ·2preflight failed (nothing changed) ·3step failed, rollback completed ·4rollback 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);
installprintsnext: switchbot doctoras a hint instead.
switchbot uninstall— reverse of install. Removes the skill symlink (default yes, confirm), credentials (opt-in via--remove-creds), andpolicy.yaml(opt-in via--remove-policysince user edits may live there).--yesassumes yes to every confirmation;--dry-runpreviews;--jsonemits structured outcomes.- Unlike install, uninstall is not rollback-safe — it keeps going
on per-step failure and exits
3if 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.
src/commands/policy.tsexportsscaffoldPolicyFile()so the install step can reuse the exact scaffolding logicpolicy newuses — no drift risk between the two code paths.src/commands/config.tsexportspromptTokenAndSecret()andreadCredentialsFile()for the install step to reuse the same terminal-prompt behaviorconfig set-tokenhas.
- No API/CLI surface breakage. Existing scripts continue to work.
- Path for the skill symlink:
~/.claude/skills/switchboton 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).
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.
policy new --version 0.2emits a v0.2 starter file carrying the newautomation.rules[]block.policy newstill defaults to v0.1 so existing CLI builds keep parsing the output — the default flip is tracked on the Roadmap for v3.0.policy validatedispatches the validator by schema version (0.1or0.2), so a mixed install can hold two policy files at different versions without false-positive errors.policy migrate 0.1 → 0.2walks the YAML withyaml@2's CST, rewrites theversion:scalar, and appends anautomation:block stub. Comments and key order are preserved byte-for-byte.- Destructive-command validator hook — the v0.2 schema rejects
rules whose
then.commandwould fireunlock,garage-door open,keypad createKey, or other destructive actions. The rejection is a schema error, not a runtime surprise. doctor,agent-bootstrap --compact, MCPpolicy_validatenow surface the detected policy schema version.
switchbot rules lint— static checks againstpolicy.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_runstate, 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-rulethrottleanddry_runblocks.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 torule-*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 v2 —
rule-fire,rule-fire-dry,rule-throttled, andrule-webhook-rejectedrecord types. Format is documented indocs/audit-log.md. - MQTT trigger — subscribes to the cloud-issued broker, uses the
same
extractShadowEventhelper asevents 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_statecondition — per-tick cached lookups so a single firing doesn't hit the API N times for N conditions.
src/credentials/keychain.tsabstraction with four backends: macOSsecurity, Windowscmdkey, Linuxsecret-tool(libsecret), and a0600-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.migratemoves a value from the file fallback to the OS keychain and removes the file entry on success.doctorandagent-bootstrap --compactreport the active credential source in a field namedcredentialSource.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 β).
docs/design/roadmap.md— authoritative Phase 1-4 table with the skill repo's orthogonalautonomyLevelL1/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 runningevents mqtt-tailas a long-lived service.
- README Roadmap section now points at
docs/design/roadmap.mdand 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".
- 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", andautonomyLevel: "L1". The skill's Authoritative command table adds theswitchbot rules *andswitchbot auth keychain *groups shipped in this release.
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.
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 fromswitchbot devices list --format=tsv" on alias pattern mismatches; "destructive actions (lock/unlock/delete*/factoryReset) cannot be pre-approved in policy.yaml" on a forbiddennever_confirmentry). Supports--jsonfor programmatic consumers,--no-snippetto drop source preview,--no-colorfor 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--forceis 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_PATHenv var > default~/.config/switchbot/policy.yaml. The same resolver is exported for the Phase 3agent-bootstrapinstall flow to reuse. - Exit-code taxonomy (scriptable):
0valid /1invalid /2file-not-found /3yaml-parse /4internal /5exists (onnewwithout--force) /6unsupported-version (onmigrate).--jsonmode emits the usual{schemaVersion, error}or{schemaVersion, data}envelope. - Embedded schema asset —
src/policy/schema/v0.1.jsonships in the npm package via a post-buildcopy-assets.mjsstep. The skill repository'sexamples/policy.schema.jsonis the mirror copy; a CI job diffs the two on every push to prevent drift.
- Added
yaml@^2(source-map-preserving parser),ajv@^8+ajv-formats@^3(JSON Schema 2020-12 validator viaajv/dist/2020).
- 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 withswitchbot policy new+switchbot policy validate. See the skill repo'sCHANGELOG.mdfor the matching entry.
Patch release — CI size-budget fix.
schema export --compact— dropped theresourcesblock from compact output. In v2.7.0 the resources catalog (scenes / webhooks / keys, ~12 KB) was added to the schema payload unconditionally, which pushedschema export --compact --usedpast the 15 KB agent-prompt budget enforced by CI. Theresourcesblock is still emitted under the full (non---compact) output, and is always available viacapabilities --json, which is the canonical source for CLI resource metadata. No behaviour change forcapabilities --jsonconsumers.
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.
- 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-levelproduct,domain,vendor,apiVersion,apiDocs, andproductCategories[]fields for programmatic discovery. Subcommand--help --jsonoutput is unchanged (identity is root-only to keep per-command payloads tight).- Subcommand descriptions —
catalog,schema,history,plan,doctor,capabilitiesnow 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.
- Shared IDENTITY module — extracted the product-identity constant to
src/commands/identity.ts;capabilities.ts,agent-bootstrap.ts, andutils/help-json.tsnow import from a single source of truth to prevent field drift. The canonical IDENTITY addsproductCategories: string[](8 category keywords AI agents can scan) and clarifiesconstraints.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 --jsongains additive identity fields (apiDocs,deviceCategories,productCategories,agentGuide) via the shared module; no fields removed.
AI-first maturity release. Broader field-alias coverage, richer capability metadata, and agent-discoverable resource surfaces (scenes, webhooks, keys).
- Field aliases — registry expanded from ~10 to ~51 canonical keys (~98% coverage of catalog
statusFields+ webhook payload fields), dispatched throughdevices status,devices watch, and--fieldsparsers. 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 legacydestructive: booleanflag. DeviceCatalogEntry.statusQueries— read-tier catalog entries exposing queryable status fields; derived from existingstatusFieldsplus a curatedSTATUS_FIELD_DESCRIPTIONSmap. PowerssafetyTier: 'read'and lights upcapabilities.catalog.readOnlyQueryCount.capabilities.resources— new top-levelresourcesblock incapabilities --jsonandschema 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=yamland--format=tsvfor all non-streaming commands (devices list, scenes list, catalog, etc.);id/markdownformats preserved.--jsonremains the alias for--format=json. - doctor upgrades — new
--section,--list,--fix,--yes,--probeflags; new checkscatalog-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 indocs/json-contract.md. - Events envelope — unified
{ schemaVersion, t, source, deviceId, topic, type, payload }shape acrossevents tailandevents 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.
- Error envelope — all error paths route through
exitWithError()/handleError();--jsonfailure output always carriesschemaVersion+ structurederrorobject. - Quota accounting — requests are recorded on attempt (request interceptor) instead of on success, so timeouts / 4xx / 5xx count against daily quota.
--jsonvs--format=json— both paths go through the same formatter;--jsonis now documented as the alias.
destructive: booleanon catalog entries — derived fromsafetyTier === 'destructive'. Removed in v3.0.DeviceCatalogEntry.statusFields— superseded bystatusQueries. 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/remoteonevents tail— superseded by the unified envelope. Removed in v3.0.
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).
- Quota counter no longer under-counts requests that fail at the transport or server layer.
devices describenow shows a tip for device types that supportdevices expand(Air Conditioner, Curtain, Curtain 3, Blind Tilt, Relay Switch 2PM);--jsonoutput includes anexpandHintfield withcommand,flags, and a ready-to-runexamplestring
--filter controlType=Xnow works correctly;controlTypewas documented as a filterable key but was missing from the canonical key registryfield-aliases.ts: removedcategoryfrom thecontrolTypealias list to prevent collision with the physical/IRcategoryfilter key- MCP stdio path now handles
SIGTERMandSIGINTwith the same graceful shutdown as the HTTP path (30 s force-exit timeout,isShuttingDownguard)
- Extracted
exitWithError()helper inoutput.ts; deduplicatedisJsonMode()acrossindex.tsandconfig.ts
- MCP
send_commanddry-run now strictly rejects unknown command names when catalog has a definitive match (#55) - MCP
send_commanddry-run rejects commands sent to read-only sensors (e.g. Meter) - Previous v2.6.2 fix used lenient
validateCommandwhich silently passed when catalog lookup was ambiguous
scenes execute --dry-runnow outputs structured result on stdout instead of silently exiting (#54)- MCP
send_commanddry-run validates command name against catalog before returning success (#55) - MCP
run_scenedry-run validates sceneId against scene list before returning success (#56)
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.
- MCP
send_commandnow runs the same client-side parameter validator as the CLI (setBrightness,setColor, ACsetAll, Curtain / Blind TiltsetPosition, RelaysetMode). Out-of-range values fail withisError:trueandusageclass before the API is called, in both thedryRun:trueand live paths.setColorhex / named-colour / comma forms are normalised to theR:G:Bwire format before dispatch, and the dry-runwouldSend.parameterreflects the normalised value. (R-2)
--filterempty-value rejection is now called out in the README. A clause likename~ortype=with an empty RHS exits2— the parser refuses to guess between "no constraint" and "match empty string". Drop the clause outright to remove the constraint. (R-4)
- Added coverage locking in plaintext
scenes describe <unknown>rendering ofDid 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)
- 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 intests/utils/string.test.tsis intentional — it exercises the CJK branch ofnormalizeDeviceName(\u4e00-\u9fffinsrc/utils/string.ts:20), which is a supported product feature.
- R-1 (MCP error envelope for Zod validation failures) — rejected
by design. The MCP SDK wraps every tool handler error as
CallToolResult.isError:true; onlyUrlElicitationRequiredescapes as a JSON-RPCerror. 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 fromcontent[0].textif they need it.
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.
--filter key!=value(negated clause) acrossdevices list,devices batch, andevents tail/mqtt-tail.neqis a negated substring by default;categorystays exact. (B-5)--allow-unknown-deviceand--skip-param-validationondevices command. Escape hatches for the stricter defaults below. (B-1, B-3)capabilities --usedfilters the per-type summary to devices present in the local cache. Same semantics asschema export --used. (B-9)catalog list(alias forshow) andcatalog search <keyword>(fuzzy match across type, aliases, role, commands). (B-8)--retry-on-5xx <n>(default2) transparently retries502/503/504and request timeouts on idempotentGETs with exponential backoff. Mutating calls still never auto-retry — use--idempotency-keyfor safePOSTretries. (B-11)commandMetaflat manifest incapabilities --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)
--flag=valueequals form is now recognized everywhere.--format=json,--timeout=5000,--config=/path,--fields=a,b,cetc. previously dropped silently throughgetFlagValue; they now work identically to the space form. (B-6)devices commands <type...>iterates truly variadic:devices commands Bot Curtainprints 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 rendersDid you mean: …?candidates — previously only the--jsonbranch did. (B-10)setColoracceptsR:G:B,R,G,B,#RRGGBB,#RGB, and 16 CSS named colors (red,blue,orange, …); all normalize toR:G:Bbefore hitting the API. (B-12)- Ambiguous
--namehint now explicitly lists--name-strategy=fuzzy/--name-strategy=firstso users know how to break ties. (B-15) - Empty results under
--table-style markdownrender as_(empty)_rather than a header-only skeleton. (B-16) - Error-path formatting is consistent with
--format=json: every terminal error path now routes throughemitJsonError/handleError, so piped consumers can alwaysJSON.parsestderr (or stdout in JSON mode) without string parsing. (B-14)
setBrightness,setColor, and CurtainsetPositionsingle-value form are now validated client-side.setBrightnessrequires integer1-100;setColorvalidates each component0-255;setPositionvalidates0-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-validationas a narrow escape hatch. (B-1)- Unknown
deviceIdindevices commandnow exits 2 by default with a candidate list, rather than warning-and-passing-through. This makes--dry-runa reliable pre-flight gate for agents and plans. Soft-breaking: pass--allow-unknown-deviceto restore pass-through behavior for scripted workflows. (B-3)
- Roadmap section at the bottom of README for the v3.x track
(daemon mode, standalone
npxMCP package,self-testharness, record/replay) — audit items B-17 / B-18 / B-19 / B-21 are parked there rather than folded into this minor. - Clarified that
devices expandis intentionally limited to multi-parameter commands (setAll,setPosition,setMode); single-parameter commands likesetBrightness 50orsetColor #FF0000don't need semantic flags. (B-13)
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.
--filtergrammar unified across three surfaces —devices list,devices batch, andevents tail/mqtt-tailnow share one DSL:key=value(case-insensitive substring; exact only forcategory),key~value(explicit case-insensitive substring), andkey=/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 treatsBotas a substring (matchesBot Plustoo). 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).
- Breaking:
devices command --dry-run --jsonno longer emits empty stdout — the single-device write path was hittinghandleError's silentDryRunSignalexit before the JSON serializer ran. Now mirrors the MCPsend_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 structure —
send_command/describe_device/run_scenewere lettingApiErrors escape to the SDK's genericcreateToolError, collapsing{code, subKind, transient, hint, retryAfterMs, errorClass}to a plain-text string. Errors now returnstructuredContent.erroralongsideisError:trueso agents can branch onsubKindinstead of parsing English. Also narrowed themcpError()option types sosubKind/errorClassare compile-time checked. (bug #38) devices batchpropagatesverification+subKindfor 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 includesubKind:'ir-no-feedback'and the verification object for IR devices, plussummary.unverifiableCount. (bug #28)- Device & status cache scoped per profile —
devices.jsonandstatus.jsonlived 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 somcp serverequest-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 likewebhook querywith no webhook configured. Thedevice-busysubKind 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., FanlowSpeed/middleSpeed/highSpeedon stock IR remotes that only work under--type customize). Now returns a useful subKind + hint pointing todevices commands <type>and--type customize. (bug #29) scenes executepre-validates sceneId —scenes execute <bogus>returnedok:truebecause the API does not validate sceneIds.scenes describealready guarded against this viascene_not_found— port the same check so agents do not silently burn quota. (bug #31)devices meta set --aliasenforces 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;--forcereassigns (clears the old holder's alias) with a log line. (bug #41)
--fields id/--fields namealiases restored ondevices list— the 2.5.0 alias-map refactor dropped the short forms that 2.4.0 accepted, breaking scripts.id → deviceIdis back alongsidename → deviceName. (bug #22)cache clear --statusand--listshorthand aliases — the old--key statusform still works, but the shorter flags no longer error withunknown option. Using them with--keyor together raisesUsageError. (bug #35)history aggregate --metricmarkedrequiredOption— help text said(default: [])implying optional; the command actually required at least one metric and threw a custom error. Now Commander enforces it and--helpsaysrequired. (bug #42)plan validatehelp text clarifies scope — now says "structural only; does not verify device or scene existence" and points toplan run --dry-runfor semantic checks. (bug #32)cachehelp text documents TTL behavior — the cache TTL is computed from thelastUpdatedfield inside the JSON, not file mtime. Operators whotouched cache files to force a refresh were surprised. One-line note added tocache show --help. (bug #34)devices metasurfaced in agent-bootstrap and capabilities — the local metadata system was completely undiscoverable in 2.5.0.meta set / get / list / clearnow appear incapabilitieswith correct safety tiers, andagent-bootstrap'squickReferencegains ametaentry. (bug #40)~/.switchbot/device-history/<id>.jsoncompanion file documented — the 100-entry ring buffer read by MCPget_device_historyhad no docs, while only the append-only.jsonlwas mentioned.docs/agent-guide.mdnow describes both files and__control.jsonl. (bug #43)
--jsonerrors now emit on stdout instead of stderr — piped consumers (cli --json ... | jq) could not decode failure envelopes becausehandleErrorwrote 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 acrossbatch,config,devices,expand,history,mcp, andformatwere consolidated through a newemitJsonErrorhelper. (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 returnsubKind:'device-not-found'with a hint to runlist_devicesfirst. Happy path unchanged for cached IDs. (bug #SYS-3)
devices batch --idempotency-keyaccepted as alias for--idempotency-key-prefix. Still uses prefix semantics internally (auto-appends-<deviceId>per step). (bug #30)
devices batch --skip-offline(default off) skips devices whose cached status is offline, with each skip recorded undersummary.skippedwithskippedReason:'offline'. Reads the local status cache only — no new API calls. Off by default preserves 2.5.0 behavior. (bug #33)--for <duration>alias ondevices watch,events tail, andevents 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--forand--maxare set, the first limit to hit wins. (bug #52)- Duration parser accepts
d(days) andw(weeks) in addition toms/s/m/h. Unsupported units like1y/1monthnow produce a usage error that lists the supported unit set. (bug #54) events mqtt-tail --jsonemits a__session_startenvelope 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/__disconnectevents. (bug #56)
--name-strategyhelp +agent-bootstraplist all six strategies —exact,prefix,substring,fuzzy,first,require-unique.ALL_STRATEGIESinname-resolver.tsis the single source of truth; help text is generated from it. (bug #51)- MCP
search_catalogrejects empty queries with a usage error pointing tolist_catalog_typesfor enumeration. Silent "return everything" behavior was surprising and agent-hostile. (bug #57) - Negative positional parameters reach the validation layer —
setBrightness -1was being swallowed by Commander as "unknown option-1".devices commandnow 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)
- 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 viasafeParseAsync→ 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 statusoutlier) — 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 --jsonrewording) — already works via the global--jsonflag; pure doc rewording scheduled with other doc sweeps. - MCP / CLI naming alignment (
livevsincludeStatus,metricvsmetrics) flagged in Round-3 §4. devices meta import/export(Round-2 #40 follow-up).
- Report bug #58 (parallel
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 aggregateentry inCOMMAND_META; newaggregate_device_historyentry insurfaces.mcp.tools. scenes describe <sceneId>— returns{sceneId, sceneName, stepCount:null, note}; SwitchBot API v1.1 does not expose scene steps. Unknown sceneId returns structuredscene_not_foundwith a candidate list. (bug #17)--no-colorflag +NO_COLORenv 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 tablewith--table-style markdownforced at render time, independent of the user's--table-styleflag. (bug #8)cache status— alias forcache show, matching thequotasubcommand's status/show parity. (bug #9)
- MCP strict input schemas on all 11 tools — unknown keys now
reject with JSON-RPC
-32602. Fixes the v2.4.0 hole wheresend_command {dryRun:true}silently fired the command anyway — particularly dangerous for Smart Lock / Garage. (bug #4) - MCP
dryRunon mutating tools —send_commandandrun_sceneacceptdryRun:true; when set, no API call is made and the response is{ok:true, dryRun:true, wouldSend:{...}}. (bug #4) - MCP
serverInfo.version— wired topackage.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) --namerequire-unique + exact-match — exact-name short-circuit inname-resolverwas returning the exact hit even when substring matches existed, defeating the write-pathrequire-uniquedefault. Exact hits now enter the candidate list underrequire-uniqueand go through the ambiguity check like any other match. (bug #1)history verifyon missing audit.log — exits 0 withstatus:"warn"andfileMissing:truerather than exit 1. Malformed/unversioned content still exits 1 as before. (bug #11)events mqtt-tailcontrol events —__connect/__reconnect/__disconnect/__heartbeatnow append to~/.switchbot/device-history/__control.jsonlalongside per-device files, honoring the v2.4.0 "every event is persisted" claim. (bug #10)
--idempotency-keyhelp text ondevices command,devices batch,plan run,history replaynow explicitly mentions the process-local 60s scope — independent CLI invocations do NOT share the cache. (bug #14)mcp --helpnow 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-unsafeopt-out. (bug #16) plan schemanow includesagentNotes.deviceNameStrategydeclaring that plan steps usingdeviceNameresolve withrequire-unique. (bug #18)agent-bootstraphintsfield carries JSDoc +schema exportdeclares it incliAddedFields— empty array means "no hints", never null. (bug #13)
- 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: trueand a per-bucketnotes[]entry.count / min / max / avg / sumremain 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.
- Cross-device aggregation (agents merge locally).
- Trend / rate-of-change helpers (derivable from bucket series).
--fill-emptyfor missing buckets.- Disk-persisted idempotency cache for cross-invocation replay
(report bug #2). Process-local is the documented 2.4.0 contract;
the
--helptext now states this plainly — no code change. Revisit only if a concrete use case forces it. capabilities --types/--fields/--used(report bug #7).schema exportalready offers these for the agent bootstrap path;capabilities --compact --surface <s>covers the payload-size story.
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.
- IR command verifiability tag —
devices commandresponses for IR devices now carryverification: { verifiable: false, reason, suggestedFollowup }. Human output adds a stderr hint that IR transmissions cannot be acknowledged by the device. MCPsend_commandmirrors the same field. config set-tokensecret scrubbing — positional invocations have their token/secret replaced inprocess.argvbefore any hook, audit log, or verbose trace can observe them. Interactiveset-token(hidden-echo readline) is now the primary path; positional form prints a discouragement warning but still works for backwards compatibility.
--namescope fix —devices status/devices commandnow accept--namedirectly on the subcommand (previously root-only).capabilitiesreflects the change.- Fuzzy name resolution contract — new
src/devices/resolve-name.tsexports six strategies (exact | prefix | substring | fuzzy | first | require-unique). Reads default tofuzzy; writes default torequire-uniqueand fail with exit code 2 +error: "ambiguous_name_match"and a candidate list when multiple devices match. Global filters--type,--room,--category, and--name-strategycompose with--name. - Smaller schema/capabilities payloads + pipe hygiene —
schema exportandcapabilitiesgrew--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
capabilitiesnow carries{ mutating, consumesQuota, idempotencySupported, agentSafetyTier: "read"|"action"|"destructive", verifiability, typicalLatencyMs }. MCP tools mirror the tier inmeta.agentSafetyTier.
- JSONL storage — every
events mqtt-tailevent / MCP status refresh is appended to~/.switchbot/device-history/<deviceId>.jsonl. The file rotates at 50 MB into.jsonl.1 → .jsonl.2 → .jsonl.3with the oldest discarded. Writes are best-effort with0o600perms. 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 streamingreadlineso 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.
doctor --jsonstable contract — locked shape{ ok, generatedAt, checks[], summary }; eachcheckis{ name, status: ok|warn|fail, detail }. Theclockcheck now probes the real API once and reportsskewMs.events mqtt-tailcontrol events — synthesized JSONL records of__connect/__reconnect/__disconnect/__heartbeat. Every real event gets a UUIDv4eventIdwhen the broker doesn't supply one.devices batch --stagger/--max-concurrent/--plan— throttled concurrent execution with per-stepstartedAt/finishedAt/durationMs/replayedtelemetry and a planner (--dry-run --plan) that prints the plan JSON without executing.- Idempotency contract +
replayedflag — cache hits now returnreplayed: true. A reused key with a different(command, parameter)shape within the 60 s window exits 2 witherror: "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 batchrun,plan run,history replay— and do not carry across independent CLI invocations. - Profile label / description / daily cap / default flags —
config set-tokengrew--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 statussurface the label. --verboseheader redaction —Authorization,token,sign,t,nonce, cookies, etc. are mid-masked in verbose output.--trace-unsafeopts in to raw output with a prominent one-time warning.
quota showalias forquota status.showSuggestionAfterErroracross the full subcommand tree — typos likedevices lstnow suggestdevices list.schema exportdeclares CLI-added fields — top-levelcliAddedFieldsdocuments_fetchedAt,replayed, andverificationso 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--compactmode. Offline-safe; no API calls.--table-style <unicode|ascii|simple|markdown>+--format markdown— non-TTY now defaults toascii;markdownemits fenced|col|col|tables for agent UI embedding.- Audit log versioning — every line now carries
"auditVersion": 1. Newdocs/audit-log.mddocuments the format, crash-safety, and rotation guidance. Newswitchbot history verifyreports parsed / malformed / version counts and exits non-zero on malformed content.
- 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 --compactonce per session instead of combiningdoctor+capabilities+schema+devices list. - Upgraders who manage profiles with sensitive daily budgets should run
switchbot config set-token --profile <name> --label "..." --daily-cap Nto take advantage of the pre-flight refusal guard. - Audit logs written by 2.3.0 coexist unchanged with 2.4.0 records;
history verifyreports them asunversioned.
See git history.