@switchbot/openapi-cli 1.1.0 → 1.2.0

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 (90) hide show
  1. package/README.md +174 -18
  2. package/dist/api/client.d.ts +7 -1
  3. package/dist/api/client.js +44 -8
  4. package/dist/api/client.js.map +1 -1
  5. package/dist/commands/batch.d.ts +2 -0
  6. package/dist/commands/batch.js +252 -0
  7. package/dist/commands/batch.js.map +1 -0
  8. package/dist/commands/cache.d.ts +2 -0
  9. package/dist/commands/cache.js +108 -0
  10. package/dist/commands/cache.js.map +1 -0
  11. package/dist/commands/capabilities.d.ts +2 -0
  12. package/dist/commands/capabilities.js +91 -0
  13. package/dist/commands/capabilities.js.map +1 -0
  14. package/dist/commands/catalog.d.ts +2 -0
  15. package/dist/commands/catalog.js +291 -0
  16. package/dist/commands/catalog.js.map +1 -0
  17. package/dist/commands/config.js +123 -10
  18. package/dist/commands/config.js.map +1 -1
  19. package/dist/commands/devices.js +234 -147
  20. package/dist/commands/devices.js.map +1 -1
  21. package/dist/commands/doctor.d.ts +2 -0
  22. package/dist/commands/doctor.js +147 -0
  23. package/dist/commands/doctor.js.map +1 -0
  24. package/dist/commands/events.d.ts +15 -0
  25. package/dist/commands/events.js +188 -0
  26. package/dist/commands/events.js.map +1 -0
  27. package/dist/commands/explain.d.ts +2 -0
  28. package/dist/commands/explain.js +137 -0
  29. package/dist/commands/explain.js.map +1 -0
  30. package/dist/commands/history.d.ts +2 -0
  31. package/dist/commands/history.js +104 -0
  32. package/dist/commands/history.js.map +1 -0
  33. package/dist/commands/mcp.d.ts +4 -0
  34. package/dist/commands/mcp.js +386 -0
  35. package/dist/commands/mcp.js.map +1 -0
  36. package/dist/commands/plan.d.ts +37 -0
  37. package/dist/commands/plan.js +344 -0
  38. package/dist/commands/plan.js.map +1 -0
  39. package/dist/commands/quota.d.ts +2 -0
  40. package/dist/commands/quota.js +77 -0
  41. package/dist/commands/quota.js.map +1 -0
  42. package/dist/commands/scenes.js +19 -13
  43. package/dist/commands/scenes.js.map +1 -1
  44. package/dist/commands/schema.d.ts +2 -0
  45. package/dist/commands/schema.js +77 -0
  46. package/dist/commands/schema.js.map +1 -0
  47. package/dist/commands/watch.d.ts +2 -0
  48. package/dist/commands/watch.js +161 -0
  49. package/dist/commands/watch.js.map +1 -0
  50. package/dist/commands/webhook.js +37 -22
  51. package/dist/commands/webhook.js.map +1 -1
  52. package/dist/config.d.ts +11 -0
  53. package/dist/config.js +32 -6
  54. package/dist/config.js.map +1 -1
  55. package/dist/devices/cache.d.ts +50 -0
  56. package/dist/devices/cache.js +152 -1
  57. package/dist/devices/cache.js.map +1 -1
  58. package/dist/devices/catalog.d.ts +49 -0
  59. package/dist/devices/catalog.js +362 -92
  60. package/dist/devices/catalog.js.map +1 -1
  61. package/dist/index.js +31 -1
  62. package/dist/index.js.map +1 -1
  63. package/dist/lib/devices.d.ts +144 -0
  64. package/dist/lib/devices.js +329 -0
  65. package/dist/lib/devices.js.map +1 -0
  66. package/dist/lib/scenes.d.ts +7 -0
  67. package/dist/lib/scenes.js +11 -0
  68. package/dist/lib/scenes.js.map +1 -0
  69. package/dist/utils/audit.d.ts +13 -0
  70. package/dist/utils/audit.js +43 -0
  71. package/dist/utils/audit.js.map +1 -0
  72. package/dist/utils/filter.d.ts +45 -0
  73. package/dist/utils/filter.js +96 -0
  74. package/dist/utils/filter.js.map +1 -0
  75. package/dist/utils/flags.d.ts +42 -0
  76. package/dist/utils/flags.js +108 -0
  77. package/dist/utils/flags.js.map +1 -1
  78. package/dist/utils/format.d.ts +9 -0
  79. package/dist/utils/format.js +109 -0
  80. package/dist/utils/format.js.map +1 -0
  81. package/dist/utils/output.d.ts +11 -0
  82. package/dist/utils/output.js +37 -6
  83. package/dist/utils/output.js.map +1 -1
  84. package/dist/utils/quota.d.ts +48 -0
  85. package/dist/utils/quota.js +144 -0
  86. package/dist/utils/quota.js.map +1 -0
  87. package/dist/utils/retry.d.ts +23 -0
  88. package/dist/utils/retry.js +60 -0
  89. package/dist/utils/retry.js.map +1 -0
  90. package/package.json +4 -1
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Local quota counter. Tracks the SwitchBot 10k/day request budget so the
3
+ * CLI (and any AI agent) can check "how many calls have I already burned?"
4
+ * without pinging the API.
5
+ *
6
+ * Shape (`~/.switchbot/quota.json`):
7
+ * {
8
+ * "days": {
9
+ * "2026-04-18": {
10
+ * "total": 42,
11
+ * "endpoints": {
12
+ * "GET /v1.1/devices": 3,
13
+ * "GET /v1.1/devices/:id/status": 27,
14
+ * "POST /v1.1/devices/:id/commands": 12
15
+ * }
16
+ * }
17
+ * }
18
+ * }
19
+ *
20
+ * We keep the last 7 days to bound the file size and give a short-term
21
+ * trend. Writes are fire-and-forget — a failed write never breaks the
22
+ * actual API call.
23
+ */
24
+ export declare const DAILY_QUOTA = 10000;
25
+ export interface DayBucket {
26
+ total: number;
27
+ endpoints: Record<string, number>;
28
+ }
29
+ export interface QuotaFile {
30
+ days: Record<string, DayBucket>;
31
+ }
32
+ export declare function loadQuota(): QuotaFile;
33
+ /**
34
+ * Normalise a full URL into a SwitchBot-style endpoint pattern. The segment
35
+ * immediately after `devices` or `scenes` is collapsed to `:id` so we can
36
+ * bucket by endpoint shape rather than by specific deviceId/sceneId.
37
+ */
38
+ export declare function normaliseEndpoint(method: string, url: string): string;
39
+ /** Record a single request. Bucketed by local-date + endpoint pattern. */
40
+ export declare function recordRequest(method: string, url: string, now?: Date): void;
41
+ export declare function resetQuota(): void;
42
+ /** Return today's usage (convenience for `quota status`). */
43
+ export declare function todayUsage(now?: Date): {
44
+ date: string;
45
+ total: number;
46
+ remaining: number;
47
+ endpoints: Record<string, number>;
48
+ };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Local quota counter. Tracks the SwitchBot 10k/day request budget so the
3
+ * CLI (and any AI agent) can check "how many calls have I already burned?"
4
+ * without pinging the API.
5
+ *
6
+ * Shape (`~/.switchbot/quota.json`):
7
+ * {
8
+ * "days": {
9
+ * "2026-04-18": {
10
+ * "total": 42,
11
+ * "endpoints": {
12
+ * "GET /v1.1/devices": 3,
13
+ * "GET /v1.1/devices/:id/status": 27,
14
+ * "POST /v1.1/devices/:id/commands": 12
15
+ * }
16
+ * }
17
+ * }
18
+ * }
19
+ *
20
+ * We keep the last 7 days to bound the file size and give a short-term
21
+ * trend. Writes are fire-and-forget — a failed write never breaks the
22
+ * actual API call.
23
+ */
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import os from 'node:os';
27
+ export const DAILY_QUOTA = 10_000;
28
+ const MAX_RETAINED_DAYS = 7;
29
+ function quotaFilePath() {
30
+ return path.join(os.homedir(), '.switchbot', 'quota.json');
31
+ }
32
+ function today(now = new Date()) {
33
+ // Local date, not UTC — SwitchBot's quota window is loose but users
34
+ // reason about "today" in their own timezone.
35
+ const y = now.getFullYear();
36
+ const m = String(now.getMonth() + 1).padStart(2, '0');
37
+ const d = String(now.getDate()).padStart(2, '0');
38
+ return `${y}-${m}-${d}`;
39
+ }
40
+ function emptyFile() {
41
+ return { days: {} };
42
+ }
43
+ export function loadQuota() {
44
+ const file = quotaFilePath();
45
+ if (!fs.existsSync(file))
46
+ return emptyFile();
47
+ try {
48
+ const raw = fs.readFileSync(file, 'utf-8');
49
+ const parsed = JSON.parse(raw);
50
+ if (!parsed || typeof parsed !== 'object' || !parsed.days)
51
+ return emptyFile();
52
+ return parsed;
53
+ }
54
+ catch {
55
+ return emptyFile();
56
+ }
57
+ }
58
+ function saveQuota(data) {
59
+ const file = quotaFilePath();
60
+ const dir = path.dirname(file);
61
+ try {
62
+ if (!fs.existsSync(dir))
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
65
+ }
66
+ catch {
67
+ // swallow: counting is best-effort, must not break a real API call
68
+ }
69
+ }
70
+ function prune(data) {
71
+ const keys = Object.keys(data.days).sort();
72
+ if (keys.length <= MAX_RETAINED_DAYS)
73
+ return data;
74
+ const keep = keys.slice(keys.length - MAX_RETAINED_DAYS);
75
+ const next = { days: {} };
76
+ for (const k of keep)
77
+ next.days[k] = data.days[k];
78
+ return next;
79
+ }
80
+ /**
81
+ * Normalise a full URL into a SwitchBot-style endpoint pattern. The segment
82
+ * immediately after `devices` or `scenes` is collapsed to `:id` so we can
83
+ * bucket by endpoint shape rather than by specific deviceId/sceneId.
84
+ */
85
+ export function normaliseEndpoint(method, url) {
86
+ const m = (method || 'GET').toUpperCase();
87
+ let pathOnly = url;
88
+ try {
89
+ const parsed = new URL(url);
90
+ pathOnly = parsed.pathname;
91
+ }
92
+ catch {
93
+ const q = url.indexOf('?');
94
+ if (q !== -1)
95
+ pathOnly = url.slice(0, q);
96
+ }
97
+ const segments = pathOnly.split('/');
98
+ for (let i = 0; i < segments.length - 1; i++) {
99
+ if (segments[i] === 'devices' || segments[i] === 'scenes') {
100
+ // Only collapse when the next segment looks like an id (not another
101
+ // API verb); the SwitchBot API uses lower-case keywords elsewhere,
102
+ // but guard against future collisions.
103
+ const next = segments[i + 1];
104
+ if (next && next.length > 0) {
105
+ segments[i + 1] = ':id';
106
+ }
107
+ }
108
+ }
109
+ return `${m} ${segments.join('/')}`;
110
+ }
111
+ /** Record a single request. Bucketed by local-date + endpoint pattern. */
112
+ export function recordRequest(method, url, now = new Date()) {
113
+ const key = today(now);
114
+ const endpoint = normaliseEndpoint(method, url);
115
+ const data = loadQuota();
116
+ const bucket = data.days[key] ?? { total: 0, endpoints: {} };
117
+ bucket.total += 1;
118
+ bucket.endpoints[endpoint] = (bucket.endpoints[endpoint] ?? 0) + 1;
119
+ data.days[key] = bucket;
120
+ saveQuota(prune(data));
121
+ }
122
+ export function resetQuota() {
123
+ const file = quotaFilePath();
124
+ try {
125
+ if (fs.existsSync(file))
126
+ fs.unlinkSync(file);
127
+ }
128
+ catch {
129
+ // ignore
130
+ }
131
+ }
132
+ /** Return today's usage (convenience for `quota status`). */
133
+ export function todayUsage(now = new Date()) {
134
+ const key = today(now);
135
+ const data = loadQuota();
136
+ const bucket = data.days[key] ?? { total: 0, endpoints: {} };
137
+ return {
138
+ date: key,
139
+ total: bucket.total,
140
+ remaining: Math.max(0, DAILY_QUOTA - bucket.total),
141
+ endpoints: { ...bucket.endpoints },
142
+ };
143
+ }
144
+ //# sourceMappingURL=quota.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.js","sourceRoot":"","sources":["../../src/utils/quota.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Retry/backoff helpers for the axios client. Kept as pure functions so
3
+ * tests can pin attempt → delay without wall-clock sleeping.
4
+ *
5
+ * Backoff strategies:
6
+ * linear → 1s, 2s, 3s, ... (cap 30s)
7
+ * exponential → 1s, 2s, 4s, 8s, 16s (cap 30s) [default]
8
+ *
9
+ * If the server returns a `Retry-After` header we always prefer it over our
10
+ * own backoff — the API explicitly told us when to come back.
11
+ */
12
+ export type BackoffStrategy = 'linear' | 'exponential';
13
+ /**
14
+ * Parse an HTTP `Retry-After` header. Supports both the seconds form
15
+ * ("Retry-After: 42") and the HTTP-date form ("Retry-After: Wed, 21 Oct
16
+ * 2015 07:28:00 GMT"). Returns the delay in ms, or undefined on garbage.
17
+ */
18
+ export declare function parseRetryAfter(header: unknown, now?: number): number | undefined;
19
+ /** Compute the next backoff delay (ms) for a given attempt index (0-based). */
20
+ export declare function computeBackoff(attempt: number, strategy: BackoffStrategy): number;
21
+ /** Resolve the delay to use before the next retry, preferring Retry-After. */
22
+ export declare function nextRetryDelayMs(attempt: number, strategy: BackoffStrategy, retryAfterHeader: unknown, now?: number): number;
23
+ export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Retry/backoff helpers for the axios client. Kept as pure functions so
3
+ * tests can pin attempt → delay without wall-clock sleeping.
4
+ *
5
+ * Backoff strategies:
6
+ * linear → 1s, 2s, 3s, ... (cap 30s)
7
+ * exponential → 1s, 2s, 4s, 8s, 16s (cap 30s) [default]
8
+ *
9
+ * If the server returns a `Retry-After` header we always prefer it over our
10
+ * own backoff — the API explicitly told us when to come back.
11
+ */
12
+ const BASE_MS = 1_000;
13
+ const MAX_MS = 30_000;
14
+ /**
15
+ * Parse an HTTP `Retry-After` header. Supports both the seconds form
16
+ * ("Retry-After: 42") and the HTTP-date form ("Retry-After: Wed, 21 Oct
17
+ * 2015 07:28:00 GMT"). Returns the delay in ms, or undefined on garbage.
18
+ */
19
+ export function parseRetryAfter(header, now = Date.now()) {
20
+ if (typeof header !== 'string')
21
+ return undefined;
22
+ const trimmed = header.trim();
23
+ if (!trimmed)
24
+ return undefined;
25
+ // All-digits → seconds.
26
+ if (/^\d+$/.test(trimmed)) {
27
+ const seconds = Number(trimmed);
28
+ if (!Number.isFinite(seconds) || seconds < 0)
29
+ return undefined;
30
+ return Math.min(seconds * 1000, MAX_MS);
31
+ }
32
+ // HTTP-date.
33
+ const ts = Date.parse(trimmed);
34
+ if (!Number.isFinite(ts))
35
+ return undefined;
36
+ const delta = ts - now;
37
+ if (delta <= 0)
38
+ return 0;
39
+ return Math.min(delta, MAX_MS);
40
+ }
41
+ /** Compute the next backoff delay (ms) for a given attempt index (0-based). */
42
+ export function computeBackoff(attempt, strategy) {
43
+ const safe = Math.max(0, attempt);
44
+ if (strategy === 'linear') {
45
+ return Math.min((safe + 1) * BASE_MS, MAX_MS);
46
+ }
47
+ // exponential
48
+ return Math.min(BASE_MS * Math.pow(2, safe), MAX_MS);
49
+ }
50
+ /** Resolve the delay to use before the next retry, preferring Retry-After. */
51
+ export function nextRetryDelayMs(attempt, strategy, retryAfterHeader, now = Date.now()) {
52
+ const fromHeader = parseRetryAfter(retryAfterHeader, now);
53
+ if (fromHeader !== undefined)
54
+ return fromHeader;
55
+ return computeBackoff(attempt, strategy);
56
+ }
57
+ export function sleep(ms) {
58
+ return new Promise((resolve) => setTimeout(resolve, ms));
59
+ }
60
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,OAAO,GAAG,KAAK,CAAC;AACtB,MAAM,MAAM,GAAG,MAAM,CAAC;AAEtB;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAe,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACvE,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACjD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAE/B,wBAAwB;IACxB,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC/D,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,aAAa;IACb,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,KAAK,GAAG,EAAE,GAAG,GAAG,CAAC;IACvB,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACzB,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACjC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,QAAyB;IACvE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC;IAChD,CAAC;IACD,cAAc;IACd,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;AACvD,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,gBAAgB,CAC9B,OAAe,EACf,QAAyB,EACzB,gBAAyB,EACzB,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,UAAU,GAAG,eAAe,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IAC1D,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAChD,OAAO,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Command-line interface for SwitchBot API v1.1",
5
5
  "keywords": [
6
6
  "switchbot",
@@ -45,13 +45,16 @@
45
45
  "prepublishOnly": "npm run build && npm test"
46
46
  },
47
47
  "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
49
  "axios": "^1.7.9",
49
50
  "chalk": "^5.4.1",
50
51
  "cli-table3": "^0.6.5",
51
52
  "commander": "^12.1.0",
53
+ "js-yaml": "^4.1.1",
52
54
  "uuid": "^11.0.5"
53
55
  },
54
56
  "devDependencies": {
57
+ "@types/js-yaml": "^4.0.9",
55
58
  "@types/node": "^22.10.7",
56
59
  "@types/uuid": "^10.0.0",
57
60
  "@vitest/coverage-v8": "^2.1.9",