@switchbot/openapi-cli 3.2.2 → 3.3.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +111 -41
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -894,7 +894,7 @@ Queries the npm registry for the latest published version and compares it agains
894
894
 
895
895
  ```json
896
896
  {
897
- "current": "3.2.2",
897
+ "current": "3.3.0",
898
898
  "latest": "4.0.0",
899
899
  "upToDate": false,
900
900
  "updateAvailable": true,
package/dist/index.js CHANGED
@@ -28586,10 +28586,25 @@ Examples:
28586
28586
  if (isJsonMode()) {
28587
28587
  printJson(result);
28588
28588
  } else {
28589
- console.log(
28590
- `
28591
- Summary: ${result.summary.ok} ok, ${result.summary.failed} failed, ${result.summary.skipped} skipped (${result.summary.durationMs}ms)`
28592
- );
28589
+ if (dryRunned.length > 0) {
28590
+ console.log(`
28591
+ Planned (dry-run): ${dryRunned.length} device(s)`);
28592
+ for (const d of dryRunned) console.log(` - ${d.deviceId}`);
28593
+ }
28594
+ if (preSkipped.length > 0) {
28595
+ console.log(`
28596
+ Skipped (offline): ${preSkipped.length} device(s)`);
28597
+ for (const d of preSkipped) console.log(` - ${d.deviceId}`);
28598
+ }
28599
+ const parts = [
28600
+ `${result.summary.ok} ok`,
28601
+ `${result.summary.failed} failed`
28602
+ ];
28603
+ if (dryRunned.length > 0) parts.push(`${dryRunned.length} planned`);
28604
+ if (preSkipped.length > 0) parts.push(`${preSkipped.length} skipped_offline`);
28605
+ parts.push(`(${result.summary.durationMs}ms)`);
28606
+ console.log(`
28607
+ Summary: ${parts.join(", ")}`);
28593
28608
  }
28594
28609
  if (failed.length > 0) process.exit(1);
28595
28610
  }
@@ -28751,7 +28766,7 @@ function sleep2(ms, signal) {
28751
28766
  });
28752
28767
  }
28753
28768
  function registerWatchCommand(devices) {
28754
- devices.command("watch").description("Poll device status on an interval and emit field-level changes (JSONL)").argument("[deviceId...]", "One or more deviceIds to watch (or use --name for one device)").option("--name <query>", "Resolve one device by fuzzy name (combined with any positional IDs)", stringArg("--name")).option(
28769
+ devices.command("watch").description("Poll device status on an interval and emit field-level changes (human table by default; JSONL with --json for agents)").argument("[deviceId...]", "One or more deviceIds to watch (or use --name for one device)").option("--name <query>", "Resolve one device by fuzzy name (combined with any positional IDs)", stringArg("--name")).option(
28755
28770
  "--interval <dur>",
28756
28771
  `Polling interval: "30s", "1m", "500ms", ... (default 30s, min ${MIN_INTERVAL_MS / 1e3}s)`,
28757
28772
  durationArg("--interval"),
@@ -28759,16 +28774,22 @@ function registerWatchCommand(devices) {
28759
28774
  ).option("--max <n>", "Stop after N ticks (default: run until Ctrl-C)", intArg("--max", { min: 1 })).option("--for <dur>", 'Stop after elapsed time (e.g. "5m", "30s"). Combines with --max: first limit wins.', durationArg("--for")).option("--include-unchanged", "Emit a tick even when no field changed").addHelpText(
28760
28775
  "after",
28761
28776
  `
28762
- Each poll emits one JSON line per deviceId with the shape:
28777
+ Default output is a human-readable table of field changes per tick; add --json
28778
+ to get one JSON-Lines record per deviceId per tick (the agent-friendly form).
28779
+
28780
+ The very first poll emits a seed tick with "from": null for every field, so
28781
+ the initial state is observable. Subsequent ticks only include fields whose
28782
+ value changed (unless --include-unchanged is passed).
28783
+
28784
+ Each --json line has the shape:
28763
28785
  { "t": "<ISO>", "tick": <n>, "deviceId": "ID", "type": "Bot",
28764
28786
  "changed": { "power": { "from": "off", "to": "on" } } }
28765
28787
 
28766
- The very first poll has "from": null for every field (seed).
28767
-
28768
28788
  Examples:
28769
28789
  $ switchbot devices watch ABC123 --interval 10s
28770
28790
  $ switchbot devices watch ABC123 --fields battery,power --interval 1m
28771
28791
  $ switchbot devices watch ABC123 DEF456 --interval 30s --max 10
28792
+ # Agent-friendly: one JSONL record per tick, pipeable to jq
28772
28793
  $ switchbot devices watch ABC123 --json | jq 'select(.changed.power)'
28773
28794
  $ switchbot devices watch --name "Living Room AC" --interval 10s
28774
28795
  `
@@ -45370,16 +45391,30 @@ import { createRequire as createRequire3 } from "node:module";
45370
45391
 
45371
45392
  // src/policy/schema.ts
45372
45393
  init_cjs_shim();
45394
+
45395
+ // src/embedded-assets.ts
45396
+ init_cjs_shim();
45373
45397
  import { readFileSync as readFileSync2 } from "node:fs";
45374
45398
  import { fileURLToPath } from "node:url";
45399
+ function readAsset(relPath) {
45400
+ const resolved = fileURLToPath(new URL(relPath, import.meta.url));
45401
+ return readFileSync2(resolved, "utf-8");
45402
+ }
45403
+ function readPolicySchemaJson(version2) {
45404
+ return readAsset(`./policy/schema/v${version2}.json`);
45405
+ }
45406
+ function readPolicyExampleYaml() {
45407
+ return readAsset(`./policy/examples/policy.example.yaml`);
45408
+ }
45409
+
45410
+ // src/policy/schema.ts
45375
45411
  var SUPPORTED_POLICY_SCHEMA_VERSIONS = ["0.2"];
45376
45412
  var CURRENT_POLICY_SCHEMA_VERSION = "0.2";
45377
45413
  var schemaCache = /* @__PURE__ */ new Map();
45378
45414
  function loadPolicySchema(version2 = CURRENT_POLICY_SCHEMA_VERSION) {
45379
45415
  const cached2 = schemaCache.get(version2);
45380
45416
  if (cached2) return cached2;
45381
- const url2 = new URL(`./schema/v${version2}.json`, import.meta.url);
45382
- const raw = readFileSync2(fileURLToPath(url2), "utf-8");
45417
+ const raw = readPolicySchemaJson(version2);
45383
45418
  const parsed = JSON.parse(raw);
45384
45419
  schemaCache.set(version2, parsed);
45385
45420
  return parsed;
@@ -46638,7 +46673,6 @@ function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges
46638
46673
  }
46639
46674
 
46640
46675
  // src/commands/mcp.ts
46641
- import { fileURLToPath as fileURLToPath2 } from "node:url";
46642
46676
  import { dirname as pathDirname, join as pathJoin } from "node:path";
46643
46677
  import os13 from "node:os";
46644
46678
  import fs15 from "node:fs";
@@ -47581,8 +47615,7 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`
47581
47615
  context: { policyPath }
47582
47616
  });
47583
47617
  }
47584
- const templateUrl = new URL("../policy/examples/policy.example.yaml", import.meta.url);
47585
- const template = fs15.readFileSync(fileURLToPath2(templateUrl), "utf-8");
47618
+ const template = readPolicyExampleYaml();
47586
47619
  fs15.mkdirSync(pathDirname(policyPath), { recursive: true });
47587
47620
  fs15.writeFileSync(policyPath, template, { encoding: "utf-8" });
47588
47621
  const structured = {
@@ -48714,40 +48747,61 @@ Total: ${entries.length} device type(s) (source: ${source})`);
48714
48747
  handleError(error48);
48715
48748
  }
48716
48749
  });
48717
- catalog.command("search").description("Fuzzy search the effective catalog by type name, alias, role, or command name").argument("<keyword>", "Substring to match (case-insensitive) against type, alias, role, or command").action((keyword) => {
48750
+ catalog.command("search").description("Fuzzy search the effective catalog by type name, alias, role, or command name").argument("<keyword>", "Substring to match (case-insensitive) against type, alias, role, or command").option("--strict", "Only return entries whose type name matches (skip alias/role/command fallbacks)").action((keyword, options) => {
48718
48751
  try {
48719
48752
  const q = keyword.toLowerCase();
48720
48753
  const entries = getEffectiveCatalog();
48721
- const hits = entries.filter((e) => {
48722
- if (e.type.toLowerCase().includes(q)) return true;
48723
- if ((e.role ?? "").toLowerCase().includes(q)) return true;
48724
- if ((e.aliases ?? []).some((a) => a.toLowerCase().includes(q))) return true;
48725
- if (e.commands.some((c) => c.command.toLowerCase().includes(q))) return true;
48726
- return false;
48727
- });
48754
+ const strict = options.strict === true;
48755
+ const hits = [];
48756
+ for (const e of entries) {
48757
+ const matched = [];
48758
+ const typeHit = e.type.toLowerCase().includes(q);
48759
+ const aliasExact = (e.aliases ?? []).some((a) => a.toLowerCase() === q);
48760
+ const aliasSubstr = (e.aliases ?? []).some((a) => a.toLowerCase().includes(q) && a.toLowerCase() !== q);
48761
+ const roleHit = (e.role ?? "").toLowerCase().includes(q);
48762
+ const cmdMatches = e.commands.filter((c) => c.command.toLowerCase().includes(q)).map((c) => c.command);
48763
+ if (typeHit) matched.push("type");
48764
+ if (aliasExact) matched.push("alias");
48765
+ else if (aliasSubstr) matched.push("alias-only");
48766
+ if (roleHit) matched.push("role");
48767
+ if (cmdMatches.length > 0) matched.push(`commands[${cmdMatches.join(",")}]`);
48768
+ if (strict) {
48769
+ if (!typeHit) continue;
48770
+ } else if (matched.length === 0) {
48771
+ continue;
48772
+ }
48773
+ let tier;
48774
+ if (typeHit || aliasExact) tier = 0;
48775
+ else if (roleHit || cmdMatches.length > 0) tier = 1;
48776
+ else tier = 2;
48777
+ hits.push({ entry: e, tier, matched });
48778
+ }
48779
+ hits.sort((a, b2) => a.tier - b2.tier);
48728
48780
  if (isJsonMode()) {
48729
- printJson({ query: keyword, matches: hits });
48781
+ printJson({
48782
+ query: keyword,
48783
+ strict,
48784
+ matches: hits.map((h) => ({ ...h.entry, _matchedOn: h.matched, _tier: h.tier }))
48785
+ });
48730
48786
  return;
48731
48787
  }
48732
48788
  if (hits.length === 0) {
48733
- console.log(`No catalog entries match "${keyword}".`);
48789
+ const suffix = strict ? " (strict mode \u2014 try without --strict)" : "";
48790
+ console.log(`No catalog entries match "${keyword}"${suffix}.`);
48734
48791
  return;
48735
48792
  }
48736
48793
  const fmt = resolveFormat();
48737
- const headers = ["type", "category", "role", "matched"];
48738
- const rows = hits.map((e) => {
48739
- const matched = [];
48740
- if (e.type.toLowerCase().includes(q)) matched.push("type");
48741
- if ((e.aliases ?? []).some((a) => a.toLowerCase().includes(q))) matched.push("alias");
48742
- if ((e.role ?? "").toLowerCase().includes(q)) matched.push("role");
48743
- const cmdMatches = e.commands.filter((c) => c.command.toLowerCase().includes(q)).map((c) => c.command);
48744
- if (cmdMatches.length > 0) matched.push(`commands[${cmdMatches.join(",")}]`);
48745
- return [e.type, e.category, e.role ?? "\u2014", matched.join(", ") || "\u2014"];
48746
- });
48794
+ const headers = ["type", "category", "role", "matched_on"];
48795
+ const rows = hits.map((h) => [
48796
+ h.entry.type,
48797
+ h.entry.category,
48798
+ h.entry.role ?? "\u2014",
48799
+ h.matched.join(", ") || "\u2014"
48800
+ ]);
48747
48801
  renderRows(headers, rows, fmt, resolveFields());
48748
48802
  if (fmt === "table") {
48749
48803
  console.log(`
48750
- ${hits.length} match${hits.length === 1 ? "" : "es"} for "${keyword}"`);
48804
+ ${hits.length} match${hits.length === 1 ? "" : "es"} for "${keyword}"${strict ? " (strict)" : ""}`);
48751
48805
  }
48752
48806
  } catch (error48) {
48753
48807
  handleError(error48);
@@ -51331,7 +51385,6 @@ var import_yaml8 = __toESM(require_dist(), 1);
51331
51385
  init_output();
51332
51386
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync, mkdirSync, copyFileSync, statSync } from "node:fs";
51333
51387
  import { dirname, resolve as resolvePath } from "node:path";
51334
- import { fileURLToPath as fileURLToPath3 } from "node:url";
51335
51388
 
51336
51389
  // src/policy/format.ts
51337
51390
  init_cjs_shim();
@@ -51391,8 +51444,7 @@ function formatValidationResult(result, source, opts = {}) {
51391
51444
  // src/commands/policy.ts
51392
51445
  var LATEST_SUPPORTED_VERSION2 = SUPPORTED_POLICY_SCHEMA_VERSIONS[SUPPORTED_POLICY_SCHEMA_VERSIONS.length - 1];
51393
51446
  function readEmbeddedTemplate() {
51394
- const url2 = new URL("../policy/examples/policy.example.yaml", import.meta.url);
51395
- return readFileSync3(fileURLToPath3(url2), "utf-8");
51447
+ return readPolicyExampleYaml();
51396
51448
  }
51397
51449
  var PolicyFileExistsError = class extends Error {
51398
51450
  constructor(policyPath) {
@@ -56221,11 +56273,29 @@ function resolveStatusSyncRuntime(options) {
56221
56273
  }
56222
56274
  const openclawToken = options.openclawToken ?? process.env.OPENCLAW_TOKEN;
56223
56275
  if (!openclawToken) {
56224
- throw new UsageError("--openclaw-token is required or set OPENCLAW_TOKEN in the environment.");
56276
+ throw new UsageError(
56277
+ [
56278
+ "OpenClaw token missing. Provide one of:",
56279
+ " 1. --openclaw-token <token>",
56280
+ " 2. OPENCLAW_TOKEN=<token> in the environment",
56281
+ "",
56282
+ "The token is issued by your OpenClaw server admin (same token you use for `events mqtt-tail --sink openclaw`).",
56283
+ "After setting it, re-run the command and verify with `switchbot status-sync status`."
56284
+ ].join("\n")
56285
+ );
56225
56286
  }
56226
56287
  const openclawModel = options.openclawModel ?? process.env.OPENCLAW_MODEL;
56227
56288
  if (!openclawModel) {
56228
- throw new UsageError("--openclaw-model is required or set OPENCLAW_MODEL in the environment.");
56289
+ throw new UsageError(
56290
+ [
56291
+ "OpenClaw model missing. Provide one of:",
56292
+ " 1. --openclaw-model <model>",
56293
+ " 2. OPENCLAW_MODEL=<model> in the environment",
56294
+ "",
56295
+ "The model name maps this CLI to a registered agent/device on the OpenClaw side.",
56296
+ "After setting it, re-run the command and verify with `switchbot status-sync status`."
56297
+ ].join("\n")
56298
+ );
56229
56299
  }
56230
56300
  return {
56231
56301
  openclawUrl: options.openclawUrl ?? process.env.OPENCLAW_URL ?? DEFAULT_OPENCLAW_URL,
@@ -56860,7 +56930,7 @@ init_output();
56860
56930
  import { spawn as spawn5 } from "node:child_process";
56861
56931
  import fs29 from "node:fs";
56862
56932
  import path25 from "node:path";
56863
- import { fileURLToPath as fileURLToPath4 } from "node:url";
56933
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
56864
56934
  init_arg_parsers();
56865
56935
  init_source();
56866
56936
  function readDaemonPid() {
@@ -57010,7 +57080,7 @@ The daemon reads the same policy file as \`switchbot rules run\`.
57010
57080
  } catch {
57011
57081
  }
57012
57082
  }
57013
- const thisFile = fileURLToPath4(import.meta.url);
57083
+ const thisFile = fileURLToPath2(import.meta.url);
57014
57084
  const cliEntry = path25.resolve(path25.dirname(thisFile), "..", "index.js");
57015
57085
  const args = ["rules", "run"];
57016
57086
  if (opts.policy) args.push(opts.policy);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "3.2.2",
3
+ "version": "3.3.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",