@muhaven/mcp 0.3.0 → 0.4.1

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/dist/broker.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
  var os = require('os');
4
4
  var child_process = require('child_process');
5
5
  var path = require('path');
6
+ var readline = require('readline');
6
7
  var net = require('net');
7
8
  var promises = require('fs/promises');
8
9
  var accounts = require('viem/accounts');
@@ -17,6 +18,7 @@ var DEFAULT_BROKER_MAX_BYTES = 64 * 1024;
17
18
  var DEFAULT_JWT_CACHE_TTL_SEC = 30;
18
19
  var DEFAULT_BUNDLER_TIMEOUT_MS = 2e4;
19
20
  var DEFAULT_CHAIN_ID = 421614;
21
+ var DEFAULT_BROKER_RPC_URL = "https://sepolia-rollup.arbitrum.io/rpc";
20
22
  var DEFAULT_ENTRY_POINT_ADDRESS = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
21
23
  var ADDRESS_HEX_RE = /^0x[0-9a-fA-F]{40}$/;
22
24
  function defaultBrokerEndpoint() {
@@ -162,8 +164,8 @@ function loadBrokerConfig(env = process.env) {
162
164
  env.MUHAVEN_DASHBOARD_URL,
163
165
  DEFAULT_DASHBOARD_URL
164
166
  );
165
- const chainRpcUrlRaw = readEnv("MUHAVEN_BROKER_RPC_URL", env) ?? readEnv("MUHAVEN_BUNDLER_URL", env);
166
- const chainRpcUrl = chainRpcUrlRaw === void 0 ? void 0 : resolvePublicUrlEnv(
167
+ const chainRpcUrlRaw = readEnv("MUHAVEN_BROKER_RPC_URL", env) ?? readEnv("MUHAVEN_BUNDLER_URL", env) ?? DEFAULT_BROKER_RPC_URL;
168
+ const chainRpcUrl = resolvePublicUrlEnv(
167
169
  "MUHAVEN_BROKER_RPC_URL",
168
170
  chainRpcUrlRaw,
169
171
  chainRpcUrlRaw
@@ -2366,6 +2368,63 @@ async function runBrokerDaemonCli() {
2366
2368
  await new Promise(() => {
2367
2369
  });
2368
2370
  }
2371
+
2372
+ // src/broker/session-input.ts
2373
+ var SESSION_KEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
2374
+ function validateSessionKeyShape(key) {
2375
+ if (key.length === 0) return "session key is empty";
2376
+ if (!key.startsWith("0x")) return "session key must be 0x-prefixed";
2377
+ if (!SESSION_KEY_HEX_RE.test(key)) {
2378
+ return `session key must be a 0x-prefixed 32-byte hex string (got ${key.length} chars; expected 66)`;
2379
+ }
2380
+ return null;
2381
+ }
2382
+ async function resolveSessionKey(opts) {
2383
+ const { sessionFlag, policy, deps } = opts;
2384
+ if (sessionFlag !== void 0) {
2385
+ let raw;
2386
+ if (sessionFlag === "-") {
2387
+ const stdin = await deps.readStdinAll();
2388
+ raw = stdin.trim();
2389
+ if (raw.length === 0) {
2390
+ return { kind: "error", message: "--session - was given but stdin was empty" };
2391
+ }
2392
+ } else {
2393
+ raw = sessionFlag.trim();
2394
+ }
2395
+ const shapeErr2 = validateSessionKeyShape(raw);
2396
+ if (shapeErr2) return { kind: "error", message: shapeErr2 };
2397
+ return { kind: "key", key: raw };
2398
+ }
2399
+ if (!deps.isTty) {
2400
+ if (policy === "mint-fallback") return { kind: "mint" };
2401
+ return {
2402
+ kind: "error",
2403
+ message: "no session key provided and stdin is not a TTY \u2014 pass --session <key> (or `--session -` to pipe it), or run `muhaven-broker setup` to mint a fresh key"
2404
+ };
2405
+ }
2406
+ const hasKey = await deps.promptYesNo(
2407
+ "Do you have a session key from the dashboard? [Y/n] "
2408
+ );
2409
+ if (!hasKey) {
2410
+ if (policy === "mint-fallback") return { kind: "mint" };
2411
+ return {
2412
+ kind: "error",
2413
+ message: "a session key is required for this command \u2014 paste the dashboard-minted key, or run `muhaven-broker setup` to mint one"
2414
+ };
2415
+ }
2416
+ const pasted = (await deps.promptSecret("Paste the session key: ")).trim();
2417
+ const shapeErr = validateSessionKeyShape(pasted);
2418
+ if (shapeErr) {
2419
+ return {
2420
+ kind: "error",
2421
+ message: `${shapeErr} \u2014 re-run and paste the key from the dashboard's session-reveal modal`
2422
+ };
2423
+ }
2424
+ return { kind: "key", key: pasted };
2425
+ }
2426
+
2427
+ // src/broker/setup.ts
2369
2428
  var DANGEROUS_NODE_ENV_VARS = [
2370
2429
  "NODE_OPTIONS",
2371
2430
  "NODE_TLS_REJECT_UNAUTHORIZED",
@@ -2453,6 +2512,26 @@ function validateBrokerEndpointFlag(value, platformId) {
2453
2512
  }
2454
2513
  return null;
2455
2514
  }
2515
+ var LOGIN_SEED_ENV_KEYS = [
2516
+ "MUHAVEN_BACKEND_URL",
2517
+ "MUHAVEN_DASHBOARD_URL",
2518
+ "MUHAVEN_BROKER_ENDPOINT"
2519
+ ];
2520
+ async function withSeededLoginEnv(effectiveEnv, fn) {
2521
+ const originalValues = {};
2522
+ for (const k of LOGIN_SEED_ENV_KEYS) {
2523
+ originalValues[k] = process.env[k];
2524
+ if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
2525
+ }
2526
+ try {
2527
+ return await fn();
2528
+ } finally {
2529
+ for (const k of LOGIN_SEED_ENV_KEYS) {
2530
+ if (originalValues[k] === void 0) delete process.env[k];
2531
+ else process.env[k] = originalValues[k];
2532
+ }
2533
+ }
2534
+ }
2456
2535
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2457
2536
  async function waitForBroker(options) {
2458
2537
  const timeoutMs = options.timeoutMs ?? 8e3;
@@ -2490,6 +2569,7 @@ function parseSetupFlags(argv) {
2490
2569
  let brokerEndpoint;
2491
2570
  let backendBaseUrl;
2492
2571
  let dashboardBaseUrl;
2572
+ let brokerRpcUrl;
2493
2573
  let skipLogin = false;
2494
2574
  const register = [];
2495
2575
  let registerScope = "user";
@@ -2501,6 +2581,7 @@ function parseSetupFlags(argv) {
2501
2581
  else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
2502
2582
  else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
2503
2583
  else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
2584
+ else if (a === "--broker-rpc-url" && i + 1 < argv.length) brokerRpcUrl = argv[++i];
2504
2585
  else if (a === "--register" && i + 1 < argv.length) {
2505
2586
  const value = argv[++i];
2506
2587
  for (const raw of value.split(",")) {
@@ -2531,6 +2612,7 @@ function parseSetupFlags(argv) {
2531
2612
  brokerEndpoint,
2532
2613
  backendBaseUrl,
2533
2614
  dashboardBaseUrl,
2615
+ brokerRpcUrl,
2534
2616
  skipLogin,
2535
2617
  register,
2536
2618
  registerScope
@@ -2654,7 +2736,7 @@ async function runSetup(argv, deps) {
2654
2736
  } catch (err) {
2655
2737
  deps.printErr(`error: ${err.message}`);
2656
2738
  deps.printErr(
2657
- "usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL]\n [--register HOST[,HOST...]] [--register-scope user|project|local]"
2739
+ "usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL] [--broker-rpc-url URL]\n [--register HOST[,HOST...]] [--register-scope user|project|local]"
2658
2740
  );
2659
2741
  return 2;
2660
2742
  }
@@ -2679,6 +2761,13 @@ async function runSetup(argv, deps) {
2679
2761
  return 2;
2680
2762
  }
2681
2763
  }
2764
+ if (flags.brokerRpcUrl) {
2765
+ const err = validateHttpUrlFlag("--broker-rpc-url", flags.brokerRpcUrl);
2766
+ if (err) {
2767
+ deps.printErr(`error: ${err}`);
2768
+ return 2;
2769
+ }
2770
+ }
2682
2771
  const overrides = applyEnvDefaults({
2683
2772
  env: deps.env,
2684
2773
  platformId: deps.platformId,
@@ -2694,6 +2783,7 @@ async function runSetup(argv, deps) {
2694
2783
  if (flags.brokerEndpoint) effectiveEnv.MUHAVEN_BROKER_ENDPOINT = flags.brokerEndpoint;
2695
2784
  if (flags.backendBaseUrl) effectiveEnv.MUHAVEN_BACKEND_URL = flags.backendBaseUrl;
2696
2785
  if (flags.dashboardBaseUrl) effectiveEnv.MUHAVEN_DASHBOARD_URL = flags.dashboardBaseUrl;
2786
+ if (flags.brokerRpcUrl) effectiveEnv.MUHAVEN_BROKER_RPC_URL = flags.brokerRpcUrl;
2697
2787
  for (const name of overrides.preserved) {
2698
2788
  deps.print(`Env preserved: ${name} (set in your shell)`);
2699
2789
  }
@@ -2702,12 +2792,32 @@ async function runSetup(argv, deps) {
2702
2792
  }
2703
2793
  let sessionKey = effectiveEnv.MUHAVEN_BROKER_SESSION_KEY;
2704
2794
  let mintedKey = false;
2705
- if (!sessionKey || sessionKey === "") {
2706
- sessionKey = deps.mintSessionKey();
2707
- mintedKey = true;
2708
- deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
2709
- } else {
2795
+ if (sessionKey && sessionKey.length > 0) {
2710
2796
  deps.print("Session key: using MUHAVEN_BROKER_SESSION_KEY from env.");
2797
+ } else {
2798
+ const sessionInput = deps.sessionInput ?? {
2799
+ isTty: false,
2800
+ readStdinAll: async () => "",
2801
+ promptYesNo: async () => false,
2802
+ promptSecret: async () => ""
2803
+ };
2804
+ const resolution = await resolveSessionKey({
2805
+ sessionFlag: void 0,
2806
+ policy: "mint-fallback",
2807
+ deps: sessionInput
2808
+ });
2809
+ if (resolution.kind === "error") {
2810
+ deps.printErr(`error: ${resolution.message}`);
2811
+ return 2;
2812
+ }
2813
+ if (resolution.kind === "key") {
2814
+ sessionKey = resolution.key;
2815
+ deps.print("Session key: using the pasted dashboard key.");
2816
+ } else {
2817
+ sessionKey = deps.mintSessionKey();
2818
+ mintedKey = true;
2819
+ deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
2820
+ }
2711
2821
  }
2712
2822
  effectiveEnv.MUHAVEN_BROKER_SESSION_KEY = sessionKey;
2713
2823
  if (flags.foreground) {
@@ -2756,7 +2866,11 @@ async function runSetup(argv, deps) {
2756
2866
  MUHAVEN_BROKER_ENDPOINT: config.brokerEndpoint,
2757
2867
  MUHAVEN_BACKEND_URL: effectiveEnv.MUHAVEN_BACKEND_URL,
2758
2868
  MUHAVEN_DASHBOARD_URL: effectiveEnv.MUHAVEN_DASHBOARD_URL,
2759
- MUHAVEN_BROKER_SESSION_KEY: sessionKey
2869
+ MUHAVEN_BROKER_SESSION_KEY: sessionKey,
2870
+ // Forward the chain RPC URL only when resolved (flag or shell env);
2871
+ // absent → the daemon's loadBrokerConfig applies the public Arb
2872
+ // Sepolia default.
2873
+ ...effectiveEnv.MUHAVEN_BROKER_RPC_URL ? { MUHAVEN_BROKER_RPC_URL: effectiveEnv.MUHAVEN_BROKER_RPC_URL } : {}
2760
2874
  }
2761
2875
  });
2762
2876
  try {
@@ -2791,21 +2905,7 @@ async function runSetup(argv, deps) {
2791
2905
  if (flags.dashboardBaseUrl) {
2792
2906
  loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
2793
2907
  }
2794
- const restorationKeys = ["MUHAVEN_BACKEND_URL", "MUHAVEN_DASHBOARD_URL", "MUHAVEN_BROKER_ENDPOINT"];
2795
- const originalValues = {};
2796
- for (const k of restorationKeys) {
2797
- originalValues[k] = process.env[k];
2798
- if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
2799
- }
2800
- let code;
2801
- try {
2802
- code = await deps.runLogin(loginArgv);
2803
- } finally {
2804
- for (const k of restorationKeys) {
2805
- if (originalValues[k] === void 0) delete process.env[k];
2806
- else process.env[k] = originalValues[k];
2807
- }
2808
- }
2908
+ const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
2809
2909
  if (code !== 0) {
2810
2910
  deps.printErr(
2811
2911
  "Setup: login step failed \u2014 daemon is still running, re-run `muhaven-broker login` to retry."
@@ -2889,13 +2989,15 @@ async function runStop(deps) {
2889
2989
  deps.print("Broker daemon: not running, nothing to stop.");
2890
2990
  return 0;
2891
2991
  }
2892
- try {
2893
- await broker.clearJwt();
2894
- deps.print("JWT cleared from keystore.");
2895
- } catch (err) {
2896
- deps.print(
2897
- `Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
2898
- );
2992
+ if (deps.clearJwtOnStop ?? true) {
2993
+ try {
2994
+ await broker.clearJwt();
2995
+ deps.print("JWT cleared from keystore.");
2996
+ } catch (err) {
2997
+ deps.print(
2998
+ `Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
2999
+ );
3000
+ }
2899
3001
  }
2900
3002
  const pid = hello.pid;
2901
3003
  if (pid === void 0) {
@@ -2953,6 +3055,212 @@ function defaultKillProcess(pid, signal) {
2953
3055
  }
2954
3056
  }
2955
3057
 
3058
+ // src/broker/bring-up.ts
3059
+ function parseBringUpFlags(argv) {
3060
+ let session;
3061
+ let noLaunchBrowser = false;
3062
+ let skipLogin = false;
3063
+ let brokerEndpoint;
3064
+ let backendBaseUrl;
3065
+ let dashboardBaseUrl;
3066
+ let brokerRpcUrl;
3067
+ for (let i = 0; i < argv.length; i++) {
3068
+ const a = argv[i];
3069
+ if (a === "--no-launch-browser") noLaunchBrowser = true;
3070
+ else if (a === "--skip-login") skipLogin = true;
3071
+ else if (a === "--session") {
3072
+ const next = argv[i + 1];
3073
+ if (next === void 0) {
3074
+ throw new Error("--session requires a value (a 0x\u2026 key, or `-` to read from stdin)");
3075
+ }
3076
+ if (next !== "-" && next.startsWith("-")) {
3077
+ throw new Error(`--session requires a key value (or \`-\` for stdin), got flag: ${next}`);
3078
+ }
3079
+ session = argv[++i];
3080
+ } else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
3081
+ else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
3082
+ else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
3083
+ else if (a === "--broker-rpc-url" && i + 1 < argv.length) brokerRpcUrl = argv[++i];
3084
+ else throw new Error(`unknown flag: ${a}`);
3085
+ }
3086
+ return {
3087
+ session,
3088
+ noLaunchBrowser,
3089
+ skipLogin,
3090
+ brokerEndpoint,
3091
+ backendBaseUrl,
3092
+ dashboardBaseUrl,
3093
+ brokerRpcUrl
3094
+ };
3095
+ }
3096
+ function usageLine(mode) {
3097
+ return `usage: muhaven-broker ${mode} --session <key|-> [--no-launch-browser] [--skip-login]
3098
+ [--broker-endpoint PATH] [--backend-base-url URL]
3099
+ [--dashboard-base-url URL] [--broker-rpc-url URL]
3100
+ (omit --session to be asked interactively; pipe the key with \`--session -\`)`;
3101
+ }
3102
+ async function runBringUp(mode, argv, deps) {
3103
+ let flags;
3104
+ try {
3105
+ flags = parseBringUpFlags(argv);
3106
+ } catch (err) {
3107
+ deps.printErr(`error: ${err.message}`);
3108
+ deps.printErr(usageLine(mode));
3109
+ return 2;
3110
+ }
3111
+ if (flags.backendBaseUrl) {
3112
+ const e = validateHttpUrlFlag("--backend-base-url", flags.backendBaseUrl);
3113
+ if (e) {
3114
+ deps.printErr(`error: ${e}`);
3115
+ return 2;
3116
+ }
3117
+ }
3118
+ if (flags.dashboardBaseUrl) {
3119
+ const e = validateHttpUrlFlag("--dashboard-base-url", flags.dashboardBaseUrl);
3120
+ if (e) {
3121
+ deps.printErr(`error: ${e}`);
3122
+ return 2;
3123
+ }
3124
+ }
3125
+ if (flags.brokerEndpoint) {
3126
+ const e = validateBrokerEndpointFlag(flags.brokerEndpoint, deps.platformId);
3127
+ if (e) {
3128
+ deps.printErr(`error: ${e}`);
3129
+ return 2;
3130
+ }
3131
+ }
3132
+ if (flags.brokerRpcUrl) {
3133
+ const e = validateHttpUrlFlag("--broker-rpc-url", flags.brokerRpcUrl);
3134
+ if (e) {
3135
+ deps.printErr(`error: ${e}`);
3136
+ return 2;
3137
+ }
3138
+ }
3139
+ const resolution = await resolveSessionKey({
3140
+ sessionFlag: flags.session,
3141
+ policy: "require",
3142
+ deps: deps.sessionPrompt
3143
+ });
3144
+ if (resolution.kind !== "key") {
3145
+ const message = resolution.kind === "error" ? resolution.message : "no session key resolved";
3146
+ deps.printErr(`error: ${message}`);
3147
+ return 2;
3148
+ }
3149
+ const sessionKey = resolution.key;
3150
+ const overrides = applyEnvDefaults({
3151
+ env: deps.env,
3152
+ platformId: deps.platformId,
3153
+ osRelease: deps.osRelease
3154
+ });
3155
+ const effectiveEnv = {};
3156
+ for (const [k, v] of Object.entries(deps.env)) {
3157
+ if (typeof v === "string") effectiveEnv[k] = v;
3158
+ }
3159
+ for (const [k, v] of Object.entries(overrides.toSet)) effectiveEnv[k] = v;
3160
+ if (flags.brokerEndpoint) effectiveEnv.MUHAVEN_BROKER_ENDPOINT = flags.brokerEndpoint;
3161
+ if (flags.backendBaseUrl) effectiveEnv.MUHAVEN_BACKEND_URL = flags.backendBaseUrl;
3162
+ if (flags.dashboardBaseUrl) effectiveEnv.MUHAVEN_DASHBOARD_URL = flags.dashboardBaseUrl;
3163
+ if (flags.brokerRpcUrl) effectiveEnv.MUHAVEN_BROKER_RPC_URL = flags.brokerRpcUrl;
3164
+ for (const name of overrides.preserved) deps.print(`Env preserved: ${name} (set in your shell)`);
3165
+ for (const [k, v] of Object.entries(overrides.toSet)) deps.print(`Env defaulted: ${k}=${v}`);
3166
+ const config = loadMcpConfig(effectiveEnv);
3167
+ const broker = deps.newBrokerClient(config.brokerEndpoint, config.brokerTimeoutMs);
3168
+ let running = false;
3169
+ try {
3170
+ await broker.hello();
3171
+ running = true;
3172
+ } catch {
3173
+ running = false;
3174
+ }
3175
+ if (mode === "start") {
3176
+ if (running) {
3177
+ deps.printErr(
3178
+ `Broker daemon is already running at ${config.brokerEndpoint}. To rotate its key use: muhaven-broker update --session <key>`
3179
+ );
3180
+ return 1;
3181
+ }
3182
+ deps.print("Broker daemon: not running \u2014 starting one (detached) on the provided key ...");
3183
+ } else {
3184
+ if (running) {
3185
+ deps.print("Broker daemon: running \u2014 stopping it before installing the new key ...");
3186
+ const stopCode = await deps.stopDaemon(config.brokerEndpoint, config.brokerTimeoutMs);
3187
+ if (stopCode !== 0) {
3188
+ deps.printErr(
3189
+ `Broker daemon stop returned ${stopCode}; refusing to start a second daemon on the same endpoint. Resolve the running daemon (muhaven-broker doctor) and retry.`
3190
+ );
3191
+ return stopCode;
3192
+ }
3193
+ } else {
3194
+ deps.print("Broker daemon: not running \u2014 `update` will start a fresh one on the provided key.");
3195
+ }
3196
+ }
3197
+ const daemonPid = deps.spawnDaemon({
3198
+ binPath: deps.resolveBinPath(),
3199
+ env: {
3200
+ ...overrides.toSet,
3201
+ MUHAVEN_BROKER_ENDPOINT: config.brokerEndpoint,
3202
+ MUHAVEN_BACKEND_URL: effectiveEnv.MUHAVEN_BACKEND_URL,
3203
+ MUHAVEN_DASHBOARD_URL: effectiveEnv.MUHAVEN_DASHBOARD_URL,
3204
+ MUHAVEN_BROKER_SESSION_KEY: sessionKey,
3205
+ // Forward the chain RPC URL only when resolved (flag or shell env).
3206
+ // When absent, the daemon's loadBrokerConfig applies the public
3207
+ // Arb Sepolia default — no need to inject it here.
3208
+ ...effectiveEnv.MUHAVEN_BROKER_RPC_URL ? { MUHAVEN_BROKER_RPC_URL: effectiveEnv.MUHAVEN_BROKER_RPC_URL } : {}
3209
+ }
3210
+ });
3211
+ let ready;
3212
+ try {
3213
+ ready = await deps.waitForBroker({ broker });
3214
+ } catch (err) {
3215
+ deps.printErr(err.message);
3216
+ deps.printErr(
3217
+ " hint: check that no other broker is bound to the same endpoint (muhaven-broker doctor)."
3218
+ );
3219
+ return 1;
3220
+ }
3221
+ deps.print(`Broker daemon: ready (PID ${daemonPid}, endpoint ${config.brokerEndpoint}).`);
3222
+ try {
3223
+ const h = await broker.hello();
3224
+ const hasKey = h.hasSessionKey ?? true;
3225
+ if (!hasKey) {
3226
+ deps.printErr(
3227
+ "Broker came up in READ-ONLY posture \u2014 the session key did not reach the daemon, so it cannot sign. Stop it (muhaven-broker stop) and retry."
3228
+ );
3229
+ return 1;
3230
+ }
3231
+ if (h.sessionKeyAddress) deps.print(`Broker signer: ${h.sessionKeyAddress}`);
3232
+ } catch {
3233
+ }
3234
+ if (flags.skipLogin) {
3235
+ deps.print("Login: skipped per --skip-login.");
3236
+ } else if (ready.hasJwt) {
3237
+ deps.print("Login: skipped \u2014 JWT already in keystore (reused).");
3238
+ } else {
3239
+ const loginArgv = [];
3240
+ if (flags.noLaunchBrowser) loginArgv.push("--no-launch-browser");
3241
+ if (flags.brokerEndpoint) loginArgv.push("--broker-endpoint", flags.brokerEndpoint);
3242
+ if (flags.backendBaseUrl) loginArgv.push("--backend-base-url", flags.backendBaseUrl);
3243
+ if (flags.dashboardBaseUrl) loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
3244
+ const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
3245
+ if (code !== 0) {
3246
+ deps.printErr(
3247
+ "Login step failed \u2014 the daemon is running on the new key; re-run `muhaven-broker login` to retry."
3248
+ );
3249
+ return code;
3250
+ }
3251
+ }
3252
+ deps.print("");
3253
+ deps.print("================================");
3254
+ deps.print(mode === "start" ? "Broker started." : "Session key rotated.");
3255
+ deps.print(` Daemon PID : ${daemonPid}`);
3256
+ const killCmd = deps.platformId === "win32" ? `Stop-Process -Id ${daemonPid}` : `kill ${daemonPid}`;
3257
+ deps.print(` Stop daemon: ${killCmd} (or: muhaven-broker stop)`);
3258
+ deps.print(` Endpoint : ${config.brokerEndpoint}`);
3259
+ deps.print(" Rotate key : muhaven-broker update --session <new-key>");
3260
+ deps.print("================================");
3261
+ return 0;
3262
+ }
3263
+
2956
3264
  // src/broker/cli.ts
2957
3265
  function print(line) {
2958
3266
  process.stdout.write(line + "\n");
@@ -3265,6 +3573,13 @@ function printUsage() {
3265
3573
  print(" (claude-code today; claude-desktop / cursor reserved for Wave 5)");
3266
3574
  print(" [--register-scope user|project|local] scope for the host-config write");
3267
3575
  print(" (default: user \u2014 every project sees the server)");
3576
+ print(" start Bring the daemon up on a DASHBOARD-minted session key (daemon NOT running)");
3577
+ print(" --session <key|-> the key (or `-` to read it from stdin); omit to be");
3578
+ print(" asked interactively. [--skip-login] [--no-launch-browser]");
3579
+ print(" [--broker-rpc-url URL] chain RPC for Path D (default: public Arb Sepolia)");
3580
+ print(" update Rotate the session key on a running daemon (stop \u2192 swap \u2192 restart,");
3581
+ print(" reusing the existing JWT). --session <key|-> (or interactive).");
3582
+ print(" [--broker-rpc-url URL] override the daemon chain RPC for Path D");
3268
3583
  print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
3269
3584
  print(" after 5s). Also clears the keystore JWT as a best effort.");
3270
3585
  print(" login Acquire a JWT via the device-code flow + store in keystore");
@@ -3276,7 +3591,7 @@ function printUsage() {
3276
3591
  }
3277
3592
  function getBrokerPackageVersion() {
3278
3593
  {
3279
- return "0.3.0";
3594
+ return "0.4.1";
3280
3595
  }
3281
3596
  }
3282
3597
  function printVersion() {
@@ -3312,6 +3627,66 @@ function defaultShellOut(cmd, argv) {
3312
3627
  });
3313
3628
  });
3314
3629
  }
3630
+ function stdinIsTty() {
3631
+ return process.stdin.isTTY === true;
3632
+ }
3633
+ function promptYesNo(question) {
3634
+ return new Promise((resolve) => {
3635
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3636
+ rl.question(question, (answer) => {
3637
+ rl.close();
3638
+ const a = answer.trim().toLowerCase();
3639
+ resolve(a === "" || a === "y" || a === "yes");
3640
+ });
3641
+ });
3642
+ }
3643
+ function promptSecret(question) {
3644
+ return new Promise((resolve) => {
3645
+ process.stdout.write(`${question.trimEnd()} (input hidden)
3646
+ `);
3647
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3648
+ const rlAny = rl;
3649
+ rlAny._writeToOutput = () => {
3650
+ };
3651
+ rl.question("", (answer) => {
3652
+ rl.close();
3653
+ process.stdout.write("\n");
3654
+ resolve(answer);
3655
+ });
3656
+ });
3657
+ }
3658
+ async function readStdinAll() {
3659
+ if (process.stdin.isTTY) return "";
3660
+ const chunks = [];
3661
+ for await (const chunk of process.stdin) {
3662
+ chunks.push(Buffer.from(chunk));
3663
+ }
3664
+ return Buffer.concat(chunks).toString("utf8");
3665
+ }
3666
+ function makeSessionPromptDeps() {
3667
+ return {
3668
+ isTty: stdinIsTty(),
3669
+ readStdinAll,
3670
+ promptYesNo,
3671
+ promptSecret
3672
+ };
3673
+ }
3674
+ function makeStopDeps(clearJwtOnStop, override) {
3675
+ const resolved = override ?? (() => {
3676
+ const config = loadMcpConfig();
3677
+ return { endpoint: config.brokerEndpoint, brokerTimeoutMs: config.brokerTimeoutMs };
3678
+ })();
3679
+ return {
3680
+ print,
3681
+ printErr,
3682
+ newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
3683
+ killProcess: defaultKillProcess,
3684
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
3685
+ endpoint: resolved.endpoint,
3686
+ brokerTimeoutMs: resolved.brokerTimeoutMs,
3687
+ clearJwtOnStop
3688
+ };
3689
+ }
3315
3690
  async function runSetup2(argv) {
3316
3691
  const deps = {
3317
3692
  print,
@@ -3326,22 +3701,35 @@ async function runSetup2(argv) {
3326
3701
  env: process.env,
3327
3702
  platformId: process.platform,
3328
3703
  osRelease: os.release(),
3329
- shellOut: defaultShellOut
3704
+ shellOut: defaultShellOut,
3705
+ sessionInput: makeSessionPromptDeps()
3330
3706
  };
3331
3707
  return runSetup(argv, deps);
3332
3708
  }
3333
3709
  async function runStop2() {
3334
- const config = loadMcpConfig();
3335
- const deps = {
3710
+ return runStop(makeStopDeps(true));
3711
+ }
3712
+ function makeBringUpDeps() {
3713
+ return {
3336
3714
  print,
3337
3715
  printErr,
3338
3716
  newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
3339
- killProcess: defaultKillProcess,
3340
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
3341
- endpoint: config.brokerEndpoint,
3342
- brokerTimeoutMs: config.brokerTimeoutMs
3717
+ spawnDaemon,
3718
+ waitForBroker,
3719
+ // `update` stops the old daemon but PRESERVES the JWT (key rotation
3720
+ // must not force a device-code re-login). Targets the resolved
3721
+ // endpoint so a `--broker-endpoint` override stops the right daemon.
3722
+ stopDaemon: (endpoint, brokerTimeoutMs) => runStop(makeStopDeps(false, { endpoint, brokerTimeoutMs })),
3723
+ runLogin,
3724
+ resolveBinPath: resolveBrokerBinPath,
3725
+ env: process.env,
3726
+ platformId: process.platform,
3727
+ osRelease: os.release(),
3728
+ sessionPrompt: makeSessionPromptDeps()
3343
3729
  };
3344
- return runStop(deps);
3730
+ }
3731
+ async function runStartOrUpdate(mode, argv) {
3732
+ return runBringUp(mode, argv, makeBringUpDeps());
3345
3733
  }
3346
3734
  async function runCli(argv) {
3347
3735
  const [sub, ...rest] = argv;
@@ -3351,6 +3739,10 @@ async function runCli(argv) {
3351
3739
  return 0;
3352
3740
  case "setup":
3353
3741
  return runSetup2(rest);
3742
+ case "start":
3743
+ return runStartOrUpdate("start", rest);
3744
+ case "update":
3745
+ return runStartOrUpdate("update", rest);
3354
3746
  case "stop":
3355
3747
  return runStop2();
3356
3748
  case "login":
@@ -3381,4 +3773,5 @@ exports.runDoctor = runDoctor;
3381
3773
  exports.runLogin = runLogin;
3382
3774
  exports.runLogout = runLogout;
3383
3775
  exports.runSetup = runSetup2;
3776
+ exports.runStartOrUpdate = runStartOrUpdate;
3384
3777
  exports.runStop = runStop2;
package/dist/broker.d.cts CHANGED
@@ -1,3 +1,30 @@
1
+ /**
2
+ * `muhaven-broker start` / `muhaven-broker update` — install a
3
+ * DASHBOARD-minted Scoped session key onto the broker daemon in one shot.
4
+ *
5
+ * Wave 5 Option D OPEN-D (2026-05-24). Replaces the manual last mile after
6
+ * a dashboard mint / revoke:
7
+ *
8
+ * start — bring the daemon UP on a provided key (daemon NOT running).
9
+ * update — ROTATE the key on a (possibly) running daemon: stop → swap →
10
+ * restart → REUSE the existing JWT (a key rotation must not
11
+ * force a device-code re-login).
12
+ *
13
+ * Both resolve the key via the shared precedence (`--session` flag >
14
+ * interactive masked prompt > error — see `session-input.ts`). Both
15
+ * REQUIRE a key (unlike `setup`, which self-mints on the fresh-install
16
+ * path). The key is injected ONLY into the spawned daemon's child env
17
+ * (**Option B** — operator decision 2026-05-24); it never touches disk,
18
+ * so the daemon (`loadBrokerConfig`) and the keystore stay unchanged.
19
+ *
20
+ * The orchestrator is pure-ish: all IO (broker IPC, daemon spawn, stop,
21
+ * login, prompts) is injected via `BringUpDeps` so the decision tree is
22
+ * unit-testable without spawning real processes — mirrors the
23
+ * `runSetup` / `runStop` style.
24
+ */
25
+
26
+ type BringUpMode = 'start' | 'update';
27
+
1
28
  /**
2
29
  * `muhaven-broker` CLI subcommand router.
3
30
  *
@@ -8,6 +35,7 @@
8
35
  * doctor → environment + keystore capability report
9
36
  * --help, -h → usage
10
37
  */
38
+
11
39
  interface LoginFlags {
12
40
  noLaunchBrowser: boolean;
13
41
  brokerEndpoint?: string;
@@ -43,6 +71,8 @@ declare function runSetup(argv: readonly string[]): Promise<number>;
43
71
  * Wire `runStop` against the real BrokerClient + Node's process.kill.
44
72
  */
45
73
  declare function runStop(): Promise<number>;
74
+ /** Wire `muhaven-broker start` / `update` against the real cli helpers. */
75
+ declare function runStartOrUpdate(mode: BringUpMode, argv: readonly string[]): Promise<number>;
46
76
  declare function runCli(argv: readonly string[]): Promise<number>;
47
77
 
48
- export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStop };
78
+ export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStartOrUpdate, runStop };