@mrrlin-dev/mcp 0.2.4 → 0.2.6

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/bin.cjs CHANGED
@@ -18671,11 +18671,11 @@ var require_dist = __commonJS({
18671
18671
  });
18672
18672
 
18673
18673
  // src/bin.ts
18674
- var import_node_path20 = __toESM(require("node:path"), 1);
18674
+ var import_node_path19 = __toESM(require("node:path"), 1);
18675
18675
  var import_node_child_process7 = require("node:child_process");
18676
18676
  var import_node_fs14 = require("node:fs");
18677
- var import_node_os10 = require("node:os");
18678
- var import_node_path21 = __toESM(require("node:path"), 1);
18677
+ var import_node_os11 = require("node:os");
18678
+ var import_node_path20 = __toESM(require("node:path"), 1);
18679
18679
 
18680
18680
  // src/install-codex.ts
18681
18681
  var import_promises = __toESM(require("node:fs/promises"), 1);
@@ -18759,7 +18759,7 @@ async function installCodex(options) {
18759
18759
  const configPath = import_node_path.default.join(codexHome, "config.toml");
18760
18760
  const distribution = await detectInstallDistribution(options);
18761
18761
  await import_promises.default.mkdir(codexHome, { recursive: true, mode: 448 });
18762
- const writePrompts = async () => installPrompts(codexHome, options.prompts ?? []);
18762
+ const writePrompts = async () => installPrompts(codexHome, options.prompts ?? [], options.forcePrompts === true);
18763
18763
  if (!await pathExists(configPath)) {
18764
18764
  await import_promises.default.writeFile(configPath, renderBlock(options.binPath, distribution).replace(/^\n/, ""), { mode: 384 });
18765
18765
  return { action: "created", configPath, prompts: await writePrompts() };
@@ -18799,7 +18799,7 @@ async function installCodex(options) {
18799
18799
  await import_promises.default.writeFile(realPath, appended, { mode: 384 });
18800
18800
  return { action: "appended", configPath: realPath, prompts: await writePrompts() };
18801
18801
  }
18802
- async function installPrompts(codexHome, prompts) {
18802
+ async function installPrompts(codexHome, prompts, forcePrompts) {
18803
18803
  if (prompts.length === 0) return [];
18804
18804
  const promptsDir2 = import_node_path.default.join(codexHome, "prompts");
18805
18805
  await import_promises.default.mkdir(promptsDir2, { recursive: true, mode: 448 });
@@ -18812,12 +18812,21 @@ async function installPrompts(codexHome, prompts) {
18812
18812
  } catch {
18813
18813
  existing = null;
18814
18814
  }
18815
+ if (existing === null) {
18816
+ await import_promises.default.writeFile(promptPath, prompt.content, { mode: 384 });
18817
+ out.push({ name: prompt.name, action: "created", promptPath });
18818
+ continue;
18819
+ }
18815
18820
  if (existing === prompt.content) {
18816
18821
  out.push({ name: prompt.name, action: "noop", promptPath });
18817
18822
  continue;
18818
18823
  }
18824
+ if (!forcePrompts) {
18825
+ out.push({ name: prompt.name, action: "skipped-modified", promptPath });
18826
+ continue;
18827
+ }
18819
18828
  await import_promises.default.writeFile(promptPath, prompt.content, { mode: 384 });
18820
- out.push({ name: prompt.name, action: existing === null ? "created" : "updated", promptPath });
18829
+ out.push({ name: prompt.name, action: "updated", promptPath });
18821
18830
  }
18822
18831
  return out;
18823
18832
  }
@@ -18838,11 +18847,11 @@ ${block}`;
18838
18847
  }
18839
18848
 
18840
18849
  // src/director-bridge.ts
18841
- var import_node_fs11 = __toESM(require("node:fs"), 1);
18850
+ var import_node_fs10 = __toESM(require("node:fs"), 1);
18842
18851
  var import_node_http = __toESM(require("node:http"), 1);
18843
18852
  var import_node_os7 = __toESM(require("node:os"), 1);
18844
- var import_node_path12 = __toESM(require("node:path"), 1);
18845
- var import_node_child_process4 = require("node:child_process");
18853
+ var import_node_path11 = __toESM(require("node:path"), 1);
18854
+ var import_node_child_process3 = require("node:child_process");
18846
18855
  var import_node_crypto4 = __toESM(require("node:crypto"), 1);
18847
18856
 
18848
18857
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
@@ -40918,75 +40927,10 @@ var CheckoutRegistry = class {
40918
40927
  }
40919
40928
  };
40920
40929
 
40921
- // src/checkout-scan.ts
40922
- var import_node_fs8 = require("node:fs");
40923
- var import_node_path9 = require("node:path");
40924
- var import_node_child_process3 = require("node:child_process");
40925
-
40926
- // src/git-remote-match.ts
40927
- function normalizeRemote(url2) {
40928
- const trimmed = url2.trim().replace(/\.git$/i, "");
40929
- const scp = /^[^@]+@[^:]+:(.+)$/.exec(trimmed);
40930
- const pathPart = scp ? scp[1] : (() => {
40931
- try {
40932
- return new URL(trimmed).pathname.replace(/^\/+/, "");
40933
- } catch {
40934
- return null;
40935
- }
40936
- })();
40937
- if (!pathPart) return null;
40938
- const segs = pathPart.split("/").filter(Boolean);
40939
- if (segs.length < 2) return null;
40940
- return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`.toLowerCase();
40941
- }
40942
- function remoteMatchesRepo(remoteUrl, repoFullName) {
40943
- const a = normalizeRemote(remoteUrl);
40944
- return a !== null && a === repoFullName.trim().toLowerCase();
40945
- }
40946
-
40947
- // src/checkout-scan.ts
40948
- var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".cache", "Library", ".Trash"]);
40949
- function originUrl(dir) {
40950
- try {
40951
- return (0, import_node_child_process3.execFileSync)("git", ["-C", dir, "remote", "get-url", "origin"], {
40952
- encoding: "utf8",
40953
- stdio: ["ignore", "pipe", "ignore"]
40954
- }).trim();
40955
- } catch {
40956
- return null;
40957
- }
40958
- }
40959
- function scanForCheckouts(roots, repoFullName, opts = {}) {
40960
- const maxDepth = opts.maxDepth ?? 3;
40961
- const out = [];
40962
- const walk = (dir, depth) => {
40963
- if (depth > maxDepth) return;
40964
- const url2 = originUrl(dir);
40965
- if (url2) {
40966
- if (remoteMatchesRepo(url2, repoFullName)) out.push(dir);
40967
- return;
40968
- }
40969
- let entries = [];
40970
- try {
40971
- entries = (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP.has(e.name)).map((e) => e.name);
40972
- } catch {
40973
- return;
40974
- }
40975
- for (const name of entries) walk((0, import_node_path9.join)(dir, name), depth + 1);
40976
- };
40977
- for (const root of roots) walk(root, 0);
40978
- return out;
40979
- }
40980
- function defaultScanRoots(home) {
40981
- const fromEnv = (process.env.MRRLIN_DIRECTOR_BRIDGE_SCAN_ROOTS ?? "").trim();
40982
- if (fromEnv) return fromEnv.split(":").filter(Boolean);
40983
- return ["dev", "projects", "src", "work"].map((d) => (0, import_node_path9.join)(home, d));
40984
- }
40985
-
40986
40930
  // src/executor-server.ts
40987
- var import_node_fs9 = require("node:fs");
40931
+ var import_node_fs8 = require("node:fs");
40988
40932
  var import_node_os6 = require("node:os");
40989
- var import_node_path10 = require("node:path");
40933
+ var import_node_path9 = require("node:path");
40990
40934
  var DENY_READ_GLOBS = [
40991
40935
  "**/.ssh/**",
40992
40936
  "**/.aws/**",
@@ -41019,15 +40963,15 @@ function buildExecutorConfig(opts) {
41019
40963
  }
41020
40964
  function operatorSecretPaths(realHome = (0, import_node_os6.homedir)()) {
41021
40965
  return [
41022
- (0, import_node_path10.join)(realHome, ".ssh"),
41023
- (0, import_node_path10.join)(realHome, ".aws"),
41024
- (0, import_node_path10.join)(realHome, ".config", "gh"),
41025
- (0, import_node_path10.join)(realHome, ".npmrc"),
41026
- (0, import_node_path10.join)(realHome, ".netrc"),
41027
- (0, import_node_path10.join)(realHome, ".gnupg"),
41028
- (0, import_node_path10.join)(realHome, ".kube"),
41029
- (0, import_node_path10.join)(realHome, ".docker", "config.json"),
41030
- (0, import_node_path10.join)(realHome, ".config", "gcloud")
40966
+ (0, import_node_path9.join)(realHome, ".ssh"),
40967
+ (0, import_node_path9.join)(realHome, ".aws"),
40968
+ (0, import_node_path9.join)(realHome, ".config", "gh"),
40969
+ (0, import_node_path9.join)(realHome, ".npmrc"),
40970
+ (0, import_node_path9.join)(realHome, ".netrc"),
40971
+ (0, import_node_path9.join)(realHome, ".gnupg"),
40972
+ (0, import_node_path9.join)(realHome, ".kube"),
40973
+ (0, import_node_path9.join)(realHome, ".docker", "config.json"),
40974
+ (0, import_node_path9.join)(realHome, ".config", "gcloud")
41031
40975
  ];
41032
40976
  }
41033
40977
  var SECRET_DIRECTORY_BASENAMES = /* @__PURE__ */ new Set([".ssh", ".aws", "gh", ".gnupg", ".kube", "gcloud"]);
@@ -41036,8 +40980,8 @@ function isSecretDirectoryPath(p) {
41036
40980
  return SECRET_DIRECTORY_BASENAMES.has(base);
41037
40981
  }
41038
40982
  function writeExecutorCodexHome(scratchDir, opts) {
41039
- const codexHome = (0, import_node_path10.join)(scratchDir, ".codex");
41040
- (0, import_node_fs9.mkdirSync)(codexHome, { recursive: true });
40983
+ const codexHome = (0, import_node_path9.join)(scratchDir, ".codex");
40984
+ (0, import_node_fs8.mkdirSync)(codexHome, { recursive: true });
41041
40985
  const hasEgress = opts.egressDomains.length > 0;
41042
40986
  const networkSection = hasEgress ? [
41043
40987
  "[permissions.executor.network]",
@@ -41072,21 +41016,21 @@ function writeExecutorCodexHome(scratchDir, opts) {
41072
41016
  networkSection,
41073
41017
  ""
41074
41018
  ].join("\n");
41075
- (0, import_node_fs9.writeFileSync)((0, import_node_path10.join)(codexHome, "config.toml"), toml3, "utf8");
41019
+ (0, import_node_fs8.writeFileSync)((0, import_node_path9.join)(codexHome, "config.toml"), toml3, "utf8");
41076
41020
  return codexHome;
41077
41021
  }
41078
41022
  async function startExecutorServer(opts) {
41079
41023
  const egressDomains = opts.egressDomains ?? DEFAULT_EGRESS_DOMAINS;
41080
41024
  const realHome = (0, import_node_os6.homedir)();
41081
41025
  const denySecretPaths = operatorSecretPaths(realHome);
41082
- const scratchDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path10.join)((0, import_node_os6.tmpdir)(), "mrrlin-executor-"));
41026
+ const scratchDir = (0, import_node_fs8.mkdtempSync)((0, import_node_path9.join)((0, import_node_os6.tmpdir)(), "mrrlin-executor-"));
41083
41027
  const scratchHome = scratchDir;
41084
41028
  let scratchCleaned = false;
41085
41029
  const cleanScratch = () => {
41086
41030
  if (scratchCleaned) return;
41087
41031
  scratchCleaned = true;
41088
41032
  try {
41089
- (0, import_node_fs9.rmSync)(scratchDir, { recursive: true, force: true });
41033
+ (0, import_node_fs8.rmSync)(scratchDir, { recursive: true, force: true });
41090
41034
  } catch {
41091
41035
  }
41092
41036
  };
@@ -41277,17 +41221,10 @@ function extractVerificationSection(markdown) {
41277
41221
  }
41278
41222
 
41279
41223
  // src/bridge-log.ts
41280
- var import_node_fs10 = require("node:fs");
41281
- var import_node_path11 = require("node:path");
41282
- var NOOP_LOGGER = {
41283
- logIn() {
41284
- },
41285
- logOut() {
41286
- },
41287
- close() {
41288
- }
41289
- };
41290
- var REDACT_DEPTH_CAP = 8;
41224
+ var import_node_fs9 = require("node:fs");
41225
+ var import_node_path10 = require("node:path");
41226
+
41227
+ // src/redact.ts
41291
41228
  var REDACTED = "[REDACTED]";
41292
41229
  var SECRET_PATTERNS = [
41293
41230
  /ghp_[A-Za-z0-9]{20,}/g,
@@ -41303,17 +41240,28 @@ var SECRET_PATTERNS = [
41303
41240
  /\b[A-Za-z0-9+/]{40,}={0,2}\b/g
41304
41241
  // long base64-ish run (opaque tokens)
41305
41242
  ];
41306
- function scrubString(value) {
41307
- let out = value;
41243
+ function redact(input) {
41244
+ let out = input;
41308
41245
  for (const re of SECRET_PATTERNS) out = out.replace(re, REDACTED);
41309
41246
  return out;
41310
41247
  }
41248
+
41249
+ // src/bridge-log.ts
41250
+ var NOOP_LOGGER = {
41251
+ logIn() {
41252
+ },
41253
+ logOut() {
41254
+ },
41255
+ close() {
41256
+ }
41257
+ };
41258
+ var REDACT_DEPTH_CAP = 8;
41311
41259
  function redactValue(value, depth) {
41312
- if (typeof value === "string") return scrubString(value);
41260
+ if (typeof value === "string") return redact(value);
41313
41261
  if (value === null || typeof value !== "object") return value;
41314
41262
  if (depth >= REDACT_DEPTH_CAP) {
41315
41263
  try {
41316
- return scrubString(JSON.stringify(value));
41264
+ return redact(JSON.stringify(value));
41317
41265
  } catch {
41318
41266
  return REDACTED;
41319
41267
  }
@@ -41351,14 +41299,14 @@ function resolvePromptsEnabled(env) {
41351
41299
  }
41352
41300
  function createBridgeLogger(stateDir, env = process.env) {
41353
41301
  if (!resolveBridgeLogEnabled(env)) return NOOP_LOGGER;
41354
- const logsDir = (0, import_node_path11.join)(stateDir, "logs");
41302
+ const logsDir = (0, import_node_path10.join)(stateDir, "logs");
41355
41303
  try {
41356
- (0, import_node_fs10.mkdirSync)(logsDir, { recursive: true });
41304
+ (0, import_node_fs9.mkdirSync)(logsDir, { recursive: true });
41357
41305
  } catch {
41358
41306
  return NOOP_LOGGER;
41359
41307
  }
41360
41308
  const includePrompts = resolvePromptsEnabled(env);
41361
- const fileFor = () => (0, import_node_path11.join)(logsDir, `bridge-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
41309
+ const fileFor = () => (0, import_node_path10.join)(logsDir, `bridge-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
41362
41310
  const write = (dir, rec) => {
41363
41311
  try {
41364
41312
  const line = JSON.stringify({
@@ -41370,7 +41318,7 @@ function createBridgeLogger(stateDir, env = process.env) {
41370
41318
  ...rec.ms !== void 0 ? { ms: rec.ms } : {},
41371
41319
  payload: redactMessage(rec.payload, { includePrompts })
41372
41320
  }) + "\n";
41373
- (0, import_node_fs10.appendFileSync)(fileFor(), line);
41321
+ (0, import_node_fs9.appendFileSync)(fileFor(), line);
41374
41322
  } catch {
41375
41323
  }
41376
41324
  };
@@ -41404,9 +41352,9 @@ function normalizeModel(value) {
41404
41352
  return trimmed ? trimmed : null;
41405
41353
  }
41406
41354
  function neutralCheckoutCwd(stateDir) {
41407
- const dir = import_node_path12.default.join(stateDir, "no-checkout");
41355
+ const dir = import_node_path11.default.join(stateDir, "no-checkout");
41408
41356
  try {
41409
- import_node_fs11.default.mkdirSync(dir, { recursive: true });
41357
+ import_node_fs10.default.mkdirSync(dir, { recursive: true });
41410
41358
  } catch {
41411
41359
  }
41412
41360
  return dir;
@@ -41597,7 +41545,7 @@ function resolveDefaultBranch(checkoutPath) {
41597
41545
  if (!checkoutPath) return "main";
41598
41546
  const runGit = (args) => {
41599
41547
  try {
41600
- return (0, import_node_child_process4.execFileSync)("git", ["-C", checkoutPath, ...args], {
41548
+ return (0, import_node_child_process3.execFileSync)("git", ["-C", checkoutPath, ...args], {
41601
41549
  encoding: "utf8",
41602
41550
  stdio: ["ignore", "pipe", "ignore"],
41603
41551
  timeout: 15e3,
@@ -41629,7 +41577,7 @@ async function runOrphanWorktreeSweep(client, opts) {
41629
41577
  const cutoff = Date.now() - worktreeRetentionMs();
41630
41578
  const isReapable = (worktreePath) => {
41631
41579
  try {
41632
- return import_node_fs11.default.statSync(worktreePath).mtimeMs < cutoff;
41580
+ return import_node_fs10.default.statSync(worktreePath).mtimeMs < cutoff;
41633
41581
  } catch {
41634
41582
  return false;
41635
41583
  }
@@ -41666,8 +41614,8 @@ async function runOrphanWorktreeSweep(client, opts) {
41666
41614
  }
41667
41615
  }
41668
41616
  function readStateDir() {
41669
- if (process.env.CODEX_HOME) return import_node_path12.default.join(process.env.CODEX_HOME, "mrrlin", "director-bridge");
41670
- return import_node_path12.default.join(import_node_os7.default.homedir(), ".mrrlin", "director-bridge");
41617
+ if (process.env.CODEX_HOME) return import_node_path11.default.join(process.env.CODEX_HOME, "mrrlin", "director-bridge");
41618
+ return import_node_path11.default.join(import_node_os7.default.homedir(), ".mrrlin", "director-bridge");
41671
41619
  }
41672
41620
  function readAllowedOrigins() {
41673
41621
  const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_ALLOWED_ORIGINS ?? "").trim();
@@ -41931,50 +41879,6 @@ function createBridgeMessageHandler(deps) {
41931
41879
  sendForSpan({ type: "reconfigured", directorSessionId: directorSessionId2 });
41932
41880
  return;
41933
41881
  }
41934
- if (msg.type === "discover-checkouts") {
41935
- const projectSlug = typeof msg.projectSlug === "string" ? msg.projectSlug.trim() : "";
41936
- const repo = typeof msg.repo === "string" ? msg.repo.trim() : "";
41937
- if (!projectSlug || !repo) {
41938
- sendForSpan({ type: "error", error: "discover-checkouts requires projectSlug and repo." });
41939
- return;
41940
- }
41941
- const roots = deps.scanRoots ?? defaultScanRoots(import_node_os7.default.homedir());
41942
- const candidates = scanForCheckouts(roots, repo);
41943
- sendForSpan({ type: "checkout-candidates", projectSlug, candidates });
41944
- return;
41945
- }
41946
- if (msg.type === "confirm-checkout") {
41947
- const projectSlug = typeof msg.projectSlug === "string" ? msg.projectSlug.trim() : "";
41948
- const checkoutPath = typeof msg.path === "string" ? msg.path.trim() : "";
41949
- const repo = typeof msg.repo === "string" ? msg.repo.trim() : "";
41950
- if (!projectSlug || !checkoutPath) {
41951
- sendForSpan({ type: "checkout-error", projectSlug, error: "confirm-checkout requires projectSlug and path." });
41952
- return;
41953
- }
41954
- let remoteUrl = null;
41955
- try {
41956
- remoteUrl = (0, import_node_child_process4.execFileSync)("git", ["-C", checkoutPath, "remote", "get-url", "origin"], {
41957
- encoding: "utf8",
41958
- stdio: ["ignore", "pipe", "ignore"],
41959
- timeout: 15e3,
41960
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
41961
- }).trim();
41962
- } catch {
41963
- sendForSpan({ type: "checkout-error", projectSlug, error: `Path is not a git repository or has no origin remote: ${checkoutPath}` });
41964
- return;
41965
- }
41966
- if (repo && !remoteMatchesRepo(remoteUrl, repo)) {
41967
- sendForSpan({ type: "checkout-error", projectSlug, error: `Origin remote ${remoteUrl} does not match repo ${repo}.` });
41968
- return;
41969
- }
41970
- if (!deps.checkoutRegistry) {
41971
- sendForSpan({ type: "checkout-error", projectSlug, error: "No checkout registry configured." });
41972
- return;
41973
- }
41974
- deps.checkoutRegistry.confirm(projectSlug, checkoutPath, (/* @__PURE__ */ new Date()).toISOString());
41975
- sendForSpan({ type: "checkout-confirmed", projectSlug, path: checkoutPath });
41976
- return;
41977
- }
41978
41882
  if (msg.type !== "turn" || typeof msg.directorSessionId !== "string" || typeof msg.message !== "string") {
41979
41883
  sendForSpan({ type: "error", error: "Invalid message schema." });
41980
41884
  return;
@@ -42297,8 +42201,8 @@ async function startDirectorBridge() {
42297
42201
  codexCwd: config2.codexCwd,
42298
42202
  codexExecutable: config2.codexExecutable,
42299
42203
  checkoutRegistry,
42300
- worktreeRoot: import_node_path12.default.join(config2.stateDir, "worktrees"),
42301
- locksDir: import_node_path12.default.join(config2.stateDir, "locks"),
42204
+ worktreeRoot: import_node_path11.default.join(config2.stateDir, "worktrees"),
42205
+ locksDir: import_node_path11.default.join(config2.stateDir, "locks"),
42302
42206
  // Wire the profile-enforced executor server for code-execution runs. Each run gets its own
42303
42207
  // sandboxed Codex instance (scrubbed env, scratch CODEX_HOME, deny-read, egress allowlist).
42304
42208
  // The returned handle's dispose() shuts down the spawned app-server CHILD (manager.shutdown(),
@@ -42331,13 +42235,13 @@ async function startDirectorBridge() {
42331
42235
  },
42332
42236
  runVerificationTurn: createRunVerificationTurn({
42333
42237
  startSandbox: async (egressDomains) => {
42334
- const cwd = import_node_fs11.default.mkdtempSync(import_node_path12.default.join(import_node_os7.default.tmpdir(), "mrrlin-verify-"));
42238
+ const cwd = import_node_fs10.default.mkdtempSync(import_node_path11.default.join(import_node_os7.default.tmpdir(), "mrrlin-verify-"));
42335
42239
  let handle;
42336
42240
  try {
42337
42241
  handle = await startExecutorServer({ worktree: cwd, egressDomains });
42338
42242
  } catch (error51) {
42339
42243
  try {
42340
- import_node_fs11.default.rmSync(cwd, { recursive: true, force: true });
42244
+ import_node_fs10.default.rmSync(cwd, { recursive: true, force: true });
42341
42245
  } catch {
42342
42246
  }
42343
42247
  throw error51;
@@ -42348,7 +42252,7 @@ async function startDirectorBridge() {
42348
42252
  dispose: () => {
42349
42253
  handle.dispose();
42350
42254
  try {
42351
- import_node_fs11.default.rmSync(cwd, { recursive: true, force: true });
42255
+ import_node_fs10.default.rmSync(cwd, { recursive: true, force: true });
42352
42256
  } catch {
42353
42257
  }
42354
42258
  }
@@ -42581,8 +42485,8 @@ async function startDirectorBridge() {
42581
42485
  enqueueRun(
42582
42486
  () => runOrphanWorktreeSweep(client, {
42583
42487
  checkoutRegistry,
42584
- worktreeRoot: import_node_path12.default.join(config2.stateDir, "worktrees"),
42585
- locksDir: import_node_path12.default.join(config2.stateDir, "locks")
42488
+ worktreeRoot: import_node_path11.default.join(config2.stateDir, "worktrees"),
42489
+ locksDir: import_node_path11.default.join(config2.stateDir, "locks")
42586
42490
  })
42587
42491
  );
42588
42492
  scheduleClaimLoop();
@@ -42739,15 +42643,15 @@ function readDispatchBody(req) {
42739
42643
  function readOrCreateBridgeToken(stateDir) {
42740
42644
  const explicit = (process.env.MRRLIN_DIRECTOR_BRIDGE_TOKEN ?? "").trim();
42741
42645
  if (explicit) return explicit;
42742
- const tokenPath = import_node_path12.default.join(stateDir, "token.txt");
42646
+ const tokenPath = import_node_path11.default.join(stateDir, "token.txt");
42743
42647
  try {
42744
- const existing = import_node_fs11.default.readFileSync(tokenPath, "utf8").trim();
42648
+ const existing = import_node_fs10.default.readFileSync(tokenPath, "utf8").trim();
42745
42649
  if (existing) return existing;
42746
42650
  } catch {
42747
42651
  }
42748
- import_node_fs11.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
42652
+ import_node_fs10.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
42749
42653
  const token = import_node_crypto4.default.randomBytes(32).toString("base64url");
42750
- import_node_fs11.default.writeFileSync(tokenPath, `${token}
42654
+ import_node_fs10.default.writeFileSync(tokenPath, `${token}
42751
42655
  `, { mode: 384 });
42752
42656
  return token;
42753
42657
  }
@@ -46130,14 +46034,17 @@ var StdioServerTransport = class {
46130
46034
 
46131
46035
  // src/tools.ts
46132
46036
  var import_promises3 = require("node:fs/promises");
46133
- var import_node_path15 = __toESM(require("node:path"), 1);
46037
+ var import_node_fs12 = require("node:fs");
46038
+ var import_node_path14 = __toESM(require("node:path"), 1);
46039
+ var import_node_os8 = __toESM(require("node:os"), 1);
46040
+ var import_node_child_process6 = require("node:child_process");
46134
46041
 
46135
46042
  // ../../packages/wiki/dist/index.js
46136
46043
  var import_promises2 = __toESM(require("node:fs/promises"), 1);
46137
- var import_node_path13 = __toESM(require("node:path"), 1);
46044
+ var import_node_path12 = __toESM(require("node:path"), 1);
46138
46045
  var INCLUDED_DOC_FOLDERS = /* @__PURE__ */ new Set(["_meta", "explanation", "how-to", "reference", "sdd", "tutorials"]);
46139
46046
  function toPosixPath(input) {
46140
- return input.split(import_node_path13.default.sep).join("/");
46047
+ return input.split(import_node_path12.default.sep).join("/");
46141
46048
  }
46142
46049
  function slugifyWikiSegment(input) {
46143
46050
  return input.trim().toLowerCase().replace(/`/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -46151,12 +46058,12 @@ async function pathExists2(input) {
46151
46058
  }
46152
46059
  }
46153
46060
  async function findWorkspaceRoot(start = process.cwd()) {
46154
- let current = import_node_path13.default.resolve(start);
46061
+ let current = import_node_path12.default.resolve(start);
46155
46062
  while (true) {
46156
- if (await pathExists2(import_node_path13.default.join(current, "pnpm-workspace.yaml")) && await pathExists2(import_node_path13.default.join(current, "docs"))) {
46063
+ if (await pathExists2(import_node_path12.default.join(current, "pnpm-workspace.yaml")) && await pathExists2(import_node_path12.default.join(current, "docs"))) {
46157
46064
  return current;
46158
46065
  }
46159
- const parent = import_node_path13.default.dirname(current);
46066
+ const parent = import_node_path12.default.dirname(current);
46160
46067
  if (parent === current) {
46161
46068
  throw new Error(`Unable to find Mrrlin workspace root from ${start}`);
46162
46069
  }
@@ -46165,28 +46072,28 @@ async function findWorkspaceRoot(start = process.cwd()) {
46165
46072
  }
46166
46073
  async function resolveDocsRoot(input) {
46167
46074
  if (input) {
46168
- return import_node_path13.default.resolve(input);
46075
+ return import_node_path12.default.resolve(input);
46169
46076
  }
46170
46077
  if (process.env.MRRLIN_DOCS_ROOT) {
46171
- return import_node_path13.default.resolve(process.env.MRRLIN_DOCS_ROOT);
46078
+ return import_node_path12.default.resolve(process.env.MRRLIN_DOCS_ROOT);
46172
46079
  }
46173
- return import_node_path13.default.join(await findWorkspaceRoot(), "docs");
46080
+ return import_node_path12.default.join(await findWorkspaceRoot(), "docs");
46174
46081
  }
46175
46082
  async function resolveIndexPath(input) {
46176
46083
  if (input) {
46177
- return import_node_path13.default.resolve(input);
46084
+ return import_node_path12.default.resolve(input);
46178
46085
  }
46179
46086
  if (process.env.MRRLIN_WIKI_INDEX_PATH) {
46180
- return import_node_path13.default.resolve(process.env.MRRLIN_WIKI_INDEX_PATH);
46087
+ return import_node_path12.default.resolve(process.env.MRRLIN_WIKI_INDEX_PATH);
46181
46088
  }
46182
46089
  const workspaceRoot = await findWorkspaceRoot();
46183
- return import_node_path13.default.join(workspaceRoot, "apps", "web", "public", "wiki-index.json");
46090
+ return import_node_path12.default.join(workspaceRoot, "apps", "web", "public", "wiki-index.json");
46184
46091
  }
46185
46092
  async function walkMarkdownFiles(root, dir = root) {
46186
46093
  const entries = await import_promises2.default.readdir(dir, { withFileTypes: true });
46187
46094
  const files = [];
46188
46095
  for (const entry of entries) {
46189
- const absolute = import_node_path13.default.join(dir, entry.name);
46096
+ const absolute = import_node_path12.default.join(dir, entry.name);
46190
46097
  if (entry.isDirectory()) {
46191
46098
  files.push(...await walkMarkdownFiles(root, absolute));
46192
46099
  continue;
@@ -46205,7 +46112,7 @@ function parseTitle(markdown, sourcePath) {
46205
46112
  return title;
46206
46113
  }
46207
46114
  function documentFromFile(docsRoot, absolutePath, markdown) {
46208
- const relativePath = toPosixPath(import_node_path13.default.relative(docsRoot, absolutePath));
46115
+ const relativePath = toPosixPath(import_node_path12.default.relative(docsRoot, absolutePath));
46209
46116
  const [section, fileName] = relativePath.split("/");
46210
46117
  if (!section || !fileName || !INCLUDED_DOC_FOLDERS.has(section)) {
46211
46118
  return null;
@@ -46620,7 +46527,7 @@ async function runCodeGate(_target, deps) {
46620
46527
  }
46621
46528
 
46622
46529
  // src/consensus/codex-exec.ts
46623
- var import_node_child_process5 = require("node:child_process");
46530
+ var import_node_child_process4 = require("node:child_process");
46624
46531
  function extractFinalMessage(stdout) {
46625
46532
  let last = null;
46626
46533
  for (const rawLine of stdout.split(/\r?\n/)) {
@@ -46665,7 +46572,7 @@ function firstString(...values) {
46665
46572
  return null;
46666
46573
  }
46667
46574
  function runCodexExec(input, deps) {
46668
- const spawn2 = deps?.spawn ?? import_node_child_process5.spawn;
46575
+ const spawn2 = deps?.spawn ?? import_node_child_process4.spawn;
46669
46576
  const executable = input.codexExecutable ?? "codex";
46670
46577
  const sandbox = input.sandbox ?? "read-only";
46671
46578
  const fullPrompt = input.developerInstructions.trim() ? `${input.developerInstructions.trim()}
@@ -46737,12 +46644,12 @@ function errMessage(err) {
46737
46644
  }
46738
46645
 
46739
46646
  // src/consensus/wiring.ts
46740
- var import_node_fs12 = __toESM(require("node:fs"), 1);
46741
- var import_node_path14 = __toESM(require("node:path"), 1);
46647
+ var import_node_fs11 = __toESM(require("node:fs"), 1);
46648
+ var import_node_path13 = __toESM(require("node:path"), 1);
46742
46649
  var import_node_url = require("node:url");
46743
46650
 
46744
46651
  // src/consensus/code-gate-git.ts
46745
- var import_node_child_process6 = require("node:child_process");
46652
+ var import_node_child_process5 = require("node:child_process");
46746
46653
  var DIFF_MAX_BYTES = 2e5;
46747
46654
  var FASTGATE_OUTPUT_MAX_BYTES = 16e3;
46748
46655
  var GIT_TIMEOUT_MS = 6e4;
@@ -46758,7 +46665,7 @@ var defaultRunCmd = (cmd, args, opts) => new Promise((resolve) => {
46758
46665
  };
46759
46666
  let child;
46760
46667
  try {
46761
- child = (0, import_node_child_process6.spawn)(cmd, args, {
46668
+ child = (0, import_node_child_process5.spawn)(cmd, args, {
46762
46669
  cwd: opts.cwd,
46763
46670
  env: process.env,
46764
46671
  shell: false,
@@ -46973,15 +46880,15 @@ ${res.stdout}${res.stderr}`);
46973
46880
  var import_meta = {};
46974
46881
  function getPersonasDir() {
46975
46882
  if (typeof __dirname !== "undefined") {
46976
- return import_node_path14.default.join(__dirname, "consensus", "personas");
46883
+ return import_node_path13.default.join(__dirname, "consensus", "personas");
46977
46884
  }
46978
- return import_node_path14.default.join(import_node_path14.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)), "personas");
46885
+ return import_node_path13.default.join(import_node_path13.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)), "personas");
46979
46886
  }
46980
46887
  var personaCache = /* @__PURE__ */ new Map();
46981
46888
  function loadPersona(name) {
46982
46889
  const cached2 = personaCache.get(name);
46983
46890
  if (cached2 !== void 0) return cached2;
46984
- const content = import_node_fs12.default.readFileSync(import_node_path14.default.join(getPersonasDir(), `${name}.md`), "utf8");
46891
+ const content = import_node_fs11.default.readFileSync(import_node_path13.default.join(getPersonasDir(), `${name}.md`), "utf8");
46985
46892
  personaCache.set(name, content);
46986
46893
  return content;
46987
46894
  }
@@ -47290,6 +47197,27 @@ async function enqueueAsyncTool(client, descriptor2, input) {
47290
47197
  }
47291
47198
  }
47292
47199
 
47200
+ // src/git-remote-match.ts
47201
+ function normalizeRemote(url2) {
47202
+ const trimmed = url2.trim().replace(/\.git$/i, "");
47203
+ const scp = /^[^@]+@[^:]+:(.+)$/.exec(trimmed);
47204
+ const pathPart = scp ? scp[1] : (() => {
47205
+ try {
47206
+ return new URL(trimmed).pathname.replace(/^\/+/, "");
47207
+ } catch {
47208
+ return null;
47209
+ }
47210
+ })();
47211
+ if (!pathPart) return null;
47212
+ const segs = pathPart.split("/").filter(Boolean);
47213
+ if (segs.length < 2) return null;
47214
+ return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`.toLowerCase();
47215
+ }
47216
+ function remoteMatchesRepo(remoteUrl, repoFullName) {
47217
+ const a = normalizeRemote(remoteUrl);
47218
+ return a !== null && a === repoFullName.trim().toLowerCase();
47219
+ }
47220
+
47293
47221
  // src/tools.ts
47294
47222
  registerAsyncTool(consensusDescriptor);
47295
47223
  var mcpToolNames = {
@@ -47350,7 +47278,8 @@ var mcpToolNames = {
47350
47278
  runCodeReviewerGate: "run_code_reviewer_gate",
47351
47279
  uploadArtifact: "upload_artifact",
47352
47280
  listArtifactFiles: "list_artifact_files",
47353
- resolveHandoff: "resolve_handoff"
47281
+ resolveHandoff: "resolve_handoff",
47282
+ registerLocalCheckout: "register_local_checkout"
47354
47283
  };
47355
47284
  var projectScopedSchema = external_exports.object({ projectSlug: mrrlinProjectSlugSchema });
47356
47285
  var taskScopedSchema = projectScopedSchema.extend({ taskId: mrrlinTaskIdSchema });
@@ -47448,6 +47377,10 @@ var listInboxItemsInputSchema = projectScopedSchema.merge(mrrlinInboxItemListFil
47448
47377
  var createInboxItemInputSchema = projectScopedSchema.merge(mrrlinInboxItemCreateSchema);
47449
47378
  var decideInboxItemInputSchema = projectScopedSchema.extend({ itemId: mrrlinInboxItemIdSchema }).merge(mrrlinInboxItemDecideSchema);
47450
47379
  var resolveHandoffInputSchema = projectScopedSchema.extend({ runId: mrrlinExecutionRunIdSchema }).merge(mrrlinResolveHandoffSchema);
47380
+ var registerLocalCheckoutInputSchema = projectScopedSchema.extend({
47381
+ path: external_exports.string().min(1).describe("Absolute path to the local git checkout (must be a git repo whose origin matches `repo`)."),
47382
+ repo: external_exports.string().min(1).describe("The project's bound GitHub repo full name in `owner/repo` form.")
47383
+ });
47451
47384
  var mcpToolInputSchemas = {
47452
47385
  [mcpToolNames.appendExecutionArtifact]: appendExecutionArtifactInputSchema,
47453
47386
  [mcpToolNames.archivePlan]: archivePlanInputSchema,
@@ -47506,7 +47439,8 @@ var mcpToolInputSchemas = {
47506
47439
  [mcpToolNames.decideInboxItem]: decideInboxItemInputSchema,
47507
47440
  [mcpToolNames.uploadArtifact]: uploadArtifactInputSchema,
47508
47441
  [mcpToolNames.listArtifactFiles]: listArtifactFilesInputSchema,
47509
- [mcpToolNames.resolveHandoff]: resolveHandoffInputSchema
47442
+ [mcpToolNames.resolveHandoff]: resolveHandoffInputSchema,
47443
+ [mcpToolNames.registerLocalCheckout]: registerLocalCheckoutInputSchema
47510
47444
  };
47511
47445
  var mcpToolOperationIds = {
47512
47446
  [mcpToolNames.appendExecutionArtifact]: openApiOperationIds.appendExecutionArtifact,
@@ -47624,7 +47558,8 @@ var mcpToolDescriptions = {
47624
47558
  [mcpToolNames.decideInboxItem]: "Approve, reject, or acknowledge an operator-inbox item. Rejection requires a reason.",
47625
47559
  [mcpToolNames.uploadArtifact]: "Upload a local file (md/txt/html/csv/json/pdf/png/jpg/webp/gif, <=5MB) as a project artifact. Returns a stable markdown link to embed in chat/spec/handoff text. Use class=temp for one-off hand-offs, class=durable for anything referenced by specs or evidence. Pass runId to record the file in that run's artifact history; pass taskId to surface it on the task's Artifacts list.",
47626
47560
  [mcpToolNames.listArtifactFiles]: "List uploaded artifact files for the project (optionally filtered by taskId/runId), newest first.",
47627
- [mcpToolNames.resolveHandoff]: "Resolve a tool_assisted handoff for an execution run (outcome success|failure). Success returns the task to the board; failure opens a new handoff cycle. Idempotent."
47561
+ [mcpToolNames.resolveHandoff]: "Resolve a tool_assisted handoff for an execution run (outcome success|failure). Success returns the task to the board; failure opens a new handoff cycle. Idempotent.",
47562
+ [mcpToolNames.registerLocalCheckout]: "Register the operator-local git checkout path for `projectSlug` so execution-run workers run code on this machine instead of the cloud. Validates the path is a git repo whose `origin` remote matches `repo`, then writes the (slug -> absolute path) entry to the operator-local checkout registry. Call this after locating the checkout on disk (e.g. via filesystem search) and confirming the choice with the user in chat \u2014 they should NOT have to paste the path into Settings. Returns `{ projectSlug, path, confirmedAt }`. Idempotent: re-registering overwrites the prior entry."
47628
47563
  };
47629
47564
  var ARTIFACT_EXTENSION_TYPES = {
47630
47565
  ".md": "text/markdown",
@@ -48186,7 +48121,7 @@ function createMrrlinTools(options) {
48186
48121
  "ARTIFACT_FILE_UPLOAD_FAILED",
48187
48122
  "Unable to upload artifact file.",
48188
48123
  async (c, { projectSlug, path: filePath, class: artifactClass, taskId, runId, description }) => {
48189
- const extension2 = import_node_path15.default.extname(filePath).toLowerCase();
48124
+ const extension2 = import_node_path14.default.extname(filePath).toLowerCase();
48190
48125
  const contentType = ARTIFACT_EXTENSION_TYPES[extension2];
48191
48126
  if (!contentType) {
48192
48127
  throw new McpToolCodedError(
@@ -48214,7 +48149,7 @@ function createMrrlinTools(options) {
48214
48149
  "Artifact exceeds 5MB; split or summarize instead."
48215
48150
  );
48216
48151
  }
48217
- const filename = import_node_path15.default.basename(filePath);
48152
+ const filename = import_node_path14.default.basename(filePath);
48218
48153
  const created = await c.createArtifactFile(projectSlug, {
48219
48154
  class: artifactClass,
48220
48155
  contentType,
@@ -48289,6 +48224,55 @@ function createMrrlinTools(options) {
48289
48224
  "HANDOFF_RESOLVE_FAILED",
48290
48225
  "Unable to resolve handoff.",
48291
48226
  async (c, { projectSlug, runId, ...input }) => await c.resolveHandoff(projectSlug, runId, input)
48227
+ ),
48228
+ [mcpToolNames.registerLocalCheckout]: makeTool(
48229
+ client,
48230
+ mcpToolNames.registerLocalCheckout,
48231
+ "LOCAL_CHECKOUT_REGISTER_FAILED",
48232
+ "Unable to register local checkout.",
48233
+ async (_c, { projectSlug, path: checkoutPath, repo }) => {
48234
+ const trimmedPath = checkoutPath.trim();
48235
+ if (!import_node_path14.default.isAbsolute(trimmedPath)) {
48236
+ throw new McpToolCodedError(
48237
+ "LOCAL_CHECKOUT_PATH_NOT_ABSOLUTE",
48238
+ `Path must be absolute, got: ${trimmedPath}`
48239
+ );
48240
+ }
48241
+ let resolvedPath;
48242
+ try {
48243
+ resolvedPath = (0, import_node_fs12.realpathSync)(trimmedPath);
48244
+ } catch {
48245
+ throw new McpToolCodedError(
48246
+ "LOCAL_CHECKOUT_PATH_NOT_FOUND",
48247
+ `Path does not exist or is not accessible: ${trimmedPath}`
48248
+ );
48249
+ }
48250
+ let remoteUrl;
48251
+ try {
48252
+ remoteUrl = (0, import_node_child_process6.execFileSync)("git", ["-C", resolvedPath, "remote", "get-url", "origin"], {
48253
+ encoding: "utf8",
48254
+ stdio: ["ignore", "pipe", "ignore"],
48255
+ timeout: 15e3,
48256
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
48257
+ }).trim();
48258
+ } catch {
48259
+ throw new McpToolCodedError(
48260
+ "LOCAL_CHECKOUT_NOT_A_GIT_REPO",
48261
+ `Path is not a git repository or has no origin remote: ${resolvedPath}`
48262
+ );
48263
+ }
48264
+ if (!remoteMatchesRepo(remoteUrl, repo)) {
48265
+ throw new McpToolCodedError(
48266
+ "LOCAL_CHECKOUT_REMOTE_MISMATCH",
48267
+ `Origin remote ${remoteUrl} does not match repo ${repo}.`
48268
+ );
48269
+ }
48270
+ const stateDir = process.env.CODEX_HOME ? import_node_path14.default.join(process.env.CODEX_HOME, "mrrlin", "director-bridge") : import_node_path14.default.join(import_node_os8.default.homedir(), ".mrrlin", "director-bridge");
48271
+ const registry3 = new CheckoutRegistry(stateDir);
48272
+ const confirmedAt = (/* @__PURE__ */ new Date()).toISOString();
48273
+ registry3.confirm(projectSlug, resolvedPath, confirmedAt);
48274
+ return { projectSlug, path: resolvedPath, confirmedAt };
48275
+ }
48292
48276
  )
48293
48277
  };
48294
48278
  }
@@ -48450,13 +48434,13 @@ function runSetCredential(args) {
48450
48434
  }
48451
48435
 
48452
48436
  // src/install-service.ts
48453
- var import_node_os8 = __toESM(require("node:os"), 1);
48454
- var import_node_path17 = __toESM(require("node:path"), 1);
48437
+ var import_node_os9 = __toESM(require("node:os"), 1);
48438
+ var import_node_path16 = __toESM(require("node:path"), 1);
48455
48439
 
48456
48440
  // src/service-paths.ts
48457
- var import_node_path16 = __toESM(require("node:path"), 1);
48441
+ var import_node_path15 = __toESM(require("node:path"), 1);
48458
48442
  function isWorktreePath(p) {
48459
- const norm = import_node_path16.default.normalize(p).split(import_node_path16.default.sep).join("/");
48443
+ const norm = import_node_path15.default.normalize(p).split(import_node_path15.default.sep).join("/");
48460
48444
  return norm.includes("/.claude/worktrees/");
48461
48445
  }
48462
48446
  function resolveServiceCwd(opts) {
@@ -48509,7 +48493,7 @@ function installService(deps) {
48509
48493
  deps.log(resolved.reason);
48510
48494
  return { ok: false };
48511
48495
  }
48512
- const home = deps.env.HOME ?? import_node_os8.default.homedir();
48496
+ const home = deps.env.HOME ?? import_node_os9.default.homedir();
48513
48497
  const bins = ["node", "codex", "mrrlin-mcp"].map((b) => deps.which(b));
48514
48498
  if (bins.some((b) => !b)) {
48515
48499
  deps.log("Could not resolve absolute paths for node/codex/mrrlin-mcp on PATH. Install them or fix PATH.");
@@ -48520,7 +48504,7 @@ function installService(deps) {
48520
48504
  return { ok: false };
48521
48505
  }
48522
48506
  const resolvedBins = bins.filter((b) => b !== null);
48523
- const pathEnv = Array.from(new Set(resolvedBins.map((b) => import_node_path17.default.dirname(b)))).join(":");
48507
+ const pathEnv = Array.from(new Set(resolvedBins.map((b) => import_node_path16.default.dirname(b)))).join(":");
48524
48508
  const text = renderEcosystemConfig({
48525
48509
  cwd: resolved.cwd,
48526
48510
  home,
@@ -48529,7 +48513,7 @@ function installService(deps) {
48529
48513
  staging: deps.env.MRRLIN_STAGING === "1",
48530
48514
  apiBaseUrl: deps.env.MRRLIN_API_BASE_URL?.trim() || void 0
48531
48515
  });
48532
- const ecoPath = import_node_path17.default.join(home, ".mrrlin", "ecosystem.config.cjs");
48516
+ const ecoPath = import_node_path16.default.join(home, ".mrrlin", "ecosystem.config.cjs");
48533
48517
  deps.writeFile(ecoPath, text);
48534
48518
  const start = deps.runPm2(["startOrReload", ecoPath]);
48535
48519
  if (start.code !== 0) {
@@ -48560,13 +48544,13 @@ function uninstallService(deps) {
48560
48544
 
48561
48545
  // src/uninstall-codex.ts
48562
48546
  var import_promises4 = __toESM(require("node:fs/promises"), 1);
48563
- var import_node_os9 = __toESM(require("node:os"), 1);
48564
- var import_node_path18 = __toESM(require("node:path"), 1);
48547
+ var import_node_os10 = __toESM(require("node:os"), 1);
48548
+ var import_node_path17 = __toESM(require("node:path"), 1);
48565
48549
  var toml2 = __toESM(require_toml(), 1);
48566
48550
  function resolveCodexHome2(options) {
48567
48551
  if (options.codexHome) return options.codexHome;
48568
48552
  if (process.env.CODEX_HOME) return process.env.CODEX_HOME;
48569
- return import_node_path18.default.join(options.homeDir ?? import_node_os9.default.homedir(), ".codex");
48553
+ return import_node_path17.default.join(options.homeDir ?? import_node_os10.default.homedir(), ".codex");
48570
48554
  }
48571
48555
  async function pathExists3(target) {
48572
48556
  try {
@@ -48579,7 +48563,7 @@ async function pathExists3(target) {
48579
48563
  var MRRLIN_BLOCK_RE2 = /(^|\n)\[mcp_servers\.mrrlin(?:\]|\.[^\]\n]*\])[\s\S]*?(?=\n\[|$)/g;
48580
48564
  async function uninstallCodex(options = {}) {
48581
48565
  const codexHome = resolveCodexHome2(options);
48582
- const configPath = import_node_path18.default.join(codexHome, "config.toml");
48566
+ const configPath = import_node_path17.default.join(codexHome, "config.toml");
48583
48567
  const removePrompts = async () => uninstallPrompts(codexHome, options.promptNames ?? []);
48584
48568
  if (!await pathExists3(configPath)) {
48585
48569
  return { action: "missing", configPath, prompts: await removePrompts() };
@@ -48607,10 +48591,10 @@ async function uninstallCodex(options = {}) {
48607
48591
  }
48608
48592
  async function uninstallPrompts(codexHome, names) {
48609
48593
  if (names.length === 0) return [];
48610
- const promptsDir2 = import_node_path18.default.join(codexHome, "prompts");
48594
+ const promptsDir2 = import_node_path17.default.join(codexHome, "prompts");
48611
48595
  const out = [];
48612
48596
  for (const name of names) {
48613
- const promptPath = import_node_path18.default.join(promptsDir2, `${name}.md`);
48597
+ const promptPath = import_node_path17.default.join(promptsDir2, `${name}.md`);
48614
48598
  try {
48615
48599
  await import_promises4.default.unlink(promptPath);
48616
48600
  out.push({ name, action: "removed", promptPath });
@@ -48628,15 +48612,15 @@ async function uninstallPrompts(codexHome, names) {
48628
48612
 
48629
48613
  // src/report-issue-prompt.ts
48630
48614
  var import_node_fs13 = __toESM(require("node:fs"), 1);
48631
- var import_node_path19 = __toESM(require("node:path"), 1);
48615
+ var import_node_path18 = __toESM(require("node:path"), 1);
48632
48616
  var import_node_url2 = require("node:url");
48633
48617
  var import_meta2 = {};
48634
48618
  function promptsDir() {
48635
- if (typeof __dirname !== "undefined") return import_node_path19.default.join(__dirname, "prompts");
48636
- return import_node_path19.default.join(import_node_path19.default.dirname((0, import_node_url2.fileURLToPath)(import_meta2.url)), "prompts");
48619
+ if (typeof __dirname !== "undefined") return import_node_path18.default.join(__dirname, "prompts");
48620
+ return import_node_path18.default.join(import_node_path18.default.dirname((0, import_node_url2.fileURLToPath)(import_meta2.url)), "prompts");
48637
48621
  }
48638
48622
  function readReportIssuePrompt() {
48639
- return import_node_fs13.default.readFileSync(import_node_path19.default.join(promptsDir(), "report-issue.md"), "utf8");
48623
+ return import_node_fs13.default.readFileSync(import_node_path18.default.join(promptsDir(), "report-issue.md"), "utf8");
48640
48624
  }
48641
48625
 
48642
48626
  // src/bin.ts
@@ -48662,14 +48646,18 @@ Usage:
48662
48646
 
48663
48647
  mrrlin-mcp install-codex Idempotently register Mrrlin in
48664
48648
  [--force] ~/.codex/config.toml (or CODEX_HOME). Adds an
48665
- [mcp_servers.mrrlin] block AND drops the bundled
48649
+ [--force-prompts] [mcp_servers.mrrlin] block AND drops the bundled
48666
48650
  slash-command prompts (currently: /report-issue)
48667
48651
  into <CODEX_HOME>/prompts/. Development
48668
48652
  checkouts register the local dist/bin.cjs;
48669
48653
  published npm installs register
48670
48654
  \`mrrlin-mcp serve\`.
48671
- --force replaces
48672
- an existing conflicting block.
48655
+ --force replaces an existing conflicting block.
48656
+ --force-prompts overwrites prompt files that
48657
+ already exist with different content. WITHOUT it,
48658
+ local edits to <CODEX_HOME>/prompts/<name>.md are
48659
+ preserved across patch releases (reported as
48660
+ 'skipped-modified').
48673
48661
 
48674
48662
  mrrlin-mcp director-bridge Run a local ws://127.0.0.1 (plain HTTP +
48675
48663
  WebSocket) bridge that lets the web Director
@@ -48747,12 +48735,26 @@ Usage:
48747
48735
  binary) and the Settings credential revoke. Leaves
48748
48736
  global pm2 alone (it may be used elsewhere).
48749
48737
 
48738
+ mrrlin-mcp redact Scrub known secret shapes (GitHub PATs,
48739
+ Bearer/JWT tokens, long hex / base64-ish runs)
48740
+ from stdin and write the result to stdout. Same
48741
+ regex set as the bridge logger. Empty input ->
48742
+ empty output, exit 0. Used by the /report-issue
48743
+ prompt to ensure no raw text reaches Telegram:
48744
+ printf %s "$HINT" | mrrlin-mcp redact
48745
+
48750
48746
  mrrlin-mcp report-issue Print the bundled support-report prompt to
48751
48747
  stdout. Normal users don't need this \u2014 install-codex
48752
48748
  already drops it as a /report-issue slash command.
48753
- Useful for: piping it ad-hoc to a non-Codex client,
48754
- inspecting the shipped contents, or repairing a
48755
- deleted prompt file:
48749
+ Operator usage from inside Codex (paste a hint \u2014
48750
+ pasted error blurb, weird output quote, one-line
48751
+ description, or sessionId):
48752
+ /report-issue Codex hung after sending a turn
48753
+ /report-issue [paste failing stack here]
48754
+ The prompt greps the bridge log for that hint
48755
+ and only asks follow-ups the log doesn't already
48756
+ answer. Useful for: piping ad-hoc, inspecting
48757
+ shipped contents, repairing a deleted file:
48756
48758
  codex "$(mrrlin-mcp report-issue)"
48757
48759
  mrrlin-mcp report-issue > ~/.codex/prompts/report-issue.md
48758
48760
 
@@ -48779,10 +48781,12 @@ async function main() {
48779
48781
  }
48780
48782
  case "install-codex": {
48781
48783
  const force = rest.includes("--force");
48784
+ const forcePrompts = rest.includes("--force-prompts");
48782
48785
  const binPath = resolveSelfBinPath();
48783
48786
  const result = await installCodex({
48784
48787
  binPath,
48785
48788
  force,
48789
+ forcePrompts,
48786
48790
  prompts: [{ name: "report-issue", content: readReportIssuePrompt() }]
48787
48791
  });
48788
48792
  process.stderr.write(`[mrrlin-mcp install-codex] ${result.action} ${result.configPath}
@@ -48790,7 +48794,22 @@ async function main() {
48790
48794
  for (const p of result.prompts) {
48791
48795
  process.stderr.write(`[mrrlin-mcp install-codex] prompt ${p.name}: ${p.action} ${p.promptPath}
48792
48796
  `);
48797
+ if (p.action === "skipped-modified") {
48798
+ process.stderr.write(
48799
+ `[mrrlin-mcp install-codex] prompt ${p.name}: kept your local edits; run with --force-prompts to overwrite (or delete ${p.promptPath} to take the new bundled version)
48800
+ `
48801
+ );
48802
+ }
48803
+ }
48804
+ return;
48805
+ }
48806
+ case "redact": {
48807
+ const chunks = [];
48808
+ for await (const chunk of process.stdin) {
48809
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
48793
48810
  }
48811
+ const input = Buffer.concat(chunks).toString("utf8");
48812
+ process.stdout.write(redact(input));
48794
48813
  return;
48795
48814
  }
48796
48815
  case "report-issue": {
@@ -48813,7 +48832,7 @@ async function main() {
48813
48832
  env: process.env,
48814
48833
  cwd: process.cwd(),
48815
48834
  writeFile: (p, c) => {
48816
- (0, import_node_fs14.mkdirSync)(import_node_path21.default.dirname(p), { recursive: true, mode: 448 });
48835
+ (0, import_node_fs14.mkdirSync)(import_node_path20.default.dirname(p), { recursive: true, mode: 448 });
48817
48836
  (0, import_node_fs14.writeFileSync)(p, c, { mode: 384 });
48818
48837
  },
48819
48838
  runPm2: pm2Runner,
@@ -48838,14 +48857,14 @@ async function main() {
48838
48857
  purgeSecret: purge,
48839
48858
  secretPath: agentCredentialPath(),
48840
48859
  tokenPath: operatorTokenPath(),
48841
- ecoPath: import_node_path21.default.join(process.env.HOME ?? (0, import_node_os10.homedir)(), ".mrrlin", "ecosystem.config.cjs"),
48860
+ ecoPath: import_node_path20.default.join(process.env.HOME ?? (0, import_node_os11.homedir)(), ".mrrlin", "ecosystem.config.cjs"),
48842
48861
  log: (m) => process.stderr.write(`[mrrlin-mcp uninstall-service] ${m}
48843
48862
  `)
48844
48863
  });
48845
48864
  return;
48846
48865
  }
48847
48866
  case "uninstall": {
48848
- const home = process.env.HOME ?? (0, import_node_os10.homedir)();
48867
+ const home = process.env.HOME ?? (0, import_node_os11.homedir)();
48849
48868
  const log = (m) => process.stderr.write(`[mrrlin-mcp uninstall] ${m}
48850
48869
  `);
48851
48870
  uninstallService({
@@ -48859,7 +48878,7 @@ async function main() {
48859
48878
  purgeSecret: true,
48860
48879
  secretPath: agentCredentialPath(),
48861
48880
  tokenPath: operatorTokenPath(),
48862
- ecoPath: import_node_path21.default.join(home, ".mrrlin", "ecosystem.config.cjs"),
48881
+ ecoPath: import_node_path20.default.join(home, ".mrrlin", "ecosystem.config.cjs"),
48863
48882
  log
48864
48883
  });
48865
48884
  let codexOk = true;
@@ -48879,7 +48898,7 @@ async function main() {
48879
48898
  log(`codex config NOT modified: ${error51 instanceof Error ? error51.message : String(error51)}`);
48880
48899
  }
48881
48900
  try {
48882
- (0, import_node_fs14.rmSync)(import_node_path21.default.join(home, ".mrrlin"), { recursive: true, force: true });
48901
+ (0, import_node_fs14.rmSync)(import_node_path20.default.join(home, ".mrrlin"), { recursive: true, force: true });
48883
48902
  } catch {
48884
48903
  }
48885
48904
  log("removed ~/.mrrlin");
@@ -48920,7 +48939,7 @@ ${HELP_TEXT}`);
48920
48939
  }
48921
48940
  }
48922
48941
  function resolveSelfBinPath() {
48923
- return import_node_path20.default.resolve(process.argv[1] ?? process.execPath);
48942
+ return import_node_path19.default.resolve(process.argv[1] ?? process.execPath);
48924
48943
  }
48925
48944
  main().catch((error51) => {
48926
48945
  process.stderr.write(`mrrlin-mcp fatal error: ${error51 instanceof Error ? error51.message : String(error51)}
@@ -34,16 +34,32 @@ below, so leaking the token costs at most some spam in that chat — rotate via
34
34
 
35
35
  ## What to do
36
36
 
37
- ### 1. Read the Director bridge log
37
+ ### 1. Take the user's hint as the search key
38
38
 
39
- The bridge writes one JSON object per line to a daily file. Find the log dir, in
40
- this order, and use the first that exists:
39
+ When the operator invokes `/report-issue`, they typically include a hint along
40
+ with the command — a pasted error blurb, a quote of weird output, a one-line
41
+ description, or a `sessionId`/`spanId` they copied. **That hint is your primary
42
+ search key.** Do not blindly read the tail of the latest log.
43
+
44
+ Extract candidate signals from the hint:
45
+
46
+ - **Quoted substrings** — verbatim text from the failure (highest priority).
47
+ - **Error words** — "error", "failed", "timeout", "401", "500", "ENOENT", stack-trace fragments.
48
+ - **Identifiers** — `sessionId` (uuid-shaped) or `spanId` if pasted.
49
+ - **Time hints** — "just now", "5 minutes ago", "today around 14:00".
50
+
51
+ If the operator gave no hint at all, fall back to the tail (see below) — but
52
+ say so plainly when you confirm, so they know the report may have grabbed the
53
+ wrong incident.
54
+
55
+ ### 2. Find the relevant log window
56
+
57
+ Logs live at the first existing dir:
41
58
 
42
59
  1. `$CODEX_HOME/mrrlin/director-bridge/logs/` (only if `CODEX_HOME` is set)
43
60
  2. `~/.mrrlin/director-bridge/logs/`
44
61
 
45
- Pick the **most recent** `bridge-YYYY-MM-DD.jsonl` file (filenames are UTC dates;
46
- sort descending). Read roughly the **last 200 lines**. Each line looks like:
62
+ Files are `bridge-YYYY-MM-DD.jsonl` (UTC dates). Sort newest-first. Each line:
47
63
 
48
64
  ```json
49
65
  {"ts":"...","dir":"in|out","spanId":"...","sessionId":"...","type":"turn|event|error|...","ms":123,"payload":{...}}
@@ -52,58 +68,94 @@ sort descending). Read roughly the **last 200 lines**. Each line looks like:
52
68
  Secrets are already redacted by the logger (tokens show as `[REDACTED]`), so the
53
69
  log lines are safe to forward as-is.
54
70
 
55
- From the tail, extract:
71
+ **With a hint** — grep across the **3 most recent files** (today + last 2 days)
72
+ in this signal order, stopping at the first that matches:
73
+
74
+ 1. The literal quoted substring from the hint (case-insensitive).
75
+ 2. The `sessionId` or `spanId` from the hint.
76
+ 3. Error-word lines clustered near any time hint the user gave.
77
+
78
+ Pick the **most recent** matching cluster. The "relevant window" = the matching
79
+ line + ~10 lines before and ~30 lines after. If the matching line has a
80
+ `sessionId`, bound the window to that session.
81
+
82
+ **Without a hint** — read the last ~200 lines of the newest file. The window is
83
+ the last contiguous cluster of `type:"error"` lines (or, if none, the last few
84
+ `type:"turn"` lines).
56
85
 
57
- - Every line with `type:"error"` (and any `dir:"out"` payload that looks like a failure — non-2xx HTTP, stack traces, "failed", "timeout").
58
- - The last few `type:"turn"` lines for context on what the user was doing.
59
- - The latest `sessionId` and the `spanId`s tied to the errors.
60
- - `ts` of the first and last relevant lines (the time window).
86
+ From the relevant window extract:
61
87
 
62
- If no log dir or file exists, or the tail has no errors, say so plainly and rely
63
- on what the user tells you in step 2 still send the report.
88
+ - Every `type:"error"` line and any `dir:"out"` payload that looks like a failure (non-2xx HTTP, stack traces, "failed", "timeout").
89
+ - The last few `type:"turn"` lines before the failure (what the user was doing).
90
+ - The `sessionId` and `spanId`s tied to the failure.
91
+ - `ts` of the first and last lines in the window.
64
92
 
65
- ### 2. Ask the user in THEIR language
93
+ If the log dir doesn't exist, or the hint matches nothing in the last 3 days,
94
+ say so plainly and lean on the user's answers in step 3 — still send the report.
66
95
 
67
- Detect the language the user is writing in and ask in that language. Keep it to a
68
- few short questions, ask them **once**, don't interrogate. Cover:
96
+ ### 3. Ask the user in THEIR language only what the log doesn't already tell you
69
97
 
70
- - **What were you doing** when it broke?
71
- - **What actually happened** (the symptom they saw)?
72
- - **What did you expect to happen instead?** — ask this **only if the expected
73
- result is not already obvious** from the log or from what they described. If
74
- it's obvious, infer it and skip the question.
98
+ Detect the language the user is writing in and ask in that language. Keep it
99
+ short, ask **once**, and **skip any question the hint + log already answered**:
100
+
101
+ - **What were you doing** when it broke? — skip if the hint or the preceding `type:"turn"` lines make this obvious.
102
+ - **What actually happened (the symptom)?** — skip if the hint is itself a verbatim error/output.
103
+ - **What did you expect to happen instead?** — ask only if the expected result is not obvious from the log or hint.
75
104
 
76
105
  If the user gives short or partial answers, accept them and move on. Never block
77
106
  the report on a perfect answer.
78
107
 
79
- ### 3. Package the report (in ENGLISH)
108
+ ### 4. Package the report (in ENGLISH) — every string passes through `mrrlin-mcp redact`
80
109
 
81
110
  Translate the user's answers to English. Build a plain-text report. Keep the whole
82
111
  thing under **4096 characters** (Telegram's per-message limit) — trim the log
83
112
  excerpt first if needed, keeping the error lines over the context lines.
84
113
 
114
+ **Hard rule:** every free-form string going into the report — the user's hint,
115
+ their answers, the log excerpt — passes through the shipped scrubber first.
116
+ There is no "but the log is already redacted by the logger" exception: the hint
117
+ and the user's answers come from outside the logger, so they MUST be scrubbed
118
+ here. Run each through:
119
+
120
+ ```bash
121
+ HINT_REDACTED=$(printf %s "$USER_HINT_RAW" | mrrlin-mcp redact)
122
+ EXCERPT_REDACTED=$(mrrlin-mcp redact < /tmp/excerpt-raw.txt)
123
+ # (repeat for each user answer)
124
+ ```
125
+
126
+ `mrrlin-mcp redact` reads stdin and writes the redacted bytes to stdout
127
+ (empty input → empty output, exit 0). It uses the same regex set as the
128
+ bridge logger (Bearer/JWT/GitHub-PAT/long-hex/long-base64). It is best-effort,
129
+ not a guarantee — but it is the floor below which raw text is never allowed.
130
+
131
+ Use the redacted versions to fill the template:
132
+
85
133
  ```
86
134
  🛠️ Mrrlin issue report
87
135
  When (UTC): <iso timestamp of the report>
88
136
  Mrrlin MCP: v<version if known> | OS: <platform> | Node: <version>
89
137
 
138
+ ▶ User hint (verbatim, post-redaction):
139
+ <HINT_REDACTED; "(none)" if absent>
140
+
90
141
  ▶ What the user was doing:
91
- <their answer, in English>
142
+ <their answer, in English, post-redaction>
92
143
 
93
144
  ▶ Expected result:
94
- <their answer, or "(obvious from context: ...)", or "(not provided)">
145
+ <their answer, post-redaction; or "(obvious from context: ...)"; or "(not provided)">
95
146
 
96
147
  ▶ Actual result / symptom:
97
- <their answer, in English>
148
+ <their answer, in English, post-redaction>
98
149
 
99
- ▶ Errors from bridge log (<filename>):
100
- <the extracted error lines, verbatim already redacted>
150
+ ▶ Errors from bridge log (<filename>, search strategy: <hint-driven | tail-fallback>):
151
+ matched on: "<the exact line that triggered the cluster, truncated to 80 chars; or '(no hint provided)' for tail-fallback>"
152
+ <EXCERPT_REDACTED — first the error lines, then the surrounding context>
101
153
 
102
154
  ▶ Context:
103
155
  session=<sessionId> spanIds=<...> window=<first ts>..<last ts>
104
156
  ```
105
157
 
106
- ### 4. Send it — a single POST to Telegram
158
+ ### 5. Send it — a single POST to Telegram
107
159
 
108
160
  This is **just one HTTP POST** to the Telegram Bot API `sendMessage` method.
109
161
  Endpoint and shape:
@@ -141,15 +193,17 @@ curl -sS -X POST \
141
193
  -F "document=@<path-to-bridge-YYYY-MM-DD.jsonl>"
142
194
  ```
143
195
 
144
- ### 5. Confirm
196
+ ### 6. Confirm
145
197
 
146
198
  Tell the user, in their language, that the report was sent (or that it failed and
147
199
  why). Don't dump the raw report or the token back at them — just confirm.
148
200
 
149
201
  ## Rules
150
202
 
151
- - Ask the user in their language; write the report in English.
203
+ - **Every string going into the Telegram body comes out of `mrrlin-mcp redact`** — the user's hint, every translated user answer, the log excerpt. If your pipeline has a path that builds the body from raw text, you are doing it wrong. The bridge logger already redacts what it writes; the hint and the user's answers do NOT come from the logger and must be scrubbed here.
204
+ - The operator's hint drives log search. Tail-fallback is the explicit fallback when no hint is provided.
205
+ - `matched on:` must contain the literal line that triggered the cluster (truncated to 80 chars), so the channel reader can verify the match instead of trusting an LLM assertion.
206
+ - Ask the user in their language; write the report in English. Skip any question the hint + log already answered.
152
207
  - Hide the mechanics: never surface the token, the log path, or the curl command to the user.
153
- - Forward log lines as-is — they're already secret-redacted. Do not paste anything that looks like a live token even if you see one.
154
208
  - One report = one `sendMessage` POST. Keep it under 4096 chars; use `sendDocument` only for the optional full log.
155
- - If anything is missing (no log, vague answers), still send the best report you can rather than giving up.
209
+ - If anything is missing (no log match, vague answers), still send the best report you can rather than giving up — and say in the report which search strategy you used and what `matched on:` you found.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -21,11 +21,11 @@
21
21
  "esbuild": "^0.24.0",
22
22
  "tsx": "^4.22.3",
23
23
  "@mrrlin/client": "0.0.0",
24
- "@mrrlin/codex-client": "0.0.0",
25
24
  "@mrrlin/director-e2e": "0.0.0",
25
+ "@mrrlin/wiki": "0.0.0",
26
+ "@mrrlin/codex-client": "0.0.0",
26
27
  "@mrrlin/schemas": "0.0.0",
27
- "@mrrlin/tsconfig": "0.0.0",
28
- "@mrrlin/wiki": "0.0.0"
28
+ "@mrrlin/tsconfig": "0.0.0"
29
29
  },
30
30
  "dependencies": {
31
31
  "@iarna/toml": "^2.2.5",