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