@switchbot/openapi-cli 2.1.0 → 2.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.
- package/README.md +55 -3
- package/dist/commands/capabilities.js +30 -20
- package/dist/commands/devices.js +206 -66
- package/dist/commands/events.js +114 -9
- package/dist/commands/expand.js +20 -3
- package/dist/commands/mcp.js +35 -0
- package/dist/commands/plan.js +10 -2
- package/dist/commands/scenes.js +1 -1
- package/dist/commands/watch.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/devices.js +16 -1
- package/dist/mcp/device-history.js +66 -0
- package/dist/mcp/events-subscription.js +10 -3
- package/dist/mqtt/client.js +8 -0
- package/dist/mqtt/credential.js +3 -2
- package/dist/sinks/dispatcher.js +12 -0
- package/dist/sinks/file.js +19 -0
- package/dist/sinks/format.js +56 -0
- package/dist/sinks/homeassistant.js +44 -0
- package/dist/sinks/openclaw.js +33 -0
- package/dist/sinks/stdout.js +5 -0
- package/dist/sinks/telegram.js +28 -0
- package/dist/sinks/types.js +1 -0
- package/dist/sinks/webhook.js +22 -0
- package/dist/utils/flags.js +13 -12
- package/dist/utils/format.js +6 -5
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class HomeAssistantSink {
|
|
2
|
+
url;
|
|
3
|
+
token;
|
|
4
|
+
webhookId;
|
|
5
|
+
eventType;
|
|
6
|
+
constructor(opts) {
|
|
7
|
+
this.url = opts.url.replace(/\/$/, '');
|
|
8
|
+
this.token = opts.token;
|
|
9
|
+
this.webhookId = opts.webhookId;
|
|
10
|
+
this.eventType = opts.eventType ?? 'switchbot_event';
|
|
11
|
+
}
|
|
12
|
+
async write(event) {
|
|
13
|
+
try {
|
|
14
|
+
let endpoint;
|
|
15
|
+
const headers = { 'content-type': 'application/json' };
|
|
16
|
+
if (this.webhookId) {
|
|
17
|
+
// Webhook mode: no auth needed, HA triggers automations directly
|
|
18
|
+
endpoint = `${this.url}/api/webhook/${this.webhookId}`;
|
|
19
|
+
}
|
|
20
|
+
else if (this.token) {
|
|
21
|
+
// REST event API: fires a custom event on the HA event bus
|
|
22
|
+
endpoint = `${this.url}/api/events/${this.eventType}`;
|
|
23
|
+
headers['authorization'] = `Bearer ${this.token}`;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.error('[homeassistant] requires --ha-webhook-id or --ha-token');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const res = await fetch(endpoint, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers,
|
|
32
|
+
body: JSON.stringify(event),
|
|
33
|
+
signal: AbortSignal.timeout(10000),
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const body = await res.text().catch(() => '');
|
|
37
|
+
console.error(`[homeassistant] POST failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(`[homeassistant] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class OpenClawSink {
|
|
2
|
+
url;
|
|
3
|
+
token;
|
|
4
|
+
model;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.url = (opts.url ?? 'http://localhost:18789').replace(/\/$/, '');
|
|
7
|
+
this.token = opts.token;
|
|
8
|
+
this.model = opts.model;
|
|
9
|
+
}
|
|
10
|
+
async write(event) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(`${this.url}/v1/chat/completions`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
'content-type': 'application/json',
|
|
16
|
+
'authorization': `Bearer ${this.token}`,
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
model: this.model,
|
|
20
|
+
messages: [{ role: 'user', content: event.text }],
|
|
21
|
+
}),
|
|
22
|
+
signal: AbortSignal.timeout(10000),
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const body = await res.text().catch(() => '');
|
|
26
|
+
console.error(`[openclaw] POST failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.error(`[openclaw] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class TelegramSink {
|
|
2
|
+
token;
|
|
3
|
+
chatId;
|
|
4
|
+
constructor(opts) {
|
|
5
|
+
this.token = opts.token;
|
|
6
|
+
this.chatId = opts.chatId;
|
|
7
|
+
}
|
|
8
|
+
async write(event) {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(`https://api.telegram.org/bot${this.token}/sendMessage`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'content-type': 'application/json' },
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
chat_id: this.chatId,
|
|
15
|
+
text: event.text,
|
|
16
|
+
}),
|
|
17
|
+
signal: AbortSignal.timeout(10000),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const body = await res.text().catch(() => '');
|
|
21
|
+
console.error(`[telegram] POST failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error(`[telegram] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class WebhookSink {
|
|
2
|
+
url;
|
|
3
|
+
constructor(url) {
|
|
4
|
+
this.url = url;
|
|
5
|
+
}
|
|
6
|
+
async write(event) {
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch(this.url, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'content-type': 'application/json' },
|
|
11
|
+
body: JSON.stringify(event),
|
|
12
|
+
signal: AbortSignal.timeout(10000),
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
console.error(`[webhook] POST failed: HTTP ${res.status}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.error(`[webhook] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/utils/flags.js
CHANGED
|
@@ -17,7 +17,7 @@ export function isVerbose() {
|
|
|
17
17
|
export function isDryRun() {
|
|
18
18
|
return process.argv.includes('--dry-run');
|
|
19
19
|
}
|
|
20
|
-
/** HTTP request timeout in milliseconds. Default 30s. */
|
|
20
|
+
/** HTTP request timeout in milliseconds. Default 30s. Minimum 100ms (values below 100ms are ignored). */
|
|
21
21
|
export function getTimeout() {
|
|
22
22
|
const v = getFlagValue('--timeout');
|
|
23
23
|
if (!v)
|
|
@@ -25,6 +25,10 @@ export function getTimeout() {
|
|
|
25
25
|
const n = Number(v);
|
|
26
26
|
if (!Number.isFinite(n) || n <= 0)
|
|
27
27
|
return 30_000;
|
|
28
|
+
if (n < 100) {
|
|
29
|
+
process.stderr.write(`Warning: --timeout ${n}ms is too low to complete any request; using 100ms minimum.\n`);
|
|
30
|
+
return 100;
|
|
31
|
+
}
|
|
28
32
|
return n;
|
|
29
33
|
}
|
|
30
34
|
/** Override for the credentials file path. */
|
|
@@ -36,20 +40,17 @@ export function getProfile() {
|
|
|
36
40
|
return getFlagValue('--profile');
|
|
37
41
|
}
|
|
38
42
|
/**
|
|
39
|
-
* Audit log path. `--audit-log
|
|
40
|
-
*
|
|
41
|
-
*
|
|
43
|
+
* Audit log path. `--audit-log` enables JSONL append on every mutating command.
|
|
44
|
+
* Use `--audit-log-path <path>` to specify a custom file; otherwise defaults to
|
|
45
|
+
* ~/.switchbot/audit.log. Returns null when --audit-log is absent.
|
|
42
46
|
*/
|
|
43
47
|
export function getAuditLog() {
|
|
44
|
-
|
|
45
|
-
if (idx === -1)
|
|
48
|
+
if (!process.argv.includes('--audit-log'))
|
|
46
49
|
return null;
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return next;
|
|
50
|
+
const customPath = getFlagValue('--audit-log-path');
|
|
51
|
+
if (customPath)
|
|
52
|
+
return customPath;
|
|
53
|
+
return `${process.env.HOME ?? process.env.USERPROFILE ?? '.'}/.switchbot/audit.log`;
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
55
56
|
* Max 429 retries before surfacing the error. Default 3. `--no-retry`
|
package/dist/utils/format.js
CHANGED
|
@@ -32,15 +32,16 @@ export function resolveFormat() {
|
|
|
32
32
|
export function resolveFields() {
|
|
33
33
|
return getFields();
|
|
34
34
|
}
|
|
35
|
-
export function filterFields(headers, rows, fields) {
|
|
35
|
+
export function filterFields(headers, rows, fields, aliases) {
|
|
36
36
|
if (!fields || fields.length === 0)
|
|
37
37
|
return { headers, rows };
|
|
38
|
-
const
|
|
38
|
+
const resolved = aliases ? fields.map((f) => aliases[f] ?? f) : fields;
|
|
39
|
+
const unknown = fields.filter((_, i) => !headers.includes(resolved[i]));
|
|
39
40
|
if (unknown.length > 0) {
|
|
40
41
|
throw new UsageError(`Unknown field(s): ${unknown.map((f) => `"${f}"`).join(', ')}. ` +
|
|
41
42
|
`Allowed: ${headers.map((f) => `"${f}"`).join(', ')}.`);
|
|
42
43
|
}
|
|
43
|
-
const indices =
|
|
44
|
+
const indices = resolved.map((f) => headers.indexOf(f));
|
|
44
45
|
return {
|
|
45
46
|
headers: indices.map((i) => headers[i]),
|
|
46
47
|
rows: rows.map((row) => indices.map((i) => row[i])),
|
|
@@ -62,8 +63,8 @@ function rowToObject(headers, row) {
|
|
|
62
63
|
}
|
|
63
64
|
return obj;
|
|
64
65
|
}
|
|
65
|
-
export function renderRows(headers, rows, format, fields) {
|
|
66
|
-
const filtered = filterFields(headers, rows, fields);
|
|
66
|
+
export function renderRows(headers, rows, format, fields, aliases) {
|
|
67
|
+
const filtered = filterFields(headers, rows, fields, aliases);
|
|
67
68
|
const h = filtered.headers;
|
|
68
69
|
const r = filtered.rows;
|
|
69
70
|
switch (format) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchbot/openapi-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "SwitchBot smart home CLI — control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"switchbot",
|