@switchbot/openapi-cli 2.2.1 → 2.4.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.
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { intArg, stringArg } from '../utils/arg-parsers.js';
4
- import { printJson, isJsonMode, handleError } from '../utils/output.js';
5
- import { readAudit } from '../utils/audit.js';
4
+ import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
5
+ import { readAudit, verifyAudit } from '../utils/audit.js';
6
6
  import { executeCommand } from '../lib/devices.js';
7
+ import { queryDeviceHistory, queryDeviceHistoryStats, } from '../devices/history-query.js';
7
8
  const DEFAULT_AUDIT = path.join(os.homedir(), '.switchbot', 'audit.log');
8
9
  export function registerHistoryCommand(program) {
9
10
  const history = program
@@ -101,4 +102,125 @@ Examples:
101
102
  handleError(err);
102
103
  }
103
104
  });
105
+ history
106
+ .command('range')
107
+ .description('Query time-ranged device history from JSONL storage (populated by events mqtt-tail / MCP)')
108
+ .argument('<deviceId>', 'Device ID to query')
109
+ .option('--since <duration>', 'Relative window ending now, e.g. "30s", "15m", "1h", "7d" (mutually exclusive with --from/--to)', stringArg('--since'))
110
+ .option('--from <iso>', 'Range start (ISO-8601)', stringArg('--from'))
111
+ .option('--to <iso>', 'Range end (ISO-8601)', stringArg('--to'))
112
+ .option('--field <name>', 'Project a payload field (repeat to keep multiple)', (v, acc = []) => acc.concat(v), [])
113
+ .option('--limit <n>', 'Maximum records to return (default 1000)', intArg('--limit', { min: 1 }))
114
+ .addHelpText('after', `
115
+ History is the append-only JSONL mirror of the per-device ring buffer: every
116
+ 'events mqtt-tail' event and every MCP tool status-refresh is written to
117
+ ~/.switchbot/device-history/<deviceId>.jsonl (rotates at 50MB × 3 files).
118
+
119
+ Examples:
120
+ $ switchbot history range <id> --since 7d --json
121
+ $ switchbot history range <id> --since 1h --field temperature --field humidity
122
+ $ switchbot history range <id> --from 2026-04-18T00:00:00Z --to 2026-04-19T00:00:00Z
123
+ `)
124
+ .action(async (deviceId, options) => {
125
+ // Usage-level validation: keep synchronous and pre-query so handleError
126
+ // maps these to exit 2 (via UsageError) rather than runtime exit 1.
127
+ if (options.since && (options.from || options.to)) {
128
+ handleError(new UsageError('--since is mutually exclusive with --from/--to.'));
129
+ }
130
+ try {
131
+ const records = await queryDeviceHistory(deviceId, {
132
+ since: options.since,
133
+ from: options.from,
134
+ to: options.to,
135
+ fields: options.field ?? [],
136
+ limit: options.limit !== undefined ? Number(options.limit) : undefined,
137
+ });
138
+ if (isJsonMode()) {
139
+ printJson({ deviceId, count: records.length, records });
140
+ return;
141
+ }
142
+ if (records.length === 0) {
143
+ console.log(`(no history records for ${deviceId} in requested range)`);
144
+ return;
145
+ }
146
+ for (const r of records) {
147
+ const payloadStr = JSON.stringify(r.payload);
148
+ console.log(`${r.t} ${r.topic} ${payloadStr}`);
149
+ }
150
+ }
151
+ catch (err) {
152
+ // Convert history-query's plain Error range messages into UsageError so
153
+ // they exit 2 instead of 1.
154
+ if (err instanceof Error && /^(Invalid --|--from|--since)/i.test(err.message)) {
155
+ handleError(new UsageError(err.message));
156
+ }
157
+ handleError(err);
158
+ }
159
+ });
160
+ history
161
+ .command('stats')
162
+ .description('Show on-disk size + record counts for a device history')
163
+ .argument('<deviceId>', 'Device ID to inspect')
164
+ .action((deviceId) => {
165
+ try {
166
+ const stats = queryDeviceHistoryStats(deviceId);
167
+ if (isJsonMode()) {
168
+ printJson(stats);
169
+ return;
170
+ }
171
+ console.log(`Device: ${stats.deviceId}`);
172
+ console.log(`History dir: ${stats.historyDir}`);
173
+ console.log(`JSONL files: ${stats.fileCount} (${stats.jsonlFiles.join(', ') || '—'})`);
174
+ console.log(`Total size: ${stats.totalBytes.toLocaleString()} bytes`);
175
+ console.log(`Record count: ${stats.recordCount}`);
176
+ console.log(`Oldest: ${stats.oldest ?? '—'}`);
177
+ console.log(`Newest: ${stats.newest ?? '—'}`);
178
+ }
179
+ catch (err) {
180
+ handleError(err);
181
+ }
182
+ });
183
+ history
184
+ .command('verify')
185
+ .description('Check the audit log for malformed lines and schema-version drift')
186
+ .option('--file <path>', `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg('--file'))
187
+ .addHelpText('after', `
188
+ See docs/audit-log.md for the audit log format. Exit code:
189
+ 0 every line parses and carries the current auditVersion
190
+ 1 one or more lines are malformed OR the file is missing
191
+ 2 (usage) — not emitted by this subcommand
192
+
193
+ Examples:
194
+ $ switchbot history verify
195
+ $ switchbot history verify --file ./custom.log --json
196
+ `)
197
+ .action((options) => {
198
+ const file = options.file ?? DEFAULT_AUDIT;
199
+ const report = verifyAudit(file);
200
+ if (isJsonMode()) {
201
+ printJson(report);
202
+ }
203
+ else {
204
+ console.log(`Audit log: ${report.file}`);
205
+ console.log(`Parsed lines: ${report.parsedLines} / ${report.totalLines}`);
206
+ console.log(`Malformed: ${report.malformedLines}`);
207
+ console.log(`Unversioned: ${report.unversionedEntries}`);
208
+ const versions = Object.entries(report.versionCounts)
209
+ .map(([v, n]) => `${v}:${n}`)
210
+ .join(', ');
211
+ console.log(`Version counts: ${versions || '—'}`);
212
+ if (report.earliest)
213
+ console.log(`Earliest: ${report.earliest}`);
214
+ if (report.latest)
215
+ console.log(`Latest: ${report.latest}`);
216
+ if (report.problems.length > 0) {
217
+ console.log('\nProblems:');
218
+ for (const p of report.problems) {
219
+ console.log(` line ${p.line}: ${p.reason}${p.preview ? ` — "${p.preview}"` : ''}`);
220
+ }
221
+ }
222
+ }
223
+ const ok = report.malformedLines === 0 && report.problems.length === 0;
224
+ process.exit(ok ? 0 : 1);
225
+ });
104
226
  }
@@ -10,6 +10,7 @@ import { findCatalogEntry } from '../devices/catalog.js';
10
10
  import { getCachedDevice } from '../devices/cache.js';
11
11
  import { EventSubscriptionManager } from '../mcp/events-subscription.js';
12
12
  import { deviceHistoryStore } from '../mcp/device-history.js';
13
+ import { queryDeviceHistory } from '../devices/history-query.js';
13
14
  import { todayUsage } from '../utils/quota.js';
14
15
  import { describeCache } from '../devices/cache.js';
15
16
  import { withRequestContext } from '../lib/request-context.js';
@@ -146,6 +147,47 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
146
147
  structuredContent: result,
147
148
  };
148
149
  });
150
+ // ---- query_device_history --------------------------------------------------
151
+ server.registerTool('query_device_history', {
152
+ title: 'Query time-ranged device history',
153
+ description: 'Return records from the append-only JSONL history (~/.switchbot/device-history/<deviceId>.jsonl) ' +
154
+ 'filtered by a relative duration (since) or absolute ISO-8601 range (from/to). ' +
155
+ 'No API call — zero quota cost. Use for trend questions like "how many times did this switch turn on last week".',
156
+ inputSchema: {
157
+ deviceId: z.string().describe('Device ID to query'),
158
+ since: z.string().optional().describe('Relative window ending now, e.g. "30s", "15m", "1h", "7d". Mutually exclusive with from/to.'),
159
+ from: z.string().optional().describe('Range start (ISO-8601).'),
160
+ to: z.string().optional().describe('Range end (ISO-8601).'),
161
+ fields: z.array(z.string()).optional().describe('Project these payload fields; omit for the full payload.'),
162
+ limit: z.number().int().min(1).max(10000).optional().describe('Max records to return (default 1000).'),
163
+ },
164
+ outputSchema: {
165
+ deviceId: z.string(),
166
+ count: z.number().int(),
167
+ records: z.array(z.object({
168
+ t: z.string(),
169
+ topic: z.string(),
170
+ deviceType: z.string().optional(),
171
+ payload: z.unknown(),
172
+ })),
173
+ },
174
+ }, async ({ deviceId, since, from, to, fields, limit }) => {
175
+ if (since && (from || to)) {
176
+ return mcpError('usage', 2, '--since is mutually exclusive with --from/--to.');
177
+ }
178
+ try {
179
+ const records = await queryDeviceHistory(deviceId, { since, from, to, fields, limit });
180
+ const result = { deviceId, count: records.length, records };
181
+ return {
182
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
183
+ structuredContent: result,
184
+ };
185
+ }
186
+ catch (err) {
187
+ const msg = err instanceof Error ? err.message : 'history query failed';
188
+ return mcpError('usage', 2, msg);
189
+ }
190
+ });
149
191
  // ---- send_command ---------------------------------------------------------
150
192
  server.registerTool('send_command', {
151
193
  title: 'Send a control command to a device',
@@ -167,14 +209,26 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
167
209
  .optional()
168
210
  .default(false)
169
211
  .describe('Required true for destructive commands (unlock, garage open, createKey, ...)'),
212
+ idempotencyKey: z
213
+ .string()
214
+ .optional()
215
+ .describe('Deduplication key — repeat calls with the same key within 60s replay the first result (adds replayed:true). Same key + different (command, parameter) within 60s returns an idempotency_conflict guard error.'),
170
216
  },
171
217
  outputSchema: {
172
218
  ok: z.literal(true),
173
219
  command: z.string(),
174
220
  deviceId: z.string(),
175
221
  result: z.unknown().describe('API response body from SwitchBot'),
222
+ verification: z
223
+ .object({
224
+ verifiable: z.boolean(),
225
+ reason: z.string(),
226
+ suggestedFollowup: z.string(),
227
+ })
228
+ .optional()
229
+ .describe('Present when the target is an IR device. IR is unidirectional — agents should treat the success as "signal sent" not "state changed".'),
176
230
  },
177
- }, async ({ deviceId, command, parameter, commandType, confirm }) => {
231
+ }, async ({ deviceId, command, parameter, commandType, confirm, idempotencyKey }) => {
178
232
  const effectiveType = commandType ?? 'command';
179
233
  // Resolve the device's catalog type via cache or a fresh lookup so we
180
234
  // can evaluate destructive/validation without an extra round-trip if
@@ -217,8 +271,33 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
217
271
  if (!validation.ok) {
218
272
  return mcpError('usage', 2, validation.error.message, { hint: validation.error.hint, context: { validationKind: validation.error.kind } });
219
273
  }
220
- const result = await executeCommand(deviceId, command, parameter, effectiveType);
274
+ let result;
275
+ try {
276
+ result = await executeCommand(deviceId, command, parameter, effectiveType, undefined, {
277
+ idempotencyKey,
278
+ });
279
+ }
280
+ catch (err) {
281
+ if (err instanceof Error && err.name === 'IdempotencyConflictError') {
282
+ return mcpError('guard', 2, err.message, {
283
+ hint: 'Use a fresh idempotencyKey, or wait for the prior key to expire (60s TTL).',
284
+ context: {
285
+ existingShape: err.existingShape,
286
+ newShape: err.newShape,
287
+ },
288
+ });
289
+ }
290
+ throw err;
291
+ }
292
+ const isIr = getCachedDevice(deviceId)?.category === 'ir';
221
293
  const structured = { ok: true, command, deviceId, result };
294
+ if (isIr) {
295
+ structured.verification = {
296
+ verifiable: false,
297
+ reason: 'IR transmission is unidirectional; no receipt acknowledgment is possible.',
298
+ suggestedFollowup: 'Confirm visible change manually or via a paired state sensor.',
299
+ };
300
+ }
222
301
  return {
223
302
  content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
224
303
  structuredContent: structured,
@@ -11,17 +11,19 @@ is a best-effort mirror of the SwitchBot 10,000/day limit — it does not
11
11
  include requests made outside this CLI (mobile app, other scripts).
12
12
 
13
13
  Subcommands:
14
- status Show today's usage and the last 7 days
14
+ status Show today's usage and the last 7 days (alias: show)
15
15
  reset Delete the local counter file
16
16
 
17
17
  Examples:
18
18
  $ switchbot quota status
19
+ $ switchbot quota show # alias of 'status'
19
20
  $ switchbot quota status --json
20
21
  $ switchbot quota reset
21
22
  `);
22
23
  quota
23
24
  .command('status')
24
- .description("Show today's usage and the last 7 days")
25
+ .alias('show')
26
+ .description("Show today's usage and the last 7 days (alias: show)")
25
27
  .action(() => {
26
28
  const usage = todayUsage();
27
29
  const history = loadQuota();
@@ -1,6 +1,7 @@
1
1
  import { enumArg, stringArg } from '../utils/arg-parsers.js';
2
2
  import { printJson } from '../utils/output.js';
3
3
  import { getEffectiveCatalog } from '../devices/catalog.js';
4
+ import { loadCache } from '../devices/cache.js';
4
5
  function toSchemaEntry(e) {
5
6
  return {
6
7
  type: e.type,
@@ -24,6 +25,30 @@ function toSchemaCommand(c) {
24
25
  ...(c.exampleParams ? { exampleParams: c.exampleParams } : {}),
25
26
  };
26
27
  }
28
+ function toCompactEntry(e) {
29
+ return {
30
+ type: e.type,
31
+ category: e.category,
32
+ role: e.role ?? null,
33
+ readOnly: e.readOnly ?? false,
34
+ commands: e.commands.map((c) => ({
35
+ command: c.command,
36
+ parameter: c.parameter,
37
+ commandType: (c.commandType ?? 'command'),
38
+ idempotent: Boolean(c.idempotent),
39
+ destructive: Boolean(c.destructive),
40
+ })),
41
+ statusFields: e.statusFields ?? [],
42
+ };
43
+ }
44
+ function projectFields(entry, fields) {
45
+ const out = {};
46
+ for (const f of fields) {
47
+ if (f in entry)
48
+ out[f] = entry[f];
49
+ }
50
+ return out;
51
+ }
27
52
  export function registerSchemaCommand(program) {
28
53
  const ROLES = ['lighting', 'security', 'sensor', 'climate', 'media', 'cleaning', 'curtain', 'fan', 'power', 'hub', 'other'];
29
54
  const CATEGORIES = ['physical', 'ir'];
@@ -32,20 +57,41 @@ export function registerSchemaCommand(program) {
32
57
  .description('Export the device catalog as structured JSON (for agent prompts / tooling)');
33
58
  schema
34
59
  .command('export')
35
- .description('Print the full catalog as structured JSON (one object per type)')
60
+ .description('Print the catalog as structured JSON (one object per type)')
36
61
  .option('--type <type>', 'Restrict to a single device type (e.g. "Strip Light")', stringArg('--type'))
62
+ .option('--types <csv>', 'Restrict to multiple device types (comma-separated)', stringArg('--types'))
37
63
  .option('--role <role>', 'Restrict to a functional role: lighting, security, sensor, climate, media, cleaning, curtain, fan, power, hub, other', enumArg('--role', ROLES))
38
64
  .option('--category <cat>', 'Restrict to "physical" or "ir"', enumArg('--category', CATEGORIES))
65
+ .option('--compact', 'Drop descriptions/aliases/example params — emit ~60% smaller payload. Useful for agent prompts.')
66
+ .option('--used', 'Restrict to device types present in the local devices cache (run "devices list" first)')
67
+ .option('--project <csv>', 'Project per-type fields (e.g. --project type,commands,statusFields)', stringArg('--project'))
39
68
  .addHelpText('after', `
40
69
  Output is always JSON (this command ignores --format). The output is a
41
70
  catalog export — not a formal JSON Schema standard document — suitable for
42
71
  pre-baking LLM prompts or regenerating docs when the catalog changes.
43
72
 
73
+ Size tips:
74
+ --compact --used Smallest realistic payload for a given account
75
+ (< 15 KB on most accounts).
76
+ --fields type,commands Strip statusFields / role / etc. when only
77
+ commands are needed.
78
+ --type + --compact Inspect one type with minimum footprint.
79
+
80
+ Common top-level fields:
81
+ schemaVersion CLI schema version (stable for agent contracts)
82
+ data.version Catalog schema version
83
+ data.types Array of SchemaEntry (or CompactSchemaEntry with --compact)
84
+ data._fetchedAt CLI-added; present on live-query responses ('devices status'),
85
+ not on this offline export.
86
+
44
87
  Examples:
45
88
  $ switchbot schema export > catalog.json
46
- $ switchbot schema export --type Bot | jq '.types[0].commands'
47
- $ switchbot schema export --role lighting | jq '[.types[].type]'
89
+ $ switchbot schema export --compact --used | wc -c # small prompt-ready payload
90
+ $ switchbot schema export --type Bot | jq '.data.types[0].commands'
91
+ $ switchbot schema export --types "Bot,Curtain,Color Bulb"
92
+ $ switchbot schema export --role lighting | jq '[.data.types[].type]'
48
93
  $ switchbot schema export --role security --category physical
94
+ $ switchbot schema export --project type,commands,statusFields
49
95
  `)
50
96
  .action((options) => {
51
97
  const catalog = getEffectiveCatalog();
@@ -55,6 +101,11 @@ Examples:
55
101
  filtered = filtered.filter((e) => e.type.toLowerCase() === q ||
56
102
  (e.aliases ?? []).some((a) => a.toLowerCase() === q));
57
103
  }
104
+ if (options.types) {
105
+ const set = new Set(options.types.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean));
106
+ filtered = filtered.filter((e) => set.has(e.type.toLowerCase()) ||
107
+ (e.aliases ?? []).some((a) => set.has(a.toLowerCase())));
108
+ }
58
109
  if (options.role) {
59
110
  const q = options.role.toLowerCase();
60
111
  filtered = filtered.filter((e) => (e.role ?? 'other') === q);
@@ -63,11 +114,50 @@ Examples:
63
114
  const q = options.category.toLowerCase();
64
115
  filtered = filtered.filter((e) => e.category === q);
65
116
  }
117
+ if (options.used) {
118
+ const cache = loadCache();
119
+ if (cache) {
120
+ const usedTypes = new Set(Object.values(cache.devices).map((d) => d.type.toLowerCase()));
121
+ filtered = filtered.filter((e) => usedTypes.has(e.type.toLowerCase()) ||
122
+ (e.aliases ?? []).some((a) => usedTypes.has(a.toLowerCase())));
123
+ }
124
+ else {
125
+ filtered = [];
126
+ }
127
+ }
128
+ const mapped = options.compact
129
+ ? filtered.map(toCompactEntry)
130
+ : filtered.map(toSchemaEntry);
131
+ const projected = options.project
132
+ ? mapped.map((e) => projectFields(e, options.project.split(',').map((s) => s.trim()).filter(Boolean)))
133
+ : mapped;
66
134
  const payload = {
67
135
  version: '1.0',
68
- generatedAt: new Date().toISOString(),
69
- types: filtered.map(toSchemaEntry),
136
+ types: projected,
70
137
  };
138
+ if (!options.compact) {
139
+ payload.generatedAt = new Date().toISOString();
140
+ payload.cliAddedFields = [
141
+ {
142
+ field: '_fetchedAt',
143
+ appliesTo: ['devices status', 'devices describe'],
144
+ type: 'string (ISO-8601)',
145
+ description: 'CLI-synthesized timestamp indicating when this status response was fetched or served from the cache. Not part of the upstream SwitchBot API.',
146
+ },
147
+ {
148
+ field: 'replayed',
149
+ appliesTo: ['devices command (with --idempotency-key)'],
150
+ type: 'boolean',
151
+ description: 'CLI-synthesized flag — true when the response was served from the idempotency cache instead of re-executing the command.',
152
+ },
153
+ {
154
+ field: 'verification',
155
+ appliesTo: ['devices command'],
156
+ type: 'object',
157
+ description: 'CLI-synthesized receipt-acknowledgment metadata. For IR devices, verifiable:false signals that no device-side confirmation is possible.',
158
+ },
159
+ ];
160
+ }
71
161
  printJson(payload);
72
162
  });
73
163
  }
package/dist/config.js CHANGED
@@ -3,6 +3,12 @@ import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import { getConfigPath } from './utils/flags.js';
5
5
  import { getActiveProfile } from './lib/request-context.js';
6
+ function sanitizeOptionalString(v) {
7
+ if (typeof v !== 'string')
8
+ return undefined;
9
+ const trimmed = v.trim();
10
+ return trimmed ? trimmed : undefined;
11
+ }
6
12
  /**
7
13
  * Credential file resolution priority:
8
14
  * 1. --config <path> (absolute override — wins over everything)
@@ -85,15 +91,70 @@ export function tryLoadConfig() {
85
91
  return null;
86
92
  }
87
93
  }
88
- export function saveConfig(token, secret) {
94
+ export function saveConfig(token, secret, extras) {
89
95
  const file = configFilePath();
90
96
  const dir = path.dirname(file);
91
97
  if (!fs.existsSync(dir)) {
92
98
  fs.mkdirSync(dir, { recursive: true });
93
99
  }
94
- const cfg = { token, secret };
100
+ // Merge with existing file so label/limits/defaults aren't dropped when the
101
+ // user just rotates the token.
102
+ let existing = {};
103
+ if (fs.existsSync(file)) {
104
+ try {
105
+ existing = JSON.parse(fs.readFileSync(file, 'utf-8'));
106
+ }
107
+ catch {
108
+ existing = {};
109
+ }
110
+ }
111
+ const cfg = {
112
+ token,
113
+ secret,
114
+ ...(existing.label ? { label: existing.label } : {}),
115
+ ...(existing.description ? { description: existing.description } : {}),
116
+ ...(existing.limits ? { limits: existing.limits } : {}),
117
+ ...(existing.defaults ? { defaults: existing.defaults } : {}),
118
+ };
119
+ if (extras) {
120
+ const label = sanitizeOptionalString(extras.label);
121
+ const description = sanitizeOptionalString(extras.description);
122
+ if (label !== undefined)
123
+ cfg.label = label;
124
+ if (description !== undefined)
125
+ cfg.description = description;
126
+ if (extras.limits)
127
+ cfg.limits = { ...(cfg.limits ?? {}), ...extras.limits };
128
+ if (extras.defaults)
129
+ cfg.defaults = { ...(cfg.defaults ?? {}), ...extras.defaults };
130
+ }
95
131
  fs.writeFileSync(file, JSON.stringify(cfg, null, 2), { mode: 0o600 });
96
132
  }
133
+ /**
134
+ * Read a profile's metadata (label / description / limits / defaults) without
135
+ * exposing the token/secret. Returns null when the file is missing or invalid.
136
+ */
137
+ export function readProfileMeta(profile) {
138
+ const file = profile
139
+ ? profileFilePath(profile)
140
+ : path.join(os.homedir(), '.switchbot', 'config.json');
141
+ if (!fs.existsSync(file))
142
+ return null;
143
+ try {
144
+ const raw = fs.readFileSync(file, 'utf-8');
145
+ const cfg = JSON.parse(raw);
146
+ return {
147
+ label: cfg.label,
148
+ description: cfg.description,
149
+ limits: cfg.limits,
150
+ defaults: cfg.defaults,
151
+ path: file,
152
+ };
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
97
158
  export function showConfig() {
98
159
  const envToken = process.env.SWITCHBOT_TOKEN;
99
160
  const envSecret = process.env.SWITCHBOT_SECRET;
@@ -112,8 +173,16 @@ export function showConfig() {
112
173
  const raw = fs.readFileSync(file, 'utf-8');
113
174
  const cfg = JSON.parse(raw);
114
175
  console.log(`Credential source: ${file}`);
176
+ if (cfg.label)
177
+ console.log(`label : ${cfg.label}`);
178
+ if (cfg.description)
179
+ console.log(`desc : ${cfg.description}`);
115
180
  console.log(`token : ${maskCredential(cfg.token)}`);
116
181
  console.log(`secret: ${maskSecret(cfg.secret)}`);
182
+ if (cfg.limits?.dailyCap)
183
+ console.log(`limits: dailyCap=${cfg.limits.dailyCap}`);
184
+ if (cfg.defaults?.flags?.length)
185
+ console.log(`defaults: ${cfg.defaults.flags.join(' ')}`);
117
186
  }
118
187
  catch {
119
188
  console.error('Failed to read config file');