@switchbot/openapi-cli 1.3.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +30 -1
  2. package/dist/api/client.js +22 -5
  3. package/dist/auth.js +0 -1
  4. package/dist/commands/batch.js +12 -6
  5. package/dist/commands/cache.js +0 -1
  6. package/dist/commands/capabilities.js +5 -3
  7. package/dist/commands/catalog.js +0 -1
  8. package/dist/commands/completion.js +0 -1
  9. package/dist/commands/config.js +0 -1
  10. package/dist/commands/device-meta.js +0 -1
  11. package/dist/commands/devices.js +2 -2
  12. package/dist/commands/doctor.js +0 -1
  13. package/dist/commands/events.js +0 -1
  14. package/dist/commands/expand.js +0 -1
  15. package/dist/commands/explain.js +0 -1
  16. package/dist/commands/history.js +0 -1
  17. package/dist/commands/mcp.js +334 -18
  18. package/dist/commands/plan.js +0 -1
  19. package/dist/commands/quota.js +0 -1
  20. package/dist/commands/scenes.js +0 -1
  21. package/dist/commands/schema.js +2 -9
  22. package/dist/commands/watch.js +0 -1
  23. package/dist/commands/webhook.js +0 -1
  24. package/dist/config.js +5 -5
  25. package/dist/devices/cache.js +0 -1
  26. package/dist/devices/catalog.js +0 -1
  27. package/dist/devices/device-meta.js +0 -1
  28. package/dist/index.js +0 -1
  29. package/dist/lib/devices.js +22 -18
  30. package/dist/lib/idempotency.js +72 -0
  31. package/dist/lib/request-context.js +12 -0
  32. package/dist/lib/scenes.js +0 -1
  33. package/dist/logger.js +16 -0
  34. package/dist/mcp/events-subscription.js +210 -0
  35. package/dist/mqtt/client.js +184 -0
  36. package/dist/mqtt/credential.js +12 -0
  37. package/dist/utils/audit.js +0 -1
  38. package/dist/utils/filter.js +0 -1
  39. package/dist/utils/flags.js +0 -1
  40. package/dist/utils/format.js +0 -1
  41. package/dist/utils/name-resolver.js +0 -1
  42. package/dist/utils/output.js +30 -6
  43. package/dist/utils/quota.js +0 -1
  44. package/dist/utils/retry.js +0 -1
  45. package/dist/utils/string.js +0 -1
  46. package/package.json +6 -2
  47. package/dist/api/client.d.ts +0 -18
  48. package/dist/api/client.js.map +0 -1
  49. package/dist/auth.d.ts +0 -1
  50. package/dist/auth.js.map +0 -1
  51. package/dist/commands/batch.d.ts +0 -2
  52. package/dist/commands/batch.js.map +0 -1
  53. package/dist/commands/cache.d.ts +0 -2
  54. package/dist/commands/cache.js.map +0 -1
  55. package/dist/commands/capabilities.d.ts +0 -2
  56. package/dist/commands/capabilities.js.map +0 -1
  57. package/dist/commands/catalog.d.ts +0 -2
  58. package/dist/commands/catalog.js.map +0 -1
  59. package/dist/commands/completion.d.ts +0 -2
  60. package/dist/commands/completion.js.map +0 -1
  61. package/dist/commands/config.d.ts +0 -2
  62. package/dist/commands/config.js.map +0 -1
  63. package/dist/commands/device-meta.d.ts +0 -2
  64. package/dist/commands/device-meta.js.map +0 -1
  65. package/dist/commands/devices.d.ts +0 -2
  66. package/dist/commands/devices.js.map +0 -1
  67. package/dist/commands/doctor.d.ts +0 -2
  68. package/dist/commands/doctor.js.map +0 -1
  69. package/dist/commands/events.d.ts +0 -15
  70. package/dist/commands/events.js.map +0 -1
  71. package/dist/commands/expand.d.ts +0 -2
  72. package/dist/commands/expand.js.map +0 -1
  73. package/dist/commands/explain.d.ts +0 -2
  74. package/dist/commands/explain.js.map +0 -1
  75. package/dist/commands/history.d.ts +0 -2
  76. package/dist/commands/history.js.map +0 -1
  77. package/dist/commands/mcp.d.ts +0 -4
  78. package/dist/commands/mcp.js.map +0 -1
  79. package/dist/commands/plan.d.ts +0 -38
  80. package/dist/commands/plan.js.map +0 -1
  81. package/dist/commands/quota.d.ts +0 -2
  82. package/dist/commands/quota.js.map +0 -1
  83. package/dist/commands/scenes.d.ts +0 -2
  84. package/dist/commands/scenes.js.map +0 -1
  85. package/dist/commands/schema.d.ts +0 -2
  86. package/dist/commands/schema.js.map +0 -1
  87. package/dist/commands/watch.d.ts +0 -2
  88. package/dist/commands/watch.js.map +0 -1
  89. package/dist/commands/webhook.d.ts +0 -2
  90. package/dist/commands/webhook.js.map +0 -1
  91. package/dist/config.d.ts +0 -18
  92. package/dist/config.js.map +0 -1
  93. package/dist/devices/cache.d.ts +0 -79
  94. package/dist/devices/cache.js.map +0 -1
  95. package/dist/devices/catalog.d.ts +0 -70
  96. package/dist/devices/catalog.js.map +0 -1
  97. package/dist/devices/device-meta.d.ts +0 -15
  98. package/dist/devices/device-meta.js.map +0 -1
  99. package/dist/index.d.ts +0 -2
  100. package/dist/index.js.map +0 -1
  101. package/dist/lib/devices.d.ts +0 -144
  102. package/dist/lib/devices.js.map +0 -1
  103. package/dist/lib/scenes.d.ts +0 -7
  104. package/dist/lib/scenes.js.map +0 -1
  105. package/dist/utils/audit.d.ts +0 -13
  106. package/dist/utils/audit.js.map +0 -1
  107. package/dist/utils/filter.d.ts +0 -45
  108. package/dist/utils/filter.js.map +0 -1
  109. package/dist/utils/flags.d.ts +0 -52
  110. package/dist/utils/flags.js.map +0 -1
  111. package/dist/utils/format.d.ts +0 -9
  112. package/dist/utils/format.js.map +0 -1
  113. package/dist/utils/name-resolver.d.ts +0 -17
  114. package/dist/utils/name-resolver.js.map +0 -1
  115. package/dist/utils/output.d.ts +0 -23
  116. package/dist/utils/output.js.map +0 -1
  117. package/dist/utils/quota.d.ts +0 -50
  118. package/dist/utils/quota.js.map +0 -1
  119. package/dist/utils/retry.d.ts +0 -23
  120. package/dist/utils/retry.js.map +0 -1
  121. package/dist/utils/string.d.ts +0 -2
  122. package/dist/utils/string.js.map +0 -1
@@ -0,0 +1,72 @@
1
+ /**
2
+ * In-memory LRU cache for idempotent request deduplication.
3
+ * Caches the outcome of a keyed operation for 60 seconds;
4
+ * duplicate keys within the window return the cached result without re-executing.
5
+ * Process-local only — not shared across replicas.
6
+ */
7
+ const DEFAULT_TTL_MS = 60000; // 60 seconds
8
+ const DEFAULT_MAX_ENTRIES = 1024;
9
+ export class IdempotencyCache {
10
+ cache = new Map();
11
+ ttlMs;
12
+ maxEntries;
13
+ constructor(ttlMs, maxEntries) {
14
+ this.ttlMs = ttlMs ?? DEFAULT_TTL_MS;
15
+ this.maxEntries = maxEntries ?? DEFAULT_MAX_ENTRIES;
16
+ }
17
+ /**
18
+ * Execute fn if the key is not cached, or return the cached result if it is.
19
+ * On new execution, caches the result for ttlMs.
20
+ */
21
+ async run(key, fn) {
22
+ // No key = always execute (not cached)
23
+ if (!key) {
24
+ return fn();
25
+ }
26
+ const now = Date.now();
27
+ const cached = this.cache.get(key);
28
+ // Cached and not expired
29
+ if (cached && cached.expiresAt > now) {
30
+ return cached.result;
31
+ }
32
+ // Expired or uncached: execute
33
+ const result = await fn();
34
+ // Prune if over capacity (LRU: remove oldest entries)
35
+ if (this.cache.size >= this.maxEntries) {
36
+ const toRemove = Math.ceil(this.maxEntries * 0.1); // Remove 10%
37
+ let removed = 0;
38
+ for (const [k, v] of this.cache.entries()) {
39
+ if (removed >= toRemove)
40
+ break;
41
+ // Remove expired entries first, then oldest
42
+ if (v.expiresAt <= now) {
43
+ this.cache.delete(k);
44
+ removed++;
45
+ }
46
+ }
47
+ // If still over capacity, remove oldest insertion (Map is insertion-ordered)
48
+ if (this.cache.size >= this.maxEntries) {
49
+ const firstKey = this.cache.keys().next().value;
50
+ if (firstKey)
51
+ this.cache.delete(firstKey);
52
+ }
53
+ }
54
+ // Cache the result
55
+ this.cache.set(key, { result, expiresAt: now + this.ttlMs });
56
+ return result;
57
+ }
58
+ /**
59
+ * Clear all cached entries (mainly for testing).
60
+ */
61
+ clear() {
62
+ this.cache.clear();
63
+ }
64
+ /**
65
+ * Return the number of cached entries.
66
+ */
67
+ size() {
68
+ return this.cache.size;
69
+ }
70
+ }
71
+ // Global shared instance for the process
72
+ export const idempotencyCache = new IdempotencyCache();
@@ -0,0 +1,12 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { getProfile } from '../utils/flags.js';
3
+ export const requestContext = new AsyncLocalStorage();
4
+ export function withRequestContext(ctx, fn) {
5
+ return requestContext.run(ctx, fn);
6
+ }
7
+ export function getActiveProfile() {
8
+ const ctx = requestContext.getStore();
9
+ if (ctx?.profile !== undefined)
10
+ return ctx.profile;
11
+ return getProfile();
12
+ }
@@ -8,4 +8,3 @@ export async function executeScene(sceneId, client) {
8
8
  const c = client ?? createClient();
9
9
  await c.post(`/v1.1/scenes/${sceneId}/execute`);
10
10
  }
11
- //# sourceMappingURL=scenes.js.map
package/dist/logger.js ADDED
@@ -0,0 +1,16 @@
1
+ import pino from 'pino';
2
+ const logLevel = process.env.LOG_LEVEL || 'warn';
3
+ const logFormat = process.env.LOG_FORMAT || 'json';
4
+ const pinoConfig = {
5
+ level: logLevel,
6
+ transport: logFormat === 'pretty'
7
+ ? { target: 'pino-pretty' }
8
+ : undefined,
9
+ };
10
+ export const log = pino(pinoConfig);
11
+ export function setLogLevel(level) {
12
+ log.level = level;
13
+ }
14
+ export function getLogLevel() {
15
+ return log.level;
16
+ }
@@ -0,0 +1,210 @@
1
+ import { SwitchBotMqttClient } from '../mqtt/client.js';
2
+ import { parseFilter, applyFilter } from '../utils/filter.js';
3
+ import { fetchDeviceList } from '../lib/devices.js';
4
+ import { getCachedDevice } from '../devices/cache.js';
5
+ import { createClient } from '../api/client.js';
6
+ import { log } from '../logger.js';
7
+ export class EventSubscriptionManager {
8
+ mqttClient = null;
9
+ subscribers = new Map();
10
+ ringBuffer = [];
11
+ ringSize = 1000;
12
+ typeMap = new Map();
13
+ refreshTypeMapTimer = null;
14
+ idleCleanupTimer = null;
15
+ getClient;
16
+ lastRefreshAttempt = 0;
17
+ constructor(mqttClient, getClient) {
18
+ this.mqttClient = mqttClient || null;
19
+ this.getClient = getClient;
20
+ }
21
+ async initialize(mqttConfig) {
22
+ if (!this.mqttClient) {
23
+ const client = new SwitchBotMqttClient(mqttConfig, async () => {
24
+ // Auth refresh callback - would need credential resolution here
25
+ return {
26
+ username: mqttConfig.username,
27
+ password: mqttConfig.password,
28
+ };
29
+ });
30
+ client.onStateChange((state) => {
31
+ if (state === 'connected') {
32
+ this.emit({
33
+ kind: 'events.reconnected',
34
+ timestamp: Date.now(),
35
+ });
36
+ client.subscribe('$aws/things/+/shadow/update/accepted');
37
+ }
38
+ });
39
+ client.onMessage((topic, payload) => {
40
+ try {
41
+ const data = JSON.parse(payload.toString());
42
+ const deviceId = this.extractDeviceId(topic);
43
+ if (deviceId && data.state) {
44
+ this.addEvent({
45
+ kind: 'shadow.updated',
46
+ deviceId,
47
+ payload: data.state,
48
+ timestamp: Date.now(),
49
+ });
50
+ }
51
+ }
52
+ catch (err) {
53
+ log.debug({ err, topic }, 'failed to parse shadow payload');
54
+ }
55
+ });
56
+ await client.connect();
57
+ this.mqttClient = client;
58
+ }
59
+ this.scheduleIdleCleanup();
60
+ }
61
+ subscribe(id, handler, filter) {
62
+ // Validate filter syntax if provided
63
+ if (filter) {
64
+ parseFilter(filter);
65
+ }
66
+ const subscriber = {
67
+ id,
68
+ handler,
69
+ filter,
70
+ lastActivity: Date.now(),
71
+ };
72
+ this.subscribers.set(id, subscriber);
73
+ // Send recent events that match the filter
74
+ for (const event of this.ringBuffer) {
75
+ if (this.matchesFilter(event, filter)) {
76
+ handler(event);
77
+ }
78
+ }
79
+ return () => {
80
+ this.subscribers.delete(id);
81
+ };
82
+ }
83
+ addEvent(event) {
84
+ this.ringBuffer.push(event);
85
+ // Check for overflow
86
+ if (this.ringBuffer.length > this.ringSize) {
87
+ const droppedCount = this.ringBuffer.length - this.ringSize;
88
+ const oldestTimestamp = this.ringBuffer[0]?.timestamp || Date.now();
89
+ // Emit overflow notice to all subscribers
90
+ this.emit({
91
+ kind: 'events.dropped',
92
+ count: droppedCount,
93
+ sinceTs: oldestTimestamp,
94
+ });
95
+ // Trim buffer
96
+ this.ringBuffer = this.ringBuffer.slice(-this.ringSize);
97
+ }
98
+ // Broadcast to matching subscribers
99
+ this.emit(event);
100
+ }
101
+ emit(event) {
102
+ for (const subscriber of this.subscribers.values()) {
103
+ if (this.matchesFilter(event, subscriber.filter)) {
104
+ subscriber.lastActivity = Date.now();
105
+ subscriber.handler(event);
106
+ }
107
+ }
108
+ }
109
+ matchesFilter(event, filter) {
110
+ if (!filter)
111
+ return true;
112
+ // Only filter shadow events
113
+ if (event.kind !== 'shadow.updated')
114
+ return true;
115
+ try {
116
+ // Parse filter and match against device metadata
117
+ const clauses = parseFilter(filter);
118
+ const deviceId = event.deviceId;
119
+ // Get device info from cache
120
+ const cached = getCachedDevice(deviceId);
121
+ if (!cached) {
122
+ // Lazily refresh type map if device unknown
123
+ this.scheduleTypeMapRefresh();
124
+ return false; // Conservative: drop if unknown
125
+ }
126
+ // Build a Device-compatible shape for applyFilter
127
+ const device = {
128
+ deviceId,
129
+ deviceType: this.typeMap.get(deviceId) || cached.type,
130
+ deviceName: cached.name,
131
+ familyName: cached.familyName,
132
+ roomName: cached.roomName,
133
+ enableCloudService: true,
134
+ hubDeviceId: '',
135
+ };
136
+ // Use applyFilter with single device in list
137
+ const matched = applyFilter(clauses, [device], [], new Map());
138
+ return matched.length > 0;
139
+ }
140
+ catch {
141
+ return false; // Invalid filter matches nothing
142
+ }
143
+ }
144
+ scheduleTypeMapRefresh() {
145
+ if (this.refreshTypeMapTimer || Date.now() - this.lastRefreshAttempt < 5000) {
146
+ return; // Already scheduled or too recent
147
+ }
148
+ this.refreshTypeMapTimer = setTimeout(async () => {
149
+ this.refreshTypeMapTimer = null;
150
+ this.lastRefreshAttempt = Date.now();
151
+ try {
152
+ const client = this.getClient?.() || createClient();
153
+ const body = await fetchDeviceList(client);
154
+ for (const d of body.deviceList) {
155
+ if (d.deviceType)
156
+ this.typeMap.set(d.deviceId, d.deviceType);
157
+ }
158
+ for (const ir of body.infraredRemoteList) {
159
+ this.typeMap.set(ir.deviceId, ir.remoteType);
160
+ }
161
+ }
162
+ catch {
163
+ // Silently fail type map refresh
164
+ }
165
+ }, 100);
166
+ }
167
+ scheduleIdleCleanup() {
168
+ if (this.idleCleanupTimer)
169
+ return;
170
+ this.idleCleanupTimer = setInterval(() => {
171
+ const now = Date.now();
172
+ const idleThreshold = 10 * 60 * 1000; // 10 minutes
173
+ for (const [id, subscriber] of this.subscribers.entries()) {
174
+ if (now - subscriber.lastActivity > idleThreshold) {
175
+ this.subscribers.delete(id);
176
+ }
177
+ }
178
+ }, 60000); // Check every minute
179
+ }
180
+ extractDeviceId(topic) {
181
+ // Topic format: $aws/things/<deviceId>/shadow/update/accepted
182
+ const match = topic.match(/\$aws\/things\/([^/]+)\/shadow/);
183
+ return match ? match[1] : null;
184
+ }
185
+ getState() {
186
+ if (!this.mqttClient)
187
+ return 'disabled';
188
+ return this.mqttClient.getState();
189
+ }
190
+ getSubscriberCount() {
191
+ return this.subscribers.size;
192
+ }
193
+ getRecentEvents(limit = 100) {
194
+ return this.ringBuffer.slice(-limit);
195
+ }
196
+ async shutdown() {
197
+ if (this.refreshTypeMapTimer) {
198
+ clearTimeout(this.refreshTypeMapTimer);
199
+ }
200
+ if (this.idleCleanupTimer) {
201
+ clearInterval(this.idleCleanupTimer);
202
+ }
203
+ if (this.mqttClient) {
204
+ await this.mqttClient.disconnect();
205
+ this.mqttClient = null;
206
+ }
207
+ this.subscribers.clear();
208
+ this.ringBuffer = [];
209
+ }
210
+ }
@@ -0,0 +1,184 @@
1
+ import { connect } from 'mqtt';
2
+ export class SwitchBotMqttClient {
3
+ client = null;
4
+ config;
5
+ state = 'connecting';
6
+ authRefreshNeeded = false;
7
+ reconnectAttempts = 0;
8
+ maxReconnectAttempts = 10;
9
+ handlers = new Set();
10
+ messageHandlers = new Set();
11
+ authRefreshCallback;
12
+ stableTimer = null;
13
+ lastConnectionAttempt = 0;
14
+ constructor(config, onAuthRefreshNeeded) {
15
+ this.config = config;
16
+ this.authRefreshCallback = onAuthRefreshNeeded;
17
+ }
18
+ async connect() {
19
+ if (this.client && this.state === 'connected') {
20
+ return;
21
+ }
22
+ this.setState('connecting');
23
+ this.authRefreshNeeded = false;
24
+ this.reconnectAttempts = 0;
25
+ try {
26
+ const options = {
27
+ username: this.config.username,
28
+ password: this.config.password,
29
+ clean: true,
30
+ reconnectPeriod: 0, // Manual reconnect control
31
+ connectTimeout: 10000,
32
+ rejectUnauthorized: this.config.rejectUnauthorized ?? true,
33
+ };
34
+ this.client = connect(`mqtts://${this.config.host}:${this.config.port}`, options);
35
+ this.client.on('connect', () => {
36
+ this.reconnectAttempts = 0;
37
+ this.setState('connected');
38
+ this.authRefreshNeeded = false;
39
+ });
40
+ this.client.on('message', (topic, payload) => {
41
+ for (const handler of this.messageHandlers) {
42
+ handler(topic, payload);
43
+ }
44
+ });
45
+ this.client.on('error', (err) => {
46
+ // Check for auth-related errors
47
+ if ((err instanceof Error &&
48
+ (err.message.includes('401') ||
49
+ err.message.includes('Unauthorized') ||
50
+ err.message.includes('EACCES'))) ||
51
+ err.code === 'EACCES') {
52
+ this.authRefreshNeeded = true;
53
+ }
54
+ });
55
+ this.client.on('close', () => {
56
+ this.clearStableTimer();
57
+ if (this.authRefreshNeeded) {
58
+ this.setState('failed');
59
+ }
60
+ else if (this.reconnectAttempts < this.maxReconnectAttempts) {
61
+ this.attemptReconnect();
62
+ }
63
+ else {
64
+ this.setState('failed');
65
+ }
66
+ });
67
+ // Wait for connection with timeout
68
+ await new Promise((resolve, reject) => {
69
+ const timeout = setTimeout(() => {
70
+ reject(new Error('MQTT connection timeout'));
71
+ }, 15000);
72
+ const onConnect = () => {
73
+ clearTimeout(timeout);
74
+ this.client?.removeListener('error', onError);
75
+ resolve();
76
+ };
77
+ const onError = (err) => {
78
+ clearTimeout(timeout);
79
+ this.client?.removeListener('connect', onConnect);
80
+ reject(err);
81
+ };
82
+ if (this.client?.connected) {
83
+ clearTimeout(timeout);
84
+ resolve();
85
+ }
86
+ else {
87
+ this.client?.once('connect', onConnect);
88
+ this.client?.once('error', onError);
89
+ }
90
+ });
91
+ }
92
+ catch (err) {
93
+ this.setState('failed');
94
+ throw err;
95
+ }
96
+ }
97
+ async attemptReconnect() {
98
+ this.reconnectAttempts++;
99
+ this.setState('reconnecting');
100
+ if (this.authRefreshNeeded && this.authRefreshCallback) {
101
+ try {
102
+ const refreshed = await this.authRefreshCallback();
103
+ this.config.username = refreshed.username;
104
+ this.config.password = refreshed.password;
105
+ this.authRefreshNeeded = false;
106
+ }
107
+ catch (err) {
108
+ // Auth refresh failed, mark as failed
109
+ this.setState('failed');
110
+ return;
111
+ }
112
+ }
113
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s...
114
+ const delay = Math.min(30000, 1000 * Math.pow(2, this.reconnectAttempts - 1));
115
+ await new Promise((r) => setTimeout(r, delay));
116
+ try {
117
+ await this.connect();
118
+ }
119
+ catch (err) {
120
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
121
+ await this.attemptReconnect();
122
+ }
123
+ else {
124
+ this.setState('failed');
125
+ }
126
+ }
127
+ }
128
+ setState(newState) {
129
+ if (this.state !== newState) {
130
+ this.state = newState;
131
+ for (const handler of this.handlers) {
132
+ handler(newState);
133
+ }
134
+ }
135
+ }
136
+ clearStableTimer() {
137
+ if (this.stableTimer) {
138
+ clearTimeout(this.stableTimer);
139
+ this.stableTimer = null;
140
+ }
141
+ }
142
+ subscribe(topic) {
143
+ if (this.client && this.state === 'connected') {
144
+ this.client.subscribe(topic, (err) => {
145
+ if (err) {
146
+ console.error(`Failed to subscribe to ${topic}:`, err);
147
+ }
148
+ });
149
+ }
150
+ }
151
+ onStateChange(handler) {
152
+ this.handlers.add(handler);
153
+ return () => {
154
+ this.handlers.delete(handler);
155
+ };
156
+ }
157
+ onMessage(handler) {
158
+ this.messageHandlers.add(handler);
159
+ return () => {
160
+ this.messageHandlers.delete(handler);
161
+ };
162
+ }
163
+ getState() {
164
+ return this.state;
165
+ }
166
+ isConnected() {
167
+ return this.state === 'connected' && this.client?.connected === true;
168
+ }
169
+ async disconnect() {
170
+ this.clearStableTimer();
171
+ if (this.client) {
172
+ await new Promise((resolve) => {
173
+ this.client?.end(false, () => {
174
+ resolve();
175
+ });
176
+ });
177
+ this.client = null;
178
+ this.setState('failed');
179
+ }
180
+ }
181
+ setAuthRefreshCallback(callback) {
182
+ this.authRefreshCallback = callback;
183
+ }
184
+ }
@@ -0,0 +1,12 @@
1
+ export function getMqttConfig() {
2
+ const host = process.env.SWITCHBOT_MQTT_HOST;
3
+ const username = process.env.SWITCHBOT_MQTT_USERNAME;
4
+ const password = process.env.SWITCHBOT_MQTT_PASSWORD;
5
+ if (!host || !username || !password)
6
+ return null;
7
+ const rawPort = process.env.SWITCHBOT_MQTT_PORT;
8
+ const port = rawPort ? Number(rawPort) : 8883;
9
+ if (!Number.isFinite(port) || port <= 0 || port > 65535)
10
+ return null;
11
+ return { host, port, username, password };
12
+ }
@@ -40,4 +40,3 @@ export function readAudit(file) {
40
40
  }
41
41
  return out;
42
42
  }
43
- //# sourceMappingURL=audit.js.map
@@ -93,4 +93,3 @@ export function applyFilter(clauses, deviceList, infraredRemoteList, hubLocation
93
93
  return candidates;
94
94
  return candidates.filter((c) => clauses.every((clause) => matches(c, clause)));
95
95
  }
96
- //# sourceMappingURL=filter.js.map
@@ -139,4 +139,3 @@ export function getCacheMode() {
139
139
  }
140
140
  return { listTtlMs: ms, statusTtlMs: ms };
141
141
  }
142
- //# sourceMappingURL=flags.js.map
@@ -106,4 +106,3 @@ export function renderRows(headers, rows, format, fields) {
106
106
  }
107
107
  }
108
108
  }
109
- //# sourceMappingURL=format.js.map
@@ -76,4 +76,3 @@ export function resolveDeviceId(deviceId, nameQuery) {
76
76
  }
77
77
  throw new UsageError(`No device matches "${nameQuery}". Run 'switchbot devices list' to see device names.`);
78
78
  }
79
- //# sourceMappingURL=name-resolver.js.map
@@ -2,11 +2,12 @@ import Table from 'cli-table3';
2
2
  import chalk from 'chalk';
3
3
  import { ApiError, DryRunSignal } from '../api/client.js';
4
4
  import { getFormat } from './flags.js';
5
+ export const SCHEMA_VERSION = '1.1';
5
6
  export function isJsonMode() {
6
7
  return process.argv.includes('--json') || getFormat() === 'json';
7
8
  }
8
9
  export function printJson(data) {
9
- console.log(JSON.stringify(data, null, 2));
10
+ console.log(JSON.stringify({ schemaVersion: SCHEMA_VERSION, data }, null, 2));
10
11
  }
11
12
  export function printTable(headers, rows) {
12
13
  const table = new Table({
@@ -67,26 +68,50 @@ function classifyApiError(code) {
67
68
  }
68
69
  export function buildErrorPayload(error) {
69
70
  if (error instanceof StructuredUsageError) {
70
- const payload = { code: 2, kind: 'usage', message: error.message };
71
+ const payload = {
72
+ code: 2,
73
+ kind: 'usage',
74
+ message: error.message,
75
+ errorClass: 'usage',
76
+ transient: false
77
+ };
71
78
  if (error.context)
72
79
  payload.context = error.context;
73
80
  return payload;
74
81
  }
75
82
  if (error instanceof UsageError) {
76
- return { code: 2, kind: 'usage', message: error.message };
83
+ return { code: 2, kind: 'usage', message: error.message, errorClass: 'usage', transient: false };
77
84
  }
78
85
  const code = error instanceof ApiError ? error.code : 1;
79
86
  const kind = error instanceof ApiError ? 'api' : 'runtime';
80
87
  const message = error instanceof Error ? error.message : 'An unknown error occurred';
81
88
  const hint = error instanceof ApiError ? (error.hint ?? errorHint(error.code)) : null;
82
89
  const retryable = error instanceof ApiError ? error.retryable : false;
83
- const payload = { code, kind, message };
90
+ const retryAfterMs = error instanceof ApiError ? error.retryAfterMs : undefined;
91
+ const transient = error instanceof ApiError ? error.transient : false;
92
+ // Classify error
93
+ let errorClass = 'api';
94
+ if (kind === 'runtime') {
95
+ errorClass = 'api';
96
+ }
97
+ else if (transient && code >= 500) {
98
+ errorClass = 'api';
99
+ }
100
+ else if (code === 0) {
101
+ errorClass = 'network';
102
+ }
103
+ else if (code >= 400) {
104
+ errorClass = 'api';
105
+ }
106
+ const payload = { code, kind, message, errorClass, transient };
84
107
  if (error instanceof ApiError)
85
108
  payload.subKind = classifyApiError(error.code);
86
109
  if (hint)
87
110
  payload.hint = hint;
88
111
  if (retryable)
89
112
  payload.retryable = true;
113
+ if (retryAfterMs !== undefined)
114
+ payload.retryAfterMs = retryAfterMs;
90
115
  return payload;
91
116
  }
92
117
  export function handleError(error) {
@@ -95,7 +120,7 @@ export function handleError(error) {
95
120
  }
96
121
  const payload = buildErrorPayload(error);
97
122
  if (isJsonMode()) {
98
- console.error(JSON.stringify({ error: payload }));
123
+ console.error(JSON.stringify({ schemaVersion: SCHEMA_VERSION, error: payload }));
99
124
  process.exit(payload.code === 2 ? 2 : 1);
100
125
  }
101
126
  if (payload.kind === 'usage') {
@@ -135,4 +160,3 @@ function errorHint(code) {
135
160
  return null;
136
161
  }
137
162
  }
138
- //# sourceMappingURL=output.js.map
@@ -211,4 +211,3 @@ export function todayUsage(now = new Date()) {
211
211
  endpoints: { ...bucket.endpoints },
212
212
  };
213
213
  }
214
- //# sourceMappingURL=quota.js.map
@@ -57,4 +57,3 @@ export function nextRetryDelayMs(attempt, strategy, retryAfterHeader, now = Date
57
57
  export function sleep(ms) {
58
58
  return new Promise((resolve) => setTimeout(resolve, ms));
59
59
  }
60
- //# sourceMappingURL=retry.js.map
@@ -20,4 +20,3 @@ export function levenshtein(a, b) {
20
20
  export function normalizeDeviceName(s) {
21
21
  return s.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff ]/g, ' ').replace(/\s+/g, ' ').trim();
22
22
  }
23
- //# sourceMappingURL=string.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "1.3.2",
3
+ "version": "2.0.1",
4
4
  "description": "Command-line interface for SwitchBot API v1.1",
5
5
  "keywords": [
6
6
  "switchbot",
@@ -37,12 +37,14 @@
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsc",
40
+ "build:prod": "tsc -p tsconfig.build.json",
41
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
40
42
  "dev": "tsx src/index.ts",
41
43
  "start": "node dist/index.js",
42
44
  "test": "vitest run",
43
45
  "test:watch": "vitest",
44
46
  "test:coverage": "vitest run --coverage",
45
- "prepublishOnly": "npm run build && npm test"
47
+ "prepublishOnly": "npm test && npm run clean && npm run build:prod"
46
48
  },
47
49
  "dependencies": {
48
50
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -51,6 +53,8 @@
51
53
  "cli-table3": "^0.6.5",
52
54
  "commander": "^12.1.0",
53
55
  "js-yaml": "^4.1.1",
56
+ "mqtt": "^5.3.0",
57
+ "pino": "^9.0.0",
54
58
  "uuid": "^11.0.5"
55
59
  },
56
60
  "devDependencies": {