feat(devices): cache deviceId→type locally and pre-validate commands …#1
Merged
Merged
Conversation
…against catalog
- Add src/devices/cache.ts: persists deviceId → {type, name, category} at
~/.switchbot/devices.json (0o600, atomic, sibling to --config override)
- devices list / describe now refresh the cache
- devices command pre-validates against the catalog before hitting the API:
* unsupported command → exit 2 with supported-command list
* no-param command given a parameter → exit 2 with corrected invocation
* --type customize bypasses validation (arbitrary IR button names)
* unknown deviceId / stale cache gracefully fall through
- Validation is placed outside the action's try/catch so its exit(2) is
not re-handled as exit(1) by handleError
- Add 19 tests (11 cache unit tests + 8 command validation tests)
chenliuyun
pushed a commit
that referenced
this pull request
Apr 19, 2026
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).
chenliuyun
pushed a commit
that referenced
this pull request
Apr 20, 2026
…tch (bug #1) Under require-unique, the exact-name short-circuit at line 73 returned immediately on the first exact hit, bypassing the ambiguity check entirely. This meant a query like '空调' on an account with both '空调' (exact) and '卧室空调' (substring) silently resolved to the exact device instead of raising ambiguous_name_match (exit 2). Fix: gate the short-circuit by strategy. For require-unique, the exact hit is pushed as a score-0 candidate and the full candidate list is assembled before the ambiguity check runs. Fuzzy/first/exact/prefix/substring behaviour is unchanged. Tests added (4 new): - exact + substring under require-unique → ambiguous (reporter scenario) - two exact-same-name devices under require-unique → ambiguous - single exact match, no other matches under require-unique → resolves OK - fuzzy still short-circuits on exact match (regression pin) Co-Authored-By: Claude Sonnet 4.6 <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
…tch (bug #1) Under require-unique, the exact-name short-circuit at line 73 returned immediately on the first exact hit, bypassing the ambiguity check entirely. This meant a query like '空调' on an account with both '空调' (exact) and '卧室空调' (substring) silently resolved to the exact device instead of raising ambiguous_name_match (exit 2). Fix: gate the short-circuit by strategy. For require-unique, the exact hit is pushed as a score-0 candidate and the full candidate list is assembled before the ambiguity check runs. Fuzzy/first/exact/prefix/substring behaviour is unchanged. Tests added (4 new): - exact + substring under require-unique → ambiguous (reporter scenario) - two exact-same-name devices under require-unique → ambiguous - single exact match, no other matches under require-unique → resolves OK - fuzzy still short-circuits on exact match (regression pin)
10 tasks
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>
chenliuyun
pushed a commit
that referenced
this pull request
May 23, 2026
Plain copy from sibling repo (per PR #1 deviation rationale — captures in-flight uncommitted state that subtree split would miss). Package identity: - name: @switchbot/openclaw-skill (was @cly-org/switchbot-openclaw-skill) - version: 0.1.0 (reset for first publish under new scope) - repo URLs point at OpenWonderLabs/switchbot-openapi-cli - author block stripped (top-level package.json has author) - peerDep "@switchbot/openapi-cli": ">=3.7.1" (literal range, not workspace:*) - files[] adds lib/ (matches PR #1 fix — runtime imports of error-messages.js) Skipped @switchbot/agent-shared package entirely. The only candidate for sharing is lib/error-messages.js (~30 lines of static error strings) and codex's catalog is a strict superset of openclaw's. Bundling or publishing a third package adds machinery without paying off; revisit if either catalog grows materially. Documented in spec decision log. Cleanup: - codex-plugin: strip personal author/repo metadata, align with openclaw - README: add OpenClaw integration section, update Quick start callout - spec: log PR #2 deviations (plain copy, agent-shared skip) Tests: codex-plugin 31/31, openclaw-skill 29/29, root vitest 2715/2715.
chenliuyun
added a commit
that referenced
this pull request
May 23, 2026
* feat(install): add Codex CLI and plugin preflight checks for --agent codex * feat(install): add codex-checks module with CLI/npm/registration checks * feat(install): add resolvePluginId shared utility to codex-checks * feat(doctor): export Check, CHECK_REGISTRY, and runDoctorChecks helper * feat(install): extend AgentName with codex, add stepRegisterCodexPlugin * feat(codex): add switchbot codex doctor subcommand * feat(install): add codex agent path routing to stepRegisterCodexPlugin * feat(codex): add switchbot codex repair subcommand * feat(codex): register codex command in program-builder and add COMMAND_META entries * refactor(codex): extract credentialsPresent helper, share resolveCodexPackageRoot * feat(codex): hint repair command when doctor reports warn or fail * docs(spec): codex install paths design — setup subcommand + AGENTS.md + README bootstrap + hook hardening * docs(spec): fix 4 consistency issues in codex-commands design * docs(spec): fix 5 remaining consistency issues + add 3 test cases in codex-commands design * docs(codex): align spec drift on doctor-verify, auth argv, register helper - doctor-verify in both specs now spells out the same 7-check set (4 base + 3 codex), so codex setup and codex repair share semantics. - auth/re-auth argv documents --profile and --config forwarding via the buildAuthLoginArgv helper, matching codex repair re-auth. - onInstall hook path pinned to packages/codex-plugin/bin/auth.js per current .codex-plugin/hooks.json (no more "(or install.js)" wording). - stepRegisterCodexPlugin / repairStepRegisterPlugin / setup register-plugin all reference the shared registerCodexPlugin() helper; inline npm root -g and pluginId concat are forbidden. * fix(codex): tighten install warnings and extract registerCodexPlugin helper - preflight: --agent codex now fails (was warn) when the npm package is missing — this is a register-only command, so a missing package is a hard stop, not a soft hint. - codex-checks: warning text for missing/unregistered plugin now spells out the full repair recipe (npm install -g @cly-org/switchbot-codex-plugin && switchbot install --agent codex) instead of just the latter half. - codex-checks: add registerCodexPlugin() helper that wraps resolveCodexPackageRoot + resolvePluginId + runCodexPluginRegistration with normalized error strings, so the three call sites (install --agent codex, codex repair, codex setup) share one path. - default-steps.stepRegisterCodexPlugin: replace the inlined npm root -g + path.join + runCodexPluginRegistration with a single registerCodexPlugin() call. - doctor: export formatDoctorChecks for reuse by codex doctor / repair / setup output formatting. * feat(codex): add codex setup subcommand and route repair through shared helper - New `switchbot codex setup` subcommand runs five steps: check-codex-cli, install-switchbot-cli, register-plugin, auth, doctor-verify (4 base + 3 codex = 7 checks). Only install-switchbot-cli and auth are skippable; --skip on other steps exits 2. - codex repair re-auth and codex setup auth share buildAuthLoginArgv, which forwards the active --profile and --config to the spawned `auth login`. Spawn now uses process.execPath + cliPath (process.argv[1]) instead of resolving "switchbot" through PATH. - repairStepRegisterPlugin replaced with a single registerCodexPlugin() call so install --agent codex and codex repair share one registration path. The local printDoctorChecks copy is dropped in favour of formatDoctorChecks from doctor.ts. - capabilities: COMMAND_META gains an entry for `codex setup`. * docs(readme): add Codex integration quick start section - New ## Codex integration section after Quick start documents the full surface: prerequisites, npx-based one-command bootstrap (which also explains why the install-switchbot-cli step exists — it pins the global install after npx), manual install path, doctor/repair flow, --dry-run / --json / --yes / --skip flag matrix, and --profile / --config inheritance. - Quick start gains a one-line callout redirecting Codex users away from the --agent claude-code default example. - Table of contents links the new section. * docs(spec): monorepo migration design — absorb codex-plugin and openclaw-skill * docs(spec): fix 3 design issues in monorepo migration — workspace:*, publish gate, grep scope * feat(monorepo): consolidate codex-plugin via npm workspaces Enable `"workspaces": ["packages/*"]` and import the Codex plugin from the sibling `openclaw-switchbot-skill` repo into `packages/codex-plugin/`, renamed from `@cly-org/switchbot-codex-plugin` to `@switchbot/codex-plugin` (version reset to 0.1.0; old scope was never published — `npm view` 404). CLI updates: - src/install/codex-checks.ts: resolveCodexPackageRoot path now joins `@switchbot/codex-plugin`; doctor warning recipes reference the new name. - src/install/preflight.ts and default-steps.ts: same rename. - Test mocks updated; default plugin id derived from new dirname is now `switchbot@codex-plugin`. Build / CI: - Add aggregate scripts: `test:workspaces`, `test:all`, `typecheck:workspaces`, `typecheck:all`. Existing root `test`/`typecheck` keep root-only scope (preserves pre-commit/pre-push timings). - ci.yml `test` job now runs `npm test` plus `npm run test:workspaces`. Plugin import notes: - Plain copy used instead of `git subtree add`. Sibling working tree had uncommitted in-flight test files that subtree split would have skipped. History remains in the sibling repo for archival lookup. - Added `lib/` to `package.json#files`; sibling omitted it but `lib/error-messages.js` is a runtime import of `bin/auth.js`. - Dropped 3 sibling-repo orchestration tests (codex-mcp-config, codex-setup, install-scripts) that imported `../../../scripts/*` — no analog in this monorepo; superseded by `switchbot codex setup`. Spec decision log updated with the three execution-time deviations. Hard checks pass: tarball has concrete peerDep `">=3.7.1"` (not `workspace:*`), 11 files including `lib/error-messages.js`; scoped grep for `@cly-org` in src/packages/.github/tests/scripts/README.md returns zero hits. * feat(monorepo): import openclaw-skill as workspace package Plain copy from sibling repo (per PR #1 deviation rationale — captures in-flight uncommitted state that subtree split would miss). Package identity: - name: @switchbot/openclaw-skill (was @cly-org/switchbot-openclaw-skill) - version: 0.1.0 (reset for first publish under new scope) - repo URLs point at OpenWonderLabs/switchbot-openapi-cli - author block stripped (top-level package.json has author) - peerDep "@switchbot/openapi-cli": ">=3.7.1" (literal range, not workspace:*) - files[] adds lib/ (matches PR #1 fix — runtime imports of error-messages.js) Skipped @switchbot/agent-shared package entirely. The only candidate for sharing is lib/error-messages.js (~30 lines of static error strings) and codex's catalog is a strict superset of openclaw's. Bundling or publishing a third package adds machinery without paying off; revisit if either catalog grows materially. Documented in spec decision log. Cleanup: - codex-plugin: strip personal author/repo metadata, align with openclaw - README: add OpenClaw integration section, update Quick start callout - spec: log PR #2 deviations (plain copy, agent-shared skip) Tests: codex-plugin 31/31, openclaw-skill 29/29, root vitest 2715/2715. * ci(publish): per-package matrix and detect-versions gate for monorepo Restructure release pipeline now that @switchbot/codex-plugin and @switchbot/openclaw-skill ship from this repo as workspace packages. publish.yml: add detect-versions step that queries npm for each package, publish only unpublished versions, gate plugin tarballs on concrete peerDeps, set continue-on-error on plugin steps so plugin failures surface as annotations without blocking CLI promotion. npm-published-smoke.yml: per-package matrix (cli vs plugin kinds), skip-if-not-republished logic, plugin tarball-shape checks (peerDep literal, executable bin entries) without live smoke. ci.yml: drop policy-schema-sync — the skill consumer is now in this monorepo so the cross-repo sync gate is obsolete. CHANGELOG: Unreleased entry documenting monorepo absorption, Codex command group, plugin renames, and workflow restructure. No version chosen yet — release decision is separate. * ci(publish): defer @switchbot/openclaw-skill npm publish Keep packages/openclaw-skill/ in the monorepo (workspace tests still cover it) but drop it from publish.yml and npm-published-smoke.yml matrix. README OpenClaw integration section and CHANGELOG mentions removed so users do not search for an unpublished npm package. Re-enabling later only requires re-adding the publish + smoke entries. * feat(codex): 6-step setup adds install-codex-plugin; onInstall is best-effort - codex setup: add install-codex-plugin step between install-switchbot-cli and register-plugin so npx @switchbot/openapi-cli codex setup bootstraps end-to-end on a brand-new machine. Both install steps remain --skip-able. - codex-plugin onInstall hook: always exits 0. When CLI is present it runs switchbot codex setup --yes; when absent it prints a hint and lets the Codex plugin install succeed (so a missing CLI never rolls it back). - README + plugin README: 6-step list, single recommended npx path. - Specs: align design docs with @SwitchBot scope and 6-step flow. Tests: +1 root (codex setup install-codex-plugin coverage), +3 workspace (makeRunOnInstall: missing CLI / setup fail / setup ok). 2716 root + 34 codex-plugin pass. * fix(codex): junction workaround for @-path; ship corrected marketplace.json - runCodexPluginRegistration now bridges packageRoot via a junction (Windows) or symlink (POSIX) in os.tmpdir() before invoking `codex plugin marketplace add`. codex CLI 0.133.0 misparses local paths containing `@` (e.g. <npm-root>/@switchbot/codex-plugin) as `owner/repo@ref` and rejects them with `--ref is only supported for git marketplace sources`. Falls back to the original path if the link cannot be created (test mocks, restricted filesystems). - Track packages/codex-plugin/.agents/plugins/marketplace.json in git and add `.agents/` to the package `files` array so it ships in the npm tarball. The local source `path` is `../../` so codex resolves the plugin manifest at packageRoot/.codex-plugin/plugin.json (was `./`, which pointed at the wrong directory). * test(codex): add pack-install smoke for codex plugin tarball Pack the CLI and @switchbot/codex-plugin workspaces, install both into a scratch dir, and assert tarball shape before any release: - Plugin peerDependency is concrete (not workspace:*). - Required files ship: .codex-plugin/plugin.json + hooks.json, .agents/plugins/marketplace.json, .mcp.json, skills/, bin/. - marketplace.json name is "codex-plugin"; the switchbot plugin source.path is "../../" — codex resolves the path relative to .agents/plugins/marketplace.json itself, so it must climb two levels back to packageRoot/.codex-plugin/plugin.json. - onInstall hook runs bin/auth.js --hook and exits 0. - `switchbot codex setup --dry-run --json` lists the 6 setup steps. Wired into prepublishOnly so a wrong path, missing files entry, or broken hook is caught before the release tag. * fix(codex): stable Windows junction alias; add stage field to RegistrationResult resolveMarketplaceSourceRoot bridges scoped-npm packageRoots through ~/switchbot before calling `codex plugin marketplace add`. codex 0.133.0 mis-parses paths containing `@` (e.g. <root>\node_modules\@SwitchBot\ codex-plugin) as `owner/repo@ref` and rejects them. A previous attempt aliased through os.tmpdir/<pid> and deleted the link in a finally — codex persists the marketplace path, so the alias must outlive the install. Reuse ~/switchbot when it already points at the same packageRoot; only create the junction once. runCodexPluginRegistration now sets stage: 'marketplace-add' | 'plugin-add' so registerCodexPlugin can name the failing codex subcommand in its error (marketplace-add exit 1: ...). resolvePluginId splits into resolvePluginName + resolveMarketplaceName so the marketplace segment of the plugin id reads from .agents/plugins/ marketplace.json instead of basename(packageRoot). publish.yml gains a smoke:codex-pack-install step (runs when the CLI or codex-plugin version bumps). Tests cover the stage field, the not-installed warn path, the marketplace.json name source, and the Windows alias creation. * fix(codex): relocate Windows alias to LOCALAPPDATA and repair stale junctions resolveMarketplaceSourceRoot moves the @-path workaround junction from ~/switchbot to %LOCALAPPDATA%\switchbot\codex-plugin-marketplace (fallback ~/.switchbot/codex-plugin-marketplace when LOCALAPPDATA is unset). The previous location collided with user-owned directories named "switchbot" and silently fell back to the broken @ path on any mismatch. The new path is app-owned, so we treat divergent junctions as repairable (unlink + recreate) and throw when a non-junction sits at the alias path instead of returning a path Codex cannot parse. fs errors propagate to the caller. Tests cover four states: missing alias, healthy junction, stale junction pointing elsewhere, and real-directory collision. * fix(codex-plugin): mirror alias-relocation logic in install.js Mirrors the resolveMarketplaceSourceRoot rewrite from the CLI side: junction lives at %LOCALAPPDATA%\switchbot\codex-plugin-marketplace (fallback ~/.switchbot/...), divergent junctions are repaired in place, and a real directory at the alias path throws instead of silently returning the broken @ path. Adds a deps parameter for fs injection so node:test can cover all four states without touching the real filesystem. * docs(changelog): describe final alias path and repair behavior * test(openclaw-skill): use 127.0.0.1 in policy-editor fetch URLs Server listens on 127.0.0.1; on Node 18 (Linux), undici resolves 'localhost' to ::1 first and fails without IPv4 fallback, so CI test (18.x) reports 'TypeError: fetch failed'. Match the bind address explicitly to make the tests deterministic across Node versions. * fix: publish openclaw-skill and fix Windows .cmd shim invocations - publish.yml: add openclaw-skill version detection, unpublished check, and publish step (continue-on-error, provenance) mirroring codex-plugin - preflight.ts: add shell: process.platform === 'win32' to the npm spawnSync call so .cmd shims are found on Windows (aligns with the existing pattern in codex-checks.ts) - check-cli.js: add shell: SHELL to all execFile/execFileSync calls so switchbot.cmd and npm.cmd shims are launched correctly on Windows * fix(auth): release callback port immediately when open() throws Expose close() on CallbackHandle so callers can tear down the server on demand. In browserLogin, wrap open() in try/catch and call close() on failure — previously the port stayed occupied for the full 120 s timeout if open() failed (e.g. headless / no-browser environments), blocking immediate retries with EADDRINUSE. * fix(codex): pre-remove plugin before add to avoid Windows backup ACCESS_DENIED codex plugin-add backs up any existing installation before replacing it. On Windows, if the previously registered plugin path is a junction with restricted permissions, the backup hits os error 5 (ACCESS_DENIED). Running plugin-remove first forces a fresh install, bypassing the backup. * test(codex-checks): add plugin-remove mock slot in registration tests runCodexPluginRegistration now calls plugin-remove before plugin-add; update the four affected spawnSync mock sequences to match. --------- Co-authored-by: chenliuyun <chenliuyun@onero.com>
chenliuyun
added a commit
that referenced
this pull request
May 23, 2026
#53) * feat(install): add Codex CLI and plugin preflight checks for --agent codex * feat(install): add codex-checks module with CLI/npm/registration checks * feat(install): add resolvePluginId shared utility to codex-checks * feat(doctor): export Check, CHECK_REGISTRY, and runDoctorChecks helper * feat(install): extend AgentName with codex, add stepRegisterCodexPlugin * feat(codex): add switchbot codex doctor subcommand * feat(install): add codex agent path routing to stepRegisterCodexPlugin * feat(codex): add switchbot codex repair subcommand * feat(codex): register codex command in program-builder and add COMMAND_META entries * refactor(codex): extract credentialsPresent helper, share resolveCodexPackageRoot * feat(codex): hint repair command when doctor reports warn or fail * docs(spec): codex install paths design — setup subcommand + AGENTS.md + README bootstrap + hook hardening * docs(spec): fix 4 consistency issues in codex-commands design * docs(spec): fix 5 remaining consistency issues + add 3 test cases in codex-commands design * docs(codex): align spec drift on doctor-verify, auth argv, register helper - doctor-verify in both specs now spells out the same 7-check set (4 base + 3 codex), so codex setup and codex repair share semantics. - auth/re-auth argv documents --profile and --config forwarding via the buildAuthLoginArgv helper, matching codex repair re-auth. - onInstall hook path pinned to packages/codex-plugin/bin/auth.js per current .codex-plugin/hooks.json (no more "(or install.js)" wording). - stepRegisterCodexPlugin / repairStepRegisterPlugin / setup register-plugin all reference the shared registerCodexPlugin() helper; inline npm root -g and pluginId concat are forbidden. * fix(codex): tighten install warnings and extract registerCodexPlugin helper - preflight: --agent codex now fails (was warn) when the npm package is missing — this is a register-only command, so a missing package is a hard stop, not a soft hint. - codex-checks: warning text for missing/unregistered plugin now spells out the full repair recipe (npm install -g @cly-org/switchbot-codex-plugin && switchbot install --agent codex) instead of just the latter half. - codex-checks: add registerCodexPlugin() helper that wraps resolveCodexPackageRoot + resolvePluginId + runCodexPluginRegistration with normalized error strings, so the three call sites (install --agent codex, codex repair, codex setup) share one path. - default-steps.stepRegisterCodexPlugin: replace the inlined npm root -g + path.join + runCodexPluginRegistration with a single registerCodexPlugin() call. - doctor: export formatDoctorChecks for reuse by codex doctor / repair / setup output formatting. * feat(codex): add codex setup subcommand and route repair through shared helper - New `switchbot codex setup` subcommand runs five steps: check-codex-cli, install-switchbot-cli, register-plugin, auth, doctor-verify (4 base + 3 codex = 7 checks). Only install-switchbot-cli and auth are skippable; --skip on other steps exits 2. - codex repair re-auth and codex setup auth share buildAuthLoginArgv, which forwards the active --profile and --config to the spawned `auth login`. Spawn now uses process.execPath + cliPath (process.argv[1]) instead of resolving "switchbot" through PATH. - repairStepRegisterPlugin replaced with a single registerCodexPlugin() call so install --agent codex and codex repair share one registration path. The local printDoctorChecks copy is dropped in favour of formatDoctorChecks from doctor.ts. - capabilities: COMMAND_META gains an entry for `codex setup`. * docs(readme): add Codex integration quick start section - New ## Codex integration section after Quick start documents the full surface: prerequisites, npx-based one-command bootstrap (which also explains why the install-switchbot-cli step exists — it pins the global install after npx), manual install path, doctor/repair flow, --dry-run / --json / --yes / --skip flag matrix, and --profile / --config inheritance. - Quick start gains a one-line callout redirecting Codex users away from the --agent claude-code default example. - Table of contents links the new section. * docs(spec): monorepo migration design — absorb codex-plugin and openclaw-skill * docs(spec): fix 3 design issues in monorepo migration — workspace:*, publish gate, grep scope * feat(monorepo): consolidate codex-plugin via npm workspaces Enable `"workspaces": ["packages/*"]` and import the Codex plugin from the sibling `openclaw-switchbot-skill` repo into `packages/codex-plugin/`, renamed from `@cly-org/switchbot-codex-plugin` to `@switchbot/codex-plugin` (version reset to 0.1.0; old scope was never published — `npm view` 404). CLI updates: - src/install/codex-checks.ts: resolveCodexPackageRoot path now joins `@switchbot/codex-plugin`; doctor warning recipes reference the new name. - src/install/preflight.ts and default-steps.ts: same rename. - Test mocks updated; default plugin id derived from new dirname is now `switchbot@codex-plugin`. Build / CI: - Add aggregate scripts: `test:workspaces`, `test:all`, `typecheck:workspaces`, `typecheck:all`. Existing root `test`/`typecheck` keep root-only scope (preserves pre-commit/pre-push timings). - ci.yml `test` job now runs `npm test` plus `npm run test:workspaces`. Plugin import notes: - Plain copy used instead of `git subtree add`. Sibling working tree had uncommitted in-flight test files that subtree split would have skipped. History remains in the sibling repo for archival lookup. - Added `lib/` to `package.json#files`; sibling omitted it but `lib/error-messages.js` is a runtime import of `bin/auth.js`. - Dropped 3 sibling-repo orchestration tests (codex-mcp-config, codex-setup, install-scripts) that imported `../../../scripts/*` — no analog in this monorepo; superseded by `switchbot codex setup`. Spec decision log updated with the three execution-time deviations. Hard checks pass: tarball has concrete peerDep `">=3.7.1"` (not `workspace:*`), 11 files including `lib/error-messages.js`; scoped grep for `@cly-org` in src/packages/.github/tests/scripts/README.md returns zero hits. * feat(monorepo): import openclaw-skill as workspace package Plain copy from sibling repo (per PR #1 deviation rationale — captures in-flight uncommitted state that subtree split would miss). Package identity: - name: @switchbot/openclaw-skill (was @cly-org/switchbot-openclaw-skill) - version: 0.1.0 (reset for first publish under new scope) - repo URLs point at OpenWonderLabs/switchbot-openapi-cli - author block stripped (top-level package.json has author) - peerDep "@switchbot/openapi-cli": ">=3.7.1" (literal range, not workspace:*) - files[] adds lib/ (matches PR #1 fix — runtime imports of error-messages.js) Skipped @switchbot/agent-shared package entirely. The only candidate for sharing is lib/error-messages.js (~30 lines of static error strings) and codex's catalog is a strict superset of openclaw's. Bundling or publishing a third package adds machinery without paying off; revisit if either catalog grows materially. Documented in spec decision log. Cleanup: - codex-plugin: strip personal author/repo metadata, align with openclaw - README: add OpenClaw integration section, update Quick start callout - spec: log PR #2 deviations (plain copy, agent-shared skip) Tests: codex-plugin 31/31, openclaw-skill 29/29, root vitest 2715/2715. * ci(publish): per-package matrix and detect-versions gate for monorepo Restructure release pipeline now that @switchbot/codex-plugin and @switchbot/openclaw-skill ship from this repo as workspace packages. publish.yml: add detect-versions step that queries npm for each package, publish only unpublished versions, gate plugin tarballs on concrete peerDeps, set continue-on-error on plugin steps so plugin failures surface as annotations without blocking CLI promotion. npm-published-smoke.yml: per-package matrix (cli vs plugin kinds), skip-if-not-republished logic, plugin tarball-shape checks (peerDep literal, executable bin entries) without live smoke. ci.yml: drop policy-schema-sync — the skill consumer is now in this monorepo so the cross-repo sync gate is obsolete. CHANGELOG: Unreleased entry documenting monorepo absorption, Codex command group, plugin renames, and workflow restructure. No version chosen yet — release decision is separate. * ci(publish): defer @switchbot/openclaw-skill npm publish Keep packages/openclaw-skill/ in the monorepo (workspace tests still cover it) but drop it from publish.yml and npm-published-smoke.yml matrix. README OpenClaw integration section and CHANGELOG mentions removed so users do not search for an unpublished npm package. Re-enabling later only requires re-adding the publish + smoke entries. * feat(codex): 6-step setup adds install-codex-plugin; onInstall is best-effort - codex setup: add install-codex-plugin step between install-switchbot-cli and register-plugin so npx @switchbot/openapi-cli codex setup bootstraps end-to-end on a brand-new machine. Both install steps remain --skip-able. - codex-plugin onInstall hook: always exits 0. When CLI is present it runs switchbot codex setup --yes; when absent it prints a hint and lets the Codex plugin install succeed (so a missing CLI never rolls it back). - README + plugin README: 6-step list, single recommended npx path. - Specs: align design docs with @SwitchBot scope and 6-step flow. Tests: +1 root (codex setup install-codex-plugin coverage), +3 workspace (makeRunOnInstall: missing CLI / setup fail / setup ok). 2716 root + 34 codex-plugin pass. * fix(codex): junction workaround for @-path; ship corrected marketplace.json - runCodexPluginRegistration now bridges packageRoot via a junction (Windows) or symlink (POSIX) in os.tmpdir() before invoking `codex plugin marketplace add`. codex CLI 0.133.0 misparses local paths containing `@` (e.g. <npm-root>/@switchbot/codex-plugin) as `owner/repo@ref` and rejects them with `--ref is only supported for git marketplace sources`. Falls back to the original path if the link cannot be created (test mocks, restricted filesystems). - Track packages/codex-plugin/.agents/plugins/marketplace.json in git and add `.agents/` to the package `files` array so it ships in the npm tarball. The local source `path` is `../../` so codex resolves the plugin manifest at packageRoot/.codex-plugin/plugin.json (was `./`, which pointed at the wrong directory). * test(codex): add pack-install smoke for codex plugin tarball Pack the CLI and @switchbot/codex-plugin workspaces, install both into a scratch dir, and assert tarball shape before any release: - Plugin peerDependency is concrete (not workspace:*). - Required files ship: .codex-plugin/plugin.json + hooks.json, .agents/plugins/marketplace.json, .mcp.json, skills/, bin/. - marketplace.json name is "codex-plugin"; the switchbot plugin source.path is "../../" — codex resolves the path relative to .agents/plugins/marketplace.json itself, so it must climb two levels back to packageRoot/.codex-plugin/plugin.json. - onInstall hook runs bin/auth.js --hook and exits 0. - `switchbot codex setup --dry-run --json` lists the 6 setup steps. Wired into prepublishOnly so a wrong path, missing files entry, or broken hook is caught before the release tag. * fix(codex): stable Windows junction alias; add stage field to RegistrationResult resolveMarketplaceSourceRoot bridges scoped-npm packageRoots through ~/switchbot before calling `codex plugin marketplace add`. codex 0.133.0 mis-parses paths containing `@` (e.g. <root>\node_modules\@SwitchBot\ codex-plugin) as `owner/repo@ref` and rejects them. A previous attempt aliased through os.tmpdir/<pid> and deleted the link in a finally — codex persists the marketplace path, so the alias must outlive the install. Reuse ~/switchbot when it already points at the same packageRoot; only create the junction once. runCodexPluginRegistration now sets stage: 'marketplace-add' | 'plugin-add' so registerCodexPlugin can name the failing codex subcommand in its error (marketplace-add exit 1: ...). resolvePluginId splits into resolvePluginName + resolveMarketplaceName so the marketplace segment of the plugin id reads from .agents/plugins/ marketplace.json instead of basename(packageRoot). publish.yml gains a smoke:codex-pack-install step (runs when the CLI or codex-plugin version bumps). Tests cover the stage field, the not-installed warn path, the marketplace.json name source, and the Windows alias creation. * fix(codex): relocate Windows alias to LOCALAPPDATA and repair stale junctions resolveMarketplaceSourceRoot moves the @-path workaround junction from ~/switchbot to %LOCALAPPDATA%\switchbot\codex-plugin-marketplace (fallback ~/.switchbot/codex-plugin-marketplace when LOCALAPPDATA is unset). The previous location collided with user-owned directories named "switchbot" and silently fell back to the broken @ path on any mismatch. The new path is app-owned, so we treat divergent junctions as repairable (unlink + recreate) and throw when a non-junction sits at the alias path instead of returning a path Codex cannot parse. fs errors propagate to the caller. Tests cover four states: missing alias, healthy junction, stale junction pointing elsewhere, and real-directory collision. * fix(codex-plugin): mirror alias-relocation logic in install.js Mirrors the resolveMarketplaceSourceRoot rewrite from the CLI side: junction lives at %LOCALAPPDATA%\switchbot\codex-plugin-marketplace (fallback ~/.switchbot/...), divergent junctions are repaired in place, and a real directory at the alias path throws instead of silently returning the broken @ path. Adds a deps parameter for fs injection so node:test can cover all four states without touching the real filesystem. * docs(changelog): describe final alias path and repair behavior * test(openclaw-skill): use 127.0.0.1 in policy-editor fetch URLs Server listens on 127.0.0.1; on Node 18 (Linux), undici resolves 'localhost' to ::1 first and fails without IPv4 fallback, so CI test (18.x) reports 'TypeError: fetch failed'. Match the bind address explicitly to make the tests deterministic across Node versions. * fix: publish openclaw-skill and fix Windows .cmd shim invocations - publish.yml: add openclaw-skill version detection, unpublished check, and publish step (continue-on-error, provenance) mirroring codex-plugin - preflight.ts: add shell: process.platform === 'win32' to the npm spawnSync call so .cmd shims are found on Windows (aligns with the existing pattern in codex-checks.ts) - check-cli.js: add shell: SHELL to all execFile/execFileSync calls so switchbot.cmd and npm.cmd shims are launched correctly on Windows * fix(auth): release callback port immediately when open() throws Expose close() on CallbackHandle so callers can tear down the server on demand. In browserLogin, wrap open() in try/catch and call close() on failure — previously the port stayed occupied for the full 120 s timeout if open() failed (e.g. headless / no-browser environments), blocking immediate retries with EADDRINUSE. * fix(codex): pre-remove plugin before add to avoid Windows backup ACCESS_DENIED codex plugin-add backs up any existing installation before replacing it. On Windows, if the previously registered plugin path is a junction with restricted permissions, the backup hits os error 5 (ACCESS_DENIED). Running plugin-remove first forces a fresh install, bypassing the backup. * test(codex-checks): add plugin-remove mock slot in registration tests runCodexPluginRegistration now calls plugin-remove before plugin-add; update the four affected spawnSync mock sequences to match. * docs(readme): simplify Codex install section for non-technical users Remove internal step names, flag details, and profile-scope docs from the Codex integration section. The section now leads with a single npx command, a plain-English description of what it does, and a brief troubleshooting block. Advanced options are deferred to --help. chore(publish): remove openclaw-skill from npm publish workflow The openclaw-skill package is distributed via the companion OpenClaw repo and does not need its own npm publish step in this workflow. * docs(readme): split Codex section into paste-to-chat and developer blocks --------- 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.
…against catalog
♻️ Current situation
Describe the current situation. Explain current problems, if there are any. Be as descriptive as possible (e.g., including examples or code snippets).
💡 Proposed solution
Describe the proposed solution and changes. How does it affect the project? How does it affect the internal structure (e.g., refactorings)?
⚙️ Release Notes
Provide a summary of the changes or features from a user's point of view. If there are breaking changes, provide migration guides using code examples of the affected features.
➕ Additional Information
If applicable, provide additional context in this section.
Testing
Which tests were added? Which existing tests were adapted/changed? Which situations are covered, and what edge cases are missing?
Reviewer Nudging
Where should the reviewer start? what is a good entry point?