flockbay 0.10.21 → 0.10.22

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-CMWcip0F.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-B7fGICdv.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-CK43WQk8.mjs');
12404
12741
  let startedBy = void 0;
12405
12742
  let sessionId = void 0;
12406
12743
  for (let i = 1; i < args.length; i++) {