@switchbot/openapi-cli 3.1.0 → 3.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 (113) hide show
  1. package/README.md +34 -42
  2. package/dist/index.js +56945 -169
  3. package/dist/policy/schema/v0.2.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/api/client.js +0 -235
  6. package/dist/auth.js +0 -20
  7. package/dist/commands/agent-bootstrap.js +0 -182
  8. package/dist/commands/auth.js +0 -354
  9. package/dist/commands/batch.js +0 -413
  10. package/dist/commands/cache.js +0 -126
  11. package/dist/commands/capabilities.js +0 -385
  12. package/dist/commands/catalog.js +0 -359
  13. package/dist/commands/completion.js +0 -385
  14. package/dist/commands/config.js +0 -376
  15. package/dist/commands/daemon.js +0 -367
  16. package/dist/commands/device-meta.js +0 -159
  17. package/dist/commands/devices.js +0 -948
  18. package/dist/commands/doctor.js +0 -1015
  19. package/dist/commands/events.js +0 -563
  20. package/dist/commands/expand.js +0 -130
  21. package/dist/commands/explain.js +0 -139
  22. package/dist/commands/health.js +0 -113
  23. package/dist/commands/history.js +0 -320
  24. package/dist/commands/identity.js +0 -59
  25. package/dist/commands/install.js +0 -246
  26. package/dist/commands/mcp.js +0 -2017
  27. package/dist/commands/plan.js +0 -653
  28. package/dist/commands/policy.js +0 -586
  29. package/dist/commands/quota.js +0 -78
  30. package/dist/commands/rules.js +0 -875
  31. package/dist/commands/scenes.js +0 -264
  32. package/dist/commands/schema.js +0 -177
  33. package/dist/commands/status-sync.js +0 -131
  34. package/dist/commands/uninstall.js +0 -237
  35. package/dist/commands/upgrade-check.js +0 -88
  36. package/dist/commands/watch.js +0 -194
  37. package/dist/commands/webhook.js +0 -182
  38. package/dist/config.js +0 -258
  39. package/dist/credentials/backends/file.js +0 -101
  40. package/dist/credentials/backends/linux.js +0 -129
  41. package/dist/credentials/backends/macos.js +0 -129
  42. package/dist/credentials/backends/windows.js +0 -215
  43. package/dist/credentials/keychain.js +0 -88
  44. package/dist/credentials/prime.js +0 -52
  45. package/dist/devices/cache.js +0 -293
  46. package/dist/devices/catalog.js +0 -767
  47. package/dist/devices/device-meta.js +0 -56
  48. package/dist/devices/history-agg.js +0 -138
  49. package/dist/devices/history-query.js +0 -181
  50. package/dist/devices/param-validator.js +0 -433
  51. package/dist/devices/resources.js +0 -270
  52. package/dist/install/default-steps.js +0 -257
  53. package/dist/install/preflight.js +0 -212
  54. package/dist/install/steps.js +0 -67
  55. package/dist/lib/command-keywords.js +0 -17
  56. package/dist/lib/daemon-state.js +0 -46
  57. package/dist/lib/destructive-mode.js +0 -12
  58. package/dist/lib/devices.js +0 -382
  59. package/dist/lib/idempotency.js +0 -106
  60. package/dist/lib/plan-store.js +0 -68
  61. package/dist/lib/request-context.js +0 -12
  62. package/dist/lib/scenes.js +0 -10
  63. package/dist/logger.js +0 -16
  64. package/dist/mcp/device-history.js +0 -145
  65. package/dist/mcp/events-subscription.js +0 -213
  66. package/dist/mqtt/client.js +0 -180
  67. package/dist/mqtt/credential.js +0 -30
  68. package/dist/policy/add-rule.js +0 -124
  69. package/dist/policy/diff.js +0 -91
  70. package/dist/policy/format.js +0 -57
  71. package/dist/policy/load.js +0 -61
  72. package/dist/policy/migrate.js +0 -67
  73. package/dist/policy/schema.js +0 -18
  74. package/dist/policy/validate.js +0 -262
  75. package/dist/rules/action.js +0 -205
  76. package/dist/rules/audit-query.js +0 -89
  77. package/dist/rules/conflict-analyzer.js +0 -203
  78. package/dist/rules/cron-scheduler.js +0 -186
  79. package/dist/rules/destructive.js +0 -52
  80. package/dist/rules/engine.js +0 -757
  81. package/dist/rules/matcher.js +0 -230
  82. package/dist/rules/pid-file.js +0 -95
  83. package/dist/rules/quiet-hours.js +0 -45
  84. package/dist/rules/suggest.js +0 -95
  85. package/dist/rules/throttle.js +0 -116
  86. package/dist/rules/types.js +0 -34
  87. package/dist/rules/webhook-listener.js +0 -223
  88. package/dist/rules/webhook-token.js +0 -90
  89. package/dist/schema/field-aliases.js +0 -131
  90. package/dist/sinks/dispatcher.js +0 -12
  91. package/dist/sinks/file.js +0 -19
  92. package/dist/sinks/format.js +0 -56
  93. package/dist/sinks/homeassistant.js +0 -44
  94. package/dist/sinks/openclaw.js +0 -33
  95. package/dist/sinks/stdout.js +0 -5
  96. package/dist/sinks/telegram.js +0 -28
  97. package/dist/sinks/types.js +0 -1
  98. package/dist/sinks/webhook.js +0 -22
  99. package/dist/status-sync/manager.js +0 -268
  100. package/dist/utils/arg-parsers.js +0 -66
  101. package/dist/utils/audit.js +0 -117
  102. package/dist/utils/filter.js +0 -189
  103. package/dist/utils/flags.js +0 -186
  104. package/dist/utils/format.js +0 -117
  105. package/dist/utils/health.js +0 -101
  106. package/dist/utils/help-json.js +0 -54
  107. package/dist/utils/name-resolver.js +0 -137
  108. package/dist/utils/output.js +0 -404
  109. package/dist/utils/quota.js +0 -227
  110. package/dist/utils/redact.js +0 -68
  111. package/dist/utils/retry.js +0 -140
  112. package/dist/utils/string.js +0 -22
  113. package/dist/version.js +0 -4
@@ -1,385 +0,0 @@
1
- import { getEffectiveCatalog, deriveSafetyTier, deriveStatusQueries, } from '../devices/catalog.js';
2
- import { RESOURCE_CATALOG } from '../devices/resources.js';
3
- import { loadCache } from '../devices/cache.js';
4
- import { printJson } from '../utils/output.js';
5
- import { enumArg, stringArg } from '../utils/arg-parsers.js';
6
- import { IDENTITY } from './identity.js';
7
- /** Collect the distinct catalog safety tiers actually used across the given entries. Sorted. */
8
- function collectSafetyTiersInUse(entries) {
9
- const seen = new Set();
10
- for (const e of entries) {
11
- for (const c of e.commands) {
12
- seen.add(deriveSafetyTier(c, e));
13
- }
14
- // P11: statusQueries contribute the 'read' tier.
15
- if (deriveStatusQueries(e).length > 0) {
16
- seen.add('read');
17
- }
18
- }
19
- return [...seen].sort();
20
- }
21
- /** P11: total number of read-only queries exposed across the catalog. */
22
- function countStatusQueries(entries) {
23
- return entries.reduce((n, e) => n + deriveStatusQueries(e).length, 0);
24
- }
25
- const AGENT_GUIDE = {
26
- safetyTiers: {
27
- read: 'No state mutation; safe to call freely — does not consume quota unless noted.',
28
- action: 'Mutates device or cloud state but is reversible and routine (turnOn, setColor).',
29
- destructive: 'Hard to reverse / physical-world side effects (unlock, garage open, delete key). Requires explicit user confirmation.',
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
- },
41
- verifiability: {
42
- local: 'Result is fully verifiable from the CLI return value itself.',
43
- deviceConfirmed: 'Device returns an ack with an observable state field.',
44
- deviceDependent: 'Verifiability depends on the specific device (IR is never verifiable).',
45
- none: 'No feedback — e.g. IR transmission. Pair with an external sensor to confirm.',
46
- },
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);
71
- const COMMAND_META = {
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,
170
- };
171
- function metaFor(command) {
172
- return COMMAND_META[command] ?? null;
173
- }
174
- const MCP_TOOLS = [
175
- 'list_devices',
176
- 'get_device_status',
177
- 'send_command',
178
- 'describe_device',
179
- 'list_scenes',
180
- 'run_scene',
181
- 'search_catalog',
182
- 'account_overview',
183
- 'get_device_history',
184
- 'query_device_history',
185
- 'aggregate_device_history',
186
- ];
187
- const IDEMPOTENCY_CONTRACT = {
188
- flag: '--idempotency-key <key>',
189
- windowSeconds: 60,
190
- replayBehavior: 'Same (command, parameter, deviceId) within window → returns cached result with replayed:true.',
191
- conflictBehavior: 'Same key + different (command, parameter) within window → exit 2, error:"idempotency_conflict".',
192
- keyStorage: 'In-memory SHA-256 fingerprint; raw key never stored, no disk persistence.',
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.',
194
- mcp: 'MCP send_command accepts the same idempotencyKey field with identical semantics.',
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
- }
211
- function enumerateLeaves(program, prefix = '') {
212
- const out = [];
213
- for (const cmd of program.commands) {
214
- const full = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
215
- if (cmd.commands.length === 0) {
216
- const meta = metaFor(full);
217
- if (!meta)
218
- throw new Error(`capabilities metadata missing for leaf command "${full}"`);
219
- out.push({ name: full, ...meta, ...deriveRiskMeta(meta) });
220
- }
221
- else {
222
- out.push(...enumerateLeaves(cmd, full));
223
- }
224
- }
225
- return out;
226
- }
227
- function projectObject(obj, fields) {
228
- const out = {};
229
- for (const f of fields) {
230
- if (f in obj)
231
- out[f] = obj[f];
232
- }
233
- return out;
234
- }
235
- export function registerCapabilitiesCommand(program) {
236
- const SURFACES = ['cli', 'mcp', 'plan', 'mqtt', 'all'];
237
- program
238
- .command('capabilities')
239
- .description('Print a machine-readable manifest of SwitchBot CLI capabilities (for AI agent bootstrap)')
240
- .option('--minimal', 'Omit per-subcommand flag details to reduce output size (alias of --compact)')
241
- .option('--compact', 'Emit a compact summary: identity + leaf command list with safety metadata only')
242
- .option('--used', 'Restrict the catalog summary to device types present in the local cache. Mirrors `schema export --used`.')
243
- .option('--surface <s>', 'Restrict surfaces block to one of: cli, mcp, plan, mqtt, all (default: all)', enumArg('--surface', SURFACES))
244
- .option('--project <csv>', 'Project top-level fields (e.g. --project identity,commands,agentGuide)', stringArg('--project'))
245
- .action((opts) => {
246
- const coverageIssues = validateCommandMetaCoverage(program);
247
- if (coverageIssues.length > 0) {
248
- throw new Error(`capabilities metadata coverage error: ${coverageIssues.join(', ')}`);
249
- }
250
- const compact = Boolean(opts.minimal || opts.compact);
251
- const catalog = getEffectiveCatalog();
252
- const leaves = enumerateLeaves(program);
253
- const fullCommands = compact
254
- ? undefined
255
- : [
256
- ...program.commands,
257
- { name: () => 'help', description: () => 'Display help for a command', commands: [], options: [], registeredArguments: [] },
258
- ].map((c) => {
259
- const full = c.name();
260
- const entry = {
261
- name: full,
262
- description: c.description(),
263
- };
264
- entry.subcommands = c.commands.map((s) => {
265
- const leafName = `${full} ${s.name()}`;
266
- const meta = metaFor(leafName);
267
- return {
268
- name: s.name(),
269
- description: s.description(),
270
- args: s.registeredArguments.map((a) => ({
271
- name: a.name(),
272
- required: a.required,
273
- variadic: a.variadic,
274
- })),
275
- flags: s.options.map((o) => ({
276
- flags: o.flags,
277
- description: o.description,
278
- })),
279
- ...(meta ?? {}),
280
- };
281
- });
282
- const selfMeta = metaFor(full);
283
- if (selfMeta)
284
- Object.assign(entry, selfMeta);
285
- return entry;
286
- });
287
- const globalFlags = compact
288
- ? undefined
289
- : program.options.map((opt) => ({ flags: opt.flags, description: opt.description }));
290
- const surfaces = {
291
- mcp: {
292
- entry: 'mcp serve',
293
- protocol: 'stdio (default) or --port <n> for HTTP',
294
- tools: MCP_TOOLS,
295
- resources: ['switchbot://events'],
296
- toolMeta: 'Each MCP tool mirrors the CLI leaf command metadata (mutating, consumesQuota, agentSafetyTier, idempotencySupported).',
297
- },
298
- mqtt: {
299
- mode: 'consumer',
300
- authSource: 'SWITCHBOT_TOKEN + SWITCHBOT_SECRET (auto-provisioned via POST /v1.1/iot/credential)',
301
- cliCmd: 'events mqtt-tail',
302
- mcpResource: 'switchbot://events',
303
- protocol: 'MQTTS with TLS client certificates (AWS IoT)',
304
- },
305
- plan: {
306
- schemaCmd: 'plan schema',
307
- validateCmd: 'plan validate -',
308
- runCmd: 'plan run -',
309
- },
310
- cli: {
311
- catalogCmd: 'schema export',
312
- discoveryCmd: 'capabilities',
313
- healthCmd: 'doctor --json',
314
- healthCmdSchemaVersion: 1,
315
- helpFlag: '--help',
316
- idempotencyContract: IDEMPOTENCY_CONTRACT,
317
- },
318
- };
319
- const filteredSurfaces = (() => {
320
- if (!opts.surface || opts.surface === 'all')
321
- return surfaces;
322
- const picked = {};
323
- if (opts.surface in surfaces) {
324
- picked[opts.surface] = surfaces[opts.surface];
325
- }
326
- return picked;
327
- })();
328
- const roles = [...new Set(catalog.map((e) => e.role ?? 'other'))].sort();
329
- const payload = {
330
- version: program.version(),
331
- schemaVersion: '2',
332
- agentGuide: AGENT_GUIDE,
333
- identity: IDENTITY,
334
- surfaces: filteredSurfaces,
335
- commands: compact ? leaves : fullCommands,
336
- // Flat command → meta map keyed by full command path. Published in
337
- // addition to the tree (where every leaf `subcommands[*]` already
338
- // carries the same fields via spread) so agents can do O(1) lookup
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) }])),
341
- ...(globalFlags ? { globalFlags } : {}),
342
- catalog: {
343
- typeCount: catalog.length,
344
- roles,
345
- destructiveCommandCount: catalog.reduce((n, e) => n + e.commands.filter((c) => deriveSafetyTier(c, e) === 'destructive').length, 0),
346
- safetyTiersInUse: collectSafetyTiersInUse(catalog),
347
- readOnlyTypeCount: catalog.filter((e) => e.readOnly).length,
348
- readOnlyQueryCount: countStatusQueries(catalog),
349
- },
350
- resources: RESOURCE_CATALOG,
351
- };
352
- if (!compact)
353
- payload.generatedAt = new Date().toISOString();
354
- if (opts.used) {
355
- const cache = loadCache();
356
- if (!cache || Object.keys(cache.devices).length === 0) {
357
- // No cache → return the payload unchanged but add a `usedFilter` note
358
- // so agents know the filter was requested but noop'd.
359
- payload.usedFilter = { applied: false, reason: 'no local cache — run `switchbot devices list` first' };
360
- }
361
- else {
362
- const seen = new Set();
363
- for (const id of Object.keys(cache.devices)) {
364
- const t = cache.devices[id].type;
365
- if (t)
366
- seen.add(t);
367
- }
368
- const filteredCatalog = catalog.filter((e) => seen.has(e.type) || (e.aliases ?? []).some((a) => seen.has(a)));
369
- payload.catalog = {
370
- typeCount: filteredCatalog.length,
371
- roles: [...new Set(filteredCatalog.map((e) => e.role ?? 'other'))].sort(),
372
- destructiveCommandCount: filteredCatalog.reduce((n, e) => n + e.commands.filter((c) => deriveSafetyTier(c, e) === 'destructive').length, 0),
373
- safetyTiersInUse: collectSafetyTiersInUse(filteredCatalog),
374
- readOnlyTypeCount: filteredCatalog.filter((e) => e.readOnly).length,
375
- readOnlyQueryCount: countStatusQueries(filteredCatalog),
376
- };
377
- payload.usedFilter = { applied: true, typesInCache: [...seen].sort() };
378
- }
379
- }
380
- const projected = opts.project
381
- ? projectObject(payload, opts.project.split(',').map((s) => s.trim()).filter(Boolean))
382
- : payload;
383
- printJson(projected);
384
- });
385
- }