Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b5d64ab
docs: add AI MindClip design spec and TDD implementation plan
Jun 13, 2026
bde518b
feat: export clearPrimedCredentials for cache reset on account switch
Jun 13, 2026
828984c
fix: clear priming and idempotency caches on account credential change
Jun 13, 2026
b4610a0
feat: add dateArg and weekArg validators for YYYY-MM-DD and YYYY-Www …
Jun 13, 2026
79fc444
feat: add AI MindClip read-only device to catalog with 5 status fields
Jun 13, 2026
42c231f
feat: add mindclip API helper functions for 7 custom endpoints
Jun 13, 2026
b7c2bf2
feat: add mindclip command group with 7 subcommands and option valida…
Jun 13, 2026
a341169
feat: register mindclip command in program-builder and add COMMAND_ME…
Jun 13, 2026
3ddb1e1
docs: add mindclip to README and bump version to 3.8.0
Jun 13, 2026
de8d814
test: extend MCP tool-list enumeration to 31 with 7 mindclip tools
Jun 13, 2026
a2a3f2f
feat: add mindclip_list_recordings, mindclip_get_recording, mindclip_…
Jun 13, 2026
09431e7
feat: add mindclip_list_todos MCP tool with 8 optional filters
Jun 13, 2026
8ed9ed7
feat: add mindclip_daily_recall, mindclip_weekly_summary, mindclip_ur…
Jun 13, 2026
52ae444
docs: bump MCP tool count to 31 and add mindclip authority-chain rows
Jun 13, 2026
3db6580
test: update tool-profile counts to 17/20/31 and add outputSchema to …
Jun 13, 2026
c30d7a0
fix: clear in-memory caches in reset command for long-running processes
Jun 13, 2026
652dbc3
fix: wrap mindclip recording and summary action handlers in try/catch
Jun 13, 2026
b3cb5df
fix: wrap remaining mindclip action handlers (recordings, todos, dail…
Jun 13, 2026
41ccb15
fix: export COMSPEC in pre-push hook to match pre-commit (Git Bash on…
Jun 13, 2026
d9fbb33
fix: stop reset masking errors and sync capabilities MCP tool list
Jun 13, 2026
666a6ee
chore: patch bump three plugins to publish MindClip-aware skill content
Jun 13, 2026
9458e92
docs: align tool-count copy at "31 tools" (was 24) across all surfaces
Jun 13, 2026
79c579f
refactor(mcp)!: shrink tool footprint 31 -> 25 and default plugins to…
Jun 13, 2026
e83d213
test(mcp): expand consolidated tools coverage with ~75 new cases
Jun 13, 2026
ad4f97f
feat(mcp): include 3 legacy device_history names in read profile for …
Jun 13, 2026
c2f0280
test(mcp): add legacy-alias contract test for device_history (failing…
Jun 13, 2026
d401bc0
feat(mcp): re-add device_history aliases (get_/query_/aggregate_) for…
Jun 13, 2026
75ccb56
fix(mcp): add missing notes field to aggregate_device_history outputS…
Jun 13, 2026
99893a8
docs(tests): note 3.8.0 device_history-aliased + mindclip-fresh decis…
Jun 13, 2026
82d1265
docs: align tool-count copy at 28 (25 canonical + 3 deprecated device…
Jun 13, 2026
678ad6b
fix(mcp): add .describe() to alias tool schema properties (get/query/…
Jun 13, 2026
840e233
docs: align tool-count copy at 28 (25 canonical + 3 deprecated device…
Jun 13, 2026
710a3df
docs(changelog): drop BREAKING section — only device_history needs al…
Jun 13, 2026
0466279
test: bump tool-profiles count expectations to 14/17/28 (missed by T4)
Jun 13, 2026
0fa4f67
docs(quality): tighten README/CHANGELOG copy, align deprecation wordi…
Jun 13, 2026
0d6549a
test(mcp): make get_device_history equivalence test deterministic via…
Jun 13, 2026
9c48061
docs(agent-guide): add 3 missing mindclip tool rows to main table
Jun 13, 2026
ad9d172
test(mindclip): add missing handleError mock to output module stub
Jun 13, 2026
612f22e
fix(mcp): wrap runDeviceHistoryAggregate in try/catch and refresh tes…
Jun 13, 2026
e3f40a3
fix(mindclip): URL-encode recording id in lib path interpolation
Jun 13, 2026
3e2c5ea
fix(mcp): tighten mindclip Zod schemas to reject empty strings and im…
Jun 13, 2026
86608ea
fix(validation): reject W53 for short ISO years in weekArg and MCP we…
Jun 13, 2026
ec9b277
fix(mcp): replace bare mcpError with apiErrorToMcpError in mindclip h…
Jun 13, 2026
3d9a44c
fix(mcp): reclassify history-store catch errors as kind=runtime
Jun 13, 2026
5408166
fix(auth): guard clearCache/clearStatusCache against EBUSY on Windows
Jun 13, 2026
3edcb2f
fix(auth): invalidate caches after keychain set/delete/migrate
Jun 13, 2026
d87a150
fix(mcp): enforce raw-mode limit cap at 100 in device_history
Jun 13, 2026
60b76d2
fix(mcp): derive tool-count labels from TOOL_PROFILES instead of hard…
Jun 13, 2026
444a46f
fix(capabilities): exclude deprecated MCP aliases from capabilities o…
Jun 13, 2026
038a749
fix(catalog): add aliases to AI MindClip device entry
Jun 13, 2026
99c9f14
fix(idempotency): profile-scope the idempotency cache
Jun 13, 2026
5e182e0
fix(credentials): guard primeCredentials against stale write after clear
Jun 13, 2026
739fc5c
test(tool-profiles): update MCP_TOOLS sync assertion to account for d…
Jun 13, 2026
e0dd7d4
chore: release 3.8.1
Jun 13, 2026
dfb8ae5
fix(review): address 15 code-review findings from PR #67
Jun 13, 2026
4d05b17
fix(review): address 15 code-review findings from PR #67
Jun 13, 2026
c98e607
test(auth,config): assert all four caches cleared on every credential…
Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .githooks/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env sh
# Windows: Git Bash clears COMSPEC, which causes npm's child_process.spawn to
# receive an undefined shell path and fail with ERR_INVALID_ARG_TYPE.
if [ -z "${COMSPEC:-}" ] && [ -f "/c/Windows/System32/cmd.exe" ]; then
COMSPEC="C:\\Windows\\System32\\cmd.exe"
export COMSPEC
fi
3 changes: 3 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ set -eu
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

# shellcheck source=common.sh
. "$REPO_ROOT/.githooks/common.sh"

echo "[pre-commit] packaging sanity checks"
npm run verify:pre-commit
3 changes: 3 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ set -eu
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

# shellcheck source=common.sh
. "$REPO_ROOT/.githooks/common.sh"

echo "[pre-push] release gate"
npm run verify:release-gate
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,44 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **AI MindClip MCP tools** — three new read tools for AI MindClip recordings: `mindclip_recordings` (action: `"list"` paginated browse / `"get"` single recording by id / `"summary"` AI-generated summary), `mindclip_list_todos`, and `mindclip_recall` (period: `"daily"` daily recall / `"weekly"` weekly summary / `"urgent_todos"` urgent to-dos). Plus the underlying CLI command group (`switchbot mindclip recordings/recording/summary/todos/daily/weekly/urgent-todos`). Read-only; counts toward the same SwitchBot daily quota as other API reads.

### Changed

- **`device_history` MCP consolidation** — the previous `get_device_history` / `query_device_history` / `aggregate_device_history` trio collapses into a single `device_history` tool that takes a `mode: "raw" | "query" | "aggregate"` discriminator. The consolidated tool is recommended — it cuts per-session token cost (one schema instead of three). The 3 old names continue to work as deprecated aliases that delegate to the consolidated handler; no client action is required. CLI commands (`switchbot history show/range/aggregate`) are unchanged.
- **Plugins switch to the `default` tool profile** — `@switchbot/claude-code-plugin`, `@switchbot/codex-plugin`, and `@switchbot/gemini-extension` now register the MCP server as `switchbot mcp serve` (without `--tools all`). The default profile exposes 17 tools (read + action; includes the 3 deprecated device_history aliases for 3.x compat). To get the 11 admin tools (policy / audit / automation rules), users opt in by adding `--tools all` to their MCP config — the same flag the CLI has always supported. Existing installations keep working: `registerCodexPluginAuto` / `claude mcp add` / `codex plugin` re-registration writes the new args; manual configs need a one-line edit. Rationale: most agents never invoke admin tools, but every session paid for their schemas.
- **Profile counts**: `readonly` 11 → 14, `default` 14 → 17, `all` 25 → 28 (each total = 25 canonical + 3 deprecated device_history aliases).
- **`mcp tools --tools <profile>` help text**, `mcp serve` help bullet list, README/SKILL.md/GEMINI.md tables, and all package descriptions updated to reflect the new counts and the deprecation note.

### Deprecated

- **3 device_history MCP tool names** are retained as aliases in 3.x and **scheduled for removal in 4.0.0**: `get_device_history`, `query_device_history`, `aggregate_device_history`. Each alias's description is prefixed with `[DEPRECATED — use device_history(mode="…")]` and its `_meta` carries `deprecated: true, replacement: 'device_history'`. Migrate before the 4.0.0 release.

### Fixed

- **`reset` no longer aborts before printing the result summary** — the in-memory cache cleanup (`clearCache` / `clearStatusCache`) was rerunning `unlinkSync` on a file the data-file loop had already attempted to delete. On a permission-denied path that re-throw skipped both the in-memory clear and the result table. The reset command now uses the pure in-memory `resetListCache` / `resetStatusCache` helpers; disk deletion stays the sole responsibility of the data-file loop, where errors are reported into `results`.
- **`capabilities --surface mcp` lists every registered MCP tool** — `MCP_TOOLS` was a hand-maintained array that had drifted. The list is now derived from the `TOOL_PROFILES.all` single source of truth, and a new test in `tool-profiles.test.ts` asserts the advertised set matches what `createSwitchBotMcpServer({ toolProfile: 'all' })` actually registers, so any future drift fails CI.

## [3.8.1]

### Fixed

- **MindClip URL path injection** — `getRecording` and `getSummary` now call `encodeURIComponent()` on the id before interpolating into the URL path; slashes, `?`, `#`, and `..` traversal segments can no longer escape the path prefix or smuggle query parameters.
- **MindClip MCP Zod schema tightening** — `mindclip_recordings` and `mindclip_list_todos` optional string inputs (`language`, `deviceID`, `fileID`) now use `.min(1)` to reject empty strings that previously bypassed validation. `mindclip_recall` date field gains a `.refine()` check against impossible calendar dates (e.g. `2026-02-30`, `2026-13-01`).
- **ISO W53 validation** — `weekArg` (CLI) and the `mindclip_recall` MCP `week` field now reject W53 for short ISO years; only years whose January 1 falls on a Thursday (or leap years starting on Wednesday) have a 53rd ISO week.
- **MindClip MCP error envelope** — three mindclip handlers (`mindclip_recordings`, `mindclip_list_todos`, `mindclip_recall`) were using a bare `mcpError('api', 1, err.message)` that discarded `subKind`, `retryable`, `retryAfterMs`, and `hint`. Switched to `apiErrorToMcpError()` so all structured error fields are preserved.
- **History-store catch kind** — `runDeviceHistoryQuery` and `runDeviceHistoryAggregate` catch blocks changed from `kind='usage'` to `kind='runtime'`; all user-input validation happens before the try block, so any thrown error is a storage-layer fault, not a caller mistake.
- **`clearCache` / `clearStatusCache` EBUSY on Windows** — `auth login` and `config set-token` called `fs.unlinkSync` directly; on Windows a concurrent reader can cause `EBUSY` which skipped the success output. Disk-only calls are now wrapped in try/catch; the in-memory portion always clears.
- **`auth keychain set/delete/migrate` missing cache invalidation** — the three keychain subcommands wrote new credentials but left the device-list cache, status cache, primed-credentials cache, and idempotency cache untouched. All three now call `onCredentialChange()`, the same helper used by `auth login`.
- **`device_history` raw-mode limit cap** — Zod schema allowed `max(10000)` for the shared `limit` field, but the description and deprecated `get_device_history` both said max 100 for raw mode. `device_history(mode="raw")` now applies `Math.min(limit ?? 20, 100)` at runtime and the description is updated to say "max 100 enforced at runtime".
- **Stale tool-count labels** — `readonly`/`default`/`all` profile sizes were hardcoded as 11/14/25 in `mcp tools` help text, `gemini-checks.ts`, and all plugin manifests; the true values are 14/17/28. All labels now derive from `TOOL_PROFILES.*.size`.
- **`capabilities --surface mcp` advertising deprecated aliases** — `MCP_TOOLS` now filters out the 3 deprecated `*_device_history` aliases via the new `DEPRECATED_MCP_TOOLS` export from `tool-profiles.ts`. The aliases remain registered in the MCP server for backward compat.
- **AI MindClip catalog aliases missing** — the `AI MindClip` entry in `DEVICE_CATALOG` had no `aliases` array; `search_catalog` and `describe_device` queries for `"MindClip"` or `"Mind Clip"` returned no results. Aliases `['AIMindClip', 'MindClip', 'Mind Clip']` added.
- **Idempotency cache not scoped per profile** — `idempotencyCache.clear()` on credential change wiped all profiles' dedup windows. A new optional `profile` parameter on `IdempotencyCache.run()` tags each entry; the new `clearForProfile(profile)` method evicts only that profile's entries. `sendDeviceCommand` now passes `getActiveProfile()` and `auth`/`config` call `clearForProfile` instead of `clear`.
- **`primeCredentials` stale-write race** — when `clearPrimedCredentials()` fired while `store.get()` was still in flight, the resolved value could overwrite the freshly-cleared cache. A generation counter prevents this: `primeCredentials` captures the counter before awaiting and discards the result if it has changed.

## [3.7.9]

### Added
Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Run `switchbot catalog list` to see the full list including aliases and per-comm
| **Sensors** _(read-only)_ | Meter · MeterPlus · WoIOSensor · MeterPro · MeterPro(CO2) · WeatherStation · Motion Sensor · Presence Sensor · Contact Sensor · Water Detector · Wallet Finder Card |
| **Hubs** _(read-only)_ | Hub · Hub Plus · Hub Mini · Hub 2 · Hub 3 · AI Hub |
| **Cameras** _(status only)_ | Indoor Cam · Pan/Tilt Cam · Pan/Tilt Cam 2K · Pan/Tilt Cam Plus 2K · Pan/Tilt Cam Plus 3K · Outdoor Spotlight Cam |
| **Other** | Bot · AI Art Frame · Home Climate Panel · Remote |
| **Other** | Bot · AI Art Frame · AI MindClip · Home Climate Panel · Remote |
| **IR virtual remotes** _(via Hub)_ | Air Conditioner · TV · Streamer · Set Top Box · DVD · Speaker · Fan · Light · Others |

---
Expand Down Expand Up @@ -150,7 +150,7 @@ The optional skill package [`@switchbot/claude-code-plugin`](https://www.npmjs.c

## Gemini CLI integration

The Gemini extension is in [`packages/gemini-extension/`](./packages/gemini-extension/) — it provides 24 MCP tools, a GEMINI.md context file, and 23 slash commands.
The Gemini extension is in [`packages/gemini-extension/`](./packages/gemini-extension/) — it provides up to 28 MCP tools (14 readonly, 17 default, 28 with `--tools all`; see [docs/agent-guide.md](./docs/agent-guide.md) for the deprecated-aliases breakdown), a GEMINI.md context file, and 23 slash commands.

**Recommended — paste into Gemini CLI chat:**

Expand Down Expand Up @@ -249,6 +249,18 @@ switchbot scenes list
switchbot scenes execute <sceneId>
```

### `mindclip`

```bash
switchbot mindclip recordings [--device <id>] [--page <n>] [--size <n>]
switchbot mindclip recording <id> [--language en|zh]
switchbot mindclip summary <id>
switchbot mindclip todos [--completed 0|1|2] [--category 0..5]
switchbot mindclip daily [--date YYYY-MM-DD]
switchbot mindclip weekly [--week YYYY-Www]
switchbot mindclip urgent-todos [--date YYYY-MM-DD]
```

### `codex`

```bash
Expand Down Expand Up @@ -282,7 +294,7 @@ switchbot config list-profiles
### `mcp`

```bash
switchbot mcp serve # stdio MCP server — 24 tools
switchbot mcp serve # stdio MCP server — default 17 tools (use --tools all for 28)
```

### `webhook`
Expand Down
42 changes: 30 additions & 12 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
}
```

### Available tools (24)
### Available tools (28)

| Tool | Purpose | Safety tier |
| --- | --- | --- |
Expand All @@ -88,9 +88,10 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
| `search_catalog` | Look up device type by name/alias | read |
| `describe_device` | Catalog-derived capabilities + optional live status | read |
| `account_overview` | Cold-start snapshot (devices/scenes/quota/cache/MQTT) | read |
| `get_device_history` | Latest state + ring history from disk | read |
| `query_device_history` | Time-range query over JSONL history | read |
| `aggregate_device_history` | Bucketed statistics over history | read |
| `device_history` | Read locally-persisted history. mode: "raw" (latest + ring) / "query" (time-range JSONL) / "aggregate" (bucketed stats) | read |
| `mindclip_recordings` | Browse/fetch AI MindClip recordings. action: `"list"` paginated browse / `"get"` single recording by id / `"summary"` AI-generated summary | read |
| `mindclip_list_todos` | List AI-extracted to-dos pulled from voice recordings | read |
| `mindclip_recall` | AI-curated recall views. period: `"daily"` daily recall / `"weekly"` weekly summary / `"urgent_todos"` urgent to-dos | read |
| `policy_validate` | Validate policy.yaml | read |
| `policy_new` | Scaffold a starter policy file | action |
| `policy_migrate` | Upgrade policy schema in-place | action |
Expand All @@ -107,21 +108,38 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)

The MCP server refuses destructive commands (Smart Lock `unlock`, Garage Door `open`, etc.) unless the tool call includes `confirm: true`, and the default safety profile still blocks direct destructive execution in favor of the reviewed CLI flow (`plan save` → `plan review` → `plan approve` → `plan execute`). The allowed list is the `destructive: true` commands in the catalog — `switchbot schema export | jq '[.data.types[].commands[] | select(.destructive)]'` shows every one.

### `get_device_history` — zero-cost state lookup
### Deprecated aliases (scheduled for removal in 4.0.0)

Reads `~/.switchbot/device-history/<deviceId>.json` written by `events mqtt-tail`. Requires no API call and costs zero quota.
These names continue to work in 3.x but are thin wrappers over `device_history`. Migrate to the consolidated tool — it emits a single schema per session instead of three.

- `get_device_history` → `device_history(mode="raw")`
- `query_device_history` → `device_history(mode="query")`
- `aggregate_device_history` → `device_history(mode="aggregate")`

### `device_history` — zero-cost state lookup

Reads `~/.switchbot/device-history/<deviceId>.json` (mode="raw") or `<deviceId>.jsonl` (mode="query"/"aggregate") written by `events mqtt-tail`. Requires no API call and costs zero quota.

```json
// Without deviceId — list all devices with stored history
{ "tool": "get_device_history" }
// mode=raw, no deviceId — list all devices with stored history
{ "tool": "device_history", "mode": "raw" }
// → { "devices": [{ "deviceId": "ABC123", "latest": { "t": "...", "payload": {...} } }] }

// With deviceId — latest + rolling history (default 20, max 100 entries)
{ "tool": "get_device_history", "deviceId": "ABC123", "limit": 5 }
// mode=raw, with deviceId — latest + rolling history (default 20, max 100 entries)
{ "tool": "device_history", "mode": "raw", "deviceId": "ABC123", "limit": 5 }
// → { "deviceId": "ABC123", "latest": {...}, "history": [{...}, ...] }

// mode=query — time-range filtered JSONL records
{ "tool": "device_history", "mode": "query", "deviceId": "ABC123", "since": "1h" }
// → { "deviceId": "ABC123", "count": 42, "records": [{...}, ...] }

// mode=aggregate — bucketed numeric statistics
{ "tool": "device_history", "mode": "aggregate", "deviceId": "ABC123",
"metrics": ["temperature","humidity"], "since": "24h", "bucket": "1h" }
// → { "deviceId": "ABC123", "buckets": [{ "t": "...", "metrics": { "temperature": {"count":12,"avg":21.4} } }, ...], ... }
```

**Workflow**: run `switchbot events mqtt-tail` in the background (e.g. with pm2) to keep the history files fresh; then call `get_device_history` from any MCP session without consuming REST quota.
**Workflow**: run `switchbot events mqtt-tail` in the background (e.g. with pm2) to keep the history files fresh; then call `device_history` from any MCP session without consuming REST quota.

#### Device-history directory layout

Expand All @@ -131,7 +149,7 @@ After `events mqtt-tail` runs on a device, `~/.switchbot/device-history/` contai
Source of truth for `history range` and `history aggregate`.
Rotated at ~50 MB (up to 3 segments).
- `<deviceId>.json`: latest 100-entry ring buffer.
Written on every MQTT event. Read by MCP `get_device_history`
Written on every MQTT event. Read by MCP `device_history` (mode="raw")
for fast, zero-quota retrieval.
- `__control.jsonl`: MQTT connection lifecycle events
(heartbeat, connect, disconnect). Not a device log; used for diagnostics.
Expand Down
Loading
Loading