c8ctl-plugin-nano 1.36.0 → 1.36.2

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 +18 -13
  2. package/c8ctl-plugin.js +125 -36
  3. package/package.json +8 -8
package/README.md CHANGED
@@ -292,23 +292,28 @@ c8ctl nano work reviewer
292
292
 
293
293
  **Zero-config hub auto-discovery.** You usually don't even set `NANO_AGENTIC_URL`.
294
294
  When nwf runs **embedded**, the engine (`:8080`) serves the console but the
295
- `/agentic` channel is served by the **embedded app on its own loopback port**
296
- (e.g. `:3000`); the engine's console proxy deliberately refuses WebSocket
297
- upgrades (nanobpmn ADR 0057 §3 → `501`), so the channel is unreachable via the
298
- engine URL. With **no** agentic target configured, `work` therefore
299
- **auto-discovers** it: it reads `GET <engine>/console/api/projects` and, for each
300
- running app that advertises an agentic UI port (`appUi.enabled === true` and
301
- `appUi.port`), probes that app's direct `ws://127.0.0.1:<port>/agentic`. Discovery
302
- is **loopback-only** and **time-bounded** (≤2s) and never meaningfully delays job
303
- polling.
295
+ `/agentic` channel is served by the **embedded app on its own port** (e.g.
296
+ `:3000`); the engine's console proxy deliberately refuses WebSocket upgrades
297
+ (nanobpmn ADR 0057 §3 → `501`), so the channel is unreachable via the engine URL.
298
+ With **no** agentic target configured, `work` therefore **auto-discovers** it: it
299
+ reads `GET <engine>/console/api/projects` and, for each running app that
300
+ advertises an agentic UI port (`appUi.enabled === true` and `appUi.port`), probes
301
+ that app's direct `ws://<engine-host>:<port>/agentic`. Discovery runs **against
302
+ the engine's own host** a local engine keeps probing `127.0.0.1`, while a
303
+ remote/LAN engine (e.g. `merlin.local:8080`) steers the probe back at *itself*
304
+ (`merlin.local:<port>`), never at the worker's own loopback services. It is
305
+ **time-bounded** (≤2s) and never meaningfully delays job polling. The discovered
306
+ host and port are printed for debugging. (An IPv6 literal engine host is bracketed
307
+ in the URL authority, e.g. `ws://[2001:db8::1]:3000/agentic`.)
304
308
 
305
309
  - **Exactly one app →** the worker connects directly to
306
- `ws://127.0.0.1:<appUi.port>/agentic` (bypassing the WS-incapable console proxy)
307
- and appears live with **zero configuration**.
310
+ `ws://<engine-host>:<appUi.port>/agentic` (bypassing the WS-incapable console
311
+ proxy) and appears live with **zero configuration** — including cross-machine on
312
+ a trusted LAN.
308
313
  - **Two or more apps →** the worker **does not guess**: it prints an `ambiguous`
309
314
  error naming each discovered `project → :port` and **stops**. Pin the one you
310
- want and re-run: `export NANO_AGENTIC_URL=http://127.0.0.1:<port>` (or persist
311
- `agenticUrl`).
315
+ want and re-run: `export NANO_AGENTIC_URL=http://<engine-host>:<port>` (or
316
+ persist `agenticUrl`).
312
317
  - **Nothing discoverable (e.g. pointed at Camunda, or an API-only gateway) →** the
313
318
  worker prints a one-line advisory naming `NANO_AGENTIC_URL` and **continues
314
319
  doing real work** with the channel simply absent — discovery never fails the
package/c8ctl-plugin.js CHANGED
@@ -2408,11 +2408,16 @@ function sameOrigin(a, b) {
2408
2408
  }
2409
2409
  }
2410
2410
 
2411
- function resolveBrokerRestConfig(env = process.env) {
2411
+ function resolveBrokerRestConfig(env = process.env, opts = {}) {
2412
2412
  // readConfig() swallows parse/IO errors and never throws (returns {}), so no
2413
2413
  // local try/catch is needed here.
2414
2414
  const cfg = readConfig() || {};
2415
+ // `opts.baseUrl` lets a caller pin the effective base (e.g. the active c8ctl
2416
+ // profile's REST address — see resolveAutoRestConfig) while still running it
2417
+ // through the SAME token same-origin gate below, so the token logic stays
2418
+ // single-sourced (never duplicated per call site).
2415
2419
  const baseUrl =
2420
+ opts.baseUrl ||
2416
2421
  env.NANO_REST_URL ||
2417
2422
  env.NANO_BASE_URL ||
2418
2423
  cfg.nanoUrl ||
@@ -2440,6 +2445,33 @@ function resolveBrokerRestConfig(env = process.env) {
2440
2445
  return { baseUrl, token };
2441
2446
  }
2442
2447
 
2448
+ // Resolve the C8 REST config the `--auto` engine-read reader is built from.
2449
+ // This is the job-type-read analogue of resolveLinkedPromptSource: an explicit
2450
+ // NANO_REST_URL / NANO_BASE_URL / cfg.nanoUrl override still wins (operator
2451
+ // escape hatch), but with NONE of those set the base is derived from the SAME
2452
+ // c8ctl client that activates jobs (its getConfig().restAddress) rather than the
2453
+ // localhost default — so a worker that can activate jobs against a profile
2454
+ // engine can also read the deployed job types from it. Without this, an `--auto`
2455
+ // worker on an active remote profile reads from http://localhost:8080, finds no
2456
+ // engine, discovers 0 job types, and crash-loops (jwulf/c8ctl-plugin-nano#93).
2457
+ // Falls back to resolveBrokerRestConfig's localhost default only when the client
2458
+ // exposes no usable restAddress. The token same-origin gate lives in
2459
+ // resolveBrokerRestConfig (re-run against the profile base), never duplicated.
2460
+ function resolveAutoRestConfig(camunda, env = process.env) {
2461
+ const cfg = readConfig() || {};
2462
+ const hasExplicitBase = Boolean(env.NANO_REST_URL || env.NANO_BASE_URL || cfg.nanoUrl);
2463
+ if (!hasExplicitBase && camunda && typeof camunda.getConfig === 'function') {
2464
+ let profileBase = '';
2465
+ try {
2466
+ profileBase = normalizeRestBase(camunda.getConfig()?.restAddress);
2467
+ } catch {
2468
+ // ignore — degrade to the resolveBrokerRestConfig (localhost) default below
2469
+ }
2470
+ if (profileBase) return resolveBrokerRestConfig(env, { baseUrl: profileBase });
2471
+ }
2472
+ return resolveBrokerRestConfig(env);
2473
+ }
2474
+
2443
2475
  // ---------------------------------------------------------------------------
2444
2476
  // `nano work --auto`: zero-config engine-read enrolment (issue #66).
2445
2477
  //
@@ -4138,6 +4170,21 @@ function isLoopbackHost(hostname) {
4138
4170
  return /^127(?:\.\d{1,3}){3}$/.test(h);
4139
4171
  }
4140
4172
 
4173
+ /**
4174
+ * Format a hostname for the authority component of a `ws://`/`http://` URL:
4175
+ * a bare IPv6 literal (contains `:`, not already bracketed) is wrapped in `[…]`,
4176
+ * everything else is used verbatim. Idempotent — an already-bracketed host is
4177
+ * left as-is. Guards against building an invalid `ws://::1:3000/…` when a raw or
4178
+ * normalized IPv6 host (e.g. the `::1` constant) has not been bracketed.
4179
+ *
4180
+ * @param {string} host a hostname from `URL.hostname` or a normalized loopback
4181
+ * @returns {string}
4182
+ */
4183
+ function wsHostPart(host) {
4184
+ const h = String(host || '');
4185
+ return h.includes(':') && !h.startsWith('[') ? `[${h}]` : h;
4186
+ }
4187
+
4141
4188
  /**
4142
4189
  * Normalise the engine's `GET /console/api/projects` payload into the running
4143
4190
  * embedded apps that advertise an agentic UI port. Accepts the shapes the
@@ -4175,24 +4222,26 @@ function normalizeProjectApps(projects) {
4175
4222
  }
4176
4223
 
4177
4224
  /**
4178
- * Probe whether an embedded app's own loopback `/agentic` endpoint answers a
4179
- * WebSocket upgrade. Connects to `ws://127.0.0.1:<port>/agentic?token=…` and
4225
+ * Probe whether an embedded app's `/agentic` endpoint answers a WebSocket
4226
+ * upgrade. Connects to `ws://<host>:<port>/agentic?token=…` (host defaults to
4227
+ * `127.0.0.1`; a bare IPv6 literal is bracketed for the URL authority) and
4180
4228
  * resolves `true` only if the socket opens within `timeoutMs`; a refused
4181
4229
  * connection, the console proxy's deliberate `501`, a `404`, or a timeout all
4182
- * resolve `false`. Loopback-only and self-cleaning — the probe socket is closed
4183
- * as soon as the outcome is known. Never throws.
4230
+ * resolve `false`. Self-cleaning — the probe socket is closed as soon as the
4231
+ * outcome is known. Never throws.
4184
4232
  *
4185
- * @param {number} port the app's direct loopback port (`appUi.port`)
4186
- * @param {{ token?: string, WebSocketImpl?: Function, timeoutMs?: number }} [opts]
4233
+ * @param {number} port the app's direct agentic port (`appUi.port`)
4234
+ * @param {{ host?: string, token?: string, WebSocketImpl?: Function, timeoutMs?: number }} [opts]
4187
4235
  * @returns {Promise<boolean>}
4188
4236
  */
4189
4237
  function probeAgenticChannel(port, {
4238
+ host = '127.0.0.1',
4190
4239
  token = LOCAL_AGENTIC_TOKEN,
4191
4240
  WebSocketImpl = globalThis.WebSocket,
4192
4241
  timeoutMs = AGENTIC_DISCOVERY_TIMEOUT_MS,
4193
4242
  } = {}) {
4194
4243
  if (typeof WebSocketImpl !== 'function') return Promise.resolve(false);
4195
- const url = `ws://127.0.0.1:${port}/agentic?token=${encodeURIComponent(token)}`;
4244
+ const url = `ws://${wsHostPart(host)}:${port}/agentic?token=${encodeURIComponent(token)}`;
4196
4245
  return new Promise((resolve) => {
4197
4246
  let done = false;
4198
4247
  let ws;
@@ -4217,19 +4266,21 @@ function probeAgenticChannel(port, {
4217
4266
 
4218
4267
  /**
4219
4268
  * Auto-discover the embedded nwf agentic hub(s) reachable from an engine base
4220
- * URL (#75). Reads `GET <engine>/console/api/projects`, keeps the apps that
4221
- * advertise an agentic UI port, and WS-probes each app's direct loopback
4222
- * `/agentic` to confirm the channel is actually served there (bypassing the
4223
- * WS-incapable console proxy). Loopback-only (the engine host itself must be
4224
- * loopback, since the response steers a local port probe), enforces a single
4225
- * shared time budget across the fetch + probes, and is fail-open: any error
4226
- * not a nano engine (Camunda), a non-loopback engine, network failure,
4227
- * malformed body, or an overall timeout degrades to `[]` so the worker's real
4228
- * job is never blocked.
4269
+ * URL (#75, #96). Reads `GET <engine>/console/api/projects`, keeps the apps that
4270
+ * advertise an agentic UI port, and WS-probes each app's `/agentic` **on the
4271
+ * engine's own host** to confirm the channel is actually served there (bypassing
4272
+ * the WS-incapable console proxy). Works cross-machine on a trusted LAN: a
4273
+ * loopback engine probes `127.0.0.1`, a remote engine (e.g. `merlin.local`)
4274
+ * probes that same host the port is taken from the projects API but the host is
4275
+ * always the engine's, so a rogue projects API can never steer a probe at the
4276
+ * worker's own loopback (#76). Enforces a single shared time budget across the
4277
+ * fetch + probes, and is fail-open: any error — not a nano engine (Camunda),
4278
+ * network failure, malformed body, or an overall timeout — degrades to `[]` so
4279
+ * the worker's real job is never blocked.
4229
4280
  *
4230
- * @param {string} engineBaseUrl the engine base URL (e.g. `http://localhost:8080`)
4281
+ * @param {string} engineBaseUrl the engine base URL (e.g. `http://merlin.local:8080`)
4231
4282
  * @param {{ token?: string, fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
4232
- * @returns {Promise<Array<{ project: string, port: number, label?: string }>>}
4283
+ * @returns {Promise<Array<{ project: string, port: number, label?: string, host: string }>>}
4233
4284
  */
4234
4285
  async function discoverAgenticHubs(engineBaseUrl, {
4235
4286
  token = LOCAL_AGENTIC_TOKEN,
@@ -4241,16 +4292,21 @@ async function discoverAgenticHubs(engineBaseUrl, {
4241
4292
  return [];
4242
4293
  }
4243
4294
  const base = engineBaseUrl.replace(/\/+$/, '');
4244
- // Loopback-only: discovery probes 127.0.0.1:<port> using a port advertised by
4245
- // the engine's projects API, so a non-loopback (remote) engine could steer a
4246
- // local port probe. Refuse discovery unless the engine host is loopback (#76).
4295
+ // Discover against the ENGINE's own host the app is embedded in the engine,
4296
+ // so its /agentic port lives on the same host the worker already trusts as its
4297
+ // engine (that's where it pulls jobs from). A loopback engine keeps probing
4298
+ // 127.0.0.1 (unchanged local behaviour); a remote/LAN engine (e.g.
4299
+ // merlin.local) steers the probe back to ITSELF, never at the worker's own
4300
+ // loopback services — which was the actual #76 concern (a rogue projects API
4301
+ // making the worker probe its own localhost). So the port comes from the
4302
+ // engine's projects API, but the HOST is always the engine's, never guessed.
4247
4303
  let host;
4248
4304
  try {
4249
4305
  host = new URL(base).hostname;
4250
4306
  } catch {
4251
4307
  return [];
4252
4308
  }
4253
- if (!isLoopbackHost(host)) return [];
4309
+ const probeHost = isLoopbackHost(host) ? '127.0.0.1' : host;
4254
4310
  // Single discovery budget: the projects fetch and the WS probes share ONE
4255
4311
  // deadline, so total discovery can't approach 2× timeoutMs (the fetch could
4256
4312
  // consume ~timeoutMs and then each probe was previously given a fresh full
@@ -4272,10 +4328,13 @@ async function discoverAgenticHubs(engineBaseUrl, {
4272
4328
  if (apps.length === 0) return [];
4273
4329
  const remainingMs = deadline - Date.now();
4274
4330
  if (remainingMs <= 0) return [];
4275
- // Probe candidate ports concurrently within the remaining shared budget.
4331
+ // Probe candidate ports concurrently within the remaining shared budget. Each
4332
+ // surviving hub carries the engine host so the caller builds the right URL.
4276
4333
  const settled = await Promise.all(apps.map(async (app) => {
4277
4334
  try {
4278
- return (await wsProbe(app.port, { token, timeoutMs: remainingMs })) ? app : null;
4335
+ return (await wsProbe(app.port, { host: probeHost, token, timeoutMs: remainingMs }))
4336
+ ? { ...app, host: probeHost }
4337
+ : null;
4279
4338
  } catch {
4280
4339
  return null;
4281
4340
  }
@@ -4292,7 +4351,8 @@ async function discoverAgenticHubs(engineBaseUrl, {
4292
4351
  * half-configured. No discovery attempted.
4293
4352
  * - `{ status: 'connect', config }` — a target to connect to. Either the
4294
4353
  * explicit `NANO_AGENTIC_URL`/`agenticUrl` verbatim (no discovery), or the
4295
- * single discovered app's direct `ws://127.0.0.1:<port>/agentic` loopback.
4354
+ * single discovered app's `ws://<engineHost>:<port>/agentic` (loopback for a
4355
+ * local engine, the engine's LAN host for a remote one).
4296
4356
  * - `{ status: 'ambiguous', message, candidates }` — two+ apps expose a
4297
4357
  * channel. Hard stop for the worker: it must not silently pick one.
4298
4358
  * - `{ status: 'advisory', message }` — nothing discoverable (zero matches,
@@ -4310,11 +4370,24 @@ async function resolveAgenticTarget(opts = {}) {
4310
4370
 
4311
4371
  const hubs = await discoverAgenticHubs(base.url, { token: base.token, ...opts });
4312
4372
 
4373
+ // The host to suggest in operator-facing messages: the engine's own host
4374
+ // (bracketed if an IPv6 literal, so the suggested URL authority is valid), so a
4375
+ // remote-engine advisory names the reachable LAN host rather than 127.0.0.1.
4376
+ let suggestHost = '127.0.0.1';
4377
+ try {
4378
+ const h = new URL(base.url).hostname;
4379
+ suggestHost = wsHostPart(isLoopbackHost(h) ? '127.0.0.1' : h);
4380
+ } catch { /* keep the loopback default */ }
4381
+
4313
4382
  if (hubs.length === 1) {
4314
- const { project, port } = hubs[0];
4383
+ const { project, port, host } = hubs[0];
4315
4384
  return {
4316
4385
  status: 'connect',
4317
- config: { ...base, url: `http://127.0.0.1:${port}`, discovered: { project, port } },
4386
+ config: {
4387
+ ...base,
4388
+ url: `http://${wsHostPart(host)}:${port}`,
4389
+ discovered: { project, port, host },
4390
+ },
4318
4391
  };
4319
4392
  }
4320
4393
  if (hubs.length > 1) {
@@ -4323,14 +4396,14 @@ async function resolveAgenticTarget(opts = {}) {
4323
4396
  status: 'ambiguous',
4324
4397
  candidates: hubs,
4325
4398
  message: `multiple embedded apps expose an agentic channel (${list}); refusing to guess. `
4326
- + 'Disambiguate by setting NANO_AGENTIC_URL=http://127.0.0.1:<port> (or persisted agenticUrl) to the one you want.',
4399
+ + `Disambiguate by setting NANO_AGENTIC_URL=http://${suggestHost}:<port> (or persisted agenticUrl) to the one you want.`,
4327
4400
  };
4328
4401
  }
4329
4402
  return {
4330
4403
  status: 'advisory',
4331
4404
  message: `agentic visibility was not discoverable at ${base.url} — the embedded app port could `
4332
4405
  + 'not be found (not a nano engine, or its console projects API is absent). Set '
4333
- + 'NANO_AGENTIC_URL=http://127.0.0.1:<appUi.port> to enable the visibility channel. Continuing without it.',
4406
+ + `NANO_AGENTIC_URL=http://${suggestHost}:<appUi.port> to enable the visibility channel. Continuing without it.`,
4334
4407
  };
4335
4408
  }
4336
4409
 
@@ -4558,10 +4631,14 @@ async function workAgent(req, flags) {
4558
4631
  }
4559
4632
  const camunda = globalThis.c8ctl.createClient();
4560
4633
 
4561
- // Broker REST endpoint for live linked-resource prompts (issue #63) the same
4562
- // nano endpoint this worker already talks to. Resolved once at startup, and
4563
- // reused as the C8 REST source for `--auto`'s engine-read enrolment.
4564
- const restConfig = resolveBrokerRestConfig();
4634
+ // Broker REST endpoint for live linked-resource prompts (issue #63) and the
4635
+ // C8 REST source for `--auto`'s engine-read enrolment. Derived from the SAME
4636
+ // client that activates jobs (its profile REST address) when no explicit
4637
+ // NANO_REST_URL/NANO_BASE_URL/cfg.nanoUrl override is set — so a worker that
4638
+ // can activate jobs against the active profile engine also reads job types
4639
+ // from it, instead of a localhost default that crash-loops when nothing is
4640
+ // listening on :8080 (jwulf/c8ctl-plugin-nano#93). Resolved once at startup.
4641
+ const restConfig = resolveAutoRestConfig(camunda);
4565
4642
 
4566
4643
  // The desired job-type set. In `--auto` it is engine-read (∪ any --job-type
4567
4644
  // extras); otherwise it is the rank×capability matrix (∪ extras). The initial
@@ -4708,7 +4785,8 @@ async function workAgent(req, flags) {
4708
4785
  const shown = redactAgenticUrl(buildAgenticUrl(agenticCfg.url, {}));
4709
4786
  const mode = agenticCfg.secure ? 'secure' : 'local';
4710
4787
  if (agenticCfg.discovered) {
4711
- logger.info(` agentic channel: auto-discovered ${agenticCfg.discovered.project} on the embedded app port :${agenticCfg.discovered.port} (bypassing the WS-incapable console proxy).`);
4788
+ const d = agenticCfg.discovered;
4789
+ logger.info(` agentic channel: auto-discovered ${d.project} on the app's /agentic port ${wsHostPart(d.host)}:${d.port} (bypassing the WS-incapable console proxy).`);
4712
4790
  }
4713
4791
  logger.info(` agentic channel (${mode}): announcing presence as ${workerName} on ${shown}`);
4714
4792
  } catch (err) {
@@ -5183,7 +5261,16 @@ async function workAgent(req, flags) {
5183
5261
  if (inFlightReconcile) return;
5184
5262
  reconcile().catch((err) => logger.warn(`--auto reconcile failed: ${err?.message || err}`));
5185
5263
  }, AUTO_POLL_INTERVAL_MS);
5186
- if (typeof autoPollTimer.unref === 'function') autoPollTimer.unref();
5264
+ // Deliberately REF'd (unlike the reaper/run-dir hygiene timers, which are
5265
+ // unref'd): in `--auto` this poll IS the retry loop, and it must keep the
5266
+ // process alive even with zero pollers. When the INITIAL engine read fails
5267
+ // (transient miss, or the engine isn't up yet) the worker registers 0
5268
+ // pollers; nothing else holds the event loop open (the SDK client with no
5269
+ // job workers doesn't, and the hygiene timers are unref'd), so an unref'd
5270
+ // poll timer would let the process exit 0 — the observed crash-loop under a
5271
+ // supervisor (jwulf/c8ctl-plugin-nano#93). Keeping it ref'd makes the worker
5272
+ // stay up and re-read on the next poll, exactly as the initial-read warning
5273
+ // promises. Shutdown clears it (clearInterval), so Ctrl-C/SIGTERM still exit.
5187
5274
  } else {
5188
5275
  // `watchFile` (polling stat) is deliberate over `fs.watch`: it survives the
5189
5276
  // atomic temp+rename that `writeConfig` does (fs.watch would rebind to the old
@@ -8547,6 +8634,7 @@ export {
8547
8634
  parseLinkedResources,
8548
8635
  pickLinkedResource,
8549
8636
  resolveBrokerRestConfig,
8637
+ resolveAutoRestConfig,
8550
8638
  resourceContentUrl,
8551
8639
  fetchLinkedResourceContent,
8552
8640
  resolveLinkedPrompt,
@@ -8602,6 +8690,7 @@ export {
8602
8690
  scanAgentTaskLeaves,
8603
8691
  readDeployedAgentJobTypes,
8604
8692
  resolveAutoJobTypes,
8693
+ workAgent,
8605
8694
  derivePollTimeoutMs,
8606
8695
  AGENT_TASK_NS,
8607
8696
  AGENT_RESULT_KEY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.36.0",
3
+ "version": "1.36.2",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -57,12 +57,12 @@
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.36.0",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.36.0",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.36.0",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.36.0",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.36.0",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.36.0",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.36.0"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.36.2",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.36.2",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.36.2",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.36.2",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.36.2",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.36.2",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.36.2"
67
67
  }
68
68
  }