@muhaven/mcp 0.1.2 → 0.1.3

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.3] — 2026-05-16
11
+
12
+ Q2 fix bundle from the post-§4 queue closing four findings from §3e⁶
13
+ (broker-session-key-required-for-reads, broker-env-divergence,
14
+ mcp-serverinfo-version-stale) and unblocking the openclaw-skill ClawScan
15
+ fix (the `noExternal: ['@muhaven/mcp']` inline bundle requires this
16
+ version on npm before the skill can be republished).
17
+
18
+ ### Added
19
+
20
+ - **Read-only daemon posture**: the broker daemon now boots WITHOUT
21
+ `MUHAVEN_BROKER_SESSION_KEY`. In that mode the daemon still serves
22
+ `hello` + the JWT verbs (so `muhaven.read.*` tools work end-to-end via
23
+ the standalone `@muhaven/mcp` install), but any `sign_hash` request
24
+ returns the new `session_key_unavailable` broker error so write paths
25
+ fail with a clear remediation message instead of the daemon dying at
26
+ startup. Closes §3e⁶ F-broker-session-key-required-for-reads.
27
+ - **`muhaven-broker login --from-daemon` flag**: resolves backend +
28
+ dashboard URLs from the running daemon's `hello.effectiveConfig`
29
+ rather than the login CLI's env. Solves the daemon-vs-CLI env-divergence
30
+ problem when the two processes inherit different shell environments
31
+ (e.g. the daemon was launched by systemd/launchd, the CLI by ssh).
32
+ Mutually exclusive with explicit `--backend-base-url` /
33
+ `--dashboard-base-url`. Closes §3e⁶ F-broker-env-divergence.
34
+ - **`muhaven-broker doctor` surfaces the daemon's effective config**
35
+ and read-only-posture status — the operator can verify which backend
36
+ URL is actually in play before driving a login.
37
+
38
+ ### Changed
39
+
40
+ - **Broker protocol bumped 0.2.0 → 0.3.0** (additive — pre-0.3.0 clients
41
+ remain compatible):
42
+ - `hello.hasSessionKey` (optional `boolean`) — absence implies `true`
43
+ for back-compat.
44
+ - `hello.effectiveConfig` (optional `{ backendBaseUrl, dashboardBaseUrl }`).
45
+ - New `session_key_unavailable` broker error code.
46
+ - **`serverInfo.version`** in the MCP server's `initialize` response is
47
+ now build-time injected from `package.json#version` (tsup `define` on
48
+ `__SERVER_VERSION__`) rather than the previously hardcoded `'0.1.0'`
49
+ string in `src/server.ts`. Closes §3e⁶ F-mcp-serverinfo-version-stale.
50
+
51
+ ### Tests
52
+
53
+ - 134 vitest pass (up from 101 in 0.1.2). Net +33 cases:
54
+ - **+6** `config.test.ts` — `loadBrokerConfig` lazy-validation
55
+ (no key, empty string, valid key, malformed key) + env-driven backend
56
+ + dashboard URL surface.
57
+ - **+5** `daemon-handler.test.ts` — 0.3.0 protocol: `hello` surfaces
58
+ `hasSessionKey` + `effectiveConfig` from options, defaults `true`
59
+ when omitted, reflects `false` when set; `sign_hash` with
60
+ `NullSigner` returns `session_key_unavailable`; re-throws non-Missing
61
+ errors verbatim.
62
+ - **+9** `cli-parse-login-flags.test.ts` — flag parser unit cases
63
+ incl. `--from-daemon` mutual-exclusion guard.
64
+ - **+8** `session-key-required.test.ts` — `signEnvelope` probe of
65
+ `hello.hasSessionKey`; short-circuit returns `SESSION_KEY_REQUIRED`
66
+ for buy + claim with mint-URL pointing at `dashboardBaseUrl`;
67
+ safety-net mapping of inner `session_key_unavailable`;
68
+ probe-cache reuse; concurrent-call coalescing (one hello round-trip
69
+ for N callers); retry-after-rejection (eager cache clear); trailing-slash
70
+ mintUrl strip.
71
+ - **+2** `server-version.test.ts` — runtime fallback returns
72
+ `package.json#version`; matches `manifest.json#version`.
73
+ - **+2** `build-artifacts.test.ts` — hostname-migration guard + new
74
+ `__SERVER_VERSION__` literal grep in bundled dist.
75
+ - **+1** `daemon-lifecycle.test.ts` — read-only-posture boot test
76
+ replaces the prior "exits on missing key" assertion; added
77
+ "exits on a malformed session key" as the second negative-path test.
78
+
10
79
  ## [0.1.2] — 2026-05-11
11
80
 
12
81
  Re-roll of the `0.1.1` workflow-validation cut. `0.1.1` never reached npm:
@@ -181,7 +250,8 @@ Workstream H)
181
250
  muhaven-mcp-0.1.0.tgz
182
251
  ```
183
252
 
184
- [Unreleased]: https://github.com/hasToDev/muhaven/compare/mcp-v0.1.2...HEAD
253
+ [Unreleased]: https://github.com/hasToDev/muhaven/compare/mcp-v0.1.3...HEAD
254
+ [0.1.3]: https://github.com/hasToDev/muhaven/releases/tag/mcp-v0.1.3
185
255
  [0.1.2]: https://github.com/hasToDev/muhaven/releases/tag/mcp-v0.1.2
186
256
  [0.1.1]: https://github.com/hasToDev/muhaven/releases/tag/mcp-v0.1.1
187
257
  [0.1.0]: https://github.com/hasToDev/muhaven/releases/tag/mcp-v0.1.0
package/dist/broker.cjs CHANGED
@@ -73,23 +73,26 @@ function loadMcpConfig(env = process.env) {
73
73
  }
74
74
  var PRIVKEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
75
75
  function loadBrokerConfig(env = process.env) {
76
- const sessionKeyHex = env.MUHAVEN_BROKER_SESSION_KEY;
77
- if (!sessionKeyHex) {
78
- throw new Error(
79
- "MUHAVEN_BROKER_SESSION_KEY is required (0x-prefixed 32-byte hex). Mint a session key via the dashboard policy-template install flow."
80
- );
81
- }
82
- if (!PRIVKEY_HEX_RE.test(sessionKeyHex)) {
83
- throw new Error("MUHAVEN_BROKER_SESSION_KEY must be a 0x-prefixed 32-byte hex string");
76
+ const sessionKeyHexRaw = env.MUHAVEN_BROKER_SESSION_KEY;
77
+ let sessionKeyHex;
78
+ if (sessionKeyHexRaw && sessionKeyHexRaw.length > 0) {
79
+ if (!PRIVKEY_HEX_RE.test(sessionKeyHexRaw)) {
80
+ throw new Error("MUHAVEN_BROKER_SESSION_KEY must be a 0x-prefixed 32-byte hex string");
81
+ }
82
+ sessionKeyHex = sessionKeyHexRaw;
84
83
  }
85
84
  const endpoint = env.MUHAVEN_BROKER_ENDPOINT ?? defaultBrokerEndpoint();
86
85
  const maxRequestBytes = readEnvInt("MUHAVEN_BROKER_MAX_BYTES", DEFAULT_BROKER_MAX_BYTES, env);
87
86
  const requestTimeoutMs = readEnvInt("MUHAVEN_BROKER_TIMEOUT_MS", DEFAULT_BROKER_TIMEOUT_MS, env);
87
+ const backendBaseUrl = trimTrailingSlash(env.MUHAVEN_BACKEND_URL ?? DEFAULT_BACKEND_URL);
88
+ const dashboardBaseUrl = trimTrailingSlash(env.MUHAVEN_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL);
88
89
  return {
89
90
  endpoint,
90
91
  sessionKeyHex,
91
92
  maxRequestBytes,
92
- requestTimeoutMs
93
+ requestTimeoutMs,
94
+ backendBaseUrl,
95
+ dashboardBaseUrl
93
96
  };
94
97
  }
95
98
  var BrokerClientError = class extends Error {
@@ -522,7 +525,7 @@ async function openKeystore(options = {}) {
522
525
  }
523
526
 
524
527
  // src/broker/protocol.ts
525
- var BROKER_PROTOCOL_VERSION = "0.2.0";
528
+ var BROKER_PROTOCOL_VERSION = "0.3.0";
526
529
  var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
527
530
  var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
528
531
  function isHashHex(value) {
@@ -608,6 +611,21 @@ function parseBrokerRequest(line) {
608
611
  function serializeResponse(res) {
609
612
  return JSON.stringify(res) + "\n";
610
613
  }
614
+ var MissingSessionKeyError = class extends Error {
615
+ constructor() {
616
+ super(
617
+ "session_key_unavailable: daemon booted in read-only posture (no MUHAVEN_BROKER_SESSION_KEY at env-load time). Mint a session key via the dashboard /agent/policy/transition flow, set MUHAVEN_BROKER_SESSION_KEY, and restart the daemon. (Note: `muhaven-broker login` mints a JWT, NOT a session key \u2014 do not loop on that command for this error.)"
618
+ );
619
+ this.name = "MissingSessionKeyError";
620
+ }
621
+ };
622
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
623
+ var NullSigner = class {
624
+ address = ZERO_ADDRESS;
625
+ async signHash(_hash) {
626
+ throw new MissingSessionKeyError();
627
+ }
628
+ };
611
629
  var ViemSigner = class {
612
630
  account;
613
631
  constructor(privateKey) {
@@ -624,7 +642,7 @@ var ViemSigner = class {
624
642
  // src/broker/daemon.ts
625
643
  var noopLogger = (_e) => {
626
644
  };
627
- async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3)) {
645
+ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}) {
628
646
  switch (req.type) {
629
647
  case "hello": {
630
648
  let hasJwt = false;
@@ -634,16 +652,26 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
634
652
  } catch {
635
653
  hasJwt = false;
636
654
  }
655
+ const hasSessionKey = options.hasSessionKey ?? true;
637
656
  return {
638
657
  type: "hello",
639
658
  version: BROKER_PROTOCOL_VERSION,
640
659
  sessionKeyAddress: signer.address,
641
- hasJwt
660
+ hasJwt,
661
+ hasSessionKey,
662
+ ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
642
663
  };
643
664
  }
644
665
  case "sign_hash": {
645
- const signature = await signer.signHash(req.hash);
646
- return { type: "sign_hash", signature, signerAddress: signer.address };
666
+ try {
667
+ const signature = await signer.signHash(req.hash);
668
+ return { type: "sign_hash", signature, signerAddress: signer.address };
669
+ } catch (err) {
670
+ if (err instanceof MissingSessionKeyError) {
671
+ return errorResponse("session_key_unavailable", err.message);
672
+ }
673
+ throw err;
674
+ }
647
675
  }
648
676
  case "store_jwt": {
649
677
  try {
@@ -715,9 +743,24 @@ var BrokerDaemon = class {
715
743
  log;
716
744
  config;
717
745
  keystore;
746
+ /**
747
+ * Whether a session-key private half is actually loaded. `false` =
748
+ * daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
749
+ * at env-load time) and uses a `NullSigner` whose `signHash` throws.
750
+ */
751
+ hasSessionKey;
718
752
  constructor(options) {
719
753
  this.config = options.config;
720
- this.signer = options.signer ?? new ViemSigner(options.config.sessionKeyHex);
754
+ if (options.signer) {
755
+ this.signer = options.signer;
756
+ this.hasSessionKey = true;
757
+ } else if (options.config.sessionKeyHex) {
758
+ this.signer = new ViemSigner(options.config.sessionKeyHex);
759
+ this.hasSessionKey = true;
760
+ } else {
761
+ this.signer = new NullSigner();
762
+ this.hasSessionKey = false;
763
+ }
721
764
  this.keystore = options.keystore ?? null;
722
765
  this.log = options.logger ?? noopLogger;
723
766
  this.server = net.createServer((socket) => this.onConnection(socket));
@@ -755,6 +798,7 @@ var BrokerDaemon = class {
755
798
  meta: {
756
799
  endpoint: this.config.endpoint,
757
800
  signer: this.signer.address,
801
+ hasSessionKey: this.hasSessionKey,
758
802
  keystore: this.keystore.backend,
759
803
  version: BROKER_PROTOCOL_VERSION
760
804
  }
@@ -836,7 +880,19 @@ var BrokerDaemon = class {
836
880
  return;
837
881
  }
838
882
  try {
839
- const res = await handleBrokerRequest(parsed, this.signer, this.keystore);
883
+ const res = await handleBrokerRequest(
884
+ parsed,
885
+ this.signer,
886
+ this.keystore,
887
+ void 0,
888
+ {
889
+ hasSessionKey: this.hasSessionKey,
890
+ effectiveConfig: {
891
+ backendBaseUrl: this.config.backendBaseUrl,
892
+ dashboardBaseUrl: this.config.dashboardBaseUrl
893
+ }
894
+ }
895
+ );
840
896
  socket.end(serializeResponse(res));
841
897
  } catch (err) {
842
898
  this.log({
@@ -852,6 +908,15 @@ var BrokerDaemon = class {
852
908
  };
853
909
  async function runBrokerDaemonCli() {
854
910
  const config = loadBrokerConfig();
911
+ if (!config.sessionKeyHex) {
912
+ process.stderr.write(
913
+ JSON.stringify({
914
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
915
+ level: "info",
916
+ msg: "broker booting in read-only posture (no MUHAVEN_BROKER_SESSION_KEY)"
917
+ }) + "\n"
918
+ );
919
+ }
855
920
  const daemon = new BrokerDaemon({
856
921
  config,
857
922
  logger: (e) => {
@@ -904,9 +969,11 @@ function parseLoginFlags(argv) {
904
969
  let brokerEndpoint;
905
970
  let backendBaseUrl;
906
971
  let dashboardBaseUrl;
972
+ let fromDaemon = false;
907
973
  for (let i = 0; i < argv.length; i++) {
908
974
  const a = argv[i];
909
975
  if (a === "--no-launch-browser") noLaunchBrowser = true;
976
+ else if (a === "--from-daemon") fromDaemon = true;
910
977
  else if (a === "--broker-endpoint" && i + 1 < argv.length) {
911
978
  brokerEndpoint = argv[++i];
912
979
  } else if (a === "--backend-base-url" && i + 1 < argv.length) {
@@ -917,7 +984,12 @@ function parseLoginFlags(argv) {
917
984
  throw new Error(`unknown flag: ${a}`);
918
985
  }
919
986
  }
920
- return { noLaunchBrowser, brokerEndpoint, backendBaseUrl, dashboardBaseUrl };
987
+ if (fromDaemon && (backendBaseUrl || dashboardBaseUrl)) {
988
+ throw new Error(
989
+ "--from-daemon is mutually exclusive with --backend-base-url / --dashboard-base-url"
990
+ );
991
+ }
992
+ return { noLaunchBrowser, brokerEndpoint, backendBaseUrl, dashboardBaseUrl, fromDaemon };
921
993
  }
922
994
  async function tryLaunchBrowser(url) {
923
995
  return new Promise((resolve) => {
@@ -931,7 +1003,9 @@ async function runLogin(argv) {
931
1003
  flags = parseLoginFlags(argv);
932
1004
  } catch (err) {
933
1005
  printErr(`error: ${err.message}`);
934
- printErr("usage: muhaven-broker login [--no-launch-browser] [--broker-endpoint PATH] [--backend-base-url URL] [--dashboard-base-url URL]");
1006
+ printErr(
1007
+ "usage: muhaven-broker login [--no-launch-browser] [--broker-endpoint PATH] [--from-daemon | (--backend-base-url URL --dashboard-base-url URL)]"
1008
+ );
935
1009
  return 2;
936
1010
  }
937
1011
  const env = process.env;
@@ -945,8 +1019,9 @@ async function runLogin(argv) {
945
1019
  endpoint: config.brokerEndpoint,
946
1020
  timeoutMs: config.brokerTimeoutMs
947
1021
  });
1022
+ let helloResult;
948
1023
  try {
949
- await broker.hello();
1024
+ helloResult = await broker.hello();
950
1025
  } catch (err) {
951
1026
  printErr(
952
1027
  `cannot reach muhaven-broker daemon at ${config.brokerEndpoint}: ${err.message}`
@@ -954,9 +1029,42 @@ async function runLogin(argv) {
954
1029
  printErr("hint: start the daemon first (`muhaven-broker` with no subcommand).");
955
1030
  return 1;
956
1031
  }
1032
+ let backendBaseUrl = config.backendBaseUrl;
1033
+ let dashboardBaseUrl = config.dashboardBaseUrl;
1034
+ if (flags.fromDaemon) {
1035
+ if (!helloResult.effectiveConfig) {
1036
+ printErr(
1037
+ "--from-daemon requested but broker did not return effectiveConfig (daemon is older than protocol 0.3.0). Upgrade the daemon (`@muhaven/mcp@0.1.3+`) or drop the flag."
1038
+ );
1039
+ return 1;
1040
+ }
1041
+ const daemonBackend = helloResult.effectiveConfig.backendBaseUrl;
1042
+ const daemonDashboard = helloResult.effectiveConfig.dashboardBaseUrl;
1043
+ if (!daemonBackend || !daemonDashboard) {
1044
+ printErr(
1045
+ "--from-daemon: daemon returned an empty backend/dashboard URL \u2014 refusing to proceed."
1046
+ );
1047
+ return 1;
1048
+ }
1049
+ if (daemonBackend !== config.backendBaseUrl) {
1050
+ print(
1051
+ `\u26A0 daemon backend (${daemonBackend}) differs from CLI env (${config.backendBaseUrl}). Using daemon's value per --from-daemon.`
1052
+ );
1053
+ }
1054
+ if (daemonDashboard !== config.dashboardBaseUrl) {
1055
+ print(
1056
+ `\u26A0 daemon dashboard (${daemonDashboard}) differs from CLI env (${config.dashboardBaseUrl}). Using daemon's value per --from-daemon.`
1057
+ );
1058
+ }
1059
+ backendBaseUrl = daemonBackend;
1060
+ dashboardBaseUrl = daemonDashboard;
1061
+ print(`Using daemon's effective config:`);
1062
+ print(` backend: ${backendBaseUrl}`);
1063
+ print(` dashboard: ${dashboardBaseUrl}`);
1064
+ }
957
1065
  const flow = new DeviceFlowClient({
958
- backendBaseUrl: config.backendBaseUrl,
959
- dashboardBaseUrl: config.dashboardBaseUrl,
1066
+ backendBaseUrl,
1067
+ dashboardBaseUrl,
960
1068
  requesterMetadata: {
961
1069
  processName: detectMcpHost(),
962
1070
  hostname: os.hostname(),
@@ -1071,7 +1179,13 @@ async function runDoctor() {
1071
1179
  });
1072
1180
  try {
1073
1181
  const h = await broker.hello();
1074
- print(`Broker daemon : reachable (proto v${h.version}, signer ${h.sessionKeyAddress}, hasJwt=${h.hasJwt})`);
1182
+ const hasKey = h.hasSessionKey ?? true;
1183
+ const keyTag = hasKey ? `signer ${h.sessionKeyAddress}` : "NO SESSION KEY (read-only posture)";
1184
+ print(`Broker daemon : reachable (proto v${h.version}, ${keyTag}, hasJwt=${h.hasJwt})`);
1185
+ if (h.effectiveConfig) {
1186
+ print(`Daemon backend URL: ${h.effectiveConfig.backendBaseUrl}`);
1187
+ print(`Daemon dashboard : ${h.effectiveConfig.dashboardBaseUrl}`);
1188
+ }
1075
1189
  return 0;
1076
1190
  } catch (err) {
1077
1191
  print(`Broker daemon : NOT reachable (${err.message})`);
@@ -1084,6 +1198,7 @@ function printUsage() {
1084
1198
  print("");
1085
1199
  print(" (no subcommand) Run the daemon (production mode)");
1086
1200
  print(" login Acquire a JWT via the device-code flow + store in keystore");
1201
+ print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
1087
1202
  print(" logout Clear the JWT from the keystore");
1088
1203
  print(" doctor Print environment + keystore + reachability report");
1089
1204
  print(" -h, --help Show this help");
@@ -1111,6 +1226,7 @@ async function runCli(argv) {
1111
1226
  }
1112
1227
  }
1113
1228
 
1229
+ exports.parseLoginFlags = parseLoginFlags;
1114
1230
  exports.runCli = runCli;
1115
1231
  exports.runDoctor = runDoctor;
1116
1232
  exports.runLogin = runLogin;
package/dist/broker.d.cts CHANGED
@@ -8,9 +8,29 @@
8
8
  * doctor → environment + keystore capability report
9
9
  * --help, -h → usage
10
10
  */
11
+ interface LoginFlags {
12
+ noLaunchBrowser: boolean;
13
+ brokerEndpoint?: string;
14
+ backendBaseUrl?: string;
15
+ dashboardBaseUrl?: string;
16
+ /**
17
+ * Resolve `backendBaseUrl` + `dashboardBaseUrl` from the running
18
+ * daemon's view (returned in `hello.effectiveConfig`) rather than the
19
+ * CLI's env. Solves the daemon-vs-CLI env-divergence problem when the
20
+ * CLI is launched from a different shell (ssh, IDE-spawned terminal)
21
+ * than the systemd / launchd-launched daemon. Closes §3e⁶
22
+ * F-broker-env-divergence.
23
+ *
24
+ * Mutually exclusive with explicit `--backend-base-url` /
25
+ * `--dashboard-base-url`; the CLI rejects the combination so the
26
+ * operator picks one source of truth.
27
+ */
28
+ fromDaemon: boolean;
29
+ }
30
+ declare function parseLoginFlags(argv: readonly string[]): LoginFlags;
11
31
  declare function runLogin(argv: readonly string[]): Promise<number>;
12
32
  declare function runLogout(): Promise<number>;
13
33
  declare function runDoctor(): Promise<number>;
14
34
  declare function runCli(argv: readonly string[]): Promise<number>;
15
35
 
16
- export { runCli, runDoctor, runLogin, runLogout };
36
+ export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };
package/dist/broker.d.ts CHANGED
@@ -8,9 +8,29 @@
8
8
  * doctor → environment + keystore capability report
9
9
  * --help, -h → usage
10
10
  */
11
+ interface LoginFlags {
12
+ noLaunchBrowser: boolean;
13
+ brokerEndpoint?: string;
14
+ backendBaseUrl?: string;
15
+ dashboardBaseUrl?: string;
16
+ /**
17
+ * Resolve `backendBaseUrl` + `dashboardBaseUrl` from the running
18
+ * daemon's view (returned in `hello.effectiveConfig`) rather than the
19
+ * CLI's env. Solves the daemon-vs-CLI env-divergence problem when the
20
+ * CLI is launched from a different shell (ssh, IDE-spawned terminal)
21
+ * than the systemd / launchd-launched daemon. Closes §3e⁶
22
+ * F-broker-env-divergence.
23
+ *
24
+ * Mutually exclusive with explicit `--backend-base-url` /
25
+ * `--dashboard-base-url`; the CLI rejects the combination so the
26
+ * operator picks one source of truth.
27
+ */
28
+ fromDaemon: boolean;
29
+ }
30
+ declare function parseLoginFlags(argv: readonly string[]): LoginFlags;
11
31
  declare function runLogin(argv: readonly string[]): Promise<number>;
12
32
  declare function runLogout(): Promise<number>;
13
33
  declare function runDoctor(): Promise<number>;
14
34
  declare function runCli(argv: readonly string[]): Promise<number>;
15
35
 
16
- export { runCli, runDoctor, runLogin, runLogout };
36
+ export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };