@ran-sh/dsh-crew 2.2.1 → 2.2.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.
@@ -42,12 +42,15 @@ import { homedir } from 'node:os';
42
42
  import * as realInstaller from './install.mjs';
43
43
  import { samePayloadContent, capturePayloadContent } from './payload-content.mjs';
44
44
  import { crewDshHome, crewProfileDir, claudeIntegrationLine } from './install.mjs';
45
+ import { integrationRoot } from './crew-paths.mjs';
45
46
  import { releaseClaimsState } from '../release-in-use.mjs';
46
47
  import { compareProcessToken, processStartToken } from '../process-identity.mjs';
47
48
  import { RELEASE_COHORT_FILENAME } from '../dsh-cohort.mjs';
48
49
  import { renameTree } from './tree-move.mjs';
49
50
  import { checkRuntimeAdvance, normalizeRuntimeState, runtimeStateMayHaveStarted } from './runtime-lifecycle.mjs';
50
- import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, gcRetainedRuntimes, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
51
+ import { ensureCrewDshRuntime, ensureCrewPluginRegistration, ensureCrewWebProfile, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, gcRetainedRuntimes, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
52
+ import { desktopAttach, desktopDetach, desktopStatus } from './desktop-profile.mjs';
53
+ import { mirrorOfficialHarnessConfig, officialConfigMirrorStatus } from './harness-config-import.mjs';
51
54
  import {
52
55
  ensureOfficialWebIntegration,
53
56
  officialWebIntegrationStatus,
@@ -970,6 +973,28 @@ function defaultNpmInstaller(stageRoot, log) {
970
973
  return true;
971
974
  }
972
975
 
976
+ // Exact-version specs only (the pinned-cohort shape). Ranges stay with normal
977
+ // resolution: a range override would both fight the resolver and violate the
978
+ // verbatim-spec rule npm applies when a direct dependency shares the name.
979
+ const EXACT_SPEC_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
980
+
981
+ /**
982
+ * Lock every exact-pinned dependency to its own version across the whole
983
+ * staged tree. The npm fallback install has no flag channel (sanitized env,
984
+ * fixed args), so a drifting transitive peer — e.g. a projection package
985
+ * peer-requiring a newer cohort than the payload's pin — otherwise aborts
986
+ * staging with ERESOLVE. npm v11 rejects an override whose spec differs from
987
+ * the direct dependency, so each override reuses the dependency's exact spec
988
+ * verbatim.
989
+ */
990
+ export function exactSpecOverrides(dependencies) {
991
+ const overrides = {};
992
+ for (const [name, spec] of Object.entries(dependencies ?? {})) {
993
+ if (typeof spec === 'string' && EXACT_SPEC_RE.test(spec)) overrides[name] = spec;
994
+ }
995
+ return Object.keys(overrides).length > 0 ? overrides : null;
996
+ }
997
+
973
998
  // ---- payload staging ---------------------------------------------------------
974
999
 
975
1000
  /**
@@ -1029,6 +1054,8 @@ export function stageCandidatePayload({
1029
1054
  ...(manifest.peerDependencies ?? {}),
1030
1055
  ...(manifest.dependencies ?? {}),
1031
1056
  };
1057
+ const overrides = exactSpecOverrides(stagedManifest.dependencies);
1058
+ if (overrides) stagedManifest.overrides = overrides;
1032
1059
  writeFileSync(join(stageDir, 'package.json'), JSON.stringify(stagedManifest, null, 2) + '\n');
1033
1060
 
1034
1061
  // Materialize dependencies: prefer replicating the exact bits the candidate
@@ -2217,7 +2244,7 @@ export function buildProductionMigration({ home, log = () => {}, stopOwned, star
2217
2244
  });
2218
2245
  }
2219
2246
 
2220
- async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, stopOwned, startOwned, verifyOwned, supervisorFactory = crewSupervisor }) { const ensure = ensureRuntime ?? ((opts) => {
2247
+ async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, ensureWebProfile, stopOwned, startOwned, verifyOwned, supervisorFactory = crewSupervisor }) { const ensure = ensureRuntime ?? ((opts) => {
2221
2248
  const r = ensureCrewDshRuntime({ ...opts, env: sanitizedPackageManagerEnv() });
2222
2249
  if (!r.ok && r.stderrTail) {
2223
2250
  log(` (runtime installer said: ${r.stderrTail})`);
@@ -2225,17 +2252,37 @@ async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, sto
2225
2252
  if (!r.ok && r.code === 'DSH_RUNTIME_COHORT_MISMATCH') {
2226
2253
  return { ok: false, code: r.code, error: r.error, installed: r.installed, target: r.target, needsMigration: true };
2227
2254
  }
2228
- return r.ok ? { ok: true, version: r.cli?.version ?? null } : { ok: false, error: r.error ?? r.code ?? 'runtime bootstrap failed' };
2255
+ return r.ok
2256
+ ? { ok: true, version: r.cli?.version ?? null, cohortRepaired: r.cohortRepaired === true }
2257
+ : { ok: false, error: r.error ?? r.code ?? 'runtime bootstrap failed' };
2229
2258
  });
2230
2259
  // Production migration wiring: ONE supervisor instance per migration,
2231
2260
  // reused by stop/start/verify/rollback so child identity stays coherent.
2232
2261
  // No legacy bridge involved. Missing callbacks fail closed inside
2233
2262
  // migrateCrewDshRuntime.
2234
2263
  const migrate = migrateRuntime ?? buildProductionMigration({ home, log, stopOwned, startOwned, verifyOwned, supervisorFactory });
2264
+ // The runtime alone cannot boot a managed 3210/3080: the launcher preflight
2265
+ // and the supervisor require <DSH_HOME>/profiles/web. Materialize it here —
2266
+ // before any supervisor convergence, on every path that ends in "runtime
2267
+ // ready" — so a fresh install cannot deadlock on a heartbeat that a
2268
+ // profile-less service can never publish.
2269
+ const convergeWebProfile = () => {
2270
+ const profile = (ensureWebProfile ?? ensureCrewWebProfile)({ home });
2271
+ if (!profile.ok) {
2272
+ log(`✗ Crew web profile bootstrap failed (${profile.code ?? 'unknown error'})`);
2273
+ return false;
2274
+ }
2275
+ if (profile.created) log(`✓ Crew web profile scaffold created (${profile.profileRoot})`);
2276
+ return true;
2277
+ };
2235
2278
  const r = await ensure({ home });
2236
2279
  if (r?.ok) {
2237
2280
  log(`✓ reusable Crew DSH runtime${r.version ? ` (@${r.version})` : ''}`);
2238
- return true;
2281
+ // The install locks the whole family when the registry had published a newer
2282
+ // prerelease than the pinned cohort; say so, because a silent repair is
2283
+ // indistinguishable from a registry that never drifted.
2284
+ if (r.cohortRepaired) log(' the DSH family had drifted from the pinned cohort; installed the pinned family instead');
2285
+ return convergeWebProfile();
2239
2286
  }
2240
2287
  if (r?.needsMigration === true) {
2241
2288
  log(`- runtime cohort stale (${r.installed} -> ${r.target}); migrating via staged transaction`);
@@ -2245,7 +2292,7 @@ async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, sto
2245
2292
  return false;
2246
2293
  }
2247
2294
  log(`✓ runtime cohort migrated (@${m.version})`);
2248
- return true;
2295
+ return convergeWebProfile();
2249
2296
  }
2250
2297
  log(`✗ reusable Crew DSH runtime unavailable: ${r?.error ?? 'unknown error'}`);
2251
2298
  return false;
@@ -2624,9 +2671,18 @@ async function activateRelease({ home, releaseDir, manifest, log, installer, sup
2624
2671
  // leave four configurations naming a directory that is gone. Writing the
2625
2672
  // release path directly is what made crash recovery unable to delete a
2626
2673
  // candidate without breaking Codex, ZCode and Claude Code.
2627
- const integrationRoot = registration.linkPath ?? releaseDir;
2628
-
2629
- const codex = installer.installCodex({ home, root: integrationRoot });
2674
+ //
2675
+ // The root is derived through the SAME integrationRoot() the readiness
2676
+ // checks use, not blindly from registration.linkPath: write and validate
2677
+ // must be one function of (home, releaseDir, name). In the healthy case the
2678
+ // gate passes right after registration re-pointed the link, so this is
2679
+ // still the link path; when the gate fails, both sides fall back to the
2680
+ // release directory together instead of the writer using the link while the
2681
+ // validator reports the release — the disagreement that made every clean
2682
+ // install read back as "needs repair".
2683
+ const integrationWriteRoot = integrationRoot({ home, root: releaseDir, name: manifest.name });
2684
+
2685
+ const codex = installer.installCodex({ home, root: integrationWriteRoot });
2630
2686
  if (codex.ok === false) {
2631
2687
  log(`✗ Codex Desktop integration failed: ${(codex.actions ?? []).join('; ')}`);
2632
2688
  return false;
@@ -2634,7 +2690,7 @@ async function activateRelease({ home, releaseDir, manifest, log, installer, sup
2634
2690
  log('✓ Codex Desktop integration');
2635
2691
 
2636
2692
  if (installer.installZCode) {
2637
- const zcode = installer.installZCode({ home, root: integrationRoot });
2693
+ const zcode = installer.installZCode({ home, root: integrationWriteRoot });
2638
2694
  if (zcode.ok === false) {
2639
2695
  log(`✗ ZCode integration failed (${zcode.code ?? 'unknown'})`);
2640
2696
  return false;
@@ -2649,7 +2705,7 @@ async function activateRelease({ home, releaseDir, manifest, log, installer, sup
2649
2705
  }
2650
2706
  if (startup?.supported) log('✓ Windows login startup');
2651
2707
 
2652
- const claude = await installer.installClaudeCode({ home, root: integrationRoot });
2708
+ const claude = await installer.installClaudeCode({ home, root: integrationWriteRoot });
2653
2709
  if (claude.ok === false) {
2654
2710
  log(claudeIntegrationLine(claude));
2655
2711
  return false;
@@ -2688,6 +2744,59 @@ export async function npxDetach({ home = homedir(), log = console.log } = {}) {
2688
2744
  return { ok: false, error: 'OFFICIAL_WEB_PROFILE_READ_ONLY' };
2689
2745
  }
2690
2746
 
2747
+ // Desktop-client integration. The official desktop app boots its Harness with the
2748
+ // app-exclusive `desktop` profile, so the Crew panel can only reach it through
2749
+ // that profile's own patch layer — the one official-home write Crew performs, and
2750
+ // only because an operator asked for it by name. It never runs during install,
2751
+ // update or supervision.
2752
+ export async function npxDesktop({ home = homedir(), args = [], dryRun = false, log = console.log } = {}) {
2753
+ const action = args[0] ?? 'status';
2754
+ if (!['attach', 'detach', 'status'].includes(action)) {
2755
+ log(`✗ unknown desktop action: ${action}`);
2756
+ return { ok: false, error: 'usage: dsh-crew desktop <attach|detach|status> [--dry-run]' };
2757
+ }
2758
+ if (action === 'status') {
2759
+ const status = desktopStatus({ home });
2760
+ if (!status.file_present) log(`- desktop patch layer: absent (${status.path})`);
2761
+ else if (!status.attached) log(`- desktop patch layer: present, no Crew bridge (${status.path})`);
2762
+ else if (status.current) log(`✓ desktop patch layer carries the current Crew bridge (revision ${status.bridge?.revision})`);
2763
+ else log(`! desktop patch layer carries a stale Crew bridge (current revision ${status.bridge?.revision ?? 'unavailable'})`);
2764
+ if (!status.bridge) log(` (${status.bridge_error})`);
2765
+ log(JSON.stringify(status, null, 2));
2766
+ return { ok: true, status };
2767
+ }
2768
+ const result = action === 'attach'
2769
+ ? desktopAttach({ home, dryRun, log })
2770
+ : desktopDetach({ home, dryRun, log });
2771
+ if (!result.ok) log(`✗ desktop ${action} failed: ${result.error ?? result.code}`);
2772
+ return result;
2773
+ }
2774
+
2775
+ // Bring the operator's user-level DSH configuration across from the official home
2776
+ // into Crew's home: the Crew hub then runs the same models, providers, policy and
2777
+ // MCP servers as the desktop app. Read-only on ~/.dsh, no secrets copied (provider
2778
+ // entries keep their apiKeyEnv references, and those variables are inherited).
2779
+ export async function npxConfig({ home = homedir(), args = [], dryRun = false, log = console.log } = {}) {
2780
+ const action = args[0] ?? 'status';
2781
+ if (!['import', 'status'].includes(action)) {
2782
+ log(`✗ unknown config action: ${action}`);
2783
+ return { ok: false, error: 'usage: dsh-crew config <import|status> [--dry-run]' };
2784
+ }
2785
+ if (action === 'status') {
2786
+ const status = officialConfigMirrorStatus({ home });
2787
+ for (const layer of [status.home_layer, status.profile_layer]) {
2788
+ log(`- ${layer.label}: ${!layer.source_present ? `no official source (${layer.source})` : layer.mirrored ? 'mirrored' : 'not imported yet (run: dsh-crew config import)'}`);
2789
+ }
2790
+ log(JSON.stringify(status, null, 2));
2791
+ return { ok: true, status };
2792
+ }
2793
+ const result = mirrorOfficialHarnessConfig({ home, dryRun, log });
2794
+ if (result.ok && result.changed && !dryRun) {
2795
+ log(' the Crew harness hot-watches its patch layers; the hub picks the imported configuration up without a restart');
2796
+ }
2797
+ return result;
2798
+ }
2799
+
2691
2800
  export async function npxInstall({
2692
2801
  home = homedir(),
2693
2802
  log = console.log,
@@ -3196,6 +3305,16 @@ export function npxStatus({
3196
3305
  }
3197
3306
  log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
3198
3307
  log(`Official 3080 UI bridge: ${officialWeb}`);
3308
+ // The desktop app carries its own bridge entry, which pins a revision: after a
3309
+ // payload update that entry is stale until it is re-pointed, so say which of the
3310
+ // three states the machine is in instead of leaving the panel silently old.
3311
+ const desktop = desktopStatus({ home });
3312
+ log(`Desktop app integration: ${
3313
+ !desktop.file_present ? 'not attached (run: dsh-crew desktop attach)'
3314
+ : desktop.attached && desktop.current ? 'attached'
3315
+ : desktop.attached ? 'attached to a stale bridge revision (run: dsh-crew desktop attach)'
3316
+ : 'not attached (run: dsh-crew desktop attach)'
3317
+ }`);
3199
3318
  log(`Codex Desktop integration: ${codex}`);
3200
3319
  log(`ZCode integration: ${zcode}`);
3201
3320
  log(`Claude Code integration: ${claude}`);
@@ -3291,6 +3410,16 @@ Commands:
3291
3410
  install persist the candidate package into Crew-owned state and register it
3292
3411
  integrate disabled: the official web profile is read-only (legacy bridge retired)
3293
3412
  detach disabled: the official web profile is read-only (legacy bridge retired)
3413
+ desktop the official DSH desktop app: attach|detach|status the Crew panel in its
3414
+ own patch layer (~/.dsh/profiles/desktop/cordis.patch.yml, hot-watched
3415
+ by the Harness). The only official-home write Crew performs, and only
3416
+ when this command is run; attach is idempotent and follows the current
3417
+ bridge revision, detach removes exactly the block it wrote
3418
+ config import|status the operator's user-level DSH configuration (models,
3419
+ providers, policy, MCP servers) from the official home into Crew's own
3420
+ home, so the Crew hub matches the desktop app. Read-only on ~/.dsh; no
3421
+ secrets are copied (provider entries keep their apiKeyEnv references);
3422
+ entries naming packages absent from Crew's runtime cohort are skipped
3294
3423
  status read-only report of launcher/installed versions and integrations
3295
3424
  inspect print the machine-readable extension capability/readiness contract
3296
3425
  jobs machine-first job API: list|get|watch|cancel|submit
@@ -3312,6 +3441,7 @@ Options:
3312
3441
  --replacement-default <id> with providers delete-plan: replacement Harness Default provider
3313
3442
  --confirm with providers delete: confirm the destructive mutation
3314
3443
  --purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
3444
+ --dry-run with desktop attach|detach: report the change without writing
3315
3445
  --help show this help
3316
3446
 
3317
3447
  Primary install: npm install -g @ran-sh/dsh-crew (then run: dsh-crew install)
@@ -3622,6 +3752,7 @@ function normalizeCommand(argv) {
3622
3752
  let expectedRevision;
3623
3753
  let replacementDefault;
3624
3754
  let confirm = false;
3755
+ let dryRun = false;
3625
3756
  let purgeOrphanCredentials = false;
3626
3757
  for (let index = 0; index < flags.length; index += 1) {
3627
3758
  if (flags[index] === '--candidate') {
@@ -3661,16 +3792,18 @@ function normalizeCommand(argv) {
3661
3792
  replacementDefault = flags[index].slice('--replacement-default='.length); flags.splice(index, 1); index -= 1;
3662
3793
  } else if (flags[index] === '--confirm') {
3663
3794
  confirm = true; flags.splice(index, 1); index -= 1;
3795
+ } else if (flags[index] === '--dry-run') {
3796
+ dryRun = true; flags.splice(index, 1); index -= 1;
3664
3797
  } else if (flags[index] === '--purge-orphan-credentials') {
3665
3798
  purgeOrphanCredentials = true; flags.splice(index, 1); index -= 1;
3666
3799
  }
3667
3800
  }
3668
- const knownFlags = new Set(['--purge']);
3801
+ const knownFlags = new Set(['--purge', '--dry-run']);
3669
3802
  const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
3670
3803
  const args = flags.filter((f) => !f.startsWith('--'));
3671
3804
  if (!Number.isInteger(after) || after < 0) unknown.push('--after');
3672
3805
  if (!['compact', 'full'].includes(detail)) unknown.push('--detail');
3673
- return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown };
3806
+ return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, dryRun, purgeOrphanCredentials, args, unknown };
3674
3807
  }
3675
3808
 
3676
3809
  /**
@@ -3682,7 +3815,7 @@ export async function runNpxCli({
3682
3815
  error = console.error,
3683
3816
  commands = {},
3684
3817
  } = {}) {
3685
- const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
3818
+ const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, dryRun, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
3686
3819
  if (command === '--help' || command === '-h' || command === 'help') {
3687
3820
  log(USAGE);
3688
3821
  return 0;
@@ -3691,7 +3824,7 @@ export async function runNpxCli({
3691
3824
  error(USAGE);
3692
3825
  return 1;
3693
3826
  }
3694
- if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
3827
+ if (unknown.length > 0 || !['install', 'integrate', 'detach', 'desktop', 'config', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
3695
3828
  error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
3696
3829
  return 1;
3697
3830
  }
@@ -3700,6 +3833,8 @@ export async function runNpxCli({
3700
3833
  install: commands.install ?? npxInstall,
3701
3834
  integrate: commands.integrate ?? npxIntegrate,
3702
3835
  detach: commands.detach ?? npxDetach,
3836
+ desktop: commands.desktop ?? npxDesktop,
3837
+ config: commands.config ?? npxConfig,
3703
3838
  status: commands.status ?? npxStatus,
3704
3839
  inspect: commands.inspect ?? npxInspect,
3705
3840
  jobs: commands.jobs ?? npxJobs,
@@ -3713,6 +3848,8 @@ export async function runNpxCli({
3713
3848
  let result;
3714
3849
  if (command === 'uninstall') result = await actions.uninstall({ purge, log });
3715
3850
  else if (command === 'update') result = await actions.update({ candidate, log });
3851
+ else if (command === 'desktop') result = await actions.desktop({ args, dryRun, log });
3852
+ else if (command === 'config') result = await actions.config({ args, dryRun, log });
3716
3853
  else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
3717
3854
  else if (command === 'providers') result = await actions.providers({ args, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, log });
3718
3855
  else if (command === 'credentials') result = await actions.credentials({ args, planId, expectedRevision, confirm, log });
@@ -406,7 +406,9 @@ export function registerOfficialWebBridge(ctx, options = {}) {
406
406
  ui_role: 'quick-controls',
407
407
  execution_plane: 'hub-3210',
408
408
  listen_port: 3210,
409
- full_control_plane_url: 'http://127.0.0.1:3210/',
409
+ // The usable entry: the hub mints its own session and redirects, because
410
+ // a bare port asks for a token only the hub's own process can sign.
411
+ full_control_plane_url: 'http://127.0.0.1:3210/_dsh/dsh-crew/control-plane',
410
412
  });
411
413
  },
412
414
  });
@@ -382,9 +382,27 @@ export function inspectProviderSettings(source) {
382
382
  return { ok: true, schema: 'harness-settings-llm-pi-ai-v1', providerIds: parsed.entries.map((entry) => entry.id), revision };
383
383
  }
384
384
 
385
+ /** Whether the settings document carries a Harness provider section at all. */
386
+ export function hasProviderSettingsSection(source) {
387
+ return typeof source === 'string' && source.split(/\r?\n/u).some((line) => /^llm-pi-ai:\s*$/u.test(line));
388
+ }
389
+
390
+ /** Whether the settings document declares a Harness default-model authority. */
391
+ export function hasHarnessDefaultBlock(source) {
392
+ return typeof source === 'string' && source.split(/\r?\n/u).some((line) => /^agent-default-model:\s*$/u.test(line));
393
+ }
394
+
385
395
  export function readProviderSettingsDeclarations(source, { file = 'harness/settings.yaml' } = {}) {
386
396
  const parsed = parseProviderMap(source);
387
- if (!parsed.ok) return { ok: false, code: parsed.code };
397
+ // A settings file with no `llm-pi-ai:` section declares no providers. That is
398
+ // not an unsupported schema — it is the state every home starts in and the
399
+ // state a provider migration writes into — so only a section that EXISTS but
400
+ // cannot be parsed is refused.
401
+ if (!parsed.ok) {
402
+ return parsed.code === 'PROVIDER_SETTINGS_SCHEMA_UNSUPPORTED' && !hasProviderSettingsSection(source)
403
+ ? { ok: true, declarations: [], absent: true, file }
404
+ : { ok: false, code: parsed.code };
405
+ }
388
406
  return {
389
407
  ok: true,
390
408
  declarations: parsed.entries.map((entry) => {
@@ -36,7 +36,7 @@ export {
36
36
  // included in the identity contract.
37
37
  const RUNTIME_ID = randomUUID();
38
38
 
39
- export const RUNTIME_VERSION = '2.2.1';
39
+ export const RUNTIME_VERSION = '2.2.2';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([