@mrrlin-dev/mcp 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.cjs +227 -91
  2. package/package.json +2 -2
package/dist/bin.cjs CHANGED
@@ -36476,6 +36476,7 @@ var openApiOperationIds = {
36476
36476
  getOperatorSettings: "getOperatorSettings",
36477
36477
  updateOperatorSettings: "updateOperatorSettings",
36478
36478
  recordOperatorPresence: "recordOperatorPresence",
36479
+ pauseAutopilotForLimit: "pauseAutopilotForLimit",
36479
36480
  getAgentPool: "getAgentPool",
36480
36481
  putAgentPool: "putAgentPool",
36481
36482
  getConsensusConfig: "getConsensusConfig",
@@ -36522,6 +36523,13 @@ var mrrlinOperatorSettingsSchema = external_exports.object({
36522
36523
  activeProjectSlug: external_exports.string().min(1).nullable(),
36523
36524
  // The last time an authenticated BROWSER operator session was observed. Null means never seen.
36524
36525
  lastPresenceAt: external_exports.string().datetime().nullable(),
36526
+ // autopilot-limit-pause: READ-ONLY status for an automatic provider-limit pause. These are never
36527
+ // writable via mrrlinOperatorSettingsUpdateSchema (an explicit operator write clears them
36528
+ // server-side). Non-null autoPauseSource => the current pause is machine-owned; autoPauseResetAt
36529
+ // is the absolute reset time (null = indefinite, manual resume required).
36530
+ autoPauseSource: external_exports.string().min(1).nullable(),
36531
+ autoPauseReason: external_exports.string().min(1).nullable(),
36532
+ autoPauseResetAt: external_exports.string().datetime().nullable(),
36525
36533
  createdAt: external_exports.string().datetime(),
36526
36534
  updatedAt: external_exports.string().datetime()
36527
36535
  });
@@ -36533,6 +36541,11 @@ var mrrlinOperatorSettingsUpdateSchema = external_exports.object({
36533
36541
  message: "OperatorSettingsUpdate requires at least one field"
36534
36542
  });
36535
36543
  var operatorSettingsResponseSchema = external_exports.object({ data: mrrlinOperatorSettingsSchema });
36544
+ var autopilotLimitPauseRequestSchema = external_exports.object({
36545
+ source: external_exports.string().min(1).default("provider_limit"),
36546
+ reason: external_exports.string().min(1).max(500),
36547
+ resetAt: external_exports.string().datetime().nullable().default(null)
36548
+ }).strict();
36536
36549
  var operatorPresenceResponseSchema = external_exports.object({ data: external_exports.object({ lastPresenceAt: external_exports.string().datetime() }) });
36537
36550
  var operatorPresenceRequestSchema = external_exports.object({ projectSlug: external_exports.string().min(1).optional() }).strict().optional();
36538
36551
  var mrrlinConsensusConfigSchema = external_exports.object({ consensusEnabled: external_exports.boolean() });
@@ -38123,7 +38136,7 @@ var wakeDirectorResponseSchema = external_exports.object({
38123
38136
  }))
38124
38137
  })
38125
38138
  });
38126
- var mrrlinInboxItemKindSchema = external_exports.enum(["proposal", "check_in", "question", "handoff", "info"]);
38139
+ var mrrlinInboxItemKindSchema = external_exports.enum(["proposal", "check_in", "question", "handoff", "info", "completed"]);
38127
38140
  var mrrlinInboxItemStatusSchema = external_exports.enum(["pending", "approved", "rejected", "acknowledged"]);
38128
38141
  var mrrlinInboxItemIdSchema = external_exports.string().regex(/^II-[A-Za-z0-9._-]+$/);
38129
38142
  var mrrlinInboxItemActionPayloadSchema = external_exports.object({
@@ -38583,6 +38596,36 @@ function addTurnTokenUsage(total, turn) {
38583
38596
  };
38584
38597
  }
38585
38598
 
38599
+ // ../../packages/codex-client/dist/rate-limit.js
38600
+ var EXHAUSTION_USED_PERCENT = 100;
38601
+ function windowExhausted(window) {
38602
+ if (!window)
38603
+ return false;
38604
+ return typeof window.usedPercent === "number" && Number.isFinite(window.usedPercent) && window.usedPercent >= EXHAUSTION_USED_PERCENT;
38605
+ }
38606
+ function normalizeResetEpochSeconds(resetsAt, nowMs) {
38607
+ if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt))
38608
+ return null;
38609
+ const resetMs = resetsAt * 1e3;
38610
+ if (resetMs <= nowMs)
38611
+ return null;
38612
+ return new Date(resetMs).toISOString();
38613
+ }
38614
+ function detectExhaustedProviderLimit(snapshot, nowMs) {
38615
+ if (!snapshot)
38616
+ return null;
38617
+ const reachedType = typeof snapshot.rateLimitReachedType === "string" ? snapshot.rateLimitReachedType.trim() : "";
38618
+ const exhausted = reachedType.length > 0 || windowExhausted(snapshot.primary) || windowExhausted(snapshot.secondary);
38619
+ if (!exhausted)
38620
+ return null;
38621
+ const candidates = [
38622
+ normalizeResetEpochSeconds(snapshot.primary?.resetsAt, nowMs),
38623
+ normalizeResetEpochSeconds(snapshot.secondary?.resetsAt, nowMs)
38624
+ ].filter((value) => value !== null);
38625
+ const resetAt = candidates.length > 0 ? candidates.reduce((a, b) => a > b ? a : b) : null;
38626
+ return { exhausted: true, resetAt };
38627
+ }
38628
+
38586
38629
  // ../../packages/client/dist/agent-token-provider.js
38587
38630
  var AgentTokenProviderError = class extends Error {
38588
38631
  code;
@@ -39493,6 +39536,15 @@ function createMrrlinClient(config2) {
39493
39536
  });
39494
39537
  return operatorSettingsResponseSchema.parse(body).data;
39495
39538
  },
39539
+ async pauseAutopilotForLimit(input) {
39540
+ const parsed = autopilotLimitPauseRequestSchema.parse(input);
39541
+ const body = await request(`/operator/autopilot/limit-pause`, {
39542
+ body: JSON.stringify(parsed),
39543
+ headers: { "content-type": "application/json" },
39544
+ method: "POST"
39545
+ });
39546
+ return operatorSettingsResponseSchema.parse(body).data;
39547
+ },
39496
39548
  async recordOperatorPresence(input) {
39497
39549
  const projectSlug = input?.projectSlug;
39498
39550
  const body = await request(`/operator/presence`, {
@@ -40135,7 +40187,7 @@ function startCodexProbe(deps = {}) {
40135
40187
  }
40136
40188
 
40137
40189
  // src/_generated/version.ts
40138
- var PKG_VERSION = "0.4.0";
40190
+ var PKG_VERSION = "0.4.1";
40139
40191
 
40140
40192
  // src/api-base-url-migrate.ts
40141
40193
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -42946,7 +42998,7 @@ function onComplete(outcome, input) {
42946
42998
  }
42947
42999
  const taskRef = parsed.taskTitle ? `${input.taskId} \u2014 ${parsed.taskTitle}` : input.taskId;
42948
43000
  return {
42949
- kind: "info",
43001
+ kind: "completed",
42950
43002
  taskId: input.taskId,
42951
43003
  body: `Spec drafted automatically for ${taskRef} \u2014 page ${parsed.pageId ?? "?"} in Specs/. The next scheduler tick will run the consensus review.`
42952
43004
  };
@@ -44006,6 +44058,10 @@ function executionTurnBudgetMessage(budgetMs) {
44006
44058
  function isExecutionTurnTimeoutReason(reason) {
44007
44059
  return reason.startsWith(EXECUTION_TURN_TIMEOUT_PREFIX) || reason.startsWith(EXECUTION_TURN_STALL_PREFIX);
44008
44060
  }
44061
+ function reportAutopilotLimitPause(client, reason, resetAt) {
44062
+ void client.pauseAutopilotForLimit({ source: "provider_limit", reason, resetAt }).catch(() => {
44063
+ });
44064
+ }
44009
44065
  function isDeliverableGateRetryableReason(reason) {
44010
44066
  return DELIVERABLE_GATE_RETRYABLE_REASON_PREFIXES.some((prefix) => reason.startsWith(prefix));
44011
44067
  }
@@ -44085,17 +44141,16 @@ var BASE_INSTRUCTIONS_PREFIX = [
44085
44141
  " qualifies, produce the file; never merely offer one.",
44086
44142
  "- Mixed output counts as retrieved: if any material part came from a retrieval, treat the whole",
44087
44143
  " result as retrieved rather than splitting it between inline text and a file.",
44088
- "- When your output is a file (markdown report, HTML, screenshot), upload it with",
44089
- " `upload_artifact` and embed the returned markdown link \u2014 never inline large blobs",
44090
- " into chat, spec, or note text.",
44144
+ "- When your output is a file (markdown report, HTML), call `deliver_file`: pass the body inline",
44145
+ " as `content` and embed the returned markdown link. It needs no writable path, so it is the",
44146
+ " default even when this run path cannot write files \u2014 never inline large blobs into chat,",
44147
+ " spec, or note text.",
44091
44148
  "- class=temp for one-off operator hand-offs (48h); class=durable for anything a spec",
44092
44149
  " or evidence page will reference (180 days). Always pass taskId/runId when known.",
44093
- "- Anything user-facing delivered as a file MUST be uploaded. Files are capped at 5MB \u2014",
44094
- " split or summarize larger outputs.",
44095
- "- `upload_artifact` needs a real local file path, so it presupposes you can write one. When this",
44096
- " run path CAN write files, produce the artifact normally \u2014 that is the default. ONLY when it",
44097
- " cannot write, do NOT attempt the file: put the summary in your result text and state what a",
44098
- " file would have held. Never fake a path you did not write.",
44150
+ "- Anything user-facing delivered as a file MUST go through `deliver_file`. Files are capped at",
44151
+ " 5MB \u2014 split or summarize larger outputs.",
44152
+ "- Use `upload_artifact` only for a file already on disk (e.g. a screenshot the browser wrote);",
44153
+ " it needs a real local path, so never fake a path you did not write.",
44099
44154
  "",
44100
44155
  "Produced URLs \u2014 the operator's per-page links come only from you:",
44101
44156
  "- Call `record_produced_url` after publishing EACH web page this task produced, so the task UI",
@@ -44425,6 +44480,17 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
44425
44480
  }
44426
44481
  const usage = !notifThreadId || notifThreadId === threadId ? extractTurnTokenUsage(method, params) : null;
44427
44482
  if (usage) turnUsage = usage;
44483
+ if (method === "account/rateLimits/updated") {
44484
+ const snapshot = params?.rateLimits ?? null;
44485
+ const signal = detectExhaustedProviderLimit(snapshot, Date.now());
44486
+ if (signal) {
44487
+ reportAutopilotLimitPause(
44488
+ deps.client,
44489
+ "Codex provider limit exhausted during an autonomous execution run.",
44490
+ signal.resetAt
44491
+ );
44492
+ }
44493
+ }
44428
44494
  const events = mapper.mapNotification(
44429
44495
  { method, params },
44430
44496
  threadId
@@ -51060,10 +51126,11 @@ function buildThreadStartParams(msg, config2, desiredModel) {
51060
51126
  " treat the whole answer as retrieved rather than splitting it between chat and a file.",
51061
51127
  '- When it qualifies, PRODUCE the file. Offering one ("I can give you this as CSV/JSON") is the',
51062
51128
  " failure, not the courtesy.",
51063
- "- When it is a file: write it (`.md` for linear prose, `.html` when structure or layout",
51064
- " carries meaning), upload with `upload_artifact` (class=temp for one-off downloads,",
51065
- " class=durable when a spec/evidence page references it; pass taskId when known), and paste",
51066
- " the returned markdown link into your reply.",
51129
+ "- When it is a file: choose the format (`.md` for linear prose, `.html` when structure or",
51130
+ " layout carries meaning), then call `deliver_file` \u2014 pass the body inline as `content` and it",
51131
+ " needs no local file (class=temp for one-off downloads, class=durable when a spec/evidence",
51132
+ " page references it; pass taskId when known), and paste the returned markdown link into your",
51133
+ " reply. Use `upload_artifact` only for a file that already exists on disk (e.g. a screenshot).",
51067
51134
  "- The chat reply must still stand alone as a complete answer even if the file is never opened:",
51068
51135
  " the headline finding, the few points that matter, then the link. Never reduce it to",
51069
51136
  ' "see attached" or "details in the report".',
@@ -51300,6 +51367,10 @@ var DEFAULT_WORKTREE_RETENTION_HOURS = 48;
51300
51367
  function worktreeRetentionMs() {
51301
51368
  return readEnvInt("MRRLIN_DIRECTOR_WORKTREE_RETENTION_HOURS", DEFAULT_WORKTREE_RETENTION_HOURS) * 36e5;
51302
51369
  }
51370
+ function reportAutopilotLimitPause2(client, reason, resetAt) {
51371
+ void client.pauseAutopilotForLimit({ source: "provider_limit", reason, resetAt }).catch(() => {
51372
+ });
51373
+ }
51303
51374
  async function runOrphanWorktreeSweep(client, opts) {
51304
51375
  const cutoff = Date.now() - worktreeRetentionMs();
51305
51376
  const isReapable = (worktreePath) => {
@@ -52310,7 +52381,13 @@ function createBridgeMessageHandler(deps) {
52310
52381
  pokeActivity();
52311
52382
  if (method === "account/rateLimits/updated") {
52312
52383
  const snapshot = params?.rateLimits ?? null;
52313
- if (snapshot) sendInTurn({ type: "rateLimits", directorSessionId, rateLimits: snapshot });
52384
+ if (snapshot) {
52385
+ sendInTurn({ type: "rateLimits", directorSessionId, rateLimits: snapshot });
52386
+ const signal = detectExhaustedProviderLimit(snapshot, Date.now());
52387
+ if (signal) {
52388
+ deps.reportLimitPause?.("Codex provider limit exhausted during a Director turn.", signal.resetAt);
52389
+ }
52390
+ }
52314
52391
  }
52315
52392
  const events = mapper.mapNotification({ method, params }, threadId);
52316
52393
  for (const event of events) {
@@ -52412,6 +52489,10 @@ ${effectiveText}` : effectiveText;
52412
52489
  forward(event);
52413
52490
  }
52414
52491
  if (outputEvents === 0) {
52492
+ deps.reportLimitPause?.(
52493
+ "Director turn completed without output; Codex likely hit its usage or credits limit.",
52494
+ null
52495
+ );
52415
52496
  sendInTurn({
52416
52497
  type: "error",
52417
52498
  directorSessionId,
@@ -52909,6 +52990,7 @@ async function startDirectorBridge() {
52909
52990
  turnInactivity: () => ({ idleMs: chatTurnIdleMs(), maxMs: chatTurnMaxMs() }),
52910
52991
  checkoutRegistry,
52911
52992
  setCodexAuth: (s) => health.setCodexAuth(s),
52993
+ reportLimitPause: (reason, resetAt) => reportAutopilotLimitPause2(client, reason, resetAt),
52912
52994
  getPairingQr: async () => {
52913
52995
  if (!relayConfigured || !remoteRelay) return { status: "not-configured" };
52914
52996
  const payload = remoteRelay.qrPayload;
@@ -57383,30 +57465,9 @@ var HUMANIZE_TEMPLATE_BY_KIND = {
57383
57465
  missing_requirement: "A required part of the task is still missing from the spec. What should it say?",
57384
57466
  acceptance_criteria_detail: "The direction is clear, but the spec still needs a sharper success check. What should count as done?"
57385
57467
  };
57386
- var HUMANIZE_CONTEXT_CAP = 200;
57387
- function truncateToSentenceBoundary(text, cap) {
57388
- if (text.length <= cap) return text;
57389
- const sentences = text.match(/.+?[.!?](?=\s|$)/g);
57390
- if (sentences) {
57391
- let kept = "";
57392
- let keptCount = 0;
57393
- for (const sentence of sentences) {
57394
- const candidate = kept ? `${kept} ${sentence.trim()}` : sentence.trim();
57395
- if (candidate.length > cap) break;
57396
- kept = candidate;
57397
- keptCount++;
57398
- }
57399
- if (kept) return keptCount < sentences.length ? `${kept} \u2026` : kept;
57400
- }
57401
- const cutIdx = text.lastIndexOf(" ", cap);
57402
- return text.slice(0, cutIdx > 0 ? cutIdx : cap).trim() + "\u2026";
57403
- }
57404
57468
  function humanizeClarificationBody(clarification) {
57405
57469
  const prefix = HUMANIZE_TEMPLATE_BY_KIND[clarification.kind];
57406
- const context = truncateToSentenceBoundary(
57407
- clarification.rawQuestion.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim(),
57408
- HUMANIZE_CONTEXT_CAP
57409
- );
57470
+ const context = clarification.rawQuestion.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim();
57410
57471
  return context ? `${prefix} ${context}` : prefix;
57411
57472
  }
57412
57473
  var defaultNativeSpecGateBuilders = { buildSpecGateDeps, runSpecGate };
@@ -57701,7 +57762,7 @@ function onComplete2(outcome, input) {
57701
57762
  };
57702
57763
  }
57703
57764
  return {
57704
- kind: "info",
57765
+ kind: "completed",
57705
57766
  ...taskId ? { taskId } : {},
57706
57767
  body: taskRef ? `Consensus CONVERGED for ${taskRef} (spec Specs/${input.pageId}); spec_ready set. Ready to create an execution run.` : `Consensus CONVERGED for Specs/${input.pageId}; spec_ready set. Ready to create an execution run.`
57707
57768
  };
@@ -57832,7 +57893,7 @@ function onComplete3(outcome, input) {
57832
57893
  };
57833
57894
  }
57834
57895
  return {
57835
- kind: "info",
57896
+ kind: "completed",
57836
57897
  ...taskId ? { taskId } : {},
57837
57898
  body: taskRef ? `Deliverable consensus CONVERGED for ${taskRef} (Deliverables/${input.pageId}); it meets the acceptance criteria.` : `Deliverable consensus CONVERGED for Deliverables/${input.pageId}; it meets the acceptance criteria.`
57838
57899
  };
@@ -57955,6 +58016,7 @@ var mcpToolNames = {
57955
58016
  runSpecConsensus: "run_spec_consensus",
57956
58017
  runCodeReviewerGate: "run_code_reviewer_gate",
57957
58018
  uploadArtifact: "upload_artifact",
58019
+ deliverFile: "deliver_file",
57958
58020
  listArtifactFiles: "list_artifact_files",
57959
58021
  reclaimExecutionRunLease: "reclaim_execution_run_lease",
57960
58022
  restartExecutionRun: "restart_execution_run",
@@ -58540,6 +58602,20 @@ var uploadArtifactInputSchema = projectScopedSchema.merge(
58540
58602
  description: external_exports.string().optional().describe("Shown in the run's artifact history; ignored when runId is not provided.")
58541
58603
  })
58542
58604
  );
58605
+ var deliverFileInputSchema = projectScopedSchema.merge(
58606
+ external_exports.object({
58607
+ filename: external_exports.string().min(1).describe(
58608
+ "Bare filename with extension (e.g. 'notes.md'). No path separators or '..' allowed."
58609
+ ),
58610
+ content: external_exports.string().describe("UTF-8 text content of the file."),
58611
+ class: mrrlinArtifactFileClassSchema.describe(
58612
+ "temp = 48h one-off hand-off; durable = 180d, for spec/evidence references."
58613
+ ),
58614
+ taskId: mrrlinTaskIdSchema.optional(),
58615
+ runId: mrrlinExecutionRunIdSchema.optional(),
58616
+ description: external_exports.string().optional().describe("Shown in the run's artifact history; ignored when runId is not provided.")
58617
+ })
58618
+ );
58543
58619
  var listArtifactFilesInputSchema = projectScopedSchema.merge(
58544
58620
  external_exports.object({
58545
58621
  taskId: mrrlinTaskIdSchema.optional(),
@@ -58625,6 +58701,7 @@ var mcpToolInputSchemas = {
58625
58701
  [mcpToolNames.createInboxItem]: createInboxItemInputSchema,
58626
58702
  [mcpToolNames.decideInboxItem]: decideInboxItemInputSchema,
58627
58703
  [mcpToolNames.uploadArtifact]: uploadArtifactInputSchema,
58704
+ [mcpToolNames.deliverFile]: deliverFileInputSchema,
58628
58705
  [mcpToolNames.listArtifactFiles]: listArtifactFilesInputSchema,
58629
58706
  [mcpToolNames.reclaimExecutionRunLease]: reclaimExecutionRunLeaseInputSchema,
58630
58707
  [mcpToolNames.restartExecutionRun]: restartExecutionRunInputSchema,
@@ -58762,6 +58839,7 @@ var mcpToolDescriptions = {
58762
58839
  [mcpToolNames.createInboxItem]: "Create an operator-inbox item (proposal, check_in, question, handoff, or info) for a project.",
58763
58840
  [mcpToolNames.decideInboxItem]: "Approve, reject, or acknowledge an operator-inbox item. Rejection requires a reason.",
58764
58841
  [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.",
58842
+ [mcpToolNames.deliverFile]: "This is the DEFAULT way to deliver a file the agent just composed; it takes the body inline as UTF-8 `content` in one call and needs no local file or writable path; `upload_artifact` is for a file that already exists on disk; use class=temp / class=durable and taskId/runId semantics with a 5MB cap.",
58765
58843
  [mcpToolNames.listArtifactFiles]: "List uploaded artifact files for the project (optionally filtered by taskId/runId), newest first.",
58766
58844
  [mcpToolNames.reclaimExecutionRunLease]: "Clear a stale pending execution-run lease when the lease has aged past staleSeconds. Returns the reclaimed run on success, null when the lease is not stale yet, and surfaces a structured conflict when reclaim retries are exhausted.",
58767
58845
  [mcpToolNames.restartExecutionRun]: "Restart a parked execution run (handoff_requested or handoff_queued) back to pending so the queue can resume it.",
@@ -58908,6 +58986,68 @@ function makeReadTool(client, name, fallbackCode, fallbackMessage, handler, proj
58908
58986
  }
58909
58987
  function createMrrlinTools(options) {
58910
58988
  const client = createMrrlinClient(withAuth(options));
58989
+ async function uploadArtifactBytes(c, args) {
58990
+ const { bytes: bytes3, class: artifactClass, contentType, description, filename, projectSlug, runId, taskId } = args;
58991
+ const created = await c.createArtifactFile(projectSlug, {
58992
+ class: artifactClass,
58993
+ contentType,
58994
+ filename,
58995
+ runId,
58996
+ sizeBytes: bytes3.byteLength,
58997
+ taskId
58998
+ });
58999
+ let stage = "upload";
59000
+ try {
59001
+ if (created.upload.kind === "presigned-post") {
59002
+ const form = new FormData();
59003
+ for (const [field, value] of Object.entries(created.upload.fields)) {
59004
+ form.append(field, value);
59005
+ }
59006
+ form.append("file", new Blob([bytes3], { type: contentType }), filename);
59007
+ const bareFetch = options.fetch ?? globalThis.fetch;
59008
+ const uploadResponse = await bareFetch(created.upload.url, { body: form, method: "POST" });
59009
+ if (!uploadResponse.ok) {
59010
+ throw new Error(
59011
+ `Presigned upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`
59012
+ );
59013
+ }
59014
+ } else {
59015
+ await c.uploadArtifactFileContent(projectSlug, created.artifact.id, bytes3, contentType);
59016
+ }
59017
+ stage = "complete";
59018
+ const completed = await c.completeArtifactFile(projectSlug, created.artifact.id);
59019
+ if (completed.runId) {
59020
+ stage = "append-run-artifact";
59021
+ await c.appendExecutionArtifact(projectSlug, completed.runId, {
59022
+ id: `EA-${completed.id}`,
59023
+ kind: "file",
59024
+ payload: {
59025
+ artifactFileId: completed.id,
59026
+ class: completed.class,
59027
+ contentType: completed.contentType,
59028
+ description: description ?? null,
59029
+ filename: completed.filename,
59030
+ sizeBytes: completed.sizeBytes,
59031
+ stableUrl: created.stableUrl
59032
+ }
59033
+ });
59034
+ }
59035
+ const markdown = contentType.startsWith("image/") ? `![${completed.filename}](${created.stableUrl})` : `[${completed.filename}](${created.stableUrl})`;
59036
+ return {
59037
+ artifactFile: completed,
59038
+ markdown,
59039
+ stableUrl: created.stableUrl
59040
+ };
59041
+ } catch (error51) {
59042
+ const original = error51 instanceof MrrlinApiError ? error51.code : error51 instanceof McpToolCodedError ? error51.code : "ARTIFACT_FILE_UPLOAD_FAILED";
59043
+ const message = error51 instanceof Error ? error51.message : String(error51);
59044
+ const note = stage === "append-run-artifact" ? "file uploaded and completed successfully; only the run-history append failed \u2014 do NOT re-upload, reference the stableUrl directly" : "a pending row may remain; it is reaped automatically after 1h";
59045
+ throw new McpToolCodedError(
59046
+ original,
59047
+ `artifactFileId=${created.artifact.id} stableUrl=${created.stableUrl} (failed at ${stage}; ${note}): ${message}`
59048
+ );
59049
+ }
59050
+ }
58911
59051
  return {
58912
59052
  [mcpToolNames.archivePlan]: makeTool(
58913
59053
  client,
@@ -59475,65 +59615,61 @@ function createMrrlinTools(options) {
59475
59615
  );
59476
59616
  }
59477
59617
  const filename = import_node_path20.default.basename(filePath);
59478
- const created = await c.createArtifactFile(projectSlug, {
59618
+ return uploadArtifactBytes(c, {
59619
+ bytes: bytes3,
59479
59620
  class: artifactClass,
59480
59621
  contentType,
59622
+ description,
59481
59623
  filename,
59624
+ projectSlug,
59482
59625
  runId,
59483
- sizeBytes: bytes3.byteLength,
59484
59626
  taskId
59485
59627
  });
59486
- let stage = "upload";
59487
- try {
59488
- if (created.upload.kind === "presigned-post") {
59489
- const form = new FormData();
59490
- for (const [field, value] of Object.entries(created.upload.fields)) {
59491
- form.append(field, value);
59492
- }
59493
- form.append("file", new Blob([bytes3], { type: contentType }), filename);
59494
- const bareFetch = options.fetch ?? globalThis.fetch;
59495
- const uploadResponse = await bareFetch(created.upload.url, { body: form, method: "POST" });
59496
- if (!uploadResponse.ok) {
59497
- throw new Error(
59498
- `Presigned upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`
59499
- );
59500
- }
59501
- } else {
59502
- await c.uploadArtifactFileContent(projectSlug, created.artifact.id, bytes3, contentType);
59503
- }
59504
- stage = "complete";
59505
- const completed = await c.completeArtifactFile(projectSlug, created.artifact.id);
59506
- if (completed.runId) {
59507
- stage = "append-run-artifact";
59508
- await c.appendExecutionArtifact(projectSlug, completed.runId, {
59509
- id: `EA-${completed.id}`,
59510
- kind: "file",
59511
- payload: {
59512
- artifactFileId: completed.id,
59513
- class: completed.class,
59514
- contentType: completed.contentType,
59515
- description: description ?? null,
59516
- filename: completed.filename,
59517
- sizeBytes: completed.sizeBytes,
59518
- stableUrl: created.stableUrl
59519
- }
59520
- });
59521
- }
59522
- const markdown = contentType.startsWith("image/") ? `![${completed.filename}](${created.stableUrl})` : `[${completed.filename}](${created.stableUrl})`;
59523
- return {
59524
- artifactFile: completed,
59525
- markdown,
59526
- stableUrl: created.stableUrl
59527
- };
59528
- } catch (error51) {
59529
- const original = error51 instanceof MrrlinApiError ? error51.code : error51 instanceof McpToolCodedError ? error51.code : "ARTIFACT_FILE_UPLOAD_FAILED";
59530
- const message = error51 instanceof Error ? error51.message : String(error51);
59531
- const note = stage === "append-run-artifact" ? "file uploaded and completed successfully; only the run-history append failed \u2014 do NOT re-upload, reference the stableUrl directly" : "a pending row may remain; it is reaped automatically after 1h";
59628
+ }
59629
+ ),
59630
+ [mcpToolNames.deliverFile]: makeTool(
59631
+ client,
59632
+ mcpToolNames.deliverFile,
59633
+ "ARTIFACT_FILE_UPLOAD_FAILED",
59634
+ "Unable to deliver artifact file.",
59635
+ async (c, { projectSlug, filename, content, class: artifactClass, taskId, runId, description }) => {
59636
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
59532
59637
  throw new McpToolCodedError(
59533
- original,
59534
- `artifactFileId=${created.artifact.id} stableUrl=${created.stableUrl} (failed at ${stage}; ${note}): ${message}`
59638
+ "ARTIFACT_FILE_NAME_INVALID",
59639
+ `Invalid filename '${filename}'; provide a bare filename with no path separators or '..'.`
59535
59640
  );
59536
59641
  }
59642
+ const extension2 = import_node_path20.default.extname(filename).toLowerCase();
59643
+ const contentType = ARTIFACT_EXTENSION_TYPES[extension2];
59644
+ if (!contentType) {
59645
+ throw new McpToolCodedError(
59646
+ "ARTIFACT_FILE_TYPE_UNSUPPORTED",
59647
+ `Unsupported artifact extension '${extension2}'. Allowed: ${Object.keys(ARTIFACT_EXTENSION_TYPES).join(", ")}.`
59648
+ );
59649
+ }
59650
+ const bytes3 = new Uint8Array(Buffer.from(content, "utf8"));
59651
+ if (bytes3.byteLength === 0) {
59652
+ throw new McpToolCodedError(
59653
+ "ARTIFACT_FILE_EMPTY",
59654
+ "File is empty (0 bytes); write content before uploading."
59655
+ );
59656
+ }
59657
+ if (bytes3.byteLength > MRRLIN_ARTIFACT_FILE_MAX_BYTES) {
59658
+ throw new McpToolCodedError(
59659
+ "ARTIFACT_FILE_TOO_LARGE",
59660
+ "Artifact exceeds 5MB; split or summarize instead."
59661
+ );
59662
+ }
59663
+ return uploadArtifactBytes(c, {
59664
+ bytes: bytes3,
59665
+ class: artifactClass,
59666
+ contentType,
59667
+ description,
59668
+ filename,
59669
+ projectSlug,
59670
+ runId,
59671
+ taskId
59672
+ });
59537
59673
  }
59538
59674
  ),
59539
59675
  [mcpToolNames.listArtifactFiles]: makeTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -47,7 +47,7 @@
47
47
  "dev": "tsx watch src/bin.ts serve",
48
48
  "lint": "eslint .",
49
49
  "start": "node dist/bin.cjs serve",
50
- "test": "node scripts/write-version.mjs && node --import tsx --test $(find src -name '*.test.ts' | sort | tr '\\n' ' ')",
50
+ "test": "pnpm --filter @mrrlin/codex-client build && node scripts/write-version.mjs && node --import tsx --test $(find src -name '*.test.ts' | sort | tr '\\n' ' ')",
51
51
  "typecheck": "node scripts/write-version.mjs && pnpm --filter @mrrlin/schemas build && pnpm --filter @mrrlin/client build && pnpm --filter @mrrlin/wiki build && pnpm --filter @mrrlin/codex-client build && pnpm --filter @mrrlin/director-e2e build && tsc -p tsconfig.json --noEmit"
52
52
  }
53
53
  }