@switchbot/openapi-cli 2.7.2 → 3.1.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 +481 -103
- package/dist/api/client.js +23 -1
- package/dist/commands/agent-bootstrap.js +47 -2
- package/dist/commands/auth.js +354 -0
- package/dist/commands/batch.js +20 -4
- package/dist/commands/capabilities.js +155 -65
- package/dist/commands/config.js +109 -0
- package/dist/commands/daemon.js +367 -0
- package/dist/commands/devices.js +62 -11
- package/dist/commands/doctor.js +417 -8
- package/dist/commands/events.js +3 -3
- package/dist/commands/explain.js +1 -2
- package/dist/commands/health.js +113 -0
- package/dist/commands/install.js +246 -0
- package/dist/commands/mcp.js +888 -7
- package/dist/commands/plan.js +379 -103
- package/dist/commands/policy.js +586 -0
- package/dist/commands/rules.js +875 -0
- package/dist/commands/scenes.js +140 -0
- package/dist/commands/schema.js +0 -2
- package/dist/commands/status-sync.js +131 -0
- package/dist/commands/uninstall.js +237 -0
- package/dist/commands/upgrade-check.js +88 -0
- package/dist/config.js +14 -0
- package/dist/credentials/backends/file.js +101 -0
- package/dist/credentials/backends/linux.js +129 -0
- package/dist/credentials/backends/macos.js +129 -0
- package/dist/credentials/backends/windows.js +215 -0
- package/dist/credentials/keychain.js +88 -0
- package/dist/credentials/prime.js +52 -0
- package/dist/devices/catalog.js +4 -10
- package/dist/index.js +30 -1
- package/dist/install/default-steps.js +257 -0
- package/dist/install/preflight.js +212 -0
- package/dist/install/steps.js +67 -0
- package/dist/lib/command-keywords.js +17 -0
- package/dist/lib/daemon-state.js +46 -0
- package/dist/lib/destructive-mode.js +12 -0
- package/dist/lib/devices.js +1 -1
- package/dist/lib/plan-store.js +68 -0
- package/dist/policy/add-rule.js +124 -0
- package/dist/policy/diff.js +91 -0
- package/dist/policy/examples/policy.example.yaml +99 -0
- package/dist/policy/format.js +57 -0
- package/dist/policy/load.js +61 -0
- package/dist/policy/migrate.js +67 -0
- package/dist/policy/schema/v0.2.json +331 -0
- package/dist/policy/schema.js +18 -0
- package/dist/policy/validate.js +262 -0
- package/dist/rules/action.js +205 -0
- package/dist/rules/audit-query.js +89 -0
- package/dist/rules/conflict-analyzer.js +203 -0
- package/dist/rules/cron-scheduler.js +186 -0
- package/dist/rules/destructive.js +52 -0
- package/dist/rules/engine.js +757 -0
- package/dist/rules/matcher.js +230 -0
- package/dist/rules/pid-file.js +95 -0
- package/dist/rules/quiet-hours.js +45 -0
- package/dist/rules/suggest.js +95 -0
- package/dist/rules/throttle.js +116 -0
- package/dist/rules/types.js +34 -0
- package/dist/rules/webhook-listener.js +223 -0
- package/dist/rules/webhook-token.js +90 -0
- package/dist/status-sync/manager.js +268 -0
- package/dist/utils/audit.js +12 -2
- package/dist/utils/health.js +101 -0
- package/dist/utils/output.js +72 -23
- package/dist/utils/retry.js +81 -0
- package/package.json +12 -4
|
@@ -28,6 +28,16 @@ const AGENT_GUIDE = {
|
|
|
28
28
|
action: 'Mutates device or cloud state but is reversible and routine (turnOn, setColor).',
|
|
29
29
|
destructive: 'Hard to reverse / physical-world side effects (unlock, garage open, delete key). Requires explicit user confirmation.',
|
|
30
30
|
},
|
|
31
|
+
riskLevels: {
|
|
32
|
+
low: 'Read-only or non-mutating. Safe to call autonomously.',
|
|
33
|
+
medium: 'Mutates state (action tier). Prefer `plan` workflow. Reversible.',
|
|
34
|
+
high: 'Destructive / hard-to-reverse. Must go through review-before-execute. Direct --yes execution is reserved for explicit dev profiles.',
|
|
35
|
+
},
|
|
36
|
+
recommendedModes: {
|
|
37
|
+
direct: 'May be called directly without a plan step.',
|
|
38
|
+
plan: 'Prefer batching in a plan for traceability and dry-run support.',
|
|
39
|
+
'review-before-execute': 'Must be reviewed/approved before execution. Use `plan save`, `plan review`, `plan approve`, then `plan execute`.',
|
|
40
|
+
},
|
|
31
41
|
verifiability: {
|
|
32
42
|
local: 'Result is fully verifiable from the CLI return value itself.',
|
|
33
43
|
deviceConfirmed: 'Device returns an ack with an observable state field.',
|
|
@@ -35,55 +45,128 @@ const AGENT_GUIDE = {
|
|
|
35
45
|
none: 'No feedback — e.g. IR transmission. Pair with an external sensor to confirm.',
|
|
36
46
|
},
|
|
37
47
|
};
|
|
48
|
+
function deriveRiskMeta(meta) {
|
|
49
|
+
const riskLevel = meta.agentSafetyTier === 'destructive' ? 'high'
|
|
50
|
+
: meta.agentSafetyTier === 'action' ? 'medium' : 'low';
|
|
51
|
+
return {
|
|
52
|
+
riskLevel,
|
|
53
|
+
requiresConfirmation: meta.agentSafetyTier === 'destructive',
|
|
54
|
+
supportsDryRun: meta.mutating,
|
|
55
|
+
idempotencyHint: meta.idempotencySupported ? 'safe' : meta.mutating ? 'non-idempotent' : 'safe',
|
|
56
|
+
recommendedMode: meta.agentSafetyTier === 'destructive' ? 'review-before-execute'
|
|
57
|
+
: meta.agentSafetyTier === 'action' ? 'plan' : 'direct',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function meta(mutating, consumesQuota, idempotencySupported, agentSafetyTier, verifiability, typicalLatencyMs) {
|
|
61
|
+
return { mutating, consumesQuota, idempotencySupported, agentSafetyTier, verifiability, typicalLatencyMs };
|
|
62
|
+
}
|
|
63
|
+
const READ_LOCAL = meta(false, false, false, 'read', 'local', 20);
|
|
64
|
+
const READ_REMOTE = meta(false, true, false, 'read', 'local', 500);
|
|
65
|
+
const ACTION_LOCAL = meta(true, false, false, 'action', 'local', 20);
|
|
66
|
+
const ACTION_REMOTE = meta(true, true, false, 'action', 'deviceDependent', 900);
|
|
67
|
+
const ACTION_REMOTE_IDEMPOTENT = meta(true, true, true, 'action', 'deviceDependent', 900);
|
|
68
|
+
const DESTRUCTIVE_LOCAL = meta(true, false, false, 'destructive', 'local', 20);
|
|
69
|
+
const DESTRUCTIVE_REMOTE = meta(true, true, false, 'destructive', 'deviceDependent', 1200);
|
|
70
|
+
const READ_NONE = meta(false, false, false, 'read', 'none', 50);
|
|
38
71
|
const COMMAND_META = {
|
|
39
|
-
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'history
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
72
|
+
'agent-bootstrap': READ_LOCAL,
|
|
73
|
+
'auth keychain describe': READ_LOCAL,
|
|
74
|
+
'auth keychain get': READ_LOCAL,
|
|
75
|
+
'auth keychain set': DESTRUCTIVE_LOCAL,
|
|
76
|
+
'auth keychain delete': DESTRUCTIVE_LOCAL,
|
|
77
|
+
'auth keychain migrate': DESTRUCTIVE_LOCAL,
|
|
78
|
+
'cache show': READ_LOCAL,
|
|
79
|
+
'cache clear': ACTION_LOCAL,
|
|
80
|
+
'capabilities': READ_LOCAL,
|
|
81
|
+
'catalog path': READ_LOCAL,
|
|
82
|
+
'catalog show': READ_LOCAL,
|
|
83
|
+
'catalog search': READ_LOCAL,
|
|
84
|
+
'catalog diff': READ_LOCAL,
|
|
85
|
+
'catalog refresh': ACTION_LOCAL,
|
|
86
|
+
'completion': READ_LOCAL,
|
|
87
|
+
'config set-token': DESTRUCTIVE_LOCAL,
|
|
88
|
+
'config show': READ_LOCAL,
|
|
89
|
+
'config list-profiles': READ_LOCAL,
|
|
90
|
+
'config agent-profile': ACTION_LOCAL,
|
|
91
|
+
'daemon start': ACTION_LOCAL,
|
|
92
|
+
'daemon stop': ACTION_LOCAL,
|
|
93
|
+
'daemon status': READ_LOCAL,
|
|
94
|
+
'daemon reload': ACTION_LOCAL,
|
|
95
|
+
'devices list': READ_REMOTE,
|
|
96
|
+
'devices status': READ_REMOTE,
|
|
97
|
+
'devices command': ACTION_REMOTE_IDEMPOTENT,
|
|
98
|
+
'devices types': READ_LOCAL,
|
|
99
|
+
'devices commands': READ_LOCAL,
|
|
100
|
+
'devices describe': READ_REMOTE,
|
|
101
|
+
'devices batch': ACTION_REMOTE_IDEMPOTENT,
|
|
102
|
+
'devices watch': READ_REMOTE,
|
|
103
|
+
'devices explain': READ_LOCAL,
|
|
104
|
+
'devices expand': READ_LOCAL,
|
|
105
|
+
'devices meta set': ACTION_LOCAL,
|
|
106
|
+
'devices meta get': READ_LOCAL,
|
|
107
|
+
'devices meta list': READ_LOCAL,
|
|
108
|
+
'devices meta clear': ACTION_LOCAL,
|
|
109
|
+
'doctor': READ_LOCAL,
|
|
110
|
+
'events tail': READ_NONE,
|
|
111
|
+
'events mqtt-tail': READ_REMOTE,
|
|
112
|
+
'health check': READ_LOCAL,
|
|
113
|
+
'health serve': READ_LOCAL,
|
|
114
|
+
'history show': READ_LOCAL,
|
|
115
|
+
'history replay': ACTION_REMOTE_IDEMPOTENT,
|
|
116
|
+
'history range': READ_LOCAL,
|
|
117
|
+
'history stats': READ_LOCAL,
|
|
118
|
+
'history verify': READ_LOCAL,
|
|
119
|
+
'history aggregate': READ_LOCAL,
|
|
120
|
+
'install': ACTION_LOCAL,
|
|
121
|
+
'mcp serve': READ_LOCAL,
|
|
122
|
+
'plan schema': READ_LOCAL,
|
|
123
|
+
'plan validate': READ_LOCAL,
|
|
124
|
+
'plan suggest': READ_LOCAL,
|
|
125
|
+
'plan run': ACTION_REMOTE_IDEMPOTENT,
|
|
126
|
+
'plan save': ACTION_LOCAL,
|
|
127
|
+
'plan list': READ_LOCAL,
|
|
128
|
+
'plan review': READ_LOCAL,
|
|
129
|
+
'plan approve': DESTRUCTIVE_LOCAL,
|
|
130
|
+
'plan execute': DESTRUCTIVE_REMOTE,
|
|
131
|
+
'policy validate': READ_LOCAL,
|
|
132
|
+
'policy new': ACTION_LOCAL,
|
|
133
|
+
'policy migrate': ACTION_LOCAL,
|
|
134
|
+
'policy diff': READ_LOCAL,
|
|
135
|
+
'policy add-rule': ACTION_LOCAL,
|
|
136
|
+
'policy backup': READ_LOCAL,
|
|
137
|
+
'policy restore': DESTRUCTIVE_LOCAL,
|
|
138
|
+
'quota status': READ_LOCAL,
|
|
139
|
+
'quota reset': ACTION_LOCAL,
|
|
140
|
+
'rules suggest': READ_LOCAL,
|
|
141
|
+
'rules lint': READ_LOCAL,
|
|
142
|
+
'rules list': READ_LOCAL,
|
|
143
|
+
'rules run': ACTION_REMOTE,
|
|
144
|
+
'rules reload': ACTION_LOCAL,
|
|
145
|
+
'rules tail': READ_LOCAL,
|
|
146
|
+
'rules replay': READ_LOCAL,
|
|
147
|
+
'rules webhook-rotate-token': DESTRUCTIVE_LOCAL,
|
|
148
|
+
'rules webhook-show-token': DESTRUCTIVE_LOCAL,
|
|
149
|
+
'rules conflicts': READ_LOCAL,
|
|
150
|
+
'rules doctor': READ_LOCAL,
|
|
151
|
+
'rules summary': READ_LOCAL,
|
|
152
|
+
'rules last-fired': READ_LOCAL,
|
|
153
|
+
'schema export': READ_LOCAL,
|
|
154
|
+
'scenes list': READ_REMOTE,
|
|
155
|
+
'scenes execute': ACTION_REMOTE,
|
|
156
|
+
'scenes describe': READ_REMOTE,
|
|
157
|
+
'scenes validate': READ_REMOTE,
|
|
158
|
+
'scenes simulate': READ_REMOTE,
|
|
159
|
+
'scenes explain': READ_REMOTE,
|
|
160
|
+
'status-sync run': ACTION_REMOTE,
|
|
161
|
+
'status-sync start': ACTION_LOCAL,
|
|
162
|
+
'status-sync stop': ACTION_LOCAL,
|
|
163
|
+
'status-sync status': READ_LOCAL,
|
|
164
|
+
'uninstall': ACTION_LOCAL,
|
|
165
|
+
'upgrade-check': READ_REMOTE,
|
|
166
|
+
'webhook setup': ACTION_REMOTE,
|
|
167
|
+
'webhook query': READ_REMOTE,
|
|
168
|
+
'webhook update': ACTION_REMOTE,
|
|
169
|
+
'webhook delete': DESTRUCTIVE_REMOTE,
|
|
87
170
|
};
|
|
88
171
|
function metaFor(command) {
|
|
89
172
|
return COMMAND_META[command] ?? null;
|
|
@@ -110,27 +193,30 @@ const IDEMPOTENCY_CONTRACT = {
|
|
|
110
193
|
scope: 'Process-local. Replay + conflict apply within a single long-lived process (MCP session, devices batch, plan run, history replay). Independent CLI invocations do NOT share cache — each fresh `node` process starts empty.',
|
|
111
194
|
mcp: 'MCP send_command accepts the same idempotencyKey field with identical semantics.',
|
|
112
195
|
};
|
|
196
|
+
function enumerateLeafNames(program, prefix = '') {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const cmd of program.commands) {
|
|
199
|
+
const full = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
|
|
200
|
+
if (cmd.commands.length === 0)
|
|
201
|
+
out.push(full);
|
|
202
|
+
else
|
|
203
|
+
out.push(...enumerateLeafNames(cmd, full));
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function validateCommandMetaCoverage(program) {
|
|
208
|
+
const leaves = enumerateLeafNames(program);
|
|
209
|
+
return leaves.filter((leaf) => !COMMAND_META[leaf]).sort().map((leaf) => `missing:${leaf}`);
|
|
210
|
+
}
|
|
113
211
|
function enumerateLeaves(program, prefix = '') {
|
|
114
212
|
const out = [];
|
|
115
213
|
for (const cmd of program.commands) {
|
|
116
214
|
const full = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
|
|
117
215
|
if (cmd.commands.length === 0) {
|
|
118
216
|
const meta = metaFor(full);
|
|
119
|
-
if (meta)
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
// Unknown leaf → default to read-safe with a warning flag so agents notice.
|
|
124
|
-
out.push({
|
|
125
|
-
name: full,
|
|
126
|
-
mutating: false,
|
|
127
|
-
consumesQuota: false,
|
|
128
|
-
idempotencySupported: false,
|
|
129
|
-
agentSafetyTier: 'read',
|
|
130
|
-
verifiability: 'local',
|
|
131
|
-
typicalLatencyMs: 50,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
217
|
+
if (!meta)
|
|
218
|
+
throw new Error(`capabilities metadata missing for leaf command "${full}"`);
|
|
219
|
+
out.push({ name: full, ...meta, ...deriveRiskMeta(meta) });
|
|
134
220
|
}
|
|
135
221
|
else {
|
|
136
222
|
out.push(...enumerateLeaves(cmd, full));
|
|
@@ -157,6 +243,10 @@ export function registerCapabilitiesCommand(program) {
|
|
|
157
243
|
.option('--surface <s>', 'Restrict surfaces block to one of: cli, mcp, plan, mqtt, all (default: all)', enumArg('--surface', SURFACES))
|
|
158
244
|
.option('--project <csv>', 'Project top-level fields (e.g. --project identity,commands,agentGuide)', stringArg('--project'))
|
|
159
245
|
.action((opts) => {
|
|
246
|
+
const coverageIssues = validateCommandMetaCoverage(program);
|
|
247
|
+
if (coverageIssues.length > 0) {
|
|
248
|
+
throw new Error(`capabilities metadata coverage error: ${coverageIssues.join(', ')}`);
|
|
249
|
+
}
|
|
160
250
|
const compact = Boolean(opts.minimal || opts.compact);
|
|
161
251
|
const catalog = getEffectiveCatalog();
|
|
162
252
|
const leaves = enumerateLeaves(program);
|
|
@@ -246,8 +336,8 @@ export function registerCapabilitiesCommand(program) {
|
|
|
246
336
|
// Flat command → meta map keyed by full command path. Published in
|
|
247
337
|
// addition to the tree (where every leaf `subcommands[*]` already
|
|
248
338
|
// carries the same fields via spread) so agents can do O(1) lookup
|
|
249
|
-
// without walking the tree.
|
|
250
|
-
commandMeta: COMMAND_META,
|
|
339
|
+
// without walking the tree. Includes derived risk metadata fields.
|
|
340
|
+
commandMeta: Object.fromEntries(Object.entries(COMMAND_META).map(([k, v]) => [k, { ...v, ...deriveRiskMeta(v) }])),
|
|
251
341
|
...(globalFlags ? { globalFlags } : {}),
|
|
252
342
|
catalog: {
|
|
253
343
|
typeCount: catalog.length,
|
package/dist/commands/config.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import readline from 'node:readline';
|
|
3
5
|
import { execFileSync } from 'node:child_process';
|
|
4
6
|
import { stringArg } from '../utils/arg-parsers.js';
|
|
@@ -89,6 +91,36 @@ async function promptSecret(question) {
|
|
|
89
91
|
void mutableStdout;
|
|
90
92
|
});
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Interactive echo-off prompt for token + secret. Used by both
|
|
96
|
+
* `switchbot config set-token` and the install orchestrator. Throws if
|
|
97
|
+
* stdin is not a TTY.
|
|
98
|
+
*/
|
|
99
|
+
export async function promptTokenAndSecret() {
|
|
100
|
+
if (!process.stdin.isTTY) {
|
|
101
|
+
throw new Error('interactive prompt requires a TTY');
|
|
102
|
+
}
|
|
103
|
+
const token = (await promptSecret('Token: ')).trim();
|
|
104
|
+
const secret = (await promptSecret('Secret: ')).trim();
|
|
105
|
+
if (!token || !secret) {
|
|
106
|
+
throw new Error('token and secret are both required');
|
|
107
|
+
}
|
|
108
|
+
return { token, secret };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Read a two-line credential file (line 1 = token, line 2 = secret)
|
|
112
|
+
* and unlink it on success. The installer's `--token-file` escape
|
|
113
|
+
* hatch uses this; keeps credentials off the command line and shell
|
|
114
|
+
* history for CI-style installs.
|
|
115
|
+
*/
|
|
116
|
+
export function readCredentialsFile(filePath) {
|
|
117
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
118
|
+
const lines = raw.split(/\r?\n/).filter((l) => l.length > 0);
|
|
119
|
+
if (lines.length < 2) {
|
|
120
|
+
throw new Error(`credential file ${filePath} must contain two lines: token, then secret`);
|
|
121
|
+
}
|
|
122
|
+
return { token: lines[0].trim(), secret: lines[1].trim() };
|
|
123
|
+
}
|
|
92
124
|
export function registerConfigCommand(program) {
|
|
93
125
|
const config = program
|
|
94
126
|
.command('config')
|
|
@@ -221,6 +253,24 @@ Files are written with mode 0600. Profiles live under ~/.switchbot/profiles/<nam
|
|
|
221
253
|
}
|
|
222
254
|
else {
|
|
223
255
|
console.log(chalk.green('✓ Credentials saved'));
|
|
256
|
+
// Keychain-first hint: proactively suggest storing in OS keychain when
|
|
257
|
+
// the user is on a supported platform and saved to file (default path).
|
|
258
|
+
try {
|
|
259
|
+
const { selectCredentialStore } = await import('../credentials/keychain.js');
|
|
260
|
+
const store = await selectCredentialStore();
|
|
261
|
+
if (store.describe().backend === 'file') {
|
|
262
|
+
const platform = process.platform;
|
|
263
|
+
if (platform === 'darwin' || platform === 'win32') {
|
|
264
|
+
console.error(chalk.grey('Tip: Your OS supports a native keychain. Run `switchbot auth keychain store` to move credentials off the plain config file for better security.'));
|
|
265
|
+
}
|
|
266
|
+
else if (platform === 'linux') {
|
|
267
|
+
console.error(chalk.grey('Tip: If you have GNOME Keyring (secret-tool) installed, run `switchbot auth keychain store` to store credentials more securely.'));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Keychain probe failed — silently skip the tip
|
|
273
|
+
}
|
|
224
274
|
}
|
|
225
275
|
});
|
|
226
276
|
config
|
|
@@ -264,4 +314,63 @@ Files are written with mode 0600. Profiles live under ~/.switchbot/profiles/<nam
|
|
|
264
314
|
console.log(bits.join(' '));
|
|
265
315
|
}
|
|
266
316
|
});
|
|
317
|
+
// switchbot config agent-profile [--write]
|
|
318
|
+
config
|
|
319
|
+
.command('agent-profile')
|
|
320
|
+
.description('Emit (or write) an agent-safe profile template with conservative rate limits and audit logging.')
|
|
321
|
+
.option('--write', 'Write the template to ~/.switchbot/profiles/agent.json (requires --profile agent config set-token to add credentials).')
|
|
322
|
+
.option('--force', 'Overwrite an existing agent.json when used with --write.')
|
|
323
|
+
.addHelpText('after', `
|
|
324
|
+
Outputs a starter profile.json suitable for AI agent / MCP integration:
|
|
325
|
+
- dailyCap: 100 (conservative; prevents runaway automation)
|
|
326
|
+
- label: "agent"
|
|
327
|
+
- description: "AI agent profile — conservative limits + audit enabled"
|
|
328
|
+
- defaults: { auditLog: true } (enables audit logging by default)
|
|
329
|
+
|
|
330
|
+
After writing, add credentials:
|
|
331
|
+
$ switchbot --profile agent config set-token <token> <secret>
|
|
332
|
+
|
|
333
|
+
Then use the profile:
|
|
334
|
+
$ switchbot --profile agent devices list
|
|
335
|
+
`)
|
|
336
|
+
.action((opts) => {
|
|
337
|
+
const template = {
|
|
338
|
+
label: 'agent',
|
|
339
|
+
description: 'AI agent profile — conservative limits + audit enabled',
|
|
340
|
+
limits: {
|
|
341
|
+
dailyCap: 100,
|
|
342
|
+
},
|
|
343
|
+
defaults: {
|
|
344
|
+
auditLog: true,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
if (opts.write) {
|
|
348
|
+
const dir = path.join(os.homedir(), '.switchbot', 'profiles');
|
|
349
|
+
const dest = path.join(dir, 'agent.json');
|
|
350
|
+
if (!opts.force && fs.existsSync(dest)) {
|
|
351
|
+
exitWithError({ code: 2, kind: 'usage', message: `Agent profile already exists: ${dest}. Use --force to overwrite.` });
|
|
352
|
+
}
|
|
353
|
+
if (!fs.existsSync(dir)) {
|
|
354
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
355
|
+
}
|
|
356
|
+
fs.writeFileSync(dest, JSON.stringify(template, null, 2), { mode: 0o600 });
|
|
357
|
+
if (isJsonMode()) {
|
|
358
|
+
printJson({ ok: true, path: dest, template });
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log(`Agent profile written: ${dest}`);
|
|
362
|
+
console.log(`Next: switchbot --profile agent config set-token <token> <secret>`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
if (isJsonMode()) {
|
|
367
|
+
printJson(template);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
console.log(JSON.stringify(template, null, 2));
|
|
371
|
+
console.log('');
|
|
372
|
+
console.log('Write with: switchbot config agent-profile --write');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
267
376
|
}
|