@mrrlin-dev/mcp 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/bin.cjs +126 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,8 @@ The bridge auto-picks up new `@mrrlin-dev/mcp` versions after `npm install -g`.
10
10
 
11
11
  - **L1 (self-update loop):** every 15 minutes (env-tunable) the bridge hashes its own `bin.cjs` and, on change, drains the current Codex turn cleanly and exits via `bridgeShutdown`. PM2 restarts. New code loads.
12
12
  - **L2 (postinstall nudge):** `npm install -g @mrrlin-dev/mcp@latest` triggers a `SIGUSR2` to every running PM2 bridge, collapsing the L1 wait to seconds.
13
- - **L3 (in-chat update offer):** every 6 hours the bridge checks `https://registry.npmjs.org/@mrrlin-dev/mcp/latest`. When a newer version is published, on your next chat turn the Director asks: *"Бачу що bridge на X, на npm уже Yоновити зараз?"*. Say "yes" Director runs `npm install -g @mrrlin-dev/mcp@latest` itself. 48h cooldown if you decline.
13
+ - **L3 (in-chat update offer):** every 6 hours the bridge checks `https://registry.npmjs.org/@mrrlin-dev/mcp/latest`. When a newer version is published, on your next chat turn the Director explains *why* to update (latest fixes/improvements, changelog link) and offers a choice let it run the update, or run `npm install -g @mrrlin-dev/mcp@latest` yourself. Say "yes" → Director runs the install itself. 48h cooldown if you decline.
14
+ - **L3.3 (inbox mirror):** the same fire also files a persistent `question` in the active project's inbox ("A newer version of the mrrlin bridge is available…", with the changelog link), so the update isn't only an ephemeral per-turn aside. The body is version-agnostic on purpose: the inbox dedup collapses it to one open row per project (reused across versions), and it re-appears at most once per 48h cooldown after you decide it. It's an opportunistic per-project signal, not a guaranteed cross-project notice.
14
15
  - **L4 (process hygiene):** the bridge refuses to start a second instance on the same port and warns about orphan `mrrlin-mcp director-bridge` processes at startup.
15
16
 
16
17
  ### Env tuning
package/dist/bin.cjs CHANGED
@@ -36728,10 +36728,11 @@ var mrrlinTaskUpdateSchema = external_exports.object({
36728
36728
  status: mrrlinTaskStatusSchema.optional(),
36729
36729
  subcategory: taxonomySubcategorySchema.optional(),
36730
36730
  tags: mrrlinTaskTagsSchema.optional(),
36731
+ title: external_exports.string().min(1).optional(),
36731
36732
  type: mrrlinTaskTypeSchema.optional(),
36732
36733
  followUpAt: external_exports.string().datetime().nullable().optional(),
36733
36734
  verificationDueAt: external_exports.string().datetime().nullable().optional()
36734
- }).refine((value) => value.assignee !== void 0 || value.autoDeploy !== void 0 || value.autonomyLevel !== void 0 || value.autonomyReason !== void 0 || value.category !== void 0 || value.directorThreadId !== void 0 || value.dueDate !== void 0 || value.followUpAt !== void 0 || value.impact !== void 0 || value.notes !== void 0 || value.planId !== void 0 || value.specApproved !== void 0 || value.specWikiPageId !== void 0 || value.status !== void 0 || value.subcategory !== void 0 || value.tags !== void 0 || value.type !== void 0 || value.verificationDueAt !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
36735
+ }).refine((value) => value.assignee !== void 0 || value.autoDeploy !== void 0 || value.autonomyLevel !== void 0 || value.autonomyReason !== void 0 || value.category !== void 0 || value.directorThreadId !== void 0 || value.dueDate !== void 0 || value.followUpAt !== void 0 || value.impact !== void 0 || value.notes !== void 0 || value.planId !== void 0 || value.specApproved !== void 0 || value.specWikiPageId !== void 0 || value.status !== void 0 || value.subcategory !== void 0 || value.tags !== void 0 || value.title !== void 0 || value.type !== void 0 || value.verificationDueAt !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
36735
36736
  if (value.autonomyLevel !== void 0 && value.autonomyReason === void 0) {
36736
36737
  context.addIssue({
36737
36738
  code: external_exports.ZodIssueCode.custom,
@@ -36857,6 +36858,10 @@ var githubInstallCallbackResponseSchema = external_exports.object({
36857
36858
  data: external_exports.object({
36858
36859
  installation: mrrlinGithubInstallationSchema,
36859
36860
  linkedProjectSlugs: external_exports.array(mrrlinProjectSlugSchema),
36861
+ // Projects the operator initiated the install for whose BOUND repo the installation cannot
36862
+ // access (repo not in the installation's selection) — deliberately NOT linked: "bound"
36863
+ // must mean the repo is actually reachable. Additive; defaults keep old clients parsing.
36864
+ unlinkedProjectSlugs: external_exports.array(mrrlinProjectSlugSchema).default([]),
36860
36865
  matchedRepos: external_exports.array(external_exports.string().regex(/^[^/]+\/[^/]+$/)),
36861
36866
  setupAction: external_exports.enum(["install", "request", "update"])
36862
36867
  })
@@ -38331,6 +38336,7 @@ var DEFAULT_STARTUP_JITTER_MS = 30 * 1e3;
38331
38336
  var DEFAULT_COOLDOWN_MS = 48 * 60 * 60 * 1e3;
38332
38337
  var REGISTRY_URL_DEFAULT = "https://registry.npmjs.org/@mrrlin-dev/mcp/latest";
38333
38338
  var STRICT_SEMVER = /^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?(\+[A-Za-z0-9.-]+)?$/;
38339
+ var UPDATE_INBOX_BODY = "A newer version of the mrrlin bridge is available. Updating pulls the latest fixes and improvements. Changelog: https://mrrlin-changelog.s3.eu-central-1.amazonaws.com/index.html . Ask the Director to update, or run `npm install -g @mrrlin-dev/mcp@latest` yourself.";
38334
38340
  function isDisabled() {
38335
38341
  return process.env.MRRLIN_DIRECTOR_BRIDGE_SELF_UPDATE === "0";
38336
38342
  }
@@ -38375,17 +38381,29 @@ function startSelfUpdate(deps) {
38375
38381
  version: pkgVersion,
38376
38382
  latestVersion
38377
38383
  });
38378
- return `[bridge:update] An update for @mrrlin-dev/mcp is available: ${pkgVersion} \u2192 ${latestVersion}.
38384
+ return `[bridge:update] An update for @mrrlin-dev/mcp is available: ${pkgVersion} -> ${latestVersion}.
38379
38385
 
38380
- This turn, at a natural pause, ASK the operator a yes/no question \u2014 e.g. "\u0411\u0430\u0447\u0443 \u0449\u043E bridge \u043D\u0430 ${pkgVersion}, \u043D\u0430 npm \u0443\u0436\u0435 ${latestVersion} \u2014 \u043E\u043D\u043E\u0432\u0438\u0442\u0438 \u0437\u0430\u0440\u0430\u0437? (\u0437\u0430\u043F\u0443\u0449\u0443 \`npm install -g @mrrlin-dev/mcp@latest\`)". Do NOT install without explicit confirmation.
38386
+ This turn, at a natural pause, RAISE this with the operator IN THEIR LANGUAGE (mirror the language they are chatting in). Frame it as a short offer with a choice, and ALWAYS explain WHY it is worth doing:
38381
38387
 
38382
- If the operator confirms (yes / \u0442\u0430\u043A / \u0430\u0433\u0430 / etc.): immediately run \`npm install -g @mrrlin-dev/mcp@latest\` via your shell tool and report the outcome in chat. After a successful install, the bridge auto-restarts via its self-update loop within a couple of minutes \u2014 you do NOT need to call pm2.
38388
+ 1. WHY (never skip this): give a one-line reason the update matters - do NOT present a bare version bump. Prefer specifics: if you can read the release notes, summarize in one short sentence what is new/fixed. The changelog is at https://mrrlin-changelog.s3.eu-central-1.amazonaws.com/index.html. If you cannot get specifics, fall back to a generic but honest reason, e.g. "it pulls the latest bug fixes and improvements for the bridge".
38389
+
38390
+ 2. THE CHOICE: offer both paths and let the operator pick - "want me to update it for you now?" OR "you can run it yourself: \`npm install -g @mrrlin-dev/mcp@latest\`". Phrase it as a yes/no, e.g. in English: "The bridge is on ${pkgVersion} but npm already has ${latestVersion}. It brings <why> - want me to run \`npm install -g @mrrlin-dev/mcp@latest\`, or would you rather run it yourself?".
38391
+
38392
+ ASK the operator first - do NOT install without explicit confirmation.
38393
+
38394
+ If the operator confirms you should do it: immediately run \`npm install -g @mrrlin-dev/mcp@latest\` via your shell tool and report the outcome in chat. After a successful install, the bridge auto-restarts via its self-update loop within a couple of minutes - you do NOT need to call pm2.
38383
38395
 
38384
38396
  If the operator declines / says "later": acknowledge briefly and do not bring it up again this turn. The bridge has a cooldown and will re-offer in 48h.
38385
38397
 
38386
38398
  If the install fails (permission denied, registry pinned, sudo required, network blocked): paste the error verbatim and show the operator the manual command \`npm install -g @mrrlin-dev/mcp@latest\` plus a one-line hint about likely cause.
38387
38399
 
38388
- Do NOT derail the current task \u2014 if the operator has an in-flight question, answer that first; the update offer is a brief aside, not the main subject.`;
38400
+ Do NOT derail the current task - if the operator has an in-flight question, answer that first; the update offer is a brief aside, not the main subject.`;
38401
+ }
38402
+ function updateInboxBody() {
38403
+ if (disabled) return null;
38404
+ if (latestVersion === null) return null;
38405
+ if (!import_semver.default.gt(latestVersion, pkgVersion)) return null;
38406
+ return UPDATE_INBOX_BODY;
38389
38407
  }
38390
38408
  let startupBinSha = null;
38391
38409
  try {
@@ -38540,12 +38558,13 @@ Do NOT derail the current task \u2014 if the operator has an in-flight question,
38540
38558
  getDrainOnQuiet: () => drainOnQuiet,
38541
38559
  getLatestVersion: () => latestVersion,
38542
38560
  isStale: () => latestVersion !== null && import_semver.default.gt(latestVersion, pkgVersion),
38543
- maybeUpdateOffer
38561
+ maybeUpdateOffer,
38562
+ updateInboxBody
38544
38563
  };
38545
38564
  }
38546
38565
 
38547
38566
  // src/_generated/version.ts
38548
- var PKG_VERSION = "0.3.12";
38567
+ var PKG_VERSION = "0.3.14";
38549
38568
 
38550
38569
  // src/api-base-url.ts
38551
38570
  var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
@@ -38970,6 +38989,10 @@ var DEFAULT_TOUCH_INTERVAL_MS = 6e4;
38970
38989
  var DEFAULT_CLAIM_INTERVAL_SECONDS = 30;
38971
38990
  var DEFAULT_EXEC_TURN_TIMEOUT_SECONDS = 900;
38972
38991
  var DEFAULT_CONTEXT_MAX_ARTIFACTS = 50;
38992
+ var EXECUTION_TURN_TIMEOUT_PREFIX = "Execution turn did not complete within";
38993
+ var EXECUTION_TURN_TIMEOUT_RETRY_CODE = "execution_turn_timeout_retry";
38994
+ var EXECUTION_TURN_TIMEOUT_RETRY_SUMMARY = "Execution turn timed out; requeued for one automatic retry.";
38995
+ var EXECUTION_TURN_TIMEOUT_RETRY_CURSOR = "execution_turn_timeout_retry:1";
38973
38996
  function readPositiveIntEnv(name, fallback) {
38974
38997
  const raw = (process.env[name] ?? "").trim();
38975
38998
  if (!raw) return fallback;
@@ -38979,6 +39002,12 @@ function readPositiveIntEnv(name, fallback) {
38979
39002
  function execTurnTimeoutMs() {
38980
39003
  return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_TIMEOUT_SECONDS", DEFAULT_EXEC_TURN_TIMEOUT_SECONDS) * 1e3;
38981
39004
  }
39005
+ function executionTurnTimeoutMessage(timeoutMs) {
39006
+ return `${EXECUTION_TURN_TIMEOUT_PREFIX} ${timeoutMs}ms.`;
39007
+ }
39008
+ function isExecutionTurnTimeoutReason(reason) {
39009
+ return reason.startsWith(EXECUTION_TURN_TIMEOUT_PREFIX);
39010
+ }
38982
39011
  function contextMaxArtifacts() {
38983
39012
  return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS", DEFAULT_CONTEXT_MAX_ARTIFACTS);
38984
39013
  }
@@ -39188,7 +39217,7 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
39188
39217
  },
39189
39218
  interrupt: () => codex.turn.interrupt({ threadId }),
39190
39219
  timeoutMs,
39191
- timeoutMessage: `Execution turn did not complete within ${timeoutMs}ms.`,
39220
+ timeoutMessage: executionTurnTimeoutMessage(timeoutMs),
39192
39221
  onInterruptFailed: () => {
39193
39222
  interruptFailed = true;
39194
39223
  }
@@ -39447,7 +39476,36 @@ async function resetWedgedThread(deps, projectSlug, taskId, interruptFailed) {
39447
39476
  });
39448
39477
  }
39449
39478
  async function failRun(deps, projectSlug, task, runId, leaseId, reason) {
39450
- if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
39479
+ let currentRun;
39480
+ try {
39481
+ currentRun = await deps.client.getExecutionRun(projectSlug, runId);
39482
+ } catch {
39483
+ return;
39484
+ }
39485
+ if (currentRun.status !== "running" || currentRun.leaseId !== leaseId) return;
39486
+ if (isExecutionTurnTimeoutReason(reason) && currentRun.checkpointCursor !== EXECUTION_TURN_TIMEOUT_RETRY_CURSOR) {
39487
+ try {
39488
+ await deps.client.updateExecutionRun(projectSlug, runId, {
39489
+ status: "pending",
39490
+ leaseId: null,
39491
+ checkpointCursor: EXECUTION_TURN_TIMEOUT_RETRY_CURSOR
39492
+ });
39493
+ await deps.client.appendExecutionArtifact(projectSlug, runId, {
39494
+ id: newArtifactId(),
39495
+ kind: "error",
39496
+ payload: {
39497
+ code: EXECUTION_TURN_TIMEOUT_RETRY_CODE,
39498
+ source: "execution-consumer",
39499
+ attempt: 1,
39500
+ reason,
39501
+ summary: EXECUTION_TURN_TIMEOUT_RETRY_SUMMARY
39502
+ }
39503
+ }).catch(() => {
39504
+ });
39505
+ return;
39506
+ } catch {
39507
+ }
39508
+ }
39451
39509
  try {
39452
39510
  await deps.client.createExecutionRunHandoff(projectSlug, runId, { reason });
39453
39511
  } catch {
@@ -44131,6 +44189,24 @@ function buildThreadStartParams(msg, config2, desiredModel) {
44131
44189
  "- Do not create a handoff for transient provider quota/runtime failures, retriable tool",
44132
44190
  " errors, or vague uncertainty; retry cheaply or explain the transient issue in chat.",
44133
44191
  "",
44192
+ "Deferred-verification contract \u2014 when the RESULT can only be judged later:",
44193
+ "- When a task's implementation is DONE but its outcome cannot be confirmed now because it",
44194
+ " depends on a delay you do not control \u2014 SEO ranking/traffic effects after (re)indexing,",
44195
+ " ad-campaign performance data accumulating, cache/DNS propagation, a scheduled external",
44196
+ " event \u2014 do NOT close the task and do NOT leave it `in_progress`. Move it to",
44197
+ " `awaiting_verification` via `update_task` and set `verificationDueAt` to the ISO-8601",
44198
+ " datetime when the effect should be measurable (e.g. ~2 weeks out for an SEO article).",
44199
+ " Use `followUpAt` instead when it is a softer check-in rather than a hard verification.",
44200
+ "- In the same `update_task`, or in the task notes, record WHAT to measure and HOW so a later",
44201
+ " turn can verify objectively (the metric, the baseline, the success threshold).",
44202
+ "- Do NOT use `awaiting_verification` for results you CAN confirm now \u2014 verify immediately",
44203
+ " (browser/HTTP/tests) and close or advance the task instead. It is only for genuinely",
44204
+ " time-delayed evidence, not for avoiding verification work.",
44205
+ "- When the task is later woken (its `verificationDueAt`/`followUpAt` has passed), gather the",
44206
+ " evidence, then close it \u2014 `closed_done` if the outcome met the bar, or `closed_no_result`",
44207
+ " if it did not (that state REQUIRES a linked Evidence wiki page); or, if the effect still",
44208
+ " is not measurable, re-set `verificationDueAt` to a new date with a reason.",
44209
+ "",
44134
44210
  "File artifacts: when you produce a file for the operator (report, HTML, screenshot),",
44135
44211
  "upload it with `upload_artifact` (class=temp for one-off downloads, class=durable when a",
44136
44212
  "spec/evidence page references it; pass taskId when known) and paste the returned markdown",
@@ -44217,7 +44293,15 @@ function tokenRefreshIntervalMs() {
44217
44293
  const seconds = Math.min(55, Math.max(15, readEnvInt("MRRLIN_DIRECTOR_BRIDGE_TOKEN_REFRESH_SECONDS", 45)));
44218
44294
  return seconds * 1e3;
44219
44295
  }
44220
- var COLLECTED_TURN_TIMEOUT_MS = 3e5;
44296
+ var COLLECTED_TURN_TIMEOUT_DEFAULT_MS = 9e5;
44297
+ function collectedTurnTimeoutMs() {
44298
+ const raw = process.env.MRRLIN_COLLECTED_TURN_TIMEOUT_MS;
44299
+ if (raw) {
44300
+ const parsed = Number(raw);
44301
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
44302
+ }
44303
+ return COLLECTED_TURN_TIMEOUT_DEFAULT_MS;
44304
+ }
44221
44305
  var DEFAULT_IMAGE_ONLY_PROMPT = "Look at the attached image.";
44222
44306
  var VERIFICATION_BASE_INSTRUCTIONS = [
44223
44307
  "You are a deployment verification agent. Make ONLY read-only HTTP requests to the single",
@@ -44283,8 +44367,8 @@ async function runCollectedCodexTurn(client, prompt, cwd, baseInstructions) {
44283
44367
  await turnEnded;
44284
44368
  },
44285
44369
  interrupt: () => client.turn.interrupt({ threadId }),
44286
- timeoutMs: COLLECTED_TURN_TIMEOUT_MS,
44287
- timeoutMessage: `Collected turn did not complete within ${COLLECTED_TURN_TIMEOUT_MS}ms.`
44370
+ timeoutMs: collectedTurnTimeoutMs(),
44371
+ timeoutMessage: `Collected turn did not complete within ${collectedTurnTimeoutMs()}ms.`
44288
44372
  });
44289
44373
  for (const ev of mapper.flush()) if (ev.type === "assistant") chunks.push(ev.content);
44290
44374
  } finally {
@@ -45003,6 +45087,10 @@ function createBridgeMessageHandler(deps) {
45003
45087
  const effectiveText = inputText.length > 0 ? inputText : imageDataUri ? DEFAULT_IMAGE_ONLY_PROMPT : "";
45004
45088
  if (effectiveText.length > 0) {
45005
45089
  const updateOffer = deps.maybeUpdateOffer ? deps.maybeUpdateOffer() : null;
45090
+ const updateOfferProjectSlug = msg.context?.projectSlug?.trim();
45091
+ if (updateOffer !== null && updateOfferProjectSlug && deps.fileUpdateInboxItem) {
45092
+ void deps.fileUpdateInboxItem(updateOfferProjectSlug);
45093
+ }
45006
45094
  const composedPrefix = updateOffer ? contextPrefix ? `${updateOffer}
45007
45095
 
45008
45096
  ${contextPrefix}` : updateOffer : contextPrefix;
@@ -45420,7 +45508,32 @@ async function startDirectorBridge() {
45420
45508
  sessionSinks.unbind(directorSessionId, send);
45421
45509
  },
45422
45510
  getSessionSink: (directorSessionId) => sessionSinks.get(directorSessionId),
45423
- maybeUpdateOffer: () => selfUpdateHandle.maybeUpdateOffer()
45511
+ maybeUpdateOffer: () => selfUpdateHandle.maybeUpdateOffer(),
45512
+ fileUpdateInboxItem: async (projectSlug) => {
45513
+ const body = selfUpdateHandle.updateInboxBody();
45514
+ if (!body) return;
45515
+ const id = `II-MCPUPD-${(0, import_node_crypto5.randomUUID)()}`;
45516
+ try {
45517
+ const row = await client.createInboxItem(projectSlug, { id, kind: "question", body });
45518
+ process.stderr.write(
45519
+ `[mrrlin-mcp director-bridge] info bridge.self_update.inbox_filed ${JSON.stringify({
45520
+ projectSlug,
45521
+ itemId: row.id,
45522
+ deduped: row.id !== id
45523
+ })}
45524
+ `
45525
+ );
45526
+ } catch (err) {
45527
+ process.stderr.write(
45528
+ `[mrrlin-mcp director-bridge] warn bridge.self_update.inbox_file_failed ${JSON.stringify({
45529
+ projectSlug,
45530
+ errorClass: "inbox_write_failed",
45531
+ message: String(err?.message ?? err)
45532
+ })}
45533
+ `
45534
+ );
45535
+ }
45536
+ }
45424
45537
  };
45425
45538
  const bridgeLogger = createBridgeLogger(config2.stateDir, process.env);
45426
45539
  let bridgeBinShaShort = "unknown00000";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"