flockbay 0.10.21 → 0.10.23

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.
@@ -1,14 +1,14 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
2
  import os, { homedir } from 'node:os';
3
3
  import { randomUUID, createCipheriv, randomBytes } from 'node:crypto';
4
- import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as readDaemonState, g as clearDaemonState, b as packageJson, r as readSettings, h as readCredentials, u as updateSettings, w as writeCredentials, i as unrealMcpPythonDir, j as acquireDaemonLock, k as writeDaemonState, m as ApiMachineClient, n as releaseDaemonLock, s as sendUnrealMcpTcpCommand, A as ApiClient, o as clearCredentials, q as clearMachineId, t as installUnrealMcpPluginToEngine, v as getLatestDaemonLog, x as normalizeServerUrlForNode } from './types-mXJc7o0P.mjs';
4
+ import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as readDaemonState, g as clearDaemonState, b as packageJson, r as readSettings, h as readCredentials, u as updateSettings, w as writeCredentials, i as unrealMcpPythonDir, j as acquireDaemonLock, k as writeDaemonState, m as ApiMachineClient, n as releaseDaemonLock, s as sendUnrealMcpTcpCommand, A as ApiClient, o as clearCredentials, q as clearMachineId, t as installUnrealMcpPluginToEngine, v as buildAndInstallUnrealMcpPlugin, x as getLatestDaemonLog, y as normalizeServerUrlForNode } from './types-CNn15BaT.mjs';
5
5
  import { spawn, execFileSync, execSync } from 'node:child_process';
6
6
  import path, { resolve, join, dirname } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
8
  import * as fs from 'node:fs';
9
9
  import fs__default, { existsSync, readFileSync, mkdirSync, readdirSync, accessSync, constants, statSync, createReadStream, writeFileSync, unlinkSync } from 'node:fs';
10
10
  import process$1 from 'node:process';
11
- import fs$1, { readFile, access as access$1, mkdir, readdir, stat } from 'node:fs/promises';
11
+ import fs$1, { readFile, access as access$1, mkdir, readdir, stat, rename, open as open$1 } from 'node:fs/promises';
12
12
  import fs$2, { watch, access } from 'fs/promises';
13
13
  import { useStdout, useInput, Box, Text, render } from 'ink';
14
14
  import React, { useState, useRef, useEffect, useCallback } from 'react';
@@ -386,7 +386,7 @@ const PLATFORM_SYSTEM_PROMPT = trimIdent(`
386
386
 
387
387
  # Policy blocks (not user rejections)
388
388
 
389
- If a tool call is **blocked by policy** (e.g. a \`FlockbayPolicy\` card, or a denial reason like \u201CBlocked by policy \u2026\u201D), this is automatic enforcement by the platform \u2014 it is **not** the user rejecting your tool call. Follow the provided next-step instructions (read docs/ledger, claim files, etc) and then retry.
389
+ If a tool call is **blocked by Policy** (e.g. a \`FlockbayPolicy\` card, or a denial reason like \u201CBlocked by Policy \u2026\u201D), this is automatic enforcement by the platform \u2014 it is **not** the user rejecting your tool call. Follow the provided next-step instructions (read docs/ledger, claim files, etc) and then retry.
390
390
 
391
391
  # Documentation Library (server-stored docs)
392
392
 
@@ -2523,7 +2523,7 @@ class PermissionHandler {
2523
2523
  const decision = args.decision;
2524
2524
  const reason = args.reason;
2525
2525
  const kind = decision === "approved" || decision === "approved_for_session" ? "policy_allow" : decision === "abort" && reason === "permission_prompt_required" ? "policy_prompt" : "policy_block";
2526
- const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by policy.";
2526
+ const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by Policy.";
2527
2527
  const callId = `policy:${args.toolCallId}:${randomUUID().slice(0, 8)}`;
2528
2528
  const payload = {
2529
2529
  kind,
@@ -2588,13 +2588,13 @@ class PermissionHandler {
2588
2588
  if (args.reason === "docs_index_read_required") {
2589
2589
  return {
2590
2590
  uiReason: "read the game Documentation index before making edits.",
2591
- modelMessage: "Blocked by policy: read the game Documentation index before making edits.\nNext: call `mcp__flockbay__docs_index_read`, then retry the edit."
2591
+ modelMessage: "Blocked by Policy: read the game Documentation index before making edits.\nNext: call `mcp__flockbay__docs_index_read`, then retry the edit."
2592
2592
  };
2593
2593
  }
2594
2594
  if (args.reason === "ledger_read_required") {
2595
2595
  return {
2596
2596
  uiReason: "read the ledger before making file edits.",
2597
- modelMessage: "Blocked by policy: read the ledger before making file edits.\nNext: call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`), then retry the edit."
2597
+ modelMessage: "Blocked by Policy: read the ledger before making file edits.\nNext: call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`), then retry the edit."
2598
2598
  };
2599
2599
  }
2600
2600
  if (args.reason === "file_claim_required") {
@@ -2602,13 +2602,13 @@ class PermissionHandler {
2602
2602
  const next = file ? `Next: claim it via \`mcp__flockbay__ledger_claim\` (files: ["${file}"]) or \`mcp__flockbay__coordination_claim_files\`, then retry the edit.` : "Next: claim the file via `mcp__flockbay__ledger_claim` (or `mcp__flockbay__coordination_claim_files`), then retry the edit.";
2603
2603
  return {
2604
2604
  uiReason: display,
2605
- modelMessage: `Blocked by policy: ${display}
2605
+ modelMessage: `Blocked by Policy: ${display}
2606
2606
  ${next}`
2607
2607
  };
2608
2608
  }
2609
2609
  return {
2610
2610
  uiReason: "this session is in read-only mode.",
2611
- modelMessage: "Blocked by policy: this session is in read-only mode.\nNext: switch permission mode to allow edits, then retry."
2611
+ modelMessage: "Blocked by Policy: this session is in read-only mode.\nNext: switch permission mode to allow edits, then retry."
2612
2612
  };
2613
2613
  }
2614
2614
  enforceCoordinationGate(toolName, input) {
@@ -7173,7 +7173,7 @@ async function uploadScreenshotViewsForSession(args) {
7173
7173
  const buf = await readFile(v.path);
7174
7174
  const filename = path.basename(v.path);
7175
7175
  const contentType = (() => {
7176
- const mime = detectImageMimeTypeFromBuffer(buf);
7176
+ const mime = detectImageMimeTypeFromBuffer$1(buf);
7177
7177
  if (mime) return mime;
7178
7178
  throw new Error(`Unsupported screenshot format (expected PNG/JPEG): ${v.path}`);
7179
7179
  })();
@@ -7217,7 +7217,7 @@ async function uploadScreenshotViewsForSession(args) {
7217
7217
  }
7218
7218
  return out;
7219
7219
  }
7220
- function detectImageMimeTypeFromBuffer(buf) {
7220
+ function detectImageMimeTypeFromBuffer$1(buf) {
7221
7221
  if (!buf || buf.length < 12) return null;
7222
7222
  if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10) {
7223
7223
  return "image/png";
@@ -7233,6 +7233,48 @@ function detectImageMimeTypeFromBuffer(buf) {
7233
7233
  }
7234
7234
  return null;
7235
7235
  }
7236
+ async function readFileHeader(filePath, bytes) {
7237
+ const fh = await open$1(filePath, "r");
7238
+ try {
7239
+ const maxBytes = Math.max(1, Math.floor(bytes));
7240
+ const buf = Buffer.allocUnsafe(maxBytes);
7241
+ const { bytesRead } = await fh.read(buf, 0, maxBytes, 0);
7242
+ return buf.subarray(0, bytesRead);
7243
+ } finally {
7244
+ try {
7245
+ await fh.close();
7246
+ } catch {
7247
+ }
7248
+ }
7249
+ }
7250
+ async function normalizeScreenshotPathExtensionToMatchBytes(filePath) {
7251
+ const abs = String(filePath || "").trim();
7252
+ if (!abs) return { path: abs, changed: false };
7253
+ if (!existsSync(abs)) return { path: abs, changed: false };
7254
+ const lower = abs.toLowerCase();
7255
+ const header = await readFileHeader(abs, 16);
7256
+ const mime = detectImageMimeTypeFromBuffer$1(header);
7257
+ if (!mime) {
7258
+ return { path: abs, changed: false, detail: "unknown_image_format" };
7259
+ }
7260
+ if (mime === "image/jpeg" && lower.endsWith(".png")) {
7261
+ const next = abs.replace(/\.png$/i, ".jpg");
7262
+ if (existsSync(next)) {
7263
+ throw new Error(`Screenshot already exists at normalized path: ${next}`);
7264
+ }
7265
+ await rename(abs, next);
7266
+ return { path: next, changed: true, detail: "renamed_png_to_jpg" };
7267
+ }
7268
+ if (mime === "image/png" && (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
7269
+ const next = abs.replace(/\.(jpg|jpeg)$/i, ".png");
7270
+ if (existsSync(next)) {
7271
+ throw new Error(`Screenshot already exists at normalized path: ${next}`);
7272
+ }
7273
+ await rename(abs, next);
7274
+ return { path: next, changed: true, detail: "renamed_jpg_to_png" };
7275
+ }
7276
+ return { path: abs, changed: false };
7277
+ }
7236
7278
  async function startFlockbayServer(client, options) {
7237
7279
  const handler = async (title) => {
7238
7280
  logger.debug("[flockbayMCP] Changing title to:", title);
@@ -8829,7 +8871,7 @@ ${String(st.stdout || "").trim()}`
8829
8871
  throw new Error(`Image too large (${st.size} bytes) to embed: ${abs}`);
8830
8872
  }
8831
8873
  const buf = await readFile(abs);
8832
- const mime = detectImageMimeTypeFromBuffer(buf);
8874
+ const mime = detectImageMimeTypeFromBuffer$1(buf);
8833
8875
  if (!mime) {
8834
8876
  throw new Error(`Unsupported image format (expected PNG/JPEG): ${abs}`);
8835
8877
  }
@@ -8879,7 +8921,7 @@ ${String(st.stdout || "").trim()}`
8879
8921
  );
8880
8922
  mcp.registerTool("unreal_latest_screenshots", {
8881
8923
  title: "Latest Unreal Screenshots (Validation)",
8882
- description: "Fetch the latest PNG screenshots from `Saved/Screenshots/Flockbay/` (for validation) and return a `{ views: [...] }` payload so the app can display them.",
8924
+ description: "Fetch the latest screenshots (PNG/JPG) from `Saved/Screenshots/Flockbay/` (for validation) and return a `{ views: [...] }` payload so the app can display them.",
8883
8925
  inputSchema: {
8884
8926
  uprojectPath: z.string().describe("Absolute path to the .uproject file."),
8885
8927
  limit: z.number().int().positive().optional().describe("Max number of screenshots to return (default 12)."),
@@ -8916,7 +8958,7 @@ ${String(st.stdout || "").trim()}`
8916
8958
  };
8917
8959
  }
8918
8960
  const files = await readdir(outDir);
8919
- const candidates = files.filter((f) => f.toLowerCase().endsWith(".png"));
8961
+ const candidates = files.filter((f) => /\.(png|jpg|jpeg)$/i.test(f));
8920
8962
  if (candidates.length === 0) {
8921
8963
  return {
8922
8964
  content: [
@@ -9171,10 +9213,26 @@ ${String(st.stdout || "").trim()}`
9171
9213
  const pluginInfoWasCached = Boolean(unrealMcpPluginInfoCache);
9172
9214
  const pluginInfo = type !== "get_plugin_info" ? await getUnrealMcpPluginInfoBestEffort(timeoutMs) : null;
9173
9215
  const response = await sendUnrealMcpTcpCommand({ type, params, timeoutMs });
9216
+ let screenshotNormalizationNote = null;
9217
+ if (type === "take_screenshot") {
9218
+ const responseObj = response && typeof response === "object" ? response : null;
9219
+ const candidate = responseObj && typeof responseObj.filepath === "string" ? responseObj : responseObj && responseObj.result && typeof responseObj.result === "object" && typeof responseObj.result.filepath === "string" ? responseObj.result : null;
9220
+ if (candidate && typeof candidate.filepath === "string") {
9221
+ const before = String(candidate.filepath || "").trim();
9222
+ if (before) {
9223
+ const normalized = await normalizeScreenshotPathExtensionToMatchBytes(before);
9224
+ if (normalized.changed) {
9225
+ candidate.filepath = normalized.path;
9226
+ screenshotNormalizationNote = `Normalized screenshot path (${normalized.detail || "extension_fixed"}): ${before} \u2192 ${normalized.path}`;
9227
+ }
9228
+ }
9229
+ }
9230
+ }
9174
9231
  unrealEditorSupervisor.noteUnrealReachable();
9175
9232
  return {
9176
9233
  content: [
9177
9234
  { type: "text", text: `UnrealMCP command ok: ${type}` },
9235
+ ...screenshotNormalizationNote ? [{ type: "text", text: screenshotNormalizationNote }] : [],
9178
9236
  { type: "text", text: JSON.stringify(response, null, 2) },
9179
9237
  ...pluginInfo && !pluginInfoWasCached ? [{ type: "text", text: formatUnrealMcpCapabilities(pluginInfo) }] : []
9180
9238
  ],
@@ -11834,6 +11892,269 @@ async function handleConnectVendor(vendor, displayName, flags) {
11834
11892
  }
11835
11893
  }
11836
11894
 
11895
+ function readArgValue$1(args, key) {
11896
+ const idx = args.indexOf(key);
11897
+ if (idx === -1) return null;
11898
+ const value = args[idx + 1];
11899
+ if (!value || value.startsWith("-")) return null;
11900
+ return value;
11901
+ }
11902
+ function splitList(value) {
11903
+ if (!value) return [];
11904
+ return value.split(/[;,]/g).map((v) => v.trim()).filter(Boolean);
11905
+ }
11906
+ function ensureDir(dir) {
11907
+ fs__default.mkdirSync(dir, { recursive: true });
11908
+ }
11909
+ function detectImageMimeTypeFromBuffer(buf) {
11910
+ if (!buf || buf.length < 12) return null;
11911
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10) {
11912
+ return "image/png";
11913
+ }
11914
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
11915
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56) return "image/gif";
11916
+ if (buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
11917
+ return "image/webp";
11918
+ }
11919
+ return null;
11920
+ }
11921
+ async function waitForUnreal(options) {
11922
+ const startedAt = Date.now();
11923
+ let lastErr = null;
11924
+ while (Date.now() - startedAt < options.timeoutMs) {
11925
+ try {
11926
+ const res = await sendUnrealMcpTcpCommand({ type: "ping", host: options.host, port: options.port, timeoutMs: 2e3 });
11927
+ const msg = typeof res?.message === "string" ? res.message : null;
11928
+ if (msg === "pong") return { ok: true };
11929
+ } catch (err) {
11930
+ lastErr = err instanceof Error ? err.message : String(err);
11931
+ }
11932
+ await new Promise((r) => setTimeout(r, 750));
11933
+ }
11934
+ return {
11935
+ ok: false,
11936
+ error: `Timed out waiting for UnrealMCP ping after ${options.timeoutMs}ms.${lastErr ? ` Last error: ${lastErr}` : ""}`
11937
+ };
11938
+ }
11939
+ function resolveUnrealEditorExe(engineRoot) {
11940
+ const exe = process.platform === "win32" ? path.join(engineRoot, "Engine", "Binaries", "Win64", "UnrealEditor.exe") : path.join(engineRoot, "Engine", "Binaries", process.platform === "darwin" ? "Mac" : "Linux", "UnrealEditor");
11941
+ return exe;
11942
+ }
11943
+ async function runRuntimeSmoke(options) {
11944
+ const editorExe = resolveUnrealEditorExe(options.engineRoot);
11945
+ if (!fs__default.existsSync(editorExe)) {
11946
+ return { ok: false, error: `Missing UnrealEditor executable: ${editorExe}` };
11947
+ }
11948
+ if (!fs__default.existsSync(options.projectPath)) {
11949
+ return { ok: false, error: `Missing .uproject: ${options.projectPath}` };
11950
+ }
11951
+ let child = null;
11952
+ const ping = await waitForUnreal({ host: options.host, port: options.port, timeoutMs: 2e3 }).catch((e) => ({ ok: false, error: String(e) }));
11953
+ if (!ping.ok && options.launchIfNeeded) {
11954
+ const args = [
11955
+ options.projectPath,
11956
+ "-NoSplash",
11957
+ "-NoSound",
11958
+ "-nop4"
11959
+ ];
11960
+ child = spawn(editorExe, args, {
11961
+ stdio: "ignore",
11962
+ detached: false
11963
+ });
11964
+ }
11965
+ const ready = await waitForUnreal({ host: options.host, port: options.port, timeoutMs: options.connectTimeoutMs });
11966
+ if (!ready.ok) {
11967
+ try {
11968
+ child?.kill();
11969
+ } catch {
11970
+ }
11971
+ return ready;
11972
+ }
11973
+ try {
11974
+ const pluginInfo = await sendUnrealMcpTcpCommand({ type: "get_plugin_info", host: options.host, port: options.port, timeoutMs: options.timeoutMs });
11975
+ const createdBy = String(pluginInfo?.createdBy || "").trim();
11976
+ const friendlyName = String(pluginInfo?.friendlyName || "").trim();
11977
+ const baseDir = String(pluginInfo?.baseDir || "").trim();
11978
+ const schemaVersion = Number(pluginInfo?.schemaVersion);
11979
+ const commands = Array.isArray(pluginInfo?.commands) ? pluginInfo.commands.filter((c) => typeof c === "string") : [];
11980
+ if (friendlyName !== "Flockbay MCP" || createdBy !== "Respaced Inc.") {
11981
+ return {
11982
+ ok: false,
11983
+ error: `Unexpected plugin identity loaded by Unreal.
11984
+ friendlyName=${friendlyName || "(missing)"} createdBy=${createdBy || "(missing)"}
11985
+ baseDir=${baseDir || "(missing)"}
11986
+ Expected FriendlyName="Flockbay MCP" CreatedBy="Respaced Inc."`
11987
+ };
11988
+ }
11989
+ if (!Number.isFinite(schemaVersion) || schemaVersion <= 0) {
11990
+ return { ok: false, error: `Invalid schemaVersion from get_plugin_info: ${String(pluginInfo?.schemaVersion)}` };
11991
+ }
11992
+ const requireCommands = [
11993
+ "ping",
11994
+ "get_plugin_info",
11995
+ "list_capabilities",
11996
+ "get_command_schema",
11997
+ "get_play_in_editor_status",
11998
+ "play_in_editor_windowed",
11999
+ "stop_play_in_editor",
12000
+ "take_screenshot",
12001
+ "create_blueprint",
12002
+ "compile_blueprint",
12003
+ "map_check"
12004
+ ];
12005
+ const missing = requireCommands.filter((c) => !commands.includes(c));
12006
+ if (missing.length > 0) {
12007
+ return { ok: false, error: `Missing required commands in this UnrealMCP build: ${missing.join(", ")}` };
12008
+ }
12009
+ const playStatus0 = await sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", host: options.host, port: options.port, timeoutMs: options.timeoutMs });
12010
+ const isPlaying = Boolean(playStatus0?.isPlaySessionInProgress);
12011
+ if (isPlaying) {
12012
+ await sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", host: options.host, port: options.port, timeoutMs: options.timeoutMs });
12013
+ }
12014
+ await sendUnrealMcpTcpCommand({ type: "play_in_editor_windowed", host: options.host, port: options.port, timeoutMs: Math.max(options.timeoutMs, 2e4) });
12015
+ await new Promise((r) => setTimeout(r, 1e3));
12016
+ await sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", host: options.host, port: options.port, timeoutMs: Math.max(options.timeoutMs, 2e4) });
12017
+ const projectDir = path.dirname(options.projectPath);
12018
+ const shotsDir = path.join(projectDir, "Saved", "Screenshots", "Flockbay");
12019
+ ensureDir(shotsDir);
12020
+ const shotPath = path.join(shotsDir, `smoke_${Date.now()}.png`);
12021
+ await sendUnrealMcpTcpCommand({
12022
+ type: "take_screenshot",
12023
+ params: { filepath: shotPath },
12024
+ host: options.host,
12025
+ port: options.port,
12026
+ timeoutMs: options.timeoutMs
12027
+ });
12028
+ if (!fs__default.existsSync(shotPath)) return { ok: false, error: `Screenshot did not exist on disk after take_screenshot: ${shotPath}` };
12029
+ const bytes = fs__default.readFileSync(shotPath);
12030
+ const mime = detectImageMimeTypeFromBuffer(bytes);
12031
+ if (mime !== "image/png") {
12032
+ return { ok: false, error: `Screenshot bytes do not match .png extension (detected ${mime || "unknown"}): ${shotPath}` };
12033
+ }
12034
+ const bpName = `BP_Smoke_${Date.now()}`;
12035
+ await sendUnrealMcpTcpCommand({
12036
+ type: "create_blueprint",
12037
+ params: { name: bpName, path: "/Game/FlockbaySmoke/", parent_class: "Actor" },
12038
+ host: options.host,
12039
+ port: options.port,
12040
+ timeoutMs: Math.max(options.timeoutMs, 2e4)
12041
+ });
12042
+ await sendUnrealMcpTcpCommand({
12043
+ type: "compile_blueprint",
12044
+ params: { blueprint_name: bpName },
12045
+ host: options.host,
12046
+ port: options.port,
12047
+ timeoutMs: Math.max(options.timeoutMs, 6e4)
12048
+ });
12049
+ await sendUnrealMcpTcpCommand({
12050
+ type: "map_check",
12051
+ host: options.host,
12052
+ port: options.port,
12053
+ timeoutMs: Math.max(options.timeoutMs, 3e4)
12054
+ });
12055
+ return { ok: true };
12056
+ } finally {
12057
+ if (options.killAfter && child) {
12058
+ try {
12059
+ if (process.platform === "win32") {
12060
+ spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
12061
+ } else {
12062
+ child.kill();
12063
+ }
12064
+ } catch {
12065
+ }
12066
+ }
12067
+ }
12068
+ }
12069
+ async function runUnrealMcpMatrixSmoke(args) {
12070
+ const engineRoots = splitList(readArgValue$1(args, "--engine-roots")) || [];
12071
+ const engineRootSingle = readArgValue$1(args, "--engine-root");
12072
+ if (engineRootSingle) engineRoots.push(engineRootSingle.trim());
12073
+ const project = readArgValue$1(args, "--project");
12074
+ const host = (readArgValue$1(args, "--host") || "127.0.0.1").trim() || "127.0.0.1";
12075
+ const port = Number(readArgValue$1(args, "--port") || "55557");
12076
+ const connectTimeoutMs = Number(readArgValue$1(args, "--connect-timeout-ms") || "180000");
12077
+ const timeoutMs = Number(readArgValue$1(args, "--timeout-ms") || "30000");
12078
+ const doBuild = !args.includes("--runtime-only");
12079
+ const doRuntime = !args.includes("--build-only");
12080
+ const launch = args.includes("--launch-editor");
12081
+ const killAfter = args.includes("--kill-editor");
12082
+ if (engineRoots.length === 0) {
12083
+ console.error(chalk.red("Missing --engine-root or --engine-roots."));
12084
+ console.error(chalk.gray('Example: flockbay doctor unreal-mcp-smoke --engine-roots "C:\\\\Epic\\\\UE_5.5;C:\\\\Epic\\\\UE_5.6" --project "C:\\\\Projects\\\\MyProj\\\\MyProj.uproject" --launch-editor --kill-editor'));
12085
+ process.exit(1);
12086
+ }
12087
+ if (doRuntime && !project) {
12088
+ console.error(chalk.red("Missing --project (required for runtime smoke)."));
12089
+ process.exit(1);
12090
+ }
12091
+ console.log(chalk.bold("\nUnrealMCP Matrix Smoke\n"));
12092
+ console.log(chalk.gray(`Platform: ${process.platform}`));
12093
+ console.log(chalk.gray(`Host: ${host}:${port}`));
12094
+ console.log(chalk.gray(`Engines: ${engineRoots.join(", ")}`));
12095
+ if (project) console.log(chalk.gray(`Project: ${project}`));
12096
+ console.log(chalk.gray(`Build: ${doBuild ? "yes" : "no"} Runtime: ${doRuntime ? "yes" : "no"} Launch: ${launch ? "yes" : "no"} Kill: ${killAfter ? "yes" : "no"}`));
12097
+ console.log("");
12098
+ const failures = [];
12099
+ for (const engineRootRaw of engineRoots) {
12100
+ const engineRoot = engineRootRaw.trim();
12101
+ if (!engineRoot) continue;
12102
+ console.log(chalk.bold(`== Engine: ${engineRoot} ==`));
12103
+ if (doBuild) {
12104
+ console.log(chalk.cyan("Build: installing UnrealMCP plugin sources..."));
12105
+ const installed = installUnrealMcpPluginToEngine(engineRoot);
12106
+ if (!installed.ok) {
12107
+ failures.push({ engineRoot, phase: "build", error: installed.errorMessage });
12108
+ console.log(chalk.red(`Build: failed (install)
12109
+ ${installed.errorMessage}
12110
+ `));
12111
+ if (!doRuntime) continue;
12112
+ } else {
12113
+ console.log(chalk.green(`Build: sources installed to ${installed.destDir}`));
12114
+ console.log(chalk.cyan("Build: compiling plugin via RunUAT BuildPlugin..."));
12115
+ const built = await buildAndInstallUnrealMcpPlugin({ engineRoot, flockbayHomeDir: configuration.flockbayHomeDir });
12116
+ if (!built.ok) {
12117
+ failures.push({ engineRoot, phase: "build", error: built.errorMessage });
12118
+ console.log(chalk.red(`Build: failed
12119
+ ${built.errorMessage}
12120
+ `));
12121
+ } else {
12122
+ console.log(chalk.green(`Build: ok (log: ${built.buildLogPath})`));
12123
+ }
12124
+ }
12125
+ }
12126
+ if (doRuntime) {
12127
+ console.log(chalk.cyan("Runtime: running command smoke..."));
12128
+ const res = await runRuntimeSmoke({
12129
+ engineRoot,
12130
+ projectPath: project,
12131
+ host,
12132
+ port,
12133
+ connectTimeoutMs,
12134
+ timeoutMs,
12135
+ launchIfNeeded: launch,
12136
+ killAfter
12137
+ });
12138
+ if (!res.ok) {
12139
+ failures.push({ engineRoot, phase: "runtime", error: res.error });
12140
+ console.log(chalk.red(`Runtime: failed
12141
+ ${res.error}
12142
+ `));
12143
+ } else {
12144
+ console.log(chalk.green("Runtime: ok\n"));
12145
+ }
12146
+ }
12147
+ }
12148
+ if (failures.length > 0) {
12149
+ console.error(chalk.red("\nMatrix smoke failed.\n"));
12150
+ for (const f of failures) {
12151
+ console.error(chalk.red(`- ${f.engineRoot} (${f.phase}): ${f.error}`));
12152
+ }
12153
+ process.exit(1);
12154
+ }
12155
+ console.log(chalk.green("\nMatrix smoke passed.\n"));
12156
+ }
12157
+
11837
12158
  function readTailUtf8(filePath, maxBytes) {
11838
12159
  try {
11839
12160
  const stat = fs.statSync(filePath);
@@ -12194,6 +12515,16 @@ async function authAndSetupMachineIfNeeded() {
12194
12515
  }
12195
12516
  if (!args.includes("--version")) ;
12196
12517
  if (subcommand === "doctor") {
12518
+ if (args[1] === "unreal-mcp-smoke") {
12519
+ try {
12520
+ await runUnrealMcpMatrixSmoke(args.slice(2));
12521
+ } catch (error) {
12522
+ console.error(chalk.red("UnrealMCP smoke failed:"), error instanceof Error ? error.message : String(error));
12523
+ if (process.env.DEBUG) console.error(error);
12524
+ process.exit(1);
12525
+ }
12526
+ return;
12527
+ }
12197
12528
  if (args[1] === "clean") {
12198
12529
  const result = await killRunawayFlockbayProcesses();
12199
12530
  console.log(`Cleaned up ${result.killed} runaway processes`);
@@ -12305,7 +12636,7 @@ ${engineRoot}`, {
12305
12636
  } else if (subcommand === "codex") {
12306
12637
  try {
12307
12638
  await chdirToNearestUprojectRootIfPresent();
12308
- const { runCodex } = await import('./runCodex-d2KQX2mn.mjs');
12639
+ const { runCodex } = await import('./runCodex-BC3IImzm.mjs');
12309
12640
  let startedBy = void 0;
12310
12641
  let sessionId = void 0;
12311
12642
  for (let i = 1; i < args.length; i++) {
@@ -12331,7 +12662,13 @@ ${engineRoot}`, {
12331
12662
  const geminiSubcommand = args[1];
12332
12663
  if (geminiSubcommand === "model" && args[2] === "set" && args[3]) {
12333
12664
  const modelName = args[3];
12334
- const validModels = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"];
12665
+ const validModels = [
12666
+ "gemini-2.5-pro",
12667
+ "gemini-2.5-flash",
12668
+ "gemini-2.5-flash-lite",
12669
+ "gemini-3-pro-preview",
12670
+ "gemini-3-flash-preview"
12671
+ ];
12335
12672
  if (!validModels.includes(modelName)) {
12336
12673
  console.error(`Invalid model: ${modelName}`);
12337
12674
  console.error(`Available models: ${validModels.join(", ")}`);
@@ -12400,7 +12737,7 @@ ${engineRoot}`, {
12400
12737
  }
12401
12738
  try {
12402
12739
  await chdirToNearestUprojectRootIfPresent();
12403
- const { runGemini } = await import('./runGemini-Cn0C7MS1.mjs');
12740
+ const { runGemini } = await import('./runGemini-C43IKGUU.mjs');
12404
12741
  let startedBy = void 0;
12405
12742
  let sessionId = void 0;
12406
12743
  for (let i = 1; i < args.length; i++) {
@@ -12731,4 +13068,4 @@ ${chalk.bold("Examples:")}
12731
13068
  }
12732
13069
  }
12733
13070
 
12734
- export { ElicitationHub as E, MessageQueue2 as M, PLATFORM_SYSTEM_PROMPT as P, setLatestUserImages as a, MessageBuffer as b, consumeToolQuota as c, startFlockbayServer as d, detectUnrealProject as e, formatQuotaDeniedReason as f, buildProjectCapsule as g, hashObject as h, initialMachineMetadata as i, autoFinalizeCoordinationWorkItem as j, detectScreenshotsForGate as k, applyCoordinationSideEffectsFromMcpToolResult as l, stopCaffeinate as m, notifyDaemonSessionStarted as n, extractUserImagesMarker as o, getLatestUserImages as p, registerKillSessionHandler as r, shouldCountToolCall as s, trimIdent as t, withUserImagesMarker as w };
13071
+ export { ElicitationHub as E, MessageQueue2 as M, PLATFORM_SYSTEM_PROMPT as P, setLatestUserImages as a, MessageBuffer as b, consumeToolQuota as c, startFlockbayServer as d, buildProjectCapsule as e, formatQuotaDeniedReason as f, autoFinalizeCoordinationWorkItem as g, hashObject as h, initialMachineMetadata as i, detectScreenshotsForGate as j, applyCoordinationSideEffectsFromMcpToolResult as k, stopCaffeinate as l, extractUserImagesMarker as m, notifyDaemonSessionStarted as n, getLatestUserImages as o, registerKillSessionHandler as r, shouldCountToolCall as s, trimIdent as t, withUserImagesMarker as w };
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  require('chalk');
4
- require('./index-Bhkn02hu.cjs');
5
- require('./types-DeH24uWs.cjs');
4
+ require('./index-BtB1Sqpy.cjs');
5
+ require('./types-DvlwEGpS.cjs');
6
6
  require('zod');
7
7
  require('node:child_process');
8
8
  require('node:fs');
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import 'chalk';
2
- import './index-By332wvJ.mjs';
3
- import './types-mXJc7o0P.mjs';
2
+ import './index-CRGcIpET.mjs';
3
+ import './types-CNn15BaT.mjs';
4
4
  import 'zod';
5
5
  import 'node:child_process';
6
6
  import 'node:fs';
package/dist/lib.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var types = require('./types-DeH24uWs.cjs');
3
+ var types = require('./types-DvlwEGpS.cjs');
4
4
  require('axios');
5
5
  require('node:fs');
6
6
  require('node:os');
package/dist/lib.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, l as logger } from './types-mXJc7o0P.mjs';
1
+ export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, l as logger } from './types-CNn15BaT.mjs';
2
2
  import 'axios';
3
3
  import 'node:fs';
4
4
  import 'node:os';
@@ -1,6 +1,6 @@
1
1
  import { useStdout, useInput, Box, Text, render } from 'ink';
2
2
  import React, { useState, useRef, useEffect, useCallback } from 'react';
3
- import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-mXJc7o0P.mjs';
3
+ import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-CNn15BaT.mjs';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
6
  import { z } from 'zod';
@@ -10,7 +10,7 @@ import fs__default from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import path, { resolve, join } from 'node:path';
12
12
  import { spawnSync } from 'node:child_process';
13
- import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, E as ElicitationHub, n as notifyDaemonSessionStarted, M as MessageQueue2, P as PLATFORM_SYSTEM_PROMPT, a as setLatestUserImages, w as withUserImagesMarker, r as registerKillSessionHandler, b as MessageBuffer, d as startFlockbayServer, e as detectUnrealProject, g as buildProjectCapsule, t as trimIdent, j as autoFinalizeCoordinationWorkItem, k as detectScreenshotsForGate, l as applyCoordinationSideEffectsFromMcpToolResult, m as stopCaffeinate } from './index-By332wvJ.mjs';
13
+ import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, E as ElicitationHub, n as notifyDaemonSessionStarted, M as MessageQueue2, P as PLATFORM_SYSTEM_PROMPT, a as setLatestUserImages, w as withUserImagesMarker, r as registerKillSessionHandler, b as MessageBuffer, d as startFlockbayServer, e as buildProjectCapsule, t as trimIdent, g as autoFinalizeCoordinationWorkItem, j as detectScreenshotsForGate, k as applyCoordinationSideEffectsFromMcpToolResult, l as stopCaffeinate } from './index-CRGcIpET.mjs';
14
14
  import 'axios';
15
15
  import 'node:events';
16
16
  import 'socket.io-client';
@@ -250,18 +250,18 @@ function buildMcpElicitationResult(decision, requestedSchemaRaw, options) {
250
250
  if (!raw) return "";
251
251
  if (/^Blocked\b/.test(raw)) return raw;
252
252
  if (raw === "ledger_read_required") {
253
- return "Blocked by policy (automatic; not the user): read the ledger before making file edits. Next: call mcp__flockbay__ledger_read, then retry the edit.";
253
+ return "Blocked by Policy (automatic; not the user): read the ledger before making file edits. Next: call mcp__flockbay__ledger_read, then retry the edit.";
254
254
  }
255
255
  if (raw === "docs_index_read_required") {
256
- return "Blocked by policy (automatic; not the user): read the game Documentation index before making edits. Next: call mcp__flockbay__docs_index_read, then retry the edit.";
256
+ return "Blocked by Policy (automatic; not the user): read the game Documentation index before making edits. Next: call mcp__flockbay__docs_index_read, then retry the edit.";
257
257
  }
258
258
  if (raw.startsWith("file_claim_required:")) {
259
259
  const withoutPrefix = raw.slice("file_claim_required:".length);
260
260
  const file = withoutPrefix.split("(")[0]?.trim() || "the file";
261
- return `Blocked by policy (automatic; not the user): claim ${file} before editing it. Next: claim the file via mcp__flockbay__ledger_claim (or mcp__flockbay__coordination_claim_files), then retry the edit.`;
261
+ return `Blocked by Policy (automatic; not the user): claim ${file} before editing it. Next: claim the file via mcp__flockbay__ledger_claim (or mcp__flockbay__coordination_claim_files), then retry the edit.`;
262
262
  }
263
263
  if (raw === "read_only_mode") {
264
- return "Blocked by policy (automatic; not the user): this session is in read-only mode. Next: switch permission mode to allow edits, then retry.";
264
+ return "Blocked by Policy (automatic; not the user): this session is in read-only mode. Next: switch permission mode to allow edits, then retry.";
265
265
  }
266
266
  return `Blocked: ${raw}`;
267
267
  };
@@ -1740,7 +1740,7 @@ function buildPolicyHint(reason, decision, gate) {
1740
1740
  };
1741
1741
  }
1742
1742
  return {
1743
- summary: "Blocked by policy.",
1743
+ summary: "Blocked by Policy.",
1744
1744
  nextSteps: []
1745
1745
  };
1746
1746
  }
@@ -1752,7 +1752,7 @@ function buildPolicyHint(reason, decision, gate) {
1752
1752
  }
1753
1753
  if (raw === "ledger_read_required") {
1754
1754
  return {
1755
- summary: "Blocked by policy (automatic; not the user): read the ledger before making file edits.",
1755
+ summary: "Blocked by Policy (automatic; not the user): read the ledger before making file edits.",
1756
1756
  nextSteps: [
1757
1757
  "Call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`).",
1758
1758
  "Then retry the file edit."
@@ -1761,7 +1761,7 @@ function buildPolicyHint(reason, decision, gate) {
1761
1761
  }
1762
1762
  if (raw === "docs_index_read_required") {
1763
1763
  return {
1764
- summary: "Blocked by policy (automatic; not the user): read the game Documentation index before making edits.",
1764
+ summary: "Blocked by Policy (automatic; not the user): read the game Documentation index before making edits.",
1765
1765
  nextSteps: [
1766
1766
  "Call `mcp__flockbay__docs_index_read`.",
1767
1767
  "Then retry the edit."
@@ -1772,7 +1772,7 @@ function buildPolicyHint(reason, decision, gate) {
1772
1772
  const withoutPrefix = raw.slice("file_claim_required:".length);
1773
1773
  const file = withoutPrefix.split("(")[0]?.trim() || "the file";
1774
1774
  return {
1775
- summary: `Blocked by policy (automatic; not the user): claim \`${file}\` before editing it.`,
1775
+ summary: `Blocked by Policy (automatic; not the user): claim \`${file}\` before editing it.`,
1776
1776
  nextSteps: [
1777
1777
  `Claim the file via \`mcp__flockbay__ledger_claim\` or \`mcp__flockbay__coordination_claim_files\` (files: ["${file}"]).`,
1778
1778
  "Then retry the file edit."
@@ -1781,7 +1781,7 @@ function buildPolicyHint(reason, decision, gate) {
1781
1781
  }
1782
1782
  if (raw === "read_only_mode") {
1783
1783
  return {
1784
- summary: "Blocked by policy (automatic; not the user): this session is in read-only mode.",
1784
+ summary: "Blocked by Policy (automatic; not the user): this session is in read-only mode.",
1785
1785
  nextSteps: [
1786
1786
  "Switch permission mode to allow edits (disable read-only).",
1787
1787
  "Then retry the action."
@@ -2381,7 +2381,6 @@ async function runCodex(opts) {
2381
2381
  appendSystemPrompt: PLATFORM_SYSTEM_PROMPT
2382
2382
  });
2383
2383
  }
2384
- const bypassUeGates = String(process.env.FLOCKBAY_DEV_BYPASS_UE_GATES || "") === "1";
2385
2384
  let currentPermissionMode = void 0;
2386
2385
  let currentModel = void 0;
2387
2386
  const defaultAppendSystemPrompt = PLATFORM_SYSTEM_PROMPT;
@@ -3449,22 +3448,6 @@ Error: ${message}`,
3449
3448
  try {
3450
3449
  resetScreenshotGateForTurn();
3451
3450
  const overrides = { approvalPolicy: "untrusted", sandbox: "workspace-write" };
3452
- if (!bypassUeGates) {
3453
- const detection = await detectUnrealProject(process.cwd());
3454
- if (!detection.ok) {
3455
- session.sendCodexMessage({
3456
- type: "message",
3457
- message: "Select an Unreal project folder (the folder containing a *.uproject) to continue.",
3458
- id: randomUUID()
3459
- });
3460
- if (wasCreated) {
3461
- client.clearSession();
3462
- wasCreated = false;
3463
- currentModeHash = null;
3464
- }
3465
- continue;
3466
- }
3467
- }
3468
3451
  if (!wasCreated) {
3469
3452
  const projectCapsule = await buildProjectCapsule({ startDir: process.cwd() });
3470
3453
  const modelOverride = resolveCodexModelOverride(message.mode.model);