feat: v1.6.0 — unified AI-agent interface (P0-P5)#5
Closed
chenliuyun wants to merge 17 commits into
Closed
Conversation
added 17 commits
April 19, 2026 11:38
…ackoff Phase A implementation: MQTT infrastructure for event streaming. - src/mqtt/types.ts: MqttCredential, DeviceShadowEvent, StreamFilter interfaces - src/mqtt/credential.ts: fetch credential from /v1.1/iot/credential endpoint, cache with 1h TTL - src/mqtt/client.ts: TLS client wrapper with exponential backoff reconnection - Parameters: initial 1s delay, 2x multiplier, 60s max, ±20% jitter, 5 max attempts - Connection stability tracking: reset attempt counter after 30s stable - AbortSignal support for graceful SIGINT handling - package.json: version 1.4.0, add mqtt ^5.15.1 dependency - package-lock.json: synced via npm install --package-lock-only No observable behavior change yet; next phases add CLI commands.
Tests for Phase A MQTT infrastructure: - credential.test.ts: credential fetching, TTL-based caching, cache invalidation - client.test.ts: connection setup, exponential backoff configuration, jitter, cancellation All tests pass; full test suite still green (672 passing + Phase A 14 passing).
Added parseEventStreamFilter and matchEventStreamFilter for MQTT event matching. Supports simple deviceId/type filters like 'deviceId=ABC' or 'type=Motion\ Sensor'. Separate from existing device list filter (which uses FilterClause).
Phase B implementation: 'switchbot events stream' for real-time device state. - src/commands/events.ts: new stream subcommand alongside existing tail (webhook receiver) - Options: --filter deviceId=/type=X, --max N, --probe (connectivity check), --no-cache - JSONL output to stdout, human-readable status to stderr - MQTT message handler extracts shadow update and applies filters - Reuses AbortController + SIGINT/SIGTERM cleanup from events tail - src/utils/filter.ts: added parseEventStreamFilter() and matchEventStreamFilter() - Simple key=value syntax separate from device list filters (FilterClause) Feature depends on SwitchBot IoT MQTT service (non-standard, documented in help). All tests still passing (673/673). No breaking changes to existing commands.
Phase C implementation: MQTT-backed device monitoring. - src/commands/watch.ts: new --via-mqtt flag switches to MQTT push instead of polling - watchViaMqtt(): subscribes to MQTT shadow updates, emits same TickEvent format - Field-level diff tracking works identically to polling mode - Respects --max N and --include-unchanged flags - Uses shared AbortController/SIGINT cleanup pattern - Extracted watchViaPolling() for code clarity (existing polling logic unchanged) Note: MQTT mode does NOT fall back to polling on broker unavailable (per design). If MQTT connection fails, user sees error; they can retry with --interval instead. All tests passing (673/673).
Phase D implementation: documentation update. - Add Release notes link to top navigation (per release rule: publish to npm requires README link) - Add 'events' entry to Table of Contents - New 'events — receive MQTT device updates' section with: - Command examples (stream, filter, probe, no-cache, JSON output) - Clear disclaimer: MQTT service is non-standard, undocumented, subject to change - Expected output format (JSONL with shadow update schema) - Credential caching info (1h TTL) Explains the feature clearly without using "experimental" label (user requested).
- Import IClientOptions from mqtt for proper type checking - Use Partial<IClientOptions> for TLS configuration - Wrap Buffer instances in arrays (mqtt package requirement) - Add explicit return types to event handlers - Fix mqtt.end() call signature (force disconnect + callback) All tests passing (673/673). Build clean.
Covers all seven findings from the post-implementation review plus the /iot/credential integration fixes found during smoke testing: 1. events stream filter mismatch — `--filter deviceId=` and `--filter type=` silently matched nothing because the matcher read ctx.deviceMac/ctx.deviceType from the shadow payload, but those fields live on webhook bodies. Added matchShadowEventFilter that reads top-level deviceId/deviceType from the parsed shadow event. 2. ErrorSubKind extensions — introduced MqttError with subKind values mqtt-tls-failed, mqtt-connect-timeout, mqtt-disconnected (wired into ErrorSubKind union, buildErrorPayload, handleError). Credential fetch now maps 401/429 to ApiError(auth-failed/quota-exceeded) and network timeouts to MqttError(mqtt-connect-timeout). Classify initial connect failures via classifyMqttConnectError so cert errors surface as mqtt-tls-failed. 3. Runtime reconnect loop — previously MqttTlsClient only retried the initial connect; mid-session disconnects silently stopped streaming with reconnectAttempts and checkConnectionStability sitting as dead code. Now attaches a close handler post-connect that drives connectWithRetry on drop, exhausting 5 attempts before emitting an mqtt-disconnected MqttError to the registered runtime-error handler. Sleep between attempts is now abortable, so end() cancels a pending backoff immediately instead of waiting out the delay. 4. Credential cache path — replaced the literal '~' fallback with os.homedir(). Also clean up the .tmp file if the atomic rename fails so we do not leave orphan writes behind. 5. events stream smoke tests — added tests/commands/events-stream.test.ts covering extractShadowEvent parsing and the end-to-end filter path that broke before finding #1. Added tests/mqtt/errors.test.ts for MqttError classification and buildErrorPayload integration. Extended credential tests for 401/429/null-body handling. 6. Quota SIGINT/SIGTERM — quota.ts previously registered global signal handlers that called process.exit(130/143), short-circuiting command-layer cleanup (watch / events stream finally blocks). The handlers now only flush the counter; they fall back to the conventional exit code only when quota is the sole listener, so short one-shot commands keep their old behavior while long-running commands retain control of their own exit path. 7. Status cache cross-process staleness — status.json was read into a process-local hot cache with no invalidation, so a long-running MCP server could not see writes from a concurrent one-shot CLI. loadStatusCache now stats mtime before every read and reloads when the file has changed on disk; saveStatusCache/clearStatusCache/ resetStatusCache update the tracked mtime accordingly. Same-process reads remain zero-IO when mtime is unchanged. 8. /iot/credential integration — the endpoint rejected POST {} with statusCode 190 "param is invalid". The signing convention differs from the public OpenAPI: nonce is the literal string "OpenClaw" (not a UUID), the HMAC signature is NOT uppercased, the `t` header is numeric, and the body requires a 12-char random `instanceId`. Response shape is also nested under body.channels.mqtt with topics: {status: string} (wrap to single-element array for our subscribe path). Error messages surface at the outer `message` field, not body.message. Split buildCredentialHeaders from the OpenAPI buildAuthHeaders to keep both conventions clean. 9. TLS material encoding — caBase64/certBase64/keyBase64 are a misnomer: the /iot/credential response carries literal PEM text in those fields. Decoding them as base64 corrupted the material ("PEM routines::no start line"). Pass the strings through to mqtt as-is. Also align connect options with OpenClaw's reference implementation (keepalive: 60, reschedulePings: true) and dispose the prior client before reconnecting so stale listeners from a dead TCP socket do not leak. Tests: 697/697 passing (+24 new).
…credential preemptive refresh, error classification - Extract shadow event parsing to shared src/mqtt/shadow.ts - Add status cache writes for MQTT events stream + watch --via-mqtt - Credential preemptive refresh (refresh 10min before expiry) - Profile-aware credential cache path: mqtt-credential.<profile>.json - SIGINT handler consistency in events stream (process.on + finally cleanup) - Extend MQTT error classification: add mqtt-network-unreachable for ECONNREFUSED/EHOSTUNREACH/ENETUNREACH - Verbose JSON parse error logging (--verbose shows malformed message info) - Improve error messages (config file path + reason) - Add JSDoc to MqttCredential.topics field - package.json: exports field, version 1.5.0, typecheck script - CI: add npm run typecheck step - Update tests to match new error message format
Every --json response now wraps the payload in a stable envelope so agents
(OpenClaw, Claude Code, GPT-Actions, etc.) can parse one shape across every
command:
{ "schemaVersion": "1", "ok": true, "data": <payload>,
"meta": { "command": "devices.status", "durationMs": 123 } }
Errors use the same envelope with ok=false and go to stdout (previously
stderr) so agents can consume a single stream for both success and failure.
Human mode output is unchanged.
Streaming commands (devices watch, events stream/tail) keep emitting bare
JSON per line — the envelope applies to one-shot responses only.
Back-compat: --json-legacy opts out of the envelope and restores the
v1.5.0 bare-payload shape. Planned removal in v1.7.0.
Bump package 1.5.0 -> 1.6.0.
Adds an MCP resource switchbot://events that agents can subscribe to via resources/subscribe. A shared ref-counted MqttTlsClient starts on the first subscriber, pushes notifications/resources/updated on every shadow event, and tears down on the last unsubscribe. A ring buffer keeps the last 100 events so a new events_recent tool can serve agents that prefer polling over subscription. Shadow events are also written through to the status cache for any downstream reader. Advertises capabilities.resources.subscribe=true and tears down the subscription manager on server close.
…6.0 P2) Add MCP tools so AI agents no longer have to shell out for common tasks: - devices_batch — run one command across many devices (yes:true for destructive) - plan_run — validate + execute a SwitchBot plan (v1.0) - webhook_setup / webhook_query / webhook_update / webhook_delete - quota_status — today's local API counter To avoid code duplication, the shared executors are extracted from the CLI actions: - runBatchCommand() in commands/batch.ts — pool + destructive pre-flight - runPlan() in commands/plan.ts — step-by-step execution loop Both helpers return structured results that the CLI and MCP paths format the same way. Destructive guards are preserved in both surfaces (yes:true via MCP maps to --yes via CLI) so agents cannot bypass them. Catalog size: 8 tools -> 15 tools. No new runtime dependencies.
Multi-tenant MCP hosts can now route each request to a different SwitchBot
account by sending an `x-switchbot-profile: <name>` header (or `?profile=`
query string). The profile name maps to `~/.switchbot/profiles/<name>.json`
and is resolved fresh per request.
Changes:
- createClient() accepts a `{ token, secret }` override, bypassing
loadConfig() when provided.
- loadConfigForProfile(profile?) — like loadConfig() but does NOT read
process.argv, so it's safe for server contexts. Throws instead of
calling process.exit so the HTTP handler can surface errors cleanly.
- createSwitchBotMcpServer({ configResolver }) — optional resolver
invoked lazily on every tool call. Defaults to loadConfigForProfile().
Every tool handler now builds its axios client via getClient() so
per-request credentials are honored.
- HTTP transport reads x-switchbot-profile / ?profile= and wires the
resolver. Stdio transport is unchanged (one session = one profile).
SwitchBot does not publish a dedicated quota endpoint, but some responses
include an X-Ratelimit-Remaining header. Capture it opportunistically in
the axios response interceptor and report both local + server-authoritative
numbers so agents can reason about the real remaining budget.
Changes:
- utils/quota.ts: in-memory serverObservation with recordServerQuota /
getServerQuota / clearServerQuota helpers. todayUsage() now includes
an optional `server: { remaining, observedAt }` field.
- api/client.ts: response interceptor reads the ratelimit header and
calls recordServerQuota(). Local counter remains authoritative for
planning since the header is not guaranteed.
- commands/quota.ts: human mode prints "Server remaining: N (fresh)"
when the observation is <10min old. JSON mode nests it under
today.server.
- commands/mcp.ts: quota_status tool now returns `serverQuotaKnown`
as a real boolean and the server object when present.
Consolidate destructive-guard audit trail across CLI and MCP surfaces: - audit.ts: rotate to <file>.1 at 10MB; enforce 0600 perms on every write; add optional destructive/confirmed/caller fields and a new 'refused' result to AuditEntry; expose writeRefusalAudit helper. - devices.ts (single command), batch.ts (both CLI and runBatchCommand), plan.ts (runPlan destructive-skip), mcp.ts (send_command, plan_run via caller threading): call writeRefusalAudit whenever a destructive call is blocked by missing --yes / confirm:true. - lib/devices.ts executeCommand now records the destructive flag so the log shows which calls were against flagged commands. - tests: rotation at >10MB moves the log to .1 and starts a fresh file; writeRefusalAudit emits a refused/destructive/caller entry.
- MCP server now exposes 15 tools, not 8 — list them grouped by concern (control/read, plans & events, webhooks, diagnostics) and note HTTP-transport profile routing + destructive-guard audit behavior. - Test count bumped to 725 (was 592) to match the current suite.
… envelope Review response for v1.6.0 PR #5. - plan_run now threads an AxiosInstance factory through runPlan, so HTTP- transport callers with x-switchbot-profile route to the right tenant instead of the server-default credentials. - EventSubscriptionManager takes an options-object constructor with a configResolver; when unset it falls back to loadConfig() for stdio. - MQTT credential cache file is now content-addressed (sha256 of token + secret, truncated), so two tenants on one HTTP server can't share ~/.switchbot/mqtt-credential.json. The cache is written with 0600 perms and an explicit post-rename chmod. - Replace remaining bare stderr writes of {error:...} JSON in devices, history, expand, mcp port validation, batch, config, and format with printErrorEnvelope so JSON-mode errors reliably land on stdout. - ErrorPayload.kind now includes 'guard' for the destructive-refusal branches. - New regression tests: runPlan honors getClient, credential cache is content-addressed + 0600, EventSubscriptionManager constructor shape.
chenliuyun
pushed a commit
that referenced
this pull request
Apr 20, 2026
MCP initialize response now reports accurate serverInfo.version from package.json instead of hardcoded '2.0.0'. This fixes misleading version to MCP clients that gate capabilities on reported version. Updated four references in mcp.ts (initialize response, account_overview, /healthz, /ready endpoints) to use centralized VERSION constant imported from version.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
chenliuyun
pushed a commit
that referenced
this pull request
Apr 20, 2026
Document every fix landed in this branch beyond the history-aggregate feature: bugs #1, #4, #5, #6, #8, #9, #10, #11, #12, #13, #14, #15, #16, #17, #18 from the OpenClaw v2.4.0 smoke-test report. Call out the deferred items (#2, #7) explicitly so readers don't assume they were overlooked. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
chenliuyun
pushed a commit
that referenced
this pull request
Apr 20, 2026
MCP initialize response now reports accurate serverInfo.version from package.json instead of hardcoded '2.0.0'. This fixes misleading version to MCP clients that gate capabilities on reported version. Updated four references in mcp.ts (initialize response, account_overview, /healthz, /ready endpoints) to use centralized VERSION constant imported from version.ts.
chenliuyun
pushed a commit
that referenced
this pull request
Apr 26, 2026
Now that the esbuild bundle is the single publish source, bundle-smoke must catch bundle regressions on every supported Node version before a PR can merge. Previously it was advisory (continue-on-error: true) and single-node (20.x). Changes: - Remove continue-on-error. The stale "Node 22 CJS interop issues" comment dates from before 7bbbc44 fixed the CJS require shim; the bundle now runs cleanly on Node 18/20/22 (verified locally on 22.21). - Add strategy.matrix.node-version = [18.x, 20.x, 22.x] with fail-fast: false, so any Node version that cannot run the bundle is surfaced independently. - Update docs/release-pipeline.md: remove the "Known gaps" section, add invariant #5 making the matrix + blocking status explicit. - CHANGELOG entry added.
chenliuyun
added a commit
that referenced
this pull request
Apr 26, 2026
* fix(capabilities): export COMMAND_META, add rules explain entry, add exhaustive coverage guard tests
* test(devices): assert exit code 1 for API error 190, add happy-path exit code guard
* docs: update test count to 1889
* fix(policy): relax alias deviceId pattern to accept hex MAC and lowercase IDs
* docs: update test count to 1895
* feat(agent-bootstrap): add --sections flag to project top-level payload keys
* docs: update test count to 1898
* perf: switch build:prod to esbuild bundler, inline pure-JS deps into single file
* docs: update test count to 1900
* refactor(capabilities,schema): add schema export --capabilities, replace inline catalog with pointer note
* test(daemon): add stop, status, reload subcommand coverage
* test(rules): add conflicts, doctor, summary, last-fired coverage
* test: add webhook token, suggest, health-check, scenes validate/simulate, status-sync, upgrade-check coverage
* test: address code review findings — timer cleanup, bundle guard, status-sync start/run, test count
- Add afterEach vi.useRealTimers() to daemon stop/reload describe blocks to prevent fake timer leak
- Fix bundle-size test to skip size assertion when tsc output is present (detects esbuild bundle by absence of relative imports)
- Add status-sync start (--json + human) and run (exit 0 and exit 1) test coverage
- Update README test count to 1959
* chore: bump version to 3.2.0
* fix: remove duplicate shebang — src/index.ts had shebang AND bundle.mjs banner both added it
* chore: bump version to 3.2.1
* test: add esbuild bundle validation tests (shebang, syntax, size, version smoke)
* fix(bundle): resolve CJS require shim and duplicate shebang in esbuild output
- Remove shebang from src/index.ts; bundle.mjs banner is the sole source
- Add createRequire-based require shim in banner so bundled CJS packages
(yaml, commander) can call bare require('process') on Node 20/22
- Add scripts/cjs-shim.mjs as esbuild inject target for require polyfill
- Fix agent-bootstrap and upgrade-check to import version from src/version.ts
instead of require('../../package.json') which breaks when bundled to
a non-root dist/ location
- Rewrite bundle-size test: build to dist/bundle-test.js (same level as
dist/index.js so ../package.json resolves correctly), add shebang-count,
node --check, --version semver, and size < 15 MB assertions
* Harden npm package publish and smoke verification
* chore(ci): align pack-install smoke with publish artifact, document release pipeline
Add a parallel pack-install-smoke-tsc CI job that runs `npm run build` (tsc)
+ smoke:pack-install, mirroring exactly what publish.yml does before npm publish.
Previously only the esbuild bundle was smoke-tested on PRs while publish shipped
the tsc output, so a tsc-specific packaging regression would only surface after
the release tag was already pushed.
Add docs/release-pipeline.md with the tsc vs esbuild split, the full gate
sequence, and the invariants that must hold for future pipeline changes. Link
it from README and the CHANGELOG 3.2.1 entry.
* fix(ci): align publish.yml with prepublishOnly — single esbuild publish source
publish.yml was running `npm run build` (tsc) + smoke:pack-install, but
`npm publish` triggers prepublishOnly, which does `clean && build:prod`
unconditionally. So the tarball that smoke validated was thrown away and
replaced with the esbuild bundle right before upload — smoke was verifying
a different artifact than what shipped.
Fix by making esbuild the single publish source:
- publish.yml step 2: `npm run build` -> `npm run build:prod`, so the
artifact validated by smoke:pack-install is byte-identical to what
prepublishOnly produces and what npm publish uploads.
- ci.yml: remove the `pack-install-smoke-tsc` job added in the previous
commit — it was validating the tsc output that never ships.
- docs/release-pipeline.md: rewrite to reflect the single-publish-source
model; add explicit invariant that publish.yml and prepublishOnly must
use the same builder.
- CHANGELOG: update the 3.2.1 release-pipeline entry.
* ci(bundle-smoke): make blocking + matrix across Node 18/20/22
Now that the esbuild bundle is the single publish source, bundle-smoke
must catch bundle regressions on every supported Node version before a
PR can merge. Previously it was advisory (continue-on-error: true) and
single-node (20.x).
Changes:
- Remove continue-on-error. The stale "Node 22 CJS interop issues"
comment dates from before 7bbbc44 fixed the CJS require shim; the
bundle now runs cleanly on Node 18/20/22 (verified locally on 22.21).
- Add strategy.matrix.node-version = [18.x, 20.x, 22.x] with fail-fast:
false, so any Node version that cannot run the bundle is surfaced
independently.
- Update docs/release-pipeline.md: remove the "Known gaps" section,
add invariant #5 making the matrix + blocking status explicit.
- CHANGELOG entry added.
* test(status-sync): replace vi.spyOn(process.kill) with direct assignment
On some Linux CI runners, vi.spyOn(process, 'kill') failed to reliably
intercept process.kill(pid, 0), so isProcessRunning hit the real kill
syscall against a PID that happened to exist in the container, got back
EPERM, and reported the stale process as still running. Replace the
spy with a direct property assignment and restore the original kill in
afterAll so the interception is deterministic across Node 18/20/22.
* refactor(build): collapse build and build:prod into a single release pipeline
npm run build is now the only path that produces the published artifact.
It drives a 5-stage orchestrator in scripts/build.mjs:
1. clean remove dist/
2. typecheck tsc --noEmit
3. bundle scripts/bundle.mjs (esbuild)
4. copy-assets scripts/copy-assets.mjs (policy assets only)
5. ensure-binary scripts/ensure-binary.mjs (shebang + chmod 0755 guard)
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 any step.
scripts/copy-assets.mjs no longer injects the shebang or chmods the entry.
The new scripts/ensure-binary.mjs is a regression guard: it asserts the
shebang is present and fails loudly (pointing at scripts/bundle.mjs) if
the esbuild banner ever drops it, rather than silently repairing the
output the way copy-assets used to.
Dropped scripts: build:prod, clean (folded into build.mjs).
Added script: typecheck (tsc --noEmit).
* chore: bump version to 3.2.2
---------
Co-authored-by: chenliuyun <chenliuyun@onero.com>
chenliuyun
added a commit
that referenced
this pull request
May 7, 2026
…M condition + rules simulate) (#41) * feat: Track κ — AI decision loop (PRs 1–4) PR 1 — Decision Trace - src/rules/trace.ts: deepSortedJson, ruleVersion (sha256/8-char), TraceBuilder, shouldWriteTrace (full/sampled/off), filterTraceRecords - engine.ts: fireId before eval, TraceBuilder threaded, emitTrace closure - matcher.ts: trace? param, pushConditionTrace after eval, isLlmCondition guard - audit.ts: rule-evaluate/llm-condition/llm-budget-exceeded kinds; writeEvaluateTrace - schema v0.2.json: automation.audit block (evaluate_trace, evaluate_retention_days) - tests/rules/trace.test.ts: 30 tests PR 2 — rules explain - src/rules/explain.ts: loadTraceRecords, loadRelatedAudit, formatExplainText/Json - rules.ts: trace-explain subcommand (avoids collision with static explain) - mcp.ts: rules_explain tool - tests/rules/explain.test.ts: 19 tests PR 3 — LLM condition - src/rules/types.ts: LlmCondition interface + isLlmCondition guard; AutomationLlmBudgetConfig - llm/provider.ts: decide() + DecideResult/DecideOptions on LLMProvider interface - llm/providers/anthropic.ts: decide() via tool-use API - llm/providers/openai.ts: decide() via function-calling API - src/rules/llm-condition.ts: LlmConditionEvaluator (cache, budget, on_error) Cache key: sha256(JSON.stringify([ruleVersion, prompt, deepSortedJson(ctx)])) — fixes spec gap #5 - matcher.ts: llm condition branch with provider injection - schema: llm condition oneOf branch + automation.llm_budget - engine.ts: 4 lint rules (condition-llm-no-provider, no-cache-ttl-high-freq, budget-zero, on-error-pass) - tests/rules/llm-condition.test.ts: 22 tests PR 4 — rules simulate - src/rules/simulate.ts: simulateRule(), as-of state fetcher, against-file replay, ThrottleGate simulation, LLM skip marker - rules.ts: simulate subcommand - mcp.ts: rules_simulate tool - capabilities.ts: trace-explain + simulate entries - tests/rules/simulate.test.ts: 18 tests Spec gaps fixed: #1 canonicalize undefined → deepSortedJson() recursive key-sort #5 cache key raw concat → JSON.stringify([...]) structured array * fix: guard Array.isArray before iterating rules/then in checkNotifyConnectivity --------- Co-authored-by: chenliuyun <chenliuyun@onero.com>
chenliuyun
added a commit
that referenced
this pull request
May 7, 2026
* feat: Track κ — AI decision loop (PRs 1–4) PR 1 — Decision Trace - src/rules/trace.ts: deepSortedJson, ruleVersion (sha256/8-char), TraceBuilder, shouldWriteTrace (full/sampled/off), filterTraceRecords - engine.ts: fireId before eval, TraceBuilder threaded, emitTrace closure - matcher.ts: trace? param, pushConditionTrace after eval, isLlmCondition guard - audit.ts: rule-evaluate/llm-condition/llm-budget-exceeded kinds; writeEvaluateTrace - schema v0.2.json: automation.audit block (evaluate_trace, evaluate_retention_days) - tests/rules/trace.test.ts: 30 tests PR 2 — rules explain - src/rules/explain.ts: loadTraceRecords, loadRelatedAudit, formatExplainText/Json - rules.ts: trace-explain subcommand (avoids collision with static explain) - mcp.ts: rules_explain tool - tests/rules/explain.test.ts: 19 tests PR 3 — LLM condition - src/rules/types.ts: LlmCondition interface + isLlmCondition guard; AutomationLlmBudgetConfig - llm/provider.ts: decide() + DecideResult/DecideOptions on LLMProvider interface - llm/providers/anthropic.ts: decide() via tool-use API - llm/providers/openai.ts: decide() via function-calling API - src/rules/llm-condition.ts: LlmConditionEvaluator (cache, budget, on_error) Cache key: sha256(JSON.stringify([ruleVersion, prompt, deepSortedJson(ctx)])) — fixes spec gap #5 - matcher.ts: llm condition branch with provider injection - schema: llm condition oneOf branch + automation.llm_budget - engine.ts: 4 lint rules (condition-llm-no-provider, no-cache-ttl-high-freq, budget-zero, on-error-pass) - tests/rules/llm-condition.test.ts: 22 tests PR 4 — rules simulate - src/rules/simulate.ts: simulateRule(), as-of state fetcher, against-file replay, ThrottleGate simulation, LLM skip marker - rules.ts: simulate subcommand - mcp.ts: rules_simulate tool - capabilities.ts: trace-explain + simulate entries - tests/rules/simulate.test.ts: 18 tests Spec gaps fixed: #1 canonicalize undefined → deepSortedJson() recursive key-sort #5 cache key raw concat → JSON.stringify([...]) structured array * fix: guard Array.isArray before iterating rules/then in checkNotifyConnectivity * 3.4.0: AI decision loop — trace, LLM condition, trace-explain, simulate - automation.audit.evaluate_trace records per-evaluation decisions - llm: condition type gates rules on LLM yes/no judgement (cache, budget, on_error) - rules trace-explain: inspect why a rule fired or was blocked - rules simulate: replay historical events against a rule offline - MCP: rules_explain + rules_simulate tools - 2204 tests (+245) --------- Co-authored-by: chenliuyun <chenliuyun@onero.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Make the CLI the single entry point for AI agents (Claude Code, OpenClaw,
custom runners) controlling SwitchBot — replacing scattered skills, Channel
bots, and one-off scripts. Six coordinated phases, each one commit, each
independently shippable.
P0 — Unified JSON envelope. Every
--jsonresponse now ships as{schemaVersion,ok,data,meta:{command,durationMs,requestId}}. Errors alsouse the envelope and go to stdout in JSON mode (not stderr) so
agents can parse one stream.
--json-legacyis available as a one-releaseescape hatch and is removed in v1.7.
P1 — MCP shadow-event subscription. New
switchbot://eventsresourcepushes
notifications/resources/updatedon every MQTT shadow event via aref-counted subscription manager.
events_recenttool exposes thein-process 100-event ring buffer for polling-style clients.
P2 — MCP tool coverage parity. 8 → 15 tools. Adds
devices_batch,plan_run,webhook_setup/query/update/delete,quota_status— all gothrough the same shared executors the CLI uses (no re-implementation).
P3 — MCP per-request profile (HTTP transport). Multi-tenant hosts can
route requests to different credential profiles via
x-switchbot-profile:<name>header or?profile=<name>query. Stdio isunchanged (boot-time
loadConfig).createSwitchBotMcpServeraccepts anoptional
configResolverfor dependency injection.P4 — Server-authoritative quota. Response interceptor captures
X-Ratelimit-Remainingopportunistically;quota statusnow shows boththe local (authoritative for planning) and server (advisory) numbers, with
a freshness indicator.
P5 — Destructive-guard + audit trail. Refused destructive attempts
are recorded with
result: "refused"(pluscaller: "cli"|"mcp"anddestructive/confirmedflags) across single-command, batch, plan, andevery MCP tool. The audit log rotates at 10MB and writes with 0600 perms.
Test plan
npm run buildcleannpx tsc --noEmitcleannpx vitest run— 725/725 pass--versionguard passes (1.6.0)switchbot://events; subscribe fires on device changex-switchbot-profile: <name>routes to the matching profile--audit-logwrites arefusedentry when destructive command rejectedNotes
--json-legacy).