@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 +246 -227
- package/dist/prompts/report-issue.md +85 -31
- package/package.json +4 -4
package/dist/bin.cjs
CHANGED
|
@@ -18671,11 +18671,11 @@ var require_dist = __commonJS({
|
|
|
18671
18671
|
});
|
|
18672
18672
|
|
|
18673
18673
|
// src/bin.ts
|
|
18674
|
-
var
|
|
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
|
|
18678
|
-
var
|
|
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:
|
|
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
|
|
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
|
|
18845
|
-
var
|
|
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
|
|
40931
|
+
var import_node_fs8 = require("node:fs");
|
|
40988
40932
|
var import_node_os6 = require("node:os");
|
|
40989
|
-
var
|
|
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,
|
|
41023
|
-
(0,
|
|
41024
|
-
(0,
|
|
41025
|
-
(0,
|
|
41026
|
-
(0,
|
|
41027
|
-
(0,
|
|
41028
|
-
(0,
|
|
41029
|
-
(0,
|
|
41030
|
-
(0,
|
|
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,
|
|
41040
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
41281
|
-
var
|
|
41282
|
-
|
|
41283
|
-
|
|
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
|
|
41307
|
-
let out =
|
|
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
|
|
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
|
|
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,
|
|
41302
|
+
const logsDir = (0, import_node_path10.join)(stateDir, "logs");
|
|
41355
41303
|
try {
|
|
41356
|
-
(0,
|
|
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,
|
|
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,
|
|
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 =
|
|
41355
|
+
const dir = import_node_path11.default.join(stateDir, "no-checkout");
|
|
41408
41356
|
try {
|
|
41409
|
-
|
|
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,
|
|
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
|
|
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
|
|
41670
|
-
return
|
|
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:
|
|
42301
|
-
locksDir:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
42585
|
-
locksDir:
|
|
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 =
|
|
42646
|
+
const tokenPath = import_node_path11.default.join(stateDir, "token.txt");
|
|
42743
42647
|
try {
|
|
42744
|
-
const existing =
|
|
42648
|
+
const existing = import_node_fs10.default.readFileSync(tokenPath, "utf8").trim();
|
|
42745
42649
|
if (existing) return existing;
|
|
42746
42650
|
} catch {
|
|
42747
42651
|
}
|
|
42748
|
-
|
|
42652
|
+
import_node_fs10.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
|
|
42749
42653
|
const token = import_node_crypto4.default.randomBytes(32).toString("base64url");
|
|
42750
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
46061
|
+
let current = import_node_path12.default.resolve(start);
|
|
46155
46062
|
while (true) {
|
|
46156
|
-
if (await pathExists2(
|
|
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 =
|
|
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
|
|
46075
|
+
return import_node_path12.default.resolve(input);
|
|
46169
46076
|
}
|
|
46170
46077
|
if (process.env.MRRLIN_DOCS_ROOT) {
|
|
46171
|
-
return
|
|
46078
|
+
return import_node_path12.default.resolve(process.env.MRRLIN_DOCS_ROOT);
|
|
46172
46079
|
}
|
|
46173
|
-
return
|
|
46080
|
+
return import_node_path12.default.join(await findWorkspaceRoot(), "docs");
|
|
46174
46081
|
}
|
|
46175
46082
|
async function resolveIndexPath(input) {
|
|
46176
46083
|
if (input) {
|
|
46177
|
-
return
|
|
46084
|
+
return import_node_path12.default.resolve(input);
|
|
46178
46085
|
}
|
|
46179
46086
|
if (process.env.MRRLIN_WIKI_INDEX_PATH) {
|
|
46180
|
-
return
|
|
46087
|
+
return import_node_path12.default.resolve(process.env.MRRLIN_WIKI_INDEX_PATH);
|
|
46181
46088
|
}
|
|
46182
46089
|
const workspaceRoot = await findWorkspaceRoot();
|
|
46183
|
-
return
|
|
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 =
|
|
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(
|
|
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
|
|
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 ??
|
|
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
|
|
46741
|
-
var
|
|
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
|
|
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,
|
|
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
|
|
46883
|
+
return import_node_path13.default.join(__dirname, "consensus", "personas");
|
|
46977
46884
|
}
|
|
46978
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
48454
|
-
var
|
|
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
|
|
48441
|
+
var import_node_path15 = __toESM(require("node:path"), 1);
|
|
48458
48442
|
function isWorktreePath(p) {
|
|
48459
|
-
const norm =
|
|
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 ??
|
|
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) =>
|
|
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 =
|
|
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
|
|
48564
|
-
var
|
|
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
|
|
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 =
|
|
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 =
|
|
48594
|
+
const promptsDir2 = import_node_path17.default.join(codexHome, "prompts");
|
|
48611
48595
|
const out = [];
|
|
48612
48596
|
for (const name of names) {
|
|
48613
|
-
const promptPath =
|
|
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
|
|
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
|
|
48636
|
-
return
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48754
|
-
|
|
48755
|
-
|
|
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)(
|
|
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:
|
|
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,
|
|
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:
|
|
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)(
|
|
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
|
|
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.
|
|
37
|
+
### 1. Take the user's hint as the search key
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
###
|
|
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: ...)"
|
|
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
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
-
|
|
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.
|
|
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",
|