@mrrlin-dev/mcp 0.4.53 → 0.4.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.cjs +367 -86
  2. package/package.json +2 -2
package/dist/bin.cjs CHANGED
@@ -20659,6 +20659,7 @@ var toml = __toESM(require_toml(), 1);
20659
20659
  // src/api-base-url.ts
20660
20660
  var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
20661
20661
  var STAGING_API_BASE_URL = "https://api-staging.mrrlin.com";
20662
+ var PRODUCTION_API_BASE_URL = "https://api.mrrlin.com";
20662
20663
  function normalizeUrl(value) {
20663
20664
  return value.replace(/\/$/, "");
20664
20665
  }
@@ -20668,7 +20669,9 @@ var RETIRED_API_BASE_URLS = {
20668
20669
  "mrrlin-api-fnnzzz-fnnzzzs-projects.vercel.app": STAGING_API_BASE_URL,
20669
20670
  // Retired 2026-08-14 when staging decoupled onto the stable custom domain api-staging.mrrlin.com.
20670
20671
  // A persisted operator configuration may still pin the mrrlin3 Vercel alias; retarget it in one hop.
20671
- "mrrlin-api-mrrlin3.vercel.app": STAGING_API_BASE_URL
20672
+ "mrrlin-api-mrrlin3.vercel.app": STAGING_API_BASE_URL,
20673
+ // Retired 2026-09-07 when production moved from the transitional api-prod host to api.mrrlin.com.
20674
+ "api-prod.mrrlin.com": PRODUCTION_API_BASE_URL
20672
20675
  };
20673
20676
  function isSameApiHost(a, b) {
20674
20677
  const origin = (value) => {
@@ -20685,6 +20688,9 @@ function isSameApiHost(a, b) {
20685
20688
  function isStagingApiBaseUrl(input) {
20686
20689
  return isSameApiHost(input, STAGING_API_BASE_URL);
20687
20690
  }
20691
+ function isProductionApiBaseUrl(input) {
20692
+ return isSameApiHost(input, PRODUCTION_API_BASE_URL);
20693
+ }
20688
20694
  function retargetRetiredApiBaseUrl(input) {
20689
20695
  const normalized = normalizeUrl(input.trim());
20690
20696
  let hostname3;
@@ -20703,7 +20709,6 @@ function retiredApiHostnames() {
20703
20709
  function bridgeUrlImpliesStaging(input) {
20704
20710
  const raw = input?.trim();
20705
20711
  if (!raw) return false;
20706
- if (isLoopbackUrl(raw)) return true;
20707
20712
  let hostname3;
20708
20713
  try {
20709
20714
  hostname3 = new URL(raw).hostname.toLowerCase();
@@ -20728,8 +20733,14 @@ function readConfiguredApiBaseUrl(explicit) {
20728
20733
  if (process.env.MRRLIN_STAGING === "1") return STAGING_API_BASE_URL;
20729
20734
  return DEFAULT_API_BASE_URL;
20730
20735
  }
20736
+ function readDirectorBridgeApiBaseUrl(explicit) {
20737
+ const direct = explicit ?? process.env.MRRLIN_API_BASE_URL;
20738
+ if (direct?.trim()) return retargetRetiredApiBaseUrl(direct.trim()).baseUrl;
20739
+ if (process.env.MRRLIN_STAGING === "1") return STAGING_API_BASE_URL;
20740
+ return PRODUCTION_API_BASE_URL;
20741
+ }
20731
20742
  function isRemoteApiAllowed(apiBaseUrl) {
20732
- return isLoopbackUrl(apiBaseUrl) || isStagingApiBaseUrl(apiBaseUrl) || process.env.MRRLIN_API_ALLOW_REMOTE === "1" || process.env.MRRLIN_STAGING === "1";
20743
+ return isLoopbackUrl(apiBaseUrl) || isStagingApiBaseUrl(apiBaseUrl) || isProductionApiBaseUrl(apiBaseUrl) || process.env.MRRLIN_API_ALLOW_REMOTE === "1";
20733
20744
  }
20734
20745
  function readProbeTimeoutMs(env) {
20735
20746
  const raw = Number((env.MRRLIN_API_PROBE_TIMEOUT_MS ?? "").trim());
@@ -20770,7 +20781,7 @@ async function resolveApiBaseUrlDynamic(opts = {}) {
20770
20781
  return { baseUrl: STAGING_API_BASE_URL, source: "staging-autodetect" };
20771
20782
  }
20772
20783
  throw new Error(
20773
- `Local Mrrlin API not reachable at ${DEFAULT_API_BASE_URL}. Start it, or set MRRLIN_STAGING=1 to target staging (or MRRLIN_API_AUTODETECT=1 to auto-fall back to staging when local is down).`
20784
+ `Local Mrrlin API not reachable at ${DEFAULT_API_BASE_URL}. Start it, or set MRRLIN_API_BASE_URL=${STAGING_API_BASE_URL} to target staging (or MRRLIN_API_AUTODETECT=1 to auto-fall back to staging when local is down).`
20774
20785
  );
20775
20786
  }
20776
20787
 
@@ -20994,6 +21005,24 @@ function blockEnvHasRetiredApiUrl(actual) {
20994
21005
  }
20995
21006
  return retiredApiHostnames().includes(hostname3);
20996
21007
  }
21008
+ function isFirstPartyOrLoopbackApiUrl(value) {
21009
+ return isStagingApiBaseUrl(value) || isProductionApiBaseUrl(value) || isLoopbackUrl(value);
21010
+ }
21011
+ function normalizedApiUrlFromEnv(env) {
21012
+ const pinned = env.MRRLIN_API_BASE_URL;
21013
+ if (typeof pinned === "string" && pinned.trim()) return retargetRetiredApiBaseUrl(pinned.trim()).baseUrl;
21014
+ return env.MRRLIN_STAGING === "1" ? STAGING_API_BASE_URL : void 0;
21015
+ }
21016
+ function blockEnvNeedsApiRoutingMigration(actual) {
21017
+ if (typeof actual !== "object" || actual === null) return false;
21018
+ const env = actual.env;
21019
+ if (typeof env !== "object" || env === null) return false;
21020
+ const vars = env;
21021
+ if (vars.MRRLIN_STAGING === "1") return true;
21022
+ if (blockEnvHasRetiredApiUrl(actual)) return true;
21023
+ const apiUrl = normalizedApiUrlFromEnv(vars);
21024
+ return apiUrl !== void 0 && (isProductionApiBaseUrl(apiUrl) || vars.MRRLIN_API_ALLOW_REMOTE === "1" && isFirstPartyOrLoopbackApiUrl(apiUrl));
21025
+ }
20997
21026
  function renderEnvPreservedNotice(blockId, envKeys) {
20998
21027
  const keys = [...envKeys].sort().join(", ");
20999
21028
  return `[mrrlin-mcp configure bridge] kept your existing [mcp_servers.${blockId}] env keys (${keys}) so your API target is preserved. Re-run with --force to replace or remove it.`;
@@ -21001,16 +21030,30 @@ function renderEnvPreservedNotice(blockId, envKeys) {
21001
21030
  function reconcileMcpServerBlock(existing, desired) {
21002
21031
  if (existing === void 0) return { block: desired, preservedEnvKeys: [] };
21003
21032
  if (tablesEqual(existing, desired)) return { block: existing, preservedEnvKeys: [] };
21004
- if (blockEnvHasRetiredApiUrl(existing)) {
21033
+ if (blockEnvNeedsApiRoutingMigration(existing)) {
21005
21034
  const existingEnv = existing.env ?? {};
21035
+ const apiUrl = normalizedApiUrlFromEnv(existingEnv);
21006
21036
  const healedEnv = {};
21007
21037
  for (const [key, value] of Object.entries(existingEnv)) {
21008
- if (key === "MRRLIN_API_BASE_URL") continue;
21038
+ if (key === "MRRLIN_API_BASE_URL" || key === "MRRLIN_STAGING" || key === "MRRLIN_API_ALLOW_REMOTE") {
21039
+ continue;
21040
+ }
21009
21041
  if (typeof value === "string") healedEnv[key] = value;
21010
21042
  }
21011
- healedEnv.MRRLIN_STAGING = "1";
21012
- delete healedEnv.MRRLIN_API_ALLOW_REMOTE;
21013
- return { block: { ...desired, env: healedEnv }, preservedEnvKeys: [] };
21043
+ if (apiUrl && !isProductionApiBaseUrl(apiUrl)) {
21044
+ healedEnv.MRRLIN_API_BASE_URL = apiUrl;
21045
+ if (existingEnv.MRRLIN_API_ALLOW_REMOTE === "1" && !isFirstPartyOrLoopbackApiUrl(apiUrl)) {
21046
+ healedEnv.MRRLIN_API_ALLOW_REMOTE = "1";
21047
+ }
21048
+ }
21049
+ const finalEnv = { ...desired.env, ...healedEnv };
21050
+ const block = { ...desired };
21051
+ if (Object.keys(finalEnv).length > 0) {
21052
+ block.env = finalEnv;
21053
+ } else {
21054
+ delete block.env;
21055
+ }
21056
+ return { block, preservedEnvKeys: [] };
21014
21057
  }
21015
21058
  if (classifyExistingBlock(existing, desired) === "preserve-env-noop") {
21016
21059
  const env = existing.env ?? {};
@@ -21057,11 +21100,15 @@ function deriveServeEnvFromProcessEnv(env) {
21057
21100
  const rawApiUrl = env.MRRLIN_API_BASE_URL?.trim();
21058
21101
  const bridgeWantsStaging = bridgeUrlImpliesStaging(env.MRRLIN_BRIDGE_URL);
21059
21102
  const apiUrl = rawApiUrl ? retargetRetiredApiBaseUrl(rawApiUrl).baseUrl : void 0;
21060
- if (apiUrl && !isStagingApiBaseUrl(apiUrl)) {
21061
- out.MRRLIN_API_BASE_URL = apiUrl;
21062
- if (env.MRRLIN_API_ALLOW_REMOTE === "1") out.MRRLIN_API_ALLOW_REMOTE = "1";
21063
- } else if (apiUrl || env.MRRLIN_STAGING === "1" || bridgeWantsStaging) {
21064
- out.MRRLIN_STAGING = "1";
21103
+ if (apiUrl) {
21104
+ if (!isProductionApiBaseUrl(apiUrl)) {
21105
+ out.MRRLIN_API_BASE_URL = apiUrl;
21106
+ if (env.MRRLIN_API_ALLOW_REMOTE === "1" && !isStagingApiBaseUrl(apiUrl) && !isLoopbackUrl(apiUrl)) {
21107
+ out.MRRLIN_API_ALLOW_REMOTE = "1";
21108
+ }
21109
+ }
21110
+ } else if (env.MRRLIN_STAGING === "1" || bridgeWantsStaging) {
21111
+ out.MRRLIN_API_BASE_URL = STAGING_API_BASE_URL;
21065
21112
  }
21066
21113
  return Object.keys(out).length > 0 ? out : void 0;
21067
21114
  }
@@ -21204,7 +21251,7 @@ async function installCodex(options) {
21204
21251
  }
21205
21252
  const classification = classifyExistingBlock(existingTable, block.expected);
21206
21253
  if (classification === "equal") continue;
21207
- if (blockEnvHasRetiredApiUrl(existingTable)) {
21254
+ if (blockEnvNeedsApiRoutingMigration(existingTable)) {
21208
21255
  const { block: healedBlock } = reconcileMcpServerBlock(existingTable, block.expected);
21209
21256
  content = replaceBlock(content, block.blockRe, renderBlock(block.id, healedBlock));
21210
21257
  action = "replaced";
@@ -21377,7 +21424,7 @@ async function installClaude(options) {
21377
21424
  }
21378
21425
  const classification = classifyExistingBlock(existingEntry, entry.expected);
21379
21426
  if (classification === "equal") continue;
21380
- if (blockEnvHasRetiredApiUrl(existingEntry)) {
21427
+ if (blockEnvNeedsApiRoutingMigration(existingEntry)) {
21381
21428
  const { block: healedBlock } = reconcileMcpServerBlock(existingEntry, entry.expected);
21382
21429
  mcpServers[entry.id] = healedBlock;
21383
21430
  action = "replaced";
@@ -38200,6 +38247,10 @@ var mrrlinTaskCreateShapeSchema = external_exports.object({
38200
38247
  title: external_exports.string().min(1),
38201
38248
  type: mrrlinTaskTypeSchema.default("task"),
38202
38249
  verificationDueAt: external_exports.string().datetime().nullable().optional()
38250
+ // `declaredOutcome` is deliberately ABSENT — «required at spec approval, never at creation», as
38251
+ // the comment beside the READ schema's own field says. It is refused with a 400 at the route
38252
+ // rather than declared-and-rejected here (`OQ-084`); see `rejectsDeclaredOutcomeAtCreation` in
38253
+ // `apps/api/src/index.ts` for why the refusal cannot live in this schema.
38203
38254
  });
38204
38255
  function refineTaskCreateDomain(data, ctx) {
38205
38256
  if (data.domain === void 0 && data.parentTaskId == null) {
@@ -42437,7 +42488,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
42437
42488
  }
42438
42489
 
42439
42490
  // src/_generated/version.ts
42440
- var PKG_VERSION = "0.4.53";
42491
+ var PKG_VERSION = "0.4.55";
42441
42492
 
42442
42493
  // src/api-base-url-migrate.ts
42443
42494
  var import_node_fs4 = __toESM(require("node:fs"), 1);
@@ -42467,11 +42518,17 @@ function renderEcosystemConfig(opts) {
42467
42518
  PATH: opts.pathEnv,
42468
42519
  MRRLIN_DIRECTOR_BRIDGE_LOG: "1"
42469
42520
  };
42521
+ const telemetryProject = opts.telemetryDefaultProject?.trim();
42522
+ if (telemetryProject) env.MRRLIN_TELEMETRY_DEFAULT_PROJECT = telemetryProject;
42470
42523
  if (opts.user) env.USER = opts.user;
42471
42524
  if (opts.codexHome) env.CODEX_HOME = opts.codexHome;
42472
- const retargeted = opts.apiBaseUrl ? retargetRetiredApiBaseUrl(opts.apiBaseUrl).baseUrl : void 0;
42473
- if (opts.staging || retargeted && isStagingApiBaseUrl(retargeted)) env.MRRLIN_STAGING = "1";
42474
- else if (retargeted) env.MRRLIN_API_BASE_URL = retargeted;
42525
+ const apiBaseUrl = opts.apiBaseUrl ? retargetRetiredApiBaseUrl(opts.apiBaseUrl).baseUrl : void 0;
42526
+ if (apiBaseUrl && !isProductionApiBaseUrl(apiBaseUrl)) {
42527
+ env.MRRLIN_API_BASE_URL = apiBaseUrl;
42528
+ if (opts.apiAllowRemote && !isStagingApiBaseUrl(apiBaseUrl) && !isLoopbackUrl(apiBaseUrl)) {
42529
+ env.MRRLIN_API_ALLOW_REMOTE = "1";
42530
+ }
42531
+ }
42475
42532
  const envLines = Object.entries(env).map(([k, v]) => ` ${k}: ${JSON.stringify(v)},`).join("\n");
42476
42533
  return `module.exports = {
42477
42534
  apps: [{
@@ -42515,6 +42572,10 @@ function defaultDeps(deps) {
42515
42572
  function findRetiredHost(text) {
42516
42573
  return retiredApiHostnames().find((host) => text.toLowerCase().includes(host));
42517
42574
  }
42575
+ function hasLiveLegacyStagingFlag(text, syntax) {
42576
+ const lineRe = syntax === "js" ? /^[ \t]*MRRLIN_STAGING:\s*"1",?[ \t]*\r?$/gm : /^[ \t]*(?!#).*MRRLIN_STAGING\s*=\s*"1"/gm;
42577
+ return lineRe.test(text);
42578
+ }
42518
42579
  function isRetiredApiUrl(value) {
42519
42580
  if (!value?.trim()) return false;
42520
42581
  try {
@@ -42523,6 +42584,19 @@ function isRetiredApiUrl(value) {
42523
42584
  return false;
42524
42585
  }
42525
42586
  }
42587
+ function migratedApiUrl(value) {
42588
+ const { baseUrl, retargeted } = retargetRetiredApiBaseUrl(value);
42589
+ return retargeted ? baseUrl : void 0;
42590
+ }
42591
+ function isFirstPartyOrLoopbackUrl(value) {
42592
+ return isStagingApiBaseUrl(value) || isProductionApiBaseUrl(value) || isLoopbackUrl(value);
42593
+ }
42594
+ function isRecord(value) {
42595
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42596
+ }
42597
+ function removeStandaloneAllowRemoteLine(text) {
42598
+ return text.replace(/^[ \t]*MRRLIN_API_ALLOW_REMOTE:\s*"1",?[ \t]*\r?\n/gm, "");
42599
+ }
42526
42600
  function backupOnce(file2, text, deps) {
42527
42601
  const bak = `${file2}.bak-retired-api-url`;
42528
42602
  if (!deps.exists(bak)) deps.writeFile(bak, text);
@@ -42532,7 +42606,8 @@ function migrateEcosystemConfig(deps = {}) {
42532
42606
  const file2 = import_node_path8.default.join(d.home, ".mrrlin", "ecosystem.config.cjs");
42533
42607
  if (!d.exists(file2)) return { file: file2, status: "absent" };
42534
42608
  const text = d.readFile(file2);
42535
- if (!findRetiredHost(text)) return { file: file2, status: "clean" };
42609
+ if (!findRetiredHost(text) && !hasLiveLegacyStagingFlag(text, "js"))
42610
+ return { file: file2, status: "clean" };
42536
42611
  const fields = {
42537
42612
  cwd: /cwd:\s*"((?:[^"\\]|\\.)*)"/.exec(text)?.[1],
42538
42613
  home: /HOME:\s*"((?:[^"\\]|\\.)*)"/.exec(text)?.[1],
@@ -42541,14 +42616,48 @@ function migrateEcosystemConfig(deps = {}) {
42541
42616
  };
42542
42617
  if (fields.cwd && fields.home && fields.pathEnv) {
42543
42618
  const retiredUrl = /MRRLIN_API_BASE_URL:\s*"((?:[^"\\]|\\.)*)"/.exec(text)?.[1];
42544
- const legacyShape = retiredUrl && isRetiredApiUrl(retiredUrl) ? renderEcosystemConfigLegacy({
42619
+ const legacyUrlShape = retiredUrl && isRetiredApiUrl(retiredUrl) ? renderEcosystemConfigLegacy({
42545
42620
  cwd: fields.cwd,
42546
42621
  home: fields.home,
42547
42622
  pathEnv: fields.pathEnv,
42548
42623
  codexHome: fields.codexHome,
42549
42624
  apiBaseUrl: retiredUrl
42550
42625
  }) : void 0;
42551
- if (legacyShape && legacyShape === text) {
42626
+ const legacyStagingShape = renderEcosystemConfigWithEnv(
42627
+ { cwd: fields.cwd, home: fields.home, pathEnv: fields.pathEnv, codexHome: fields.codexHome },
42628
+ { MRRLIN_STAGING: "1" }
42629
+ );
42630
+ if (legacyUrlShape && legacyUrlShape === text) {
42631
+ const nextUrl = migratedApiUrl(retiredUrl ?? "");
42632
+ if (!nextUrl) {
42633
+ return {
42634
+ file: file2,
42635
+ status: "skipped-drifted",
42636
+ detail: "the pinned API URL did not resolve to a known successor"
42637
+ };
42638
+ }
42639
+ backupOnce(file2, text, d);
42640
+ d.writeFile(
42641
+ file2,
42642
+ isProductionApiBaseUrl(nextUrl) ? renderEcosystemConfigWithEnv(
42643
+ {
42644
+ cwd: fields.cwd,
42645
+ home: fields.home,
42646
+ pathEnv: fields.pathEnv,
42647
+ codexHome: fields.codexHome
42648
+ },
42649
+ { MRRLIN_API_BASE_URL: nextUrl }
42650
+ ) : renderEcosystemConfig({
42651
+ cwd: fields.cwd,
42652
+ home: fields.home,
42653
+ pathEnv: fields.pathEnv,
42654
+ codexHome: fields.codexHome,
42655
+ apiBaseUrl: STAGING_API_BASE_URL
42656
+ })
42657
+ );
42658
+ return { file: file2, status: "migrated", detail: "regenerated from the canonical template" };
42659
+ }
42660
+ if (legacyStagingShape === text) {
42552
42661
  backupOnce(file2, text, d);
42553
42662
  d.writeFile(
42554
42663
  file2,
@@ -42557,8 +42666,7 @@ function migrateEcosystemConfig(deps = {}) {
42557
42666
  home: fields.home,
42558
42667
  pathEnv: fields.pathEnv,
42559
42668
  codexHome: fields.codexHome,
42560
- staging: true,
42561
- apiBaseUrl: void 0
42669
+ apiBaseUrl: STAGING_API_BASE_URL
42562
42670
  })
42563
42671
  );
42564
42672
  return { file: file2, status: "migrated", detail: "regenerated from the canonical template" };
@@ -42568,16 +42676,49 @@ function migrateEcosystemConfig(deps = {}) {
42568
42676
  const matches = (text.match(lineRe) ?? []).filter((line) => !line.trimStart().startsWith("//"));
42569
42677
  const target = matches.length === 1 ? matches[0] : void 0;
42570
42678
  if (target !== void 0 && isRetiredApiUrl(/"((?:[^"\\]|\\.)*)"/.exec(target)?.[1])) {
42679
+ const nextUrl = migratedApiUrl(/"((?:[^"\\]|\\.)*)"/.exec(target)?.[1] ?? "");
42680
+ if (!nextUrl) {
42681
+ return {
42682
+ file: file2,
42683
+ status: "skipped-drifted",
42684
+ detail: "the pinned API URL did not resolve to a known successor"
42685
+ };
42686
+ }
42571
42687
  backupOnce(file2, text, d);
42572
42688
  const indent = /^[ \t]*/.exec(target)?.[0] ?? " ";
42573
- d.writeFile(file2, text.replace(target, () => `${indent}MRRLIN_STAGING: "1",
42574
- `));
42575
- return { file: file2, status: "migrated", detail: "replaced the pinned URL line with MRRLIN_STAGING" };
42689
+ let out = text.replace(
42690
+ target,
42691
+ () => `${indent}MRRLIN_API_BASE_URL: ${JSON.stringify(nextUrl)},
42692
+ `
42693
+ );
42694
+ if (isFirstPartyOrLoopbackUrl(nextUrl)) out = removeStandaloneAllowRemoteLine(out);
42695
+ return d.writeFile(file2, out), { file: file2, status: "migrated", detail: "replaced the pinned URL line" };
42696
+ }
42697
+ const stagingLineRe = /^[ \t]*MRRLIN_STAGING:\s*"1",?[ \t]*\r?\n/gm;
42698
+ const stagingMatches = (text.match(stagingLineRe) ?? []).filter(
42699
+ (line) => !line.trimStart().startsWith("//")
42700
+ );
42701
+ const stagingTarget = stagingMatches.length === 1 ? stagingMatches[0] : void 0;
42702
+ if (stagingTarget !== void 0) {
42703
+ backupOnce(file2, text, d);
42704
+ const indent = /^[ \t]*/.exec(stagingTarget)?.[0] ?? " ";
42705
+ let out = text.replace(
42706
+ stagingTarget,
42707
+ () => `${indent}MRRLIN_API_BASE_URL: ${JSON.stringify(STAGING_API_BASE_URL)},
42708
+ `
42709
+ );
42710
+ out = removeStandaloneAllowRemoteLine(out);
42711
+ d.writeFile(file2, out);
42712
+ return {
42713
+ file: file2,
42714
+ status: "migrated",
42715
+ detail: "replaced the legacy staging flag with the explicit API URL"
42716
+ };
42576
42717
  }
42577
42718
  return {
42578
42719
  file: file2,
42579
42720
  status: "skipped-drifted",
42580
- detail: matches.length === 0 ? "a retired host appears in the file but no MRRLIN_API_BASE_URL line was found \u2014 fix it by hand or re-run install-service" : `found ${matches.length} MRRLIN_API_BASE_URL lines; refusing to guess \u2014 fix by hand or re-run install-service`
42721
+ detail: matches.length === 0 ? "a retired host or legacy staging flag appears in the file but no live routing env line was found \u2014 fix it by hand or re-run install-service" : `found ${matches.length} MRRLIN_API_BASE_URL lines; refusing to guess \u2014 fix by hand or re-run install-service`
42581
42722
  };
42582
42723
  }
42583
42724
  function renderEcosystemConfigLegacy(opts) {
@@ -42616,7 +42757,7 @@ function migrateCodexConfig(deps = {}) {
42616
42757
  if (!d.exists(file2)) return { file: file2, status: "absent" };
42617
42758
  const text = d.readFile(file2);
42618
42759
  const host = findRetiredHost(text);
42619
- if (!host) return { file: file2, status: "clean" };
42760
+ if (!host && !hasLiveLegacyStagingFlag(text, "toml")) return { file: file2, status: "clean" };
42620
42761
  const managedBlockRe = /^\[mcp_servers\.mrrlin\][^\n]*\n(?:(?!\[)[^\n]*\n?)*/gm;
42621
42762
  const blocks = text.match(managedBlockRe) ?? [];
42622
42763
  if (blocks.length === 0) {
@@ -42627,21 +42768,39 @@ function migrateCodexConfig(deps = {}) {
42627
42768
  };
42628
42769
  }
42629
42770
  const assignRe = /MRRLIN_API_BASE_URL\s*=\s*"([^"]*)"/g;
42771
+ const stagingAssignRe = /MRRLIN_STAGING\s*=\s*"1"/g;
42772
+ const allowRemoteAssignRe = /,?\s*MRRLIN_API_ALLOW_REMOTE\s*=\s*"1"\s*,?/g;
42630
42773
  let out = text;
42631
42774
  let changed = 0;
42632
42775
  for (const block of blocks) {
42633
42776
  let blockChanged = false;
42634
42777
  const healedBlock = block.split("\n").map((line) => {
42635
42778
  if (line.trimStart().startsWith("#")) return line;
42636
- const healedLine = line.replace(
42637
- assignRe,
42638
- (match, url2) => isRetiredApiUrl(url2) ? `MRRLIN_STAGING = "1"` : match
42639
- );
42779
+ let nextApiUrl;
42780
+ let healedLine = line.replace(assignRe, (match, url2) => {
42781
+ const successor = migratedApiUrl(url2);
42782
+ if (!successor) return match;
42783
+ nextApiUrl = successor;
42784
+ return `MRRLIN_API_BASE_URL = ${JSON.stringify(successor)}`;
42785
+ });
42786
+ healedLine = healedLine.replace(stagingAssignRe, () => {
42787
+ nextApiUrl = STAGING_API_BASE_URL;
42788
+ return `MRRLIN_API_BASE_URL = ${JSON.stringify(STAGING_API_BASE_URL)}`;
42789
+ });
42790
+ if (nextApiUrl && isFirstPartyOrLoopbackUrl(nextApiUrl)) {
42791
+ healedLine = healedLine.replace(
42792
+ allowRemoteAssignRe,
42793
+ (match) => match.startsWith(",") && match.endsWith(",") ? "," : ""
42794
+ ).replace(/\{\s*,\s*/, "{ ").replace(/,\s*\}/, " }").replace(/,\s*,/g, ",");
42795
+ }
42640
42796
  if (healedLine !== line) blockChanged = true;
42641
42797
  return healedLine;
42642
42798
  }).join("\n");
42643
42799
  if (!blockChanged) continue;
42644
- const deduped = healedBlock.replace(/MRRLIN_STAGING = "1"\s*,\s*MRRLIN_STAGING = "1"/g, `MRRLIN_STAGING = "1"`);
42800
+ const deduped = healedBlock.replace(
42801
+ /MRRLIN_API_BASE_URL = "https:\/\/api-staging\.mrrlin\.com"\s*,\s*MRRLIN_API_BASE_URL = "https:\/\/api-staging\.mrrlin\.com"/g,
42802
+ `MRRLIN_API_BASE_URL = "https://api-staging.mrrlin.com"`
42803
+ );
42645
42804
  out = out.replace(block, () => deduped);
42646
42805
  changed += 1;
42647
42806
  }
@@ -42654,14 +42813,67 @@ function migrateCodexConfig(deps = {}) {
42654
42813
  }
42655
42814
  backupOnce(file2, text, d);
42656
42815
  d.writeFile(file2, out);
42657
- return { file: file2, status: "migrated", detail: "swapped the pinned URL for MRRLIN_STAGING" };
42816
+ return {
42817
+ file: file2,
42818
+ status: "migrated",
42819
+ detail: "rewrote legacy API routing env to explicit URL form"
42820
+ };
42821
+ }
42822
+ function migrateClaudeConfig(deps = {}) {
42823
+ const d = defaultDeps(deps);
42824
+ const file2 = import_node_path8.default.join(d.home, ".claude.json");
42825
+ if (!d.exists(file2)) return { file: file2, status: "absent" };
42826
+ const text = d.readFile(file2);
42827
+ let parsed;
42828
+ try {
42829
+ parsed = text.trim() === "" ? {} : JSON.parse(text);
42830
+ } catch (error51) {
42831
+ const detail2 = error51 instanceof Error ? error51.message : String(error51);
42832
+ return { file: file2, status: "failed", detail: `malformed JSON: ${detail2}` };
42833
+ }
42834
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {
42835
+ return { file: file2, status: "clean" };
42836
+ }
42837
+ const entry = parsed.mcpServers.mrrlin;
42838
+ if (!isRecord(entry) || entry.command !== "mrrlin-mcp" || !Array.isArray(entry.args) || entry.args.length !== 1 || entry.args[0] !== "serve") {
42839
+ return { file: file2, status: "clean" };
42840
+ }
42841
+ const env = entry.env;
42842
+ if (!isRecord(env)) return { file: file2, status: "clean" };
42843
+ const envKeys = Object.keys(env);
42844
+ const routingKeys = /* @__PURE__ */ new Set(["MRRLIN_STAGING", "MRRLIN_API_BASE_URL", "MRRLIN_API_ALLOW_REMOTE"]);
42845
+ if (envKeys.length === 0 || !envKeys.every((key) => routingKeys.has(key)) || !envKeys.every((key) => typeof env[key] === "string")) {
42846
+ return { file: file2, status: "clean" };
42847
+ }
42848
+ let nextEnv;
42849
+ let detail;
42850
+ if (env.MRRLIN_STAGING === "1" && envKeys.length === 1) {
42851
+ nextEnv = { MRRLIN_API_BASE_URL: STAGING_API_BASE_URL };
42852
+ detail = "replaced the legacy staging flag with the explicit API URL";
42853
+ } else if (typeof env.MRRLIN_API_BASE_URL === "string" && (envKeys.length === 1 || envKeys.length === 2 && env.MRRLIN_API_ALLOW_REMOTE === "1")) {
42854
+ const nextUrl = migratedApiUrl(env.MRRLIN_API_BASE_URL);
42855
+ if (!nextUrl) return { file: file2, status: "clean" };
42856
+ nextEnv = isProductionApiBaseUrl(nextUrl) ? {} : { MRRLIN_API_BASE_URL: nextUrl };
42857
+ detail = isProductionApiBaseUrl(nextUrl) ? "removed the retired production routing override" : "retargeted the retired API URL";
42858
+ }
42859
+ if (!nextEnv) return { file: file2, status: "clean" };
42860
+ backupOnce(file2, text, d);
42861
+ const next = JSON.parse(JSON.stringify(parsed));
42862
+ const nextServers = next.mcpServers;
42863
+ const nextEntry = nextServers.mrrlin;
42864
+ if (Object.keys(nextEnv).length === 0) delete nextEntry.env;
42865
+ else nextEntry.env = nextEnv;
42866
+ d.writeFile(file2, `${JSON.stringify(next, null, 2)}
42867
+ `);
42868
+ return { file: file2, status: "migrated", detail };
42658
42869
  }
42659
42870
  function migrateRetiredApiBaseUrls(deps = {}) {
42660
42871
  const d = defaultDeps(deps);
42661
42872
  const results = [];
42662
42873
  const steps = [
42663
42874
  { run: migrateEcosystemConfig, file: import_node_path8.default.join(d.home, ".mrrlin", "ecosystem.config.cjs") },
42664
- { run: migrateCodexConfig, file: import_node_path8.default.join(d.codexHome, "config.toml") }
42875
+ { run: migrateCodexConfig, file: import_node_path8.default.join(d.codexHome, "config.toml") },
42876
+ { run: migrateClaudeConfig, file: import_node_path8.default.join(d.home, ".claude.json") }
42665
42877
  ];
42666
42878
  for (const step of steps) {
42667
42879
  try {
@@ -45056,7 +45268,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
45056
45268
  }
45057
45269
 
45058
45270
  // src/telemetry/pii-gate.ts
45059
- function isRecord(v) {
45271
+ function isRecord2(v) {
45060
45272
  return v !== null && typeof v === "object";
45061
45273
  }
45062
45274
  function resolvePath(root, path24) {
@@ -45068,7 +45280,7 @@ function resolvePath(root, path24) {
45068
45280
  if (Array.isArray(cur) && Number.isInteger(idx)) {
45069
45281
  if (idx < 0 || idx >= cur.length) return null;
45070
45282
  cur = cur[idx];
45071
- } else if (isRecord(cur) && part in cur) {
45283
+ } else if (isRecord2(cur) && part in cur) {
45072
45284
  cur = cur[part];
45073
45285
  } else {
45074
45286
  return null;
@@ -45080,7 +45292,7 @@ function resolvePath(root, path24) {
45080
45292
  if (lastIdx < 0 || lastIdx >= cur.length) return null;
45081
45293
  return { parent: cur, key: lastIdx };
45082
45294
  }
45083
- if (isRecord(cur) && last in cur) {
45295
+ if (isRecord2(cur) && last in cur) {
45084
45296
  return { parent: cur, key: last };
45085
45297
  }
45086
45298
  return null;
@@ -45089,7 +45301,7 @@ function withheldExcerptClone(artifact) {
45089
45301
  const clone2 = { ...artifact };
45090
45302
  if (Array.isArray(clone2.steps)) {
45091
45303
  clone2.steps = clone2.steps.map((step) => {
45092
- if (!isRecord(step)) return step;
45304
+ if (!isRecord2(step)) return step;
45093
45305
  const copy = { ...step };
45094
45306
  delete copy.excerpt;
45095
45307
  return copy;
@@ -45120,9 +45332,9 @@ async function gate(artifact, prose, ctx) {
45120
45332
  } catch {
45121
45333
  return { ok: true, gated: true, artifact: withheldExcerptClone(artifact) };
45122
45334
  }
45123
- const fields = isRecord(parsed) && Array.isArray(parsed.fields) ? parsed.fields : null;
45335
+ const fields = isRecord2(parsed) && Array.isArray(parsed.fields) ? parsed.fields : null;
45124
45336
  const isValidShape = fields !== null && fields.length === prose.length && fields.every(
45125
- (entry, i) => isRecord(entry) && typeof entry.path === "string" && typeof entry.masked === "string" && entry.path === prose[i]?.path && resolvePath(artifact, entry.path) !== null
45337
+ (entry, i) => isRecord2(entry) && typeof entry.path === "string" && typeof entry.masked === "string" && entry.path === prose[i]?.path && resolvePath(artifact, entry.path) !== null
45126
45338
  );
45127
45339
  if (!isValidShape || fields === null) {
45128
45340
  return { ok: true, gated: true, artifact: withheldExcerptClone(artifact) };
@@ -49344,27 +49556,27 @@ function redact(input) {
49344
49556
 
49345
49557
  // src/telemetry/projector.ts
49346
49558
  var import_node_crypto8 = require("node:crypto");
49347
- function isRecord2(x) {
49559
+ function isRecord3(x) {
49348
49560
  return typeof x === "object" && x !== null;
49349
49561
  }
49350
49562
  function buildProjection(payload, type, ms, telemetry) {
49351
- if (!isRecord2(payload)) return null;
49563
+ if (!isRecord3(payload)) return null;
49352
49564
  const seqRaw = payload.seq;
49353
49565
  const turnIdRaw = payload.turnId;
49354
49566
  const maybeSeq = typeof seqRaw === "number" ? seqRaw : void 0;
49355
49567
  const maybeTurnId = typeof turnIdRaw === "string" ? turnIdRaw : void 0;
49356
49568
  if (type === "event") {
49357
49569
  const rawEvent = payload.event;
49358
- if (!isRecord2(rawEvent) || typeof rawEvent.type !== "string") return null;
49570
+ if (!isRecord3(rawEvent) || typeof rawEvent.type !== "string") return null;
49359
49571
  if (rawEvent.type === "tool_use") {
49360
49572
  const tool = rawEvent.tool;
49361
- if (!isRecord2(tool)) return null;
49573
+ if (!isRecord3(tool)) return null;
49362
49574
  const toolId = tool.id;
49363
49575
  const toolName = tool.name;
49364
49576
  if (typeof toolId !== "string" || typeof toolName !== "string") return null;
49365
49577
  const input = tool.input;
49366
- const argKeys = isRecord2(input) ? Object.keys(input) : [];
49367
- const projectSlug = isRecord2(input) && typeof input.projectSlug === "string" ? input.projectSlug : void 0;
49578
+ const argKeys = isRecord3(input) ? Object.keys(input) : [];
49579
+ const projectSlug = isRecord3(input) && typeof input.projectSlug === "string" ? input.projectSlug : void 0;
49368
49580
  const proj = { kind: "tool_use", toolId, toolName, argKeys, seq: maybeSeq ?? 0 };
49369
49581
  if (maybeTurnId !== void 0) proj.turnId = maybeTurnId;
49370
49582
  if (ms !== void 0) proj.ms = ms;
@@ -49373,7 +49585,7 @@ function buildProjection(payload, type, ms, telemetry) {
49373
49585
  }
49374
49586
  if (rawEvent.type === "tool_result") {
49375
49587
  const tool = rawEvent.tool;
49376
- if (!isRecord2(tool)) return null;
49588
+ if (!isRecord3(tool)) return null;
49377
49589
  const toolId = tool.id;
49378
49590
  if (typeof toolId !== "string") return null;
49379
49591
  const exitCode = tool.exitCode;
@@ -49421,7 +49633,7 @@ function buildProjection(payload, type, ms, telemetry) {
49421
49633
  const proj = { kind: "plumbing", plumbingKind: "rate_limit_status", seq: maybeSeq ?? 0 };
49422
49634
  if (maybeTurnId !== void 0) proj.turnId = maybeTurnId;
49423
49635
  if (ms !== void 0) proj.ms = ms;
49424
- if (isRecord2(rateLimits)) proj.rateLimitReached = rateLimits.rateLimitReachedType != null;
49636
+ if (isRecord3(rateLimits)) proj.rateLimitReached = rateLimits.rateLimitReachedType != null;
49425
49637
  return proj;
49426
49638
  }
49427
49639
  const bracketMatch = /^<([a-z-]+)>$/.exec(type);
@@ -54674,7 +54886,7 @@ function dayFilesInWindow(now, windowMs) {
54674
54886
  }
54675
54887
  return files;
54676
54888
  }
54677
- function isRecord3(value) {
54889
+ function isRecord4(value) {
54678
54890
  return typeof value === "object" && value !== null;
54679
54891
  }
54680
54892
  function extractItems(lines2, sessionIds) {
@@ -54692,7 +54904,7 @@ function extractItems(lines2, sessionIds) {
54692
54904
  skipped2++;
54693
54905
  continue;
54694
54906
  }
54695
- if (!isRecord3(parsed)) {
54907
+ if (!isRecord4(parsed)) {
54696
54908
  skipped2++;
54697
54909
  continue;
54698
54910
  }
@@ -54701,11 +54913,11 @@ function extractItems(lines2, sessionIds) {
54701
54913
  const ts = obj.ts;
54702
54914
  const type = obj.type;
54703
54915
  const payload = obj.payload;
54704
- if (type === "turn" && isRecord3(payload) && typeof payload.message === "string") {
54916
+ if (type === "turn" && isRecord4(payload) && typeof payload.message === "string") {
54705
54917
  items.push({ ts, kind: "user", text: payload.message });
54706
- } else if (type === "event" && isRecord3(payload) && isRecord3(payload.event) && payload.event.type === "assistant" && typeof payload.event.content === "string") {
54918
+ } else if (type === "event" && isRecord4(payload) && isRecord4(payload.event) && payload.event.type === "assistant" && typeof payload.event.content === "string") {
54707
54919
  items.push({ ts, kind: "assistant", text: payload.event.content });
54708
- } else if (type === "event" && isRecord3(payload) && isRecord3(payload.event) && payload.event.type === "tool_use" && isRecord3(payload.event.tool) && typeof payload.event.tool.name === "string") {
54920
+ } else if (type === "event" && isRecord4(payload) && isRecord4(payload.event) && payload.event.type === "tool_use" && isRecord4(payload.event.tool) && typeof payload.event.tool.name === "string") {
54709
54921
  items.push({ ts, kind: "tool", text: payload.event.tool.name });
54710
54922
  }
54711
54923
  }
@@ -54798,7 +55010,7 @@ async function readSessionRows(logsDir, files, sessionIds) {
54798
55010
  stream.destroy();
54799
55011
  }
54800
55012
  } catch (e) {
54801
- if (isRecord3(e) && e.code === "ENOENT") {
55013
+ if (isRecord4(e) && e.code === "ENOENT") {
54802
55014
  missingFiles++;
54803
55015
  continue;
54804
55016
  }
@@ -57396,10 +57608,10 @@ async function startDirectorBridge() {
57396
57608
  migrateRetiredApiBaseUrls();
57397
57609
  } catch {
57398
57610
  }
57399
- const apiBaseUrl = readConfiguredApiBaseUrl();
57611
+ const apiBaseUrl = readDirectorBridgeApiBaseUrl();
57400
57612
  if (!isRemoteApiAllowed(apiBaseUrl)) {
57401
57613
  throw new Error(
57402
- `Refusing to run the director-bridge execution consumer against non-loopback API URL ${apiBaseUrl}. Set MRRLIN_API_ALLOW_REMOTE=1 or MRRLIN_STAGING=1 if this is intentional.`
57614
+ `Refusing to run the director-bridge execution consumer against untrusted remote API URL ${apiBaseUrl}. Set MRRLIN_API_ALLOW_REMOTE=1 for a custom remote, or set MRRLIN_API_BASE_URL=https://api-staging.mrrlin.com for the official staging API.`
57403
57615
  );
57404
57616
  }
57405
57617
  const envToken = process.env.MRRLIN_TOKEN?.trim() || void 0;
@@ -64601,7 +64813,7 @@ async function invokeCapability(server, toolName, payload) {
64601
64813
  await client.connect(transport);
64602
64814
  result = await client.callTool({
64603
64815
  name: toolName,
64604
- arguments: isRecord4(payload) ? payload : void 0
64816
+ arguments: isRecord5(payload) ? payload : void 0
64605
64817
  });
64606
64818
  } catch (error51) {
64607
64819
  throw classifyInvokeError(error51, server.url, toolName);
@@ -64653,7 +64865,7 @@ function classifyInvokeError(error51, url2, toolName) {
64653
64865
  if (error51 instanceof TypeError) return new InvokeUnreachableError(url2, error51);
64654
64866
  return new InvokeMalformedResultError(error51 instanceof Error ? error51.message : String(error51), error51);
64655
64867
  }
64656
- function isRecord4(value) {
64868
+ function isRecord5(value) {
64657
64869
  return typeof value === "object" && value !== null && !Array.isArray(value);
64658
64870
  }
64659
64871
  function extractFirstUrl(content) {
@@ -64662,7 +64874,7 @@ function extractFirstUrl(content) {
64662
64874
  function extractFirstText(content) {
64663
64875
  if (Array.isArray(content)) {
64664
64876
  for (const block of content) {
64665
- if (isRecord4(block) && typeof block.text === "string") return block.text;
64877
+ if (isRecord5(block) && typeof block.text === "string") return block.text;
64666
64878
  }
64667
64879
  }
64668
64880
  return JSON.stringify(content);
@@ -64679,7 +64891,7 @@ function findFirstMatch(value, pattern) {
64679
64891
  }
64680
64892
  return null;
64681
64893
  }
64682
- if (isRecord4(value)) {
64894
+ if (isRecord5(value)) {
64683
64895
  for (const nested of Object.values(value)) {
64684
64896
  const found = findFirstMatch(nested, pattern);
64685
64897
  if (found) return found;
@@ -64900,7 +65112,7 @@ async function recordUse(client, route, input, outcome) {
64900
65112
  }
64901
65113
  }
64902
65114
  async function callDestination(client, projectSlug, route, payload, mcpConfigPath) {
64903
- const operation = isRecord5(payload) ? payload.operation : void 0;
65115
+ const operation = isRecord6(payload) ? payload.operation : void 0;
64904
65116
  if (route.capability === "wiki") {
64905
65117
  if (operation !== "create" && operation !== "update") {
64906
65118
  throw new Error(`callDestination: payload.operation "${String(operation)}" is not valid for wiki`);
@@ -64917,7 +65129,7 @@ async function callDestination(client, projectSlug, route, payload, mcpConfigPat
64917
65129
  }
64918
65130
  throw new Error(`callDestination: no destination wired for capability "${route.capability}"`);
64919
65131
  }
64920
- function isRecord5(value) {
65132
+ function isRecord6(value) {
64921
65133
  return typeof value === "object" && value !== null;
64922
65134
  }
64923
65135
 
@@ -65967,7 +66179,7 @@ var import_node_path31 = require("node:path");
65967
66179
  function evidenceDedupKey(sessionId, reportId) {
65968
66180
  return `${sessionId}:${reportId}`;
65969
66181
  }
65970
- function isRecord6(v) {
66182
+ function isRecord7(v) {
65971
66183
  return v !== null && typeof v === "object";
65972
66184
  }
65973
66185
  async function readDayLines(stateDir, day, sessionId) {
@@ -65986,7 +66198,7 @@ async function readDayLines(stateDir, day, sessionId) {
65986
66198
  } catch {
65987
66199
  continue;
65988
66200
  }
65989
- if (!isRecord6(rec) || !isRecord6(rec.payload)) continue;
66201
+ if (!isRecord7(rec) || !isRecord7(rec.payload)) continue;
65990
66202
  if (rec.payload.directorSessionId !== sessionId) continue;
65991
66203
  kept.push(rec);
65992
66204
  }
@@ -66056,7 +66268,7 @@ async function assembleEvidence(stateDir, sessionId, reportId, gateCtx) {
66056
66268
  } catch {
66057
66269
  continue;
66058
66270
  }
66059
- if (!isRecord6(rec) || !Array.isArray(rec.daysRemoved)) continue;
66271
+ if (!isRecord7(rec) || !Array.isArray(rec.daysRemoved)) continue;
66060
66272
  for (const removedDay of rec.daysRemoved) {
66061
66273
  if (daysRead.includes(removedDay)) {
66062
66274
  windowPruned = true;
@@ -68109,7 +68321,7 @@ function resolveServerOptions(input = {}) {
68109
68321
  const remoteAllowed = isRemoteApiAllowed(apiBaseUrl) || input.allowRemoteApi === true;
68110
68322
  if (!remoteAllowed) {
68111
68323
  throw new Error(
68112
- `Refusing to start MCP server against non-loopback URL ${apiBaseUrl}. Set MRRLIN_API_ALLOW_REMOTE=1 or MRRLIN_STAGING=1 if this is intentional.`
68324
+ `Refusing to start MCP server against non-loopback URL ${apiBaseUrl}. Set MRRLIN_API_ALLOW_REMOTE=1 for a custom remote, or set MRRLIN_API_BASE_URL=https://api-staging.mrrlin.com for the official staging API.`
68113
68325
  );
68114
68326
  }
68115
68327
  return {
@@ -68279,6 +68491,62 @@ function runSetCredential(args) {
68279
68491
  var import_node_os16 = __toESM(require("node:os"), 1);
68280
68492
  var import_node_fs22 = require("node:fs");
68281
68493
  var import_node_path33 = __toESM(require("node:path"), 1);
68494
+ function readExistingTelemetryDefaultProject(deps, ecoPath) {
68495
+ try {
68496
+ const existing = deps.readFile?.(ecoPath);
68497
+ if (!existing) return void 0;
68498
+ const match = existing.match(/\bMRRLIN_TELEMETRY_DEFAULT_PROJECT:\s*("(?:[^"\\]|\\.)*")/);
68499
+ if (!match?.[1]) return void 0;
68500
+ const value = JSON.parse(match[1]);
68501
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
68502
+ } catch {
68503
+ return void 0;
68504
+ }
68505
+ }
68506
+ function isOwnedEcosystemConfig(text) {
68507
+ return /name:\s*"mrrlin-bridge"/.test(text) && /script:\s*"mrrlin-mcp"/.test(text) && /args:\s*"director-bridge"/.test(text);
68508
+ }
68509
+ function readEnvString(text, key) {
68510
+ const match = new RegExp(`\\b${key}:\\s*("(?:[^"\\\\]|\\\\.)*")`).exec(text);
68511
+ if (!match?.[1]) return void 0;
68512
+ try {
68513
+ const value = JSON.parse(match[1]);
68514
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
68515
+ } catch {
68516
+ return void 0;
68517
+ }
68518
+ }
68519
+ function normalizeServiceApiTarget(apiBaseUrl, allowRemote) {
68520
+ const normalized = retargetRetiredApiBaseUrl(apiBaseUrl).baseUrl;
68521
+ if (isProductionApiBaseUrl(normalized)) return {};
68522
+ const target = { apiBaseUrl: normalized };
68523
+ if (allowRemote && !isStagingApiBaseUrl(normalized) && !isLoopbackUrl(normalized)) target.apiAllowRemote = true;
68524
+ return target;
68525
+ }
68526
+ function readExistingServiceApiTarget(deps, ecoPath) {
68527
+ try {
68528
+ const existing = deps.readFile?.(ecoPath);
68529
+ if (!existing || !isOwnedEcosystemConfig(existing)) return void 0;
68530
+ const apiBaseUrl = readEnvString(existing, "MRRLIN_API_BASE_URL");
68531
+ if (apiBaseUrl) {
68532
+ return normalizeServiceApiTarget(apiBaseUrl, readEnvString(existing, "MRRLIN_API_ALLOW_REMOTE") === "1");
68533
+ }
68534
+ if (readEnvString(existing, "MRRLIN_STAGING") === "1") {
68535
+ return { apiBaseUrl: STAGING_API_BASE_URL };
68536
+ }
68537
+ return void 0;
68538
+ } catch {
68539
+ return void 0;
68540
+ }
68541
+ }
68542
+ function deriveServiceApiTarget(deps, ecoPath) {
68543
+ const apiBaseUrl = deps.env.MRRLIN_API_BASE_URL?.trim();
68544
+ if (apiBaseUrl) return normalizeServiceApiTarget(apiBaseUrl, deps.env.MRRLIN_API_ALLOW_REMOTE === "1");
68545
+ if (deps.env.MRRLIN_STAGING === "1" || bridgeUrlImpliesStaging(deps.env.MRRLIN_BRIDGE_URL)) {
68546
+ return { apiBaseUrl: STAGING_API_BASE_URL };
68547
+ }
68548
+ return readExistingServiceApiTarget(deps, ecoPath) ?? {};
68549
+ }
68282
68550
  var SYSTEM_PATH_DIRS = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"];
68283
68551
  function defaultHomeOwnerUid(home) {
68284
68552
  try {
@@ -68345,18 +68613,19 @@ function installService(deps) {
68345
68613
  const pathEnv = Array.from(
68346
68614
  /* @__PURE__ */ new Set([...resolvedBins.map((b) => import_node_path33.default.dirname(b)), ...SYSTEM_PATH_DIRS])
68347
68615
  ).join(":");
68348
- const apiBaseUrl = deps.env.MRRLIN_API_BASE_URL?.trim() || void 0;
68349
- const staging = deps.env.MRRLIN_STAGING === "1" || !apiBaseUrl && bridgeUrlImpliesStaging(deps.env.MRRLIN_BRIDGE_URL);
68616
+ const ecoPath = import_node_path33.default.join(home, ".mrrlin", "ecosystem.config.cjs");
68617
+ const apiTarget = deriveServiceApiTarget(deps, ecoPath);
68618
+ const telemetryDefaultProject = "MRRLIN_TELEMETRY_DEFAULT_PROJECT" in deps.env ? deps.env.MRRLIN_TELEMETRY_DEFAULT_PROJECT?.trim() || void 0 : readExistingTelemetryDefaultProject(deps, ecoPath);
68350
68619
  const text = renderEcosystemConfig({
68351
68620
  cwd: resolved.cwd,
68352
68621
  home,
68353
68622
  user,
68354
68623
  codexHome: deps.env.CODEX_HOME?.trim() || void 0,
68355
68624
  pathEnv,
68356
- staging,
68357
- apiBaseUrl
68625
+ apiBaseUrl: apiTarget.apiBaseUrl,
68626
+ apiAllowRemote: apiTarget.apiAllowRemote,
68627
+ telemetryDefaultProject
68358
68628
  });
68359
- const ecoPath = import_node_path33.default.join(home, ".mrrlin", "ecosystem.config.cjs");
68360
68629
  deps.writeFile(ecoPath, text);
68361
68630
  const start = deps.runPm2(["startOrReload", ecoPath]);
68362
68631
  if (start.code !== 0) {
@@ -68492,7 +68761,7 @@ function parseArgs(args) {
68492
68761
  }
68493
68762
  return map2;
68494
68763
  }
68495
- function isRecord7(v) {
68764
+ function isRecord8(v) {
68496
68765
  return v !== null && typeof v === "object";
68497
68766
  }
68498
68767
  async function runExtract(args, deps) {
@@ -68529,8 +68798,8 @@ Missing required flag(s): ${missing.join(
68529
68798
  } catch {
68530
68799
  continue;
68531
68800
  }
68532
- const payload = isRecord7(parsed) ? parsed.payload : void 0;
68533
- if (isRecord7(payload) && payload.directorSessionId === sessionId) {
68801
+ const payload = isRecord8(parsed) ? parsed.payload : void 0;
68802
+ if (isRecord8(payload) && payload.directorSessionId === sessionId) {
68534
68803
  matchingLines.push(line);
68535
68804
  }
68536
68805
  }
@@ -68638,8 +68907,9 @@ Usage:
68638
68907
  mrrlin-mcp serve Run the stdio MCP server. Honors
68639
68908
  MRRLIN_API_BASE_URL (default http://127.0.0.1:8787)
68640
68909
  and MRRLIN_TOKEN. Non-loopback API URLs require
68641
- MRRLIN_API_ALLOW_REMOTE=1, or use MRRLIN_STAGING=1
68642
- to target the known staging API alias.
68910
+ MRRLIN_API_ALLOW_REMOTE=1 for a custom remote. Use
68911
+ MRRLIN_API_BASE_URL=https://api-staging.mrrlin.com
68912
+ to target the known staging API.
68643
68913
 
68644
68914
  mrrlin-mcp install-browser Idempotently install the dedicated, pinned
68645
68915
  Chromium into ~/.mrrlin/browser/browsers (its own
@@ -68675,16 +68945,16 @@ Usage:
68675
68945
  Bakes API-target env vars into the block as an
68676
68946
  inline env table so \`serve\` boots even when the
68677
68947
  local 127.0.0.1:8787 is not running. Setup guide:
68678
- MRRLIN_STAGING=1 mrrlin-mcp configure bridge
68679
- is the canonical path (staging alias + remote-
68680
- allow gate in one flag). For a non-staging remote,
68948
+ MRRLIN_API_BASE_URL=https://api-staging.mrrlin.com mrrlin-mcp configure bridge
68949
+ is the canonical staging path. For a custom remote,
68681
68950
  pass BOTH the URL AND the allow gate:
68682
68951
  MRRLIN_API_BASE_URL=https://... \\
68683
68952
  MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp configure bridge
68684
68953
  Without an allow gate, \`serve\` still refuses any
68685
68954
  non-loopback URL (see \`mrrlin-mcp serve\` above).
68686
- URL and MRRLIN_STAGING are mutually exclusive at
68687
- bake time \u2014 URL wins, mirrors serve's resolver.
68955
+ An explicit URL is the only routing selector at bake
68956
+ time. Production needs no routing env; a custom
68957
+ remote also needs MRRLIN_API_ALLOW_REMOTE=1.
68688
68958
  Re-running BARE (no env vars) over a block you
68689
68959
  already pinned to staging/a remote target is safe:
68690
68960
  it keeps your env verbatim (reported as
@@ -68718,8 +68988,9 @@ Usage:
68718
68988
  pending runs, read-only). Consumer env:
68719
68989
  - MRRLIN_API_BASE_URL (default http://127.0.0.1:8787;
68720
68990
  non-loopback requires MRRLIN_API_ALLOW_REMOTE=1,
68721
- or MRRLIN_STAGING=1 for the known staging API alias)
68722
- - MRRLIN_STAGING=1 (shortcut for the public staging API)
68991
+ or an explicit staging API URL)
68992
+ - MRRLIN_API_BASE_URL=https://api-staging.mrrlin.com
68993
+ (known staging API)
68723
68994
  - MRRLIN_TOKEN (service-principal auth to the API;
68724
68995
  if unset, chat still works but claim-sweep /
68725
68996
  autonomous execution stays disabled)
@@ -68970,6 +69241,16 @@ async function main() {
68970
69241
  (0, import_node_fs23.mkdirSync)(import_node_path37.default.dirname(p), { recursive: true, mode: 448 });
68971
69242
  (0, import_node_fs23.writeFileSync)(p, c, { mode: 384 });
68972
69243
  },
69244
+ // Swallows every error on purpose — the only caller uses this to recover settings from the
69245
+ // config it is about to overwrite, and a missing or unreadable file means "no previous
69246
+ // value", never a failed install. See readExistingTelemetryDefaultProject.
69247
+ readFile: (p) => {
69248
+ try {
69249
+ return (0, import_node_fs23.readFileSync)(p, "utf8");
69250
+ } catch {
69251
+ return null;
69252
+ }
69253
+ },
68973
69254
  runPm2: pm2Runner,
68974
69255
  which: whichBin,
68975
69256
  hasSecret: () => Boolean(readAgentSecret()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.4.53",
3
+ "version": "0.4.55",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -24,8 +24,8 @@
24
24
  "@mrrlin/client": "0.0.0",
25
25
  "@mrrlin/agent-client": "0.0.0",
26
26
  "@mrrlin/director-e2e": "0.0.0",
27
- "@mrrlin/schemas": "0.0.0",
28
27
  "@mrrlin/tsconfig": "0.0.0",
28
+ "@mrrlin/schemas": "0.0.0",
29
29
  "@mrrlin/wiki": "0.0.0"
30
30
  },
31
31
  "dependencies": {