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.
- package/dist/{index-By332wvJ.mjs → index-BjZUYSzh.mjs} +353 -16
- package/dist/{index-Bhkn02hu.cjs → index-DQTqwzYd.cjs} +353 -16
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.mjs +1 -1
- package/dist/{runCodex-d2KQX2mn.mjs → runCodex-B7fGICdv.mjs} +11 -11
- package/dist/{runCodex-CH4lz1QX.cjs → runCodex-CaWagdzG.cjs} +11 -11
- package/dist/{runGemini-DNSymY04.cjs → runGemini-8w5P093W.cjs} +135 -20
- package/dist/{runGemini-Cn0C7MS1.mjs → runGemini-CK43WQk8.mjs} +135 -20
- package/dist/{types-mXJc7o0P.mjs → types-CMWcip0F.mjs} +41 -3
- package/dist/{types-DeH24uWs.cjs → types-Z2OYpI8c.cjs} +41 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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-
|
|
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 = [
|
|
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-
|
|
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++) {
|