diff --git a/packages/claude-code-plugin/bin/auth.js b/packages/claude-code-plugin/bin/auth.js old mode 100644 new mode 100755 diff --git a/packages/codex-plugin/bin/auth.js b/packages/codex-plugin/bin/auth.js old mode 100644 new mode 100755 diff --git a/packages/codex-plugin/bin/install.js b/packages/codex-plugin/bin/install.js old mode 100644 new mode 100755 diff --git a/packages/openclaw-skill/bin/policy-edit.js b/packages/openclaw-skill/bin/policy-edit.js old mode 100644 new mode 100755 diff --git a/packages/openclaw-skill/bin/start.js b/packages/openclaw-skill/bin/start.js old mode 100644 new mode 100755 diff --git a/src/config.ts b/src/config.ts index 331a62c6..f02cc794 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,7 @@ import { createHash } from 'node:crypto'; import { getConfigPath } from './utils/flags.js'; import { getActiveProfile } from './lib/request-context.js'; import { emitJsonError, isJsonMode } from './utils/output.js'; -import { getPrimedCredentials } from './credentials/prime.js'; +import { getPrimedCredentials, clearPrimedCredentials } from './credentials/prime.js'; export interface SwitchBotConfig { token: string; @@ -198,6 +198,7 @@ export function saveConfig(token: string, secret: string, extras?: Partial,"endTime":}\'', description: 'Create a passcode', idempotent: false, safetyTier: 'destructive', safetyReason: 'Provisions a new access credential — anyone with this passcode can unlock the door.' }, + { command: 'deleteKey', parameter: '\'{"id":}\'', description: 'Delete a passcode by id', idempotent: true, safetyTier: 'destructive', safetyReason: 'Permanently removes a passcode — the holder immediately loses door access.' }, ], - statusFields: ['lockState', 'doorState', 'battery', 'keyList', 'version'], + statusFields: ['battery', 'version', 'lockState', 'doorState', 'calibrate', 'onlineStatus'], }, { type: 'Lock Vision Pro', category: 'physical', - description: 'Pro-tier video smart lock with camera; supports lock, unlock, and deadbolt control.', + description: 'Premium smart lock with camera, passcode management, and advanced features.', role: 'security', commands: [ { command: 'lock', parameter: '—', description: 'Lock the door', idempotent: true }, { command: 'unlock', parameter: '—', description: 'Unlock the door', idempotent: true, safetyTier: 'destructive', safetyReason: 'Physically unlocks the door — anyone nearby can open it.' }, - { command: 'deadbolt', parameter: '—', description: 'Engage deadbolt', idempotent: true }, + { command: 'createKey', parameter: '\'{"name":"...","type":"...","password":"...","startTime":,"endTime":}\'', description: 'Create a passcode', idempotent: false, safetyTier: 'destructive', safetyReason: 'Provisions a new access credential — anyone with this passcode can unlock the door.' }, + { command: 'deleteKey', parameter: '\'{"id":}\'', description: 'Delete a passcode by id', idempotent: true, safetyTier: 'destructive', safetyReason: 'Permanently removes a passcode — the holder immediately loses door access.' }, ], - statusFields: ['lockState', 'doorState', 'battery', 'keyList', 'version'], + statusFields: ['battery', 'version', 'lockState', 'doorState', 'calibrate', 'onlineStatus'], }, { type: 'Plug', @@ -456,6 +473,19 @@ export const DEVICE_CATALOG: DeviceCatalogEntry[] = [ commands: [...onOffToggle, ...rgbOnlyLightControls0To100], statusFields: ['power', 'brightness', 'color', 'version'], }, + { + type: 'Permanent Outdoor Lights', + category: 'physical', + description: 'Weather-resistant outdoor RGBIC string lights with brightness and color temperature control.', + role: 'lighting', + commands: [ + ...onOffToggle, + { command: 'setBrightness', parameter: '{0-100}', description: 'Set brightness (0-100)', idempotent: true, exampleParams: ['50'] }, + { command: 'setColor', parameter: '\'{0-255}:{0-255}:{0-255}\'', description: 'Set RGB color', idempotent: true, exampleParams: ['255:0:0'] }, + { command: 'setColorTemperature', parameter: '{2700-6500}', description: 'Set color temperature in Kelvin', idempotent: true, exampleParams: ['4000'] }, + ], + statusFields: ['power', 'onlineStatus', 'brightness', 'color', 'colorTemperature', 'version'], + }, { type: 'Ceiling Light', category: 'physical', @@ -469,6 +499,24 @@ export const DEVICE_CATALOG: DeviceCatalogEntry[] = [ ], statusFields: ['power', 'brightness', 'colorTemperature', 'version'], }, + { + type: 'RGBICWW Ceiling Light', + category: 'physical', + description: 'RGBICWW ceiling light with separate main light and color light controls.', + role: 'lighting', + commands: [ + ...onOffToggle, + { command: 'turnOnMainLight', parameter: 'default', description: 'Turn on the main light only', idempotent: true }, + { command: 'turnOffMainLight', parameter: 'default', description: 'Turn off the main light only', idempotent: true }, + { command: 'turnOnColorLight', parameter: 'default', description: 'Turn on the ambient color light', idempotent: true }, + { command: 'turnOffColorLight', parameter: 'default', description: 'Turn off the ambient color light', idempotent: true }, + { command: 'setMainLightBrightness', parameter: '{1-100}', description: 'Set main light brightness', idempotent: true, exampleParams: ['50'] }, + { command: 'setMainLightColorTemp', parameter: '{2700-6500}', description: 'Set main light color temperature (K)', idempotent: true, exampleParams: ['4000'] }, + { command: 'setColorLightBrightness', parameter: '{1-100}', description: 'Set color light brightness', idempotent: true, exampleParams: ['50'] }, + { command: 'setColorLightRGB', parameter: '\'{0-255}:{0-255}:{0-255}\'', description: 'Set color light RGB color', idempotent: true, exampleParams: ['255:0:0'] }, + ], + statusFields: ['power', 'onlineStatus', 'mainLightPower', 'mainLightBrightness', 'mainLightColorTemp', 'colorLightPower', 'colorLightBrightness', 'colorLightRGB', 'version'], + }, { type: 'Smart Radiator Thermostat', category: 'physical', @@ -542,6 +590,19 @@ export const DEVICE_CATALOG: DeviceCatalogEntry[] = [ ], statusFields: ['mode', 'version', 'battery', 'power', 'nightStatus', 'oscillation', 'verticalOscillation', 'chargingStatus', 'fanSpeed'], }, + { + type: 'Battery Circulator Fan 2 Pro', + category: 'physical', + description: 'Battery-powered circulator fan with nightlight, wind modes, and USB-C charging.', + role: 'fan', + commands: [ + ...onOff, + { command: 'setNightLightMode', parameter: 'off | 0 | 1', description: 'Night-light mode', idempotent: true, exampleParams: ['off', '1'] }, + { command: 'setWindMode', parameter: 'direct | natural | sleep | hurricane', description: 'Wind mode', idempotent: true, exampleParams: ['natural', 'sleep'] }, + { command: 'setWindSpeed', parameter: '1-100', description: 'Fan speed', idempotent: true, exampleParams: ['50', '100'] }, + ], + statusFields: ['power', 'mode', 'version', 'onlineStatus', 'nightStatus', 'oscillation', 'verticalOscillation', 'chargingStatus', 'fanSpeed'], + }, { type: 'Blind Tilt', category: 'physical', @@ -646,16 +707,6 @@ export const DEVICE_CATALOG: DeviceCatalogEntry[] = [ commands: [], statusFields: ['temperature', 'humidity', 'battery', 'version'], }, - { - type: 'WeatherStation', - category: 'physical', - description: 'Outdoor weather station reporting temperature, humidity, and atmospheric pressure; read-only.', - role: 'sensor', - readOnly: true, - aliases: ['Weather Station'], - commands: [], - statusFields: ['temperature', 'humidity', 'atmosphericPressure', 'battery', 'version'], - }, { type: 'Motion Sensor', category: 'physical', @@ -694,6 +745,19 @@ export const DEVICE_CATALOG: DeviceCatalogEntry[] = [ commands: [], statusFields: ['battery', 'version', 'status'], }, + { + type: 'WeatherStation', + category: 'physical', + description: 'Indoor weather station display with customizable quote screen.', + role: 'sensor', + aliases: ['Weather Station'], + commands: [ + { command: 'customQuote', parameter: 'custom text', description: 'Set a custom quote on the display', idempotent: true, exampleParams: ['Hello from SwitchBot'] }, + { command: 'cancelCustom', parameter: 'default', description: 'Revert to default quote display', idempotent: true }, + { command: 'customPage', parameter: 'custom text', description: 'Set a custom page text', idempotent: true, exampleParams: ['Today is a great day'] }, + ], + statusFields: ['temperature', 'humidity', 'battery', 'version'], + }, // Status-only hub-class devices (no control commands) { type: 'Hub 2', @@ -710,7 +774,7 @@ export const DEVICE_CATALOG: DeviceCatalogEntry[] = [ description: 'IR hub that bridges BLE devices to the cloud and learns IR remotes; no direct control commands.', role: 'hub', readOnly: true, - aliases: ['Hub', 'Hub Plus', 'Hub Mini2'], + aliases: ['Hub', 'Hub Plus', 'Hub Mini Matter Enabled'], commands: [], statusFields: ['version'], }, @@ -794,6 +858,21 @@ export const DEVICE_CATALOG: DeviceCatalogEntry[] = [ aliases: ['SwitchBot Remote', 'Remote Button', 'Wireless Remote'], commands: [], }, + { + type: 'Kata Friends', + category: 'physical', + description: 'Interactive AI companion robot with comic diary generation and voice interaction.', + role: 'other', + commands: [ + { command: 'mode', parameter: '"Normal", "Standby", or "Sleep"', description: 'Set the operating mode', idempotent: true, exampleParams: ['Normal'] }, + { command: 'childLock', parameter: '"on" or "off"', description: 'Toggle child lock', idempotent: true, exampleParams: ['off'] }, + { command: 'backHome', parameter: 'default', description: 'Command Kata to return home', idempotent: true }, + { command: 'pictureTaking', parameter: '"on" or "off"', description: 'Toggle picture taking', idempotent: true, exampleParams: ['on'] }, + { command: 'snapshots', parameter: '"on" or "off"', description: 'Toggle snapshot mode', idempotent: true, exampleParams: ['on'] }, + { command: 'talk', parameter: '"on" or "off"', description: 'Toggle talk mode', idempotent: true, exampleParams: ['on'] }, + ], + statusFields: ['version', 'onlineStatus', 'mode', 'status', 'childLock', 'hospitalized', 'battery'], + }, // ---------- Virtual IR remotes ---------- { diff --git a/src/devices/param-validator.ts b/src/devices/param-validator.ts index 913f1032..589d0603 100644 --- a/src/devices/param-validator.ts +++ b/src/devices/param-validator.ts @@ -240,6 +240,15 @@ export function validateParameter( if (isCirculatorFan(dt) && command === 'closeDelay') { return validateIntRange(raw, 'closeDelay', 1, 36000, 'auto-off delay in seconds'); } + if (dt === 'Battery Circulator Fan 2 Pro' && command === 'setNightLightMode') { + return validateEnum(raw, 'setNightLightMode', ['off', '0', '1']); + } + if (dt === 'Battery Circulator Fan 2 Pro' && command === 'setWindMode') { + return validateEnum(raw, 'setWindMode', ['direct', 'natural', 'sleep', 'hurricane']); + } + if (dt === 'Battery Circulator Fan 2 Pro' && command === 'setWindSpeed') { + return validateIntRange(raw, 'setWindSpeed', 1, 100, 'fan speed percentage'); + } // --- Smart Radiator Thermostat --- if (dt === 'Smart Radiator Thermostat' && command === 'setMode') { @@ -249,11 +258,11 @@ export function validateParameter( return validateIntRange(raw, 'setManualModeTemperature', 4, 35, 'temperature in °C'); } - // --- Keypad --- - if (dt.startsWith('Keypad') && command === 'createKey') { + // --- Keypad / Lock Vision (same passcode command shape) --- + if ((dt.startsWith('Keypad') || dt.startsWith('Lock Vision')) && command === 'createKey') { return validateKeypadCreateKey(raw); } - if (dt.startsWith('Keypad') && command === 'deleteKey') { + if ((dt.startsWith('Keypad') || dt.startsWith('Lock Vision')) && command === 'deleteKey') { return validateKeypadDeleteKey(raw); } @@ -290,7 +299,8 @@ export function brightnessRange(deviceType: string): [number, number] | null { deviceType === 'RGBICWW Floor Lamp' || deviceType === 'RGBIC Neon Wire Rope Light' || deviceType === 'RGBIC Neon Rope Light' || - deviceType === 'Candle Warmer Lamp' + deviceType === 'Candle Warmer Lamp' || + deviceType === 'Permanent Outdoor Lights' ) { return [0, 100]; } @@ -315,7 +325,8 @@ function isColorDevice(deviceType: string): boolean { deviceType === 'RGBIC Neon Wire Rope Light' || deviceType === 'RGBIC Neon Rope Light' || deviceType === 'Light Strip' || - deviceType === 'Fill Light' + deviceType === 'Fill Light' || + deviceType === 'Permanent Outdoor Lights' ); } @@ -330,7 +341,8 @@ function isColorTemperatureDevice(deviceType: string): boolean { deviceType === 'RGBICWW Floor Lamp' || deviceType === 'Light Strip' || deviceType === 'Dimmer' || - deviceType === 'Fill Light' + deviceType === 'Fill Light' || + deviceType === 'Permanent Outdoor Lights' ); } diff --git a/tests/config.test.ts b/tests/config.test.ts index d4731502..e87adc62 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -446,5 +446,16 @@ describe('config', () => { fsMock.existsSync.mockReturnValue(false); expect(tryLoadConfig()).toBeNull(); }); + + it('saveConfig clears the primed credential cache so loadConfig picks up the new values (#69)', async () => { + await primeWith('default', { token: 'stale-token', secret: 'stale-secret' }); + expect(loadConfig()).toEqual({ token: 'stale-token', secret: 'stale-secret' }); + + fsMock.existsSync.mockReturnValue(true); + fsMock.readFileSync.mockReturnValue(JSON.stringify({ token: 'fresh-token', secret: 'fresh-secret' })); + saveConfig('fresh-token', 'fresh-secret'); + + expect(loadConfig()).toEqual({ token: 'fresh-token', secret: 'fresh-secret' }); + }); }); }); diff --git a/tests/devices/catalog.test.ts b/tests/devices/catalog.test.ts index 5081479f..b488d9b3 100644 --- a/tests/devices/catalog.test.ts +++ b/tests/devices/catalog.test.ts @@ -17,11 +17,13 @@ import { const OFFICIAL_API_DEVICE_TYPES = [ 'AI Art Frame', 'AI Hub', + 'AI MindClip', 'Air Purifier PM2.5', 'Air Purifier Table PM2.5', 'Air Purifier Table VOC', 'Air Purifier VOC', 'Battery Circulator Fan', + 'Battery Circulator Fan 2 Pro', 'Blind Tilt', 'Bot', 'Candle Warmer Lamp', @@ -45,6 +47,7 @@ const OFFICIAL_API_DEVICE_TYPES = [ 'Indoor Cam', 'K10+', 'K10+ Pro', + 'Kata Friends', 'Keypad', 'Keypad Touch', 'Keypad Vision', @@ -62,6 +65,7 @@ const OFFICIAL_API_DEVICE_TYPES = [ 'Pan/Tilt Cam 2K', 'Pan/Tilt Cam Plus 2K', 'Pan/Tilt Cam Plus 3K', + 'Permanent Outdoor Lights', 'Plug', 'Plug Mini (EU)', 'Plug Mini (JP)', @@ -69,6 +73,7 @@ const OFFICIAL_API_DEVICE_TYPES = [ 'Presence Sensor', 'RGBIC Neon Rope Light', 'RGBIC Neon Wire Rope Light', + 'RGBICWW Ceiling Light', 'RGBICWW Floor Lamp', 'RGBICWW Strip Light', 'Relay Switch 1', @@ -176,6 +181,12 @@ const OFFICIAL_SUPPORTED_DEVICE_LIST_NAMES = [ 'Smart Radiator Thermostat', 'Weather Station', 'AI Art Frame', + 'Lock Pro Matter Enabled', + 'Permanent Outdoor Lights', + 'RGBICWW Ceiling Light', + 'Battery Circulator Fan 2 Pro', + 'Kata Friends', + 'AI MindClip', ] as const; describe('devices/catalog', () => { @@ -307,19 +318,21 @@ describe('devices/catalog', () => { expect(cmd?.exampleParams?.[0]).toMatch(/^https:\/\//); }); - it('Smart Lock entry resolves the Matter alias "Smart Lock Pro Wifi"', () => { + it('Smart Lock Pro Wifi is a distinct Matter-enabled entry, not merely an alias of Smart Lock', () => { const match = findCatalogEntry('Smart Lock Pro Wifi'); expect(match).not.toBeNull(); expect(Array.isArray(match)).toBe(false); - expect((match as { type: string } | null)?.type).toBe('Smart Lock'); + const entry = match as { type: string; commands: { command: string }[] } | null; + expect(entry?.type).toBe('Smart Lock Pro Wifi'); + expect(entry?.commands.some((c) => c.command === 'nightLatchUnlock')).toBe(true); }); - it('Weather Station resolves and reports atmosphericPressure', () => { + it('Weather Station resolves to the WeatherStation entry with documented status fields', () => { const match = findCatalogEntry('Weather Station'); expect(match).not.toBeNull(); const entry = match as { type: string; statusFields: string[] } | null; expect(entry?.type).toBe('WeatherStation'); - expect(entry?.statusFields).toContain('atmosphericPressure'); + expect(entry?.statusFields).toEqual(['temperature', 'humidity', 'battery', 'version']); }); it('Garage Door Opener turnOn and turnOff are safetyTier: destructive', () => { diff --git a/tests/devices/param-validator.test.ts b/tests/devices/param-validator.test.ts index 9727a9b2..1080b733 100644 --- a/tests/devices/param-validator.test.ts +++ b/tests/devices/param-validator.test.ts @@ -670,6 +670,7 @@ describe('validateParameter — documented lighting ranges', () => { expect(validateParameter('Floor Lamp', 'setBrightness', '0').ok).toBe(true); expect(validateParameter('RGBICWW Strip Light', 'setBrightness', '0').ok).toBe(true); expect(validateParameter('Candle Warmer Lamp', 'setBrightness', '0').ok).toBe(true); + expect(validateParameter('Permanent Outdoor Lights', 'setBrightness', '0').ok).toBe(true); }); it('keeps Color Bulb brightness at 1-100', () => { @@ -677,6 +678,45 @@ describe('validateParameter — documented lighting ranges', () => { }); }); +describe('validateParameter — Permanent Outdoor Lights', () => { + it('rejects out-of-range brightness', () => { + expect(validateParameter('Permanent Outdoor Lights', 'setBrightness', '101').ok).toBe(false); + }); + + it('validates setColor RGB format', () => { + expect(validateParameter('Permanent Outdoor Lights', 'setColor', '255:0:0').ok).toBe(true); + expect(validateParameter('Permanent Outdoor Lights', 'setColor', '256:0:0').ok).toBe(false); + }); + + it('validates setColorTemperature 2700-6500', () => { + expect(validateParameter('Permanent Outdoor Lights', 'setColorTemperature', '4000').ok).toBe(true); + expect(validateParameter('Permanent Outdoor Lights', 'setColorTemperature', '2699').ok).toBe(false); + expect(validateParameter('Permanent Outdoor Lights', 'setColorTemperature', '6501').ok).toBe(false); + }); +}); + +describe('validateParameter — Battery Circulator Fan 2 Pro', () => { + it('validates setNightLightMode against the 2 Pro enum (off | 0 | 1)', () => { + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setNightLightMode', 'off').ok).toBe(true); + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setNightLightMode', '0').ok).toBe(true); + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setNightLightMode', '1').ok).toBe(true); + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setNightLightMode', '2').ok).toBe(false); + }); + + it('validates setWindMode against the 2 Pro enum (includes hurricane, not baby)', () => { + for (const v of ['direct', 'natural', 'sleep', 'hurricane']) { + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setWindMode', v).ok).toBe(true); + } + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setWindMode', 'baby').ok).toBe(false); + }); + + it('validates setWindSpeed 1-100', () => { + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setWindSpeed', '1').ok).toBe(true); + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setWindSpeed', '0').ok).toBe(false); + expect(validateParameter('Battery Circulator Fan 2 Pro', 'setWindSpeed', '101').ok).toBe(false); + }); +}); + describe('validateParameter — Blind Tilt setPosition', () => { it('rejects odd angle values because the API requires multiples of 2', () => { const r = validateParameter('Blind Tilt', 'setPosition', 'up;51'); @@ -729,6 +769,26 @@ describe('validateParameter — Keypad', () => { }); }); +describe('validateParameter — Lock Vision / Lock Vision Pro (same passcode shape as Keypad)', () => { + it('validates createKey', () => { + const valid = '{"name":"test","type":"permanent","password":"123456"}'; + expect(validateParameter('Lock Vision', 'createKey', valid).ok).toBe(true); + expect(validateParameter('Lock Vision Pro', 'createKey', valid).ok).toBe(true); + }); + + it('rejects createKey with invalid password length', () => { + expect(validateParameter('Lock Vision', 'createKey', '{"name":"test","type":"permanent","password":"123"}').ok).toBe(false); + }); + + it('validates deleteKey', () => { + expect(validateParameter('Lock Vision', 'deleteKey', '{"id":12345}').ok).toBe(true); + }); + + it('rejects deleteKey without id', () => { + expect(validateParameter('Lock Vision Pro', 'deleteKey', '{}').ok).toBe(false); + }); +}); + describe('validateParameter — TV SetChannel', () => { it('accepts 1-999', () => { expect(validateParameter('TV', 'SetChannel', '1').ok).toBe(true);