flockbay 0.10.15 → 0.10.17
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/codex/flockbayMcpStdioBridge.cjs +339 -0
- package/dist/codex/flockbayMcpStdioBridge.mjs +339 -0
- package/dist/{index--o4BPz5o.cjs → index-BxBuBx7C.cjs} +2706 -609
- package/dist/{index-CUp3juDS.mjs → index-CHm9r89K.mjs} +2707 -611
- package/dist/index.cjs +3 -5
- package/dist/index.mjs +3 -5
- package/dist/lib.cjs +7 -9
- package/dist/lib.d.cts +219 -531
- package/dist/lib.d.mts +219 -531
- package/dist/lib.mjs +7 -9
- package/dist/{runCodex-D3eT-TvB.cjs → runCodex-DuCGwO2K.cjs} +264 -43
- package/dist/{runCodex-o6PCbHQ7.mjs → runCodex-DudVDqNh.mjs} +263 -42
- package/dist/{runGemini-CBxZp6I7.cjs → runGemini-B25LZ4Cw.cjs} +64 -29
- package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-Ddu8UCOS.mjs} +63 -28
- package/dist/{types-C-jnUdn_.cjs → types-CGQhv7Z-.cjs} +470 -1146
- package/dist/{types-DGd6ea2Z.mjs → types-DuhcLxar.mjs} +469 -1142
- package/package.json +1 -1
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +195 -6
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +376 -5
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommandSchema.cpp +731 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +476 -8
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +1518 -94
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +7 -4
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +150 -112
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +2 -1
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +4 -1
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommandSchema.h +42 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +21 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +4 -1
- package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +0 -136
- package/dist/flockbayScreenshotGate-DkxU24cR.cjs +0 -138
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
4
|
var os = require('node:os');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
|
-
var types = require('./types-
|
|
6
|
+
var types = require('./types-CGQhv7Z-.cjs');
|
|
7
7
|
var node_child_process = require('node:child_process');
|
|
8
8
|
var path = require('node:path');
|
|
9
9
|
var node_readline = require('node:readline');
|
|
@@ -15,20 +15,17 @@ var ink = require('ink');
|
|
|
15
15
|
var React = require('react');
|
|
16
16
|
var node_url = require('node:url');
|
|
17
17
|
var axios = require('axios');
|
|
18
|
-
require('node:events');
|
|
18
|
+
var node_events = require('node:events');
|
|
19
19
|
require('socket.io-client');
|
|
20
|
-
var tweetnacl = require('tweetnacl');
|
|
21
20
|
var child_process = require('child_process');
|
|
22
21
|
var crypto = require('crypto');
|
|
23
22
|
var path$1 = require('path');
|
|
24
23
|
var os$1 = require('os');
|
|
25
24
|
require('node:net');
|
|
26
|
-
require('expo-server-sdk');
|
|
27
25
|
var fs$3 = require('fs');
|
|
28
26
|
var psList = require('ps-list');
|
|
29
27
|
var spawn = require('cross-spawn');
|
|
30
28
|
var tmp = require('tmp');
|
|
31
|
-
var qrcode = require('qrcode-terminal');
|
|
32
29
|
var open = require('open');
|
|
33
30
|
var fastify = require('fastify');
|
|
34
31
|
var z = require('zod');
|
|
@@ -36,6 +33,7 @@ var fastifyTypeProviderZod = require('fastify-type-provider-zod');
|
|
|
36
33
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
37
34
|
var node_http = require('node:http');
|
|
38
35
|
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
36
|
+
require('tweetnacl');
|
|
39
37
|
var http = require('http');
|
|
40
38
|
var util = require('util');
|
|
41
39
|
|
|
@@ -298,27 +296,68 @@ const PLATFORM_SYSTEM_PROMPT = trimIdent(`
|
|
|
298
296
|
|
|
299
297
|
UnrealMCP capability hygiene (avoid guessing):
|
|
300
298
|
- UnrealMCP command sets can differ by plugin build/version.
|
|
301
|
-
- When starting UnrealMCP work in a session (or after any "Unknown command" error),
|
|
299
|
+
- When starting UnrealMCP work in a session (or after any "Unknown command" error), prefer:
|
|
300
|
+
- \`mcp__flockbay__unreal_mcp_list_capabilities\` (fast: build info + supported commands + capability flags)
|
|
301
|
+
- or \`mcp__flockbay__unreal_mcp_command\` with \`type: "get_plugin_info"\` (supported commands list)
|
|
302
|
+
- When you're unsure about required params or accepted aliases for a command, use:
|
|
303
|
+
- \`mcp__flockbay__unreal_mcp_get_command_schema\` (returns required keys + examples for the running plugin build)
|
|
302
304
|
- This is guidance (not a hard block): you may try a command, but if it\u2019s not supported, immediately switch to the nearest supported command instead of guessing more commands.
|
|
303
305
|
|
|
306
|
+
UnrealMCP asset placement (avoid guessing \`/Game/...\` paths):
|
|
307
|
+
- If the user asks to place assets from the project\u2019s \`/Content\` folder, do NOT guess object paths.
|
|
308
|
+
- Use \`mcp__flockbay__unreal_mcp_search_assets\` to find the asset\u2019s \`objectPath\`, and optionally \`mcp__flockbay__unreal_mcp_get_asset_info\` to confirm its class.
|
|
309
|
+
- Place it via \`mcp__flockbay__unreal_mcp_place_asset\` (supports StaticMesh + Blueprint assets).
|
|
310
|
+
- If the user says \u201Cuse an asset pack\u201D, you may first call \`mcp__flockbay__unreal_mcp_list_asset_packs\` to discover top-level \`/Game\` folders, then search within the pack root.
|
|
311
|
+
|
|
312
|
+
Unreal spatial intent (stop guessing \u201Chere/there\u201D):
|
|
313
|
+
- For \u201Cput X here/there / in front of me / on that surface\u201D, prefer these primitives:
|
|
314
|
+
- \`mcp__flockbay__unreal_mcp_get_editor_context\` (map + selection + editor camera)
|
|
315
|
+
- \`mcp__flockbay__unreal_mcp_get_player_context\` (PIE player pawn + camera)
|
|
316
|
+
- \`mcp__flockbay__unreal_mcp_raycast_from_camera\` (turn \u201Cthat surface\u201D into hit location + normal)
|
|
317
|
+
- \`mcp__flockbay__unreal_mcp_raycast_down\` (drop-to-ground)
|
|
318
|
+
- \`mcp__flockbay__unreal_mcp_get_actor_transform\` / \`mcp__flockbay__unreal_mcp_get_actor_bounds\`
|
|
319
|
+
- If the user wants a one-call helper, use:
|
|
320
|
+
- \`mcp__flockbay__unreal_place_asset_relative\` (place asset relative to selection/camera/player/hit)
|
|
321
|
+
- \`mcp__flockbay__unreal_spawn_actor_relative\` (spawn actor relative to selection/camera/player/hit)
|
|
322
|
+
|
|
304
323
|
UnrealMCP PIE safety (avoid editor crashes):
|
|
305
324
|
- Before running editor-world mutation commands (spawn/delete actors, set properties/transforms, spawn Blueprint actors), check \`get_play_in_editor_status\`.
|
|
306
325
|
- If PIE is running/queued, stop it (\`stop_play_in_editor\`) before retrying the mutation.
|
|
307
326
|
|
|
327
|
+
Unreal verify + diagnose (recommended validation after changes):
|
|
328
|
+
- After making changes (especially content/Blueprint changes), prefer this sequence:
|
|
329
|
+
1) \`mcp__flockbay__unreal_mcp_compile_blueprints_all\`
|
|
330
|
+
2) \`mcp__flockbay__unreal_mcp_map_check\`
|
|
331
|
+
3) If C++ exists or was edited: \`mcp__flockbay__unreal_build_project\`
|
|
332
|
+
4) \`mcp__flockbay__unreal_smoke_test\` (PIE windowed + screenshot validation)
|
|
333
|
+
- If any step fails: stop, report the structured failures, and propose the smallest fix.
|
|
334
|
+
- After the change is validated and before concluding, save editor changes:
|
|
335
|
+
- Prefer \`mcp__flockbay__unreal_mcp_save_all\` to save all dirty packages.
|
|
336
|
+
- Or use \`mcp__flockbay__unreal_mcp_command\` with \`type: "save_all"\` to save all dirty packages.
|
|
337
|
+
- If \`save_all\` reports still-dirty packages, do not end the request; resolve the save issue and retry.
|
|
338
|
+
|
|
308
339
|
UnrealMCP editor health (detect crashes/offline):
|
|
309
340
|
- If UnrealMCP requests fail to connect or time out, run the health check tool (if available) to determine whether the editor is reachable.
|
|
310
341
|
- Treat "not reachable" as: Unreal Editor is closed/crashed/hung OR UnrealMCP plugin is disabled. The minimal fix is to reopen/restart the editor and enable the plugin.
|
|
311
342
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
-
|
|
315
|
-
-
|
|
343
|
+
Unreal Editor relaunch (manual, never auto-restart):
|
|
344
|
+
- The platform may detect that Unreal Editor crashed/unreachable and will automatically abort your current run and post a chat message.
|
|
345
|
+
- When that happens: STOP UnrealMCP calls, fix the project as needed, then (only when ready) relaunch Unreal Editor with \`mcp__flockbay__unreal_editor_launch\`.
|
|
346
|
+
- Do not \u201Cauto-restart\u201D in a loop. Relaunch is a deliberate action after fixes.
|
|
347
|
+
|
|
348
|
+
## B) Screenshots (via UnrealMCP)
|
|
349
|
+
Use UnrealMCP when the user asks for:
|
|
350
|
+
- a screenshot of the editor viewport
|
|
351
|
+
- visual validation of an in-editor change
|
|
316
352
|
|
|
317
353
|
How:
|
|
318
354
|
- Use \`mcp__flockbay__unreal_mcp_command\` with \`type: "take_screenshot"\`.
|
|
319
355
|
- Default save path is \`<Project>/Saved/Screenshots/Flockbay/\` (auto-generated filename). Optional params: \`filepath\` (absolute or relative filename) and/or \`filename\` (base name).
|
|
320
356
|
|
|
321
357
|
Do not use screenshots as a default fallback for editor queries (e.g. \u201Clist actors\u201D).
|
|
358
|
+
Do not treat \u201Ctook a screenshot\u201D as \u201Cdone\u201D:
|
|
359
|
+
- Screenshots are primarily for YOU (the agent) to validate the change.
|
|
360
|
+
- If the screenshot does not clearly confirm the requirement, continue iterating (or run a different verify tool).
|
|
322
361
|
|
|
323
362
|
# Non\u2011negotiables
|
|
324
363
|
|
|
@@ -337,9 +376,10 @@ const PLATFORM_SYSTEM_PROMPT = trimIdent(`
|
|
|
337
376
|
# Evidence strategy (how to validate)
|
|
338
377
|
|
|
339
378
|
- Choose the lightest evidence that answers the user\u2019s request.
|
|
340
|
-
- Prefer \u201Cone
|
|
379
|
+
- Prefer \u201Cone validation\u201D over \u201Cmany validations\u201D.
|
|
341
380
|
- If you need visual confirmation, ask for or produce a screenshot explicitly (don\u2019t sneak it in).
|
|
342
|
-
- Always view the resulting image
|
|
381
|
+
- Always view the resulting image after creating one, and judge the result for yourself (not just for the user).
|
|
382
|
+
- If the image is ambiguous or doesn\u2019t show the requirement, say what\u2019s missing and keep going (don\u2019t end the request prematurely).
|
|
343
383
|
|
|
344
384
|
# Session hygiene
|
|
345
385
|
|
|
@@ -858,7 +898,7 @@ async function claudeLocalLauncher(session) {
|
|
|
858
898
|
}
|
|
859
899
|
await abort();
|
|
860
900
|
}
|
|
861
|
-
session.client.rpcHandlerManager.registerHandler("
|
|
901
|
+
session.client.rpcHandlerManager.registerHandler("cancel-generation", doAbort);
|
|
862
902
|
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
|
|
863
903
|
session.queue.setOnMessage((message, mode) => {
|
|
864
904
|
doSwitch();
|
|
@@ -906,8 +946,6 @@ async function claudeLocalLauncher(session) {
|
|
|
906
946
|
}
|
|
907
947
|
} finally {
|
|
908
948
|
exutFuture.resolve(void 0);
|
|
909
|
-
session.client.rpcHandlerManager.registerHandler("abort", async () => {
|
|
910
|
-
});
|
|
911
949
|
session.client.rpcHandlerManager.registerHandler("switch", async () => {
|
|
912
950
|
});
|
|
913
951
|
session.queue.setOnMessage(null);
|
|
@@ -1234,7 +1272,7 @@ function buildDaemonSafeEnv(baseEnv, binPath) {
|
|
|
1234
1272
|
env[pathKey] = [...prepend, ...existingParts].join(pathSep);
|
|
1235
1273
|
return env;
|
|
1236
1274
|
}
|
|
1237
|
-
const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index
|
|
1275
|
+
const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-BxBuBx7C.cjs', document.baseURI).href)));
|
|
1238
1276
|
const __dirname$1 = path.join(__filename$1, "..");
|
|
1239
1277
|
function getGlobalClaudeVersion(claudeExecutable) {
|
|
1240
1278
|
try {
|
|
@@ -1913,6 +1951,139 @@ function getLatestUserImages() {
|
|
|
1913
1951
|
return latestUserImages;
|
|
1914
1952
|
}
|
|
1915
1953
|
|
|
1954
|
+
function uniqPush(into, seen, value) {
|
|
1955
|
+
const v = value.trim();
|
|
1956
|
+
if (!v) return;
|
|
1957
|
+
if (seen.has(v)) return;
|
|
1958
|
+
seen.add(v);
|
|
1959
|
+
into.push(v);
|
|
1960
|
+
}
|
|
1961
|
+
function normalizeFilePathToken(token) {
|
|
1962
|
+
return token.trim().replace(/^['"`]+/, "").replace(/['"`]+$/, "").replace(/[),.;:'"`]+$/, "").trim();
|
|
1963
|
+
}
|
|
1964
|
+
function resolveCandidatePath(candidate, cwd) {
|
|
1965
|
+
const raw = normalizeFilePathToken(candidate);
|
|
1966
|
+
if (!raw) return raw;
|
|
1967
|
+
if (raw.startsWith("~/")) {
|
|
1968
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1969
|
+
if (home) return path.join(home, raw.slice(2));
|
|
1970
|
+
}
|
|
1971
|
+
return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
1972
|
+
}
|
|
1973
|
+
function isImageBlock(block) {
|
|
1974
|
+
if (!block || typeof block !== "object") return false;
|
|
1975
|
+
if (block.type !== "image") return false;
|
|
1976
|
+
if (typeof block.data === "string" && block.data.length > 0) return true;
|
|
1977
|
+
if (block.source && typeof block.source === "object" && typeof block.source.data === "string" && block.source.data.length > 0) return true;
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
function tryParseJsonObjectWithViews(text) {
|
|
1981
|
+
const trimmed = text.trim();
|
|
1982
|
+
if (!trimmed) return null;
|
|
1983
|
+
try {
|
|
1984
|
+
const direct = JSON.parse(trimmed);
|
|
1985
|
+
if (direct && typeof direct === "object" && Array.isArray(direct.views)) return direct;
|
|
1986
|
+
} catch {
|
|
1987
|
+
}
|
|
1988
|
+
for (let i = trimmed.length - 1; i >= 0; i -= 1) {
|
|
1989
|
+
if (trimmed[i] !== "{") continue;
|
|
1990
|
+
const candidate = trimmed.slice(i);
|
|
1991
|
+
try {
|
|
1992
|
+
const parsed = JSON.parse(candidate);
|
|
1993
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.views)) return parsed;
|
|
1994
|
+
} catch {
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return null;
|
|
1998
|
+
}
|
|
1999
|
+
function extractScreenshotViewsFromText(text, cwd) {
|
|
2000
|
+
const out = [];
|
|
2001
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2002
|
+
const re = /(?:^|[\s"'(])(?:-\s*)?([^\s"'()]*Saved[\\/]+Screenshots[\\/]+Flockbay[\\/]+[^\s"'()]+\.(?:png|jpg|jpeg))/gi;
|
|
2003
|
+
for (const match of text.matchAll(re)) {
|
|
2004
|
+
const token = String(match[1] || "");
|
|
2005
|
+
const resolved = resolveCandidatePath(token, cwd);
|
|
2006
|
+
if (!resolved) continue;
|
|
2007
|
+
if (!resolved.includes(`${path.sep}Saved${path.sep}Screenshots${path.sep}Flockbay${path.sep}`) && !resolved.includes("Saved/Screenshots/Flockbay/") && !resolved.includes("Saved\\Screenshots\\Flockbay\\")) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
uniqPush(out, seen, resolved);
|
|
2011
|
+
}
|
|
2012
|
+
if (out.length === 0 && (text.includes("Saved/Screenshots/Flockbay") || text.includes("Saved\\Screenshots\\Flockbay"))) {
|
|
2013
|
+
const filenameRe = /(?:^|[\s"'(\\/])(?:-\s*)?(Flockbay_[A-Za-z0-9_.-]+\.(?:png|jpg|jpeg))/gi;
|
|
2014
|
+
for (const match of text.matchAll(filenameRe)) {
|
|
2015
|
+
const filename = normalizeFilePathToken(String(match[1] || ""));
|
|
2016
|
+
if (!filename) continue;
|
|
2017
|
+
const rel = path.join("Saved", "Screenshots", "Flockbay", filename);
|
|
2018
|
+
const resolved = resolveCandidatePath(rel, cwd);
|
|
2019
|
+
uniqPush(out, seen, resolved);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return out;
|
|
2023
|
+
}
|
|
2024
|
+
function extractViewsFromParsedJson(parsed, cwd) {
|
|
2025
|
+
const out = [];
|
|
2026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2027
|
+
const views = Array.isArray(parsed?.views) ? parsed.views : [];
|
|
2028
|
+
for (const v of views) {
|
|
2029
|
+
const p = typeof v?.path === "string" ? v.path : "";
|
|
2030
|
+
if (!p.trim()) continue;
|
|
2031
|
+
uniqPush(out, seen, resolveCandidatePath(p, cwd));
|
|
2032
|
+
}
|
|
2033
|
+
return out;
|
|
2034
|
+
}
|
|
2035
|
+
function extractFromContentBlocks(content, cwd) {
|
|
2036
|
+
const texts = [];
|
|
2037
|
+
let hasImages = false;
|
|
2038
|
+
for (const block of content) {
|
|
2039
|
+
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
2040
|
+
texts.push(block.text);
|
|
2041
|
+
}
|
|
2042
|
+
if (!hasImages && isImageBlock(block)) hasImages = true;
|
|
2043
|
+
}
|
|
2044
|
+
const text = texts.join("\n");
|
|
2045
|
+
const parsed = tryParseJsonObjectWithViews(text);
|
|
2046
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
2047
|
+
const pathMatches = extractScreenshotViewsFromText(text, cwd);
|
|
2048
|
+
const combined = [];
|
|
2049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2050
|
+
for (const p of jsonPaths) uniqPush(combined, seen, p);
|
|
2051
|
+
for (const p of pathMatches) uniqPush(combined, seen, p);
|
|
2052
|
+
return { paths: combined, hasImages };
|
|
2053
|
+
}
|
|
2054
|
+
function detectScreenshotsForGate(args) {
|
|
2055
|
+
const cwd = args.cwd && args.cwd.trim().length > 0 ? args.cwd : process.cwd();
|
|
2056
|
+
const output = args.output;
|
|
2057
|
+
if (Array.isArray(output)) {
|
|
2058
|
+
const extracted = extractFromContentBlocks(output, cwd);
|
|
2059
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
2060
|
+
}
|
|
2061
|
+
if (output && typeof output === "object" && Array.isArray(output.content)) {
|
|
2062
|
+
const extracted = extractFromContentBlocks(output.content, cwd);
|
|
2063
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
2064
|
+
}
|
|
2065
|
+
if (output && typeof output === "object" && Array.isArray(output.views)) {
|
|
2066
|
+
return { paths: extractViewsFromParsedJson(output, cwd), hasImageBlocks: false };
|
|
2067
|
+
}
|
|
2068
|
+
const candidates = [
|
|
2069
|
+
typeof output === "string" ? output : null,
|
|
2070
|
+
typeof output?.stdout === "string" ? output.stdout : null,
|
|
2071
|
+
typeof output?.stderr === "string" ? output.stderr : null,
|
|
2072
|
+
typeof output?.output === "string" ? output.output : null,
|
|
2073
|
+
typeof output?.message === "string" ? output.message : null
|
|
2074
|
+
];
|
|
2075
|
+
const combinedText = candidates.filter((v) => typeof v === "string" && v.trim().length > 0).join("\n");
|
|
2076
|
+
if (!combinedText) return { paths: [], hasImageBlocks: false };
|
|
2077
|
+
const parsed = tryParseJsonObjectWithViews(combinedText);
|
|
2078
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
2079
|
+
const pathMatches = extractScreenshotViewsFromText(combinedText, cwd);
|
|
2080
|
+
const out = [];
|
|
2081
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2082
|
+
for (const p of jsonPaths) uniqPush(out, seen, p);
|
|
2083
|
+
for (const p of pathMatches) uniqPush(out, seen, p);
|
|
2084
|
+
return { paths: out, hasImageBlocks: false };
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1916
2087
|
function buildClaudeUserContent(text, images) {
|
|
1917
2088
|
const cleanText = String(text ?? "").trim();
|
|
1918
2089
|
if (images.length === 0) return cleanText;
|
|
@@ -2023,6 +2194,61 @@ async function claudeRemote(opts) {
|
|
|
2023
2194
|
}
|
|
2024
2195
|
};
|
|
2025
2196
|
let messages = new PushableAsyncIterable();
|
|
2197
|
+
const screenshotGate = {
|
|
2198
|
+
paths: [],
|
|
2199
|
+
seen: /* @__PURE__ */ new Set(),
|
|
2200
|
+
hasImageBlocks: false,
|
|
2201
|
+
inAutoReview: false
|
|
2202
|
+
};
|
|
2203
|
+
const resetScreenshotGateForTurn = () => {
|
|
2204
|
+
screenshotGate.paths = [];
|
|
2205
|
+
screenshotGate.seen.clear();
|
|
2206
|
+
screenshotGate.hasImageBlocks = false;
|
|
2207
|
+
screenshotGate.inAutoReview = false;
|
|
2208
|
+
};
|
|
2209
|
+
const collectScreenshotsForGate = (output) => {
|
|
2210
|
+
if (!output) return;
|
|
2211
|
+
const detected = detectScreenshotsForGate({ output, cwd: opts.path });
|
|
2212
|
+
if (detected.paths.length === 0) return;
|
|
2213
|
+
if (detected.hasImageBlocks) screenshotGate.hasImageBlocks = true;
|
|
2214
|
+
for (const p of detected.paths) {
|
|
2215
|
+
const trimmed = String(p || "").trim();
|
|
2216
|
+
if (!trimmed) continue;
|
|
2217
|
+
if (screenshotGate.seen.has(trimmed)) continue;
|
|
2218
|
+
screenshotGate.seen.add(trimmed);
|
|
2219
|
+
screenshotGate.paths.push(trimmed);
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
const buildScreenshotAutoReviewPrompt = (paths) => {
|
|
2223
|
+
const unique = Array.from(new Set(paths.map((p) => String(p || "").trim()).filter(Boolean)));
|
|
2224
|
+
const toolArgs = JSON.stringify(
|
|
2225
|
+
{
|
|
2226
|
+
paths: unique,
|
|
2227
|
+
limit: unique.length,
|
|
2228
|
+
upload: false,
|
|
2229
|
+
includeToolImages: true
|
|
2230
|
+
},
|
|
2231
|
+
null,
|
|
2232
|
+
2
|
|
2233
|
+
);
|
|
2234
|
+
return trimIdent(`
|
|
2235
|
+
You just generated ${unique.length} screenshot${unique.length === 1 ? "" : "s"}.
|
|
2236
|
+
|
|
2237
|
+
Before doing anything else, call \`mcp__flockbay__read_images\` with:
|
|
2238
|
+
${toolArgs}
|
|
2239
|
+
|
|
2240
|
+
Then visually inspect EVERY screenshot and report (this is for YOUR validation, not ceremony):
|
|
2241
|
+
1) List 2\u20135 concrete acceptance criteria for the user's request.
|
|
2242
|
+
2) One short bullet per image ("Image N: ...") describing what you see.
|
|
2243
|
+
3) For each acceptance criterion: mark it as Verified / Not visible / Failed based ONLY on the screenshots.
|
|
2244
|
+
- If "Not visible", say what evidence is missing.
|
|
2245
|
+
- If "Failed", say what is wrong.
|
|
2246
|
+
4) If anything is not Verified: state the next tool/action you will take to fix or validate it (do not claim completion yet).
|
|
2247
|
+
|
|
2248
|
+
Do not run any other tools in this step.
|
|
2249
|
+
`);
|
|
2250
|
+
};
|
|
2251
|
+
resetScreenshotGateForTurn();
|
|
2026
2252
|
const initialParsed = extractUserImagesMarker(initial.message);
|
|
2027
2253
|
const initialImages = initialParsed.hasImages ? getLatestUserImages().slice(0, initialParsed.count) : [];
|
|
2028
2254
|
messages.push({
|
|
@@ -2097,6 +2323,17 @@ async function claudeRemote(opts) {
|
|
|
2097
2323
|
if (message.type === "result") {
|
|
2098
2324
|
updateThinking(false);
|
|
2099
2325
|
types.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
2326
|
+
if (!screenshotGate.inAutoReview && screenshotGate.paths.length > 0 && !screenshotGate.hasImageBlocks) {
|
|
2327
|
+
screenshotGate.inAutoReview = true;
|
|
2328
|
+
const reviewPrompt = buildScreenshotAutoReviewPrompt(screenshotGate.paths);
|
|
2329
|
+
messages.push({
|
|
2330
|
+
type: "user",
|
|
2331
|
+
message: { role: "user", content: reviewPrompt }
|
|
2332
|
+
});
|
|
2333
|
+
updateThinking(true);
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
screenshotGate.inAutoReview = false;
|
|
2100
2337
|
if (isCompactCommand) {
|
|
2101
2338
|
types.logger.debug("[claudeRemote] Compaction completed");
|
|
2102
2339
|
if (opts.onCompletionEvent) {
|
|
@@ -2111,14 +2348,19 @@ async function claudeRemote(opts) {
|
|
|
2111
2348
|
return;
|
|
2112
2349
|
}
|
|
2113
2350
|
mode = next.mode;
|
|
2351
|
+
resetScreenshotGateForTurn();
|
|
2114
2352
|
const parsed = extractUserImagesMarker(next.message);
|
|
2115
2353
|
const nextImages = parsed.hasImages ? getLatestUserImages().slice(0, parsed.count) : [];
|
|
2116
2354
|
messages.push({ type: "user", message: { role: "user", content: buildClaudeUserContent(parsed.text, nextImages) } });
|
|
2355
|
+
updateThinking(true);
|
|
2117
2356
|
}
|
|
2118
2357
|
if (message.type === "user") {
|
|
2119
2358
|
const msg = message;
|
|
2120
2359
|
if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
|
|
2121
2360
|
for (let c of msg.message.content) {
|
|
2361
|
+
if (c.type === "tool_result") {
|
|
2362
|
+
collectScreenshotsForGate(c.content ?? c);
|
|
2363
|
+
}
|
|
2122
2364
|
if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
|
|
2123
2365
|
types.logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
|
|
2124
2366
|
return;
|
|
@@ -2665,11 +2907,14 @@ ${next}`
|
|
|
2665
2907
|
}
|
|
2666
2908
|
this.pendingRequests.clear();
|
|
2667
2909
|
this.session.client.updateAgentState((currentState) => {
|
|
2668
|
-
const
|
|
2669
|
-
const
|
|
2910
|
+
const pendingRaw = currentState?.requests;
|
|
2911
|
+
const pendingRequests = pendingRaw && typeof pendingRaw === "object" ? pendingRaw : {};
|
|
2912
|
+
const completedRaw = currentState?.completedRequests;
|
|
2913
|
+
const completedRequests = completedRaw && typeof completedRaw === "object" ? { ...completedRaw } : {};
|
|
2670
2914
|
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
2915
|
+
const reqObj = request && typeof request === "object" ? request : { value: request };
|
|
2671
2916
|
completedRequests[id] = {
|
|
2672
|
-
...
|
|
2917
|
+
...reqObj,
|
|
2673
2918
|
completedAt: Date.now(),
|
|
2674
2919
|
status: "canceled",
|
|
2675
2920
|
reason: "Session switched to local mode"
|
|
@@ -3080,13 +3325,47 @@ class SDKToLogConverter {
|
|
|
3080
3325
|
}
|
|
3081
3326
|
}
|
|
3082
3327
|
|
|
3328
|
+
class AsyncLock {
|
|
3329
|
+
permits = 1;
|
|
3330
|
+
promiseResolverQueue = [];
|
|
3331
|
+
async inLock(func) {
|
|
3332
|
+
try {
|
|
3333
|
+
await this.lock();
|
|
3334
|
+
return await func();
|
|
3335
|
+
} finally {
|
|
3336
|
+
this.unlock();
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
async lock() {
|
|
3340
|
+
if (this.permits > 0) {
|
|
3341
|
+
this.permits = this.permits - 1;
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
|
|
3345
|
+
}
|
|
3346
|
+
unlock() {
|
|
3347
|
+
this.permits += 1;
|
|
3348
|
+
if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
|
|
3349
|
+
throw new Error("this.permits should never be > 0 when there is someone waiting.");
|
|
3350
|
+
} else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
|
|
3351
|
+
this.permits -= 1;
|
|
3352
|
+
const nextResolver = this.promiseResolverQueue.shift();
|
|
3353
|
+
if (nextResolver) {
|
|
3354
|
+
setTimeout(() => {
|
|
3355
|
+
nextResolver(true);
|
|
3356
|
+
}, 0);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3083
3362
|
class OutgoingMessageQueue {
|
|
3084
3363
|
constructor(sendFunction) {
|
|
3085
3364
|
this.sendFunction = sendFunction;
|
|
3086
3365
|
}
|
|
3087
3366
|
queue = [];
|
|
3088
3367
|
nextId = 1;
|
|
3089
|
-
lock = new
|
|
3368
|
+
lock = new AsyncLock();
|
|
3090
3369
|
processTimer;
|
|
3091
3370
|
delayTimers = /* @__PURE__ */ new Map();
|
|
3092
3371
|
/**
|
|
@@ -3271,7 +3550,7 @@ async function postJson(path, token, body) {
|
|
|
3271
3550
|
const res = await fetch(`${base}${path}`, {
|
|
3272
3551
|
method: "POST",
|
|
3273
3552
|
headers: {
|
|
3274
|
-
Authorization: `
|
|
3553
|
+
Authorization: `Machine ${token}`,
|
|
3275
3554
|
"Content-Type": "application/json"
|
|
3276
3555
|
},
|
|
3277
3556
|
body: JSON.stringify(body ?? {})
|
|
@@ -3413,7 +3692,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
3413
3692
|
}
|
|
3414
3693
|
await abort();
|
|
3415
3694
|
}
|
|
3416
|
-
session.client.rpcHandlerManager.registerHandler("
|
|
3695
|
+
session.client.rpcHandlerManager.registerHandler("cancel-generation", doAbort);
|
|
3417
3696
|
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
|
|
3418
3697
|
const permissionHandler = new PermissionHandler(session);
|
|
3419
3698
|
const messageQueue = new OutgoingMessageQueue(
|
|
@@ -4269,7 +4548,9 @@ async function daemonPost(path, body) {
|
|
|
4269
4548
|
}
|
|
4270
4549
|
try {
|
|
4271
4550
|
const timeoutRaw = process.env.FLOCKBAY_DAEMON_HTTP_TIMEOUT;
|
|
4272
|
-
const
|
|
4551
|
+
const fallbackTimeout = path === "/spawn-session" ? 6e4 : 1e4;
|
|
4552
|
+
const parsedTimeout = timeoutRaw ? Number.parseInt(timeoutRaw, 10) : NaN;
|
|
4553
|
+
const timeout = Number.isFinite(parsedTimeout) ? parsedTimeout : fallbackTimeout;
|
|
4273
4554
|
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
4274
4555
|
method: "POST",
|
|
4275
4556
|
headers: { "Content-Type": "application/json" },
|
|
@@ -4363,6 +4644,7 @@ async function stopDaemon() {
|
|
|
4363
4644
|
try {
|
|
4364
4645
|
await stopDaemonHttp();
|
|
4365
4646
|
await waitForProcessDeath(state.pid, 2e3);
|
|
4647
|
+
await cleanupDaemonState();
|
|
4366
4648
|
types.logger.debug("Daemon stopped gracefully via HTTP");
|
|
4367
4649
|
return;
|
|
4368
4650
|
} catch (error) {
|
|
@@ -4657,37 +4939,6 @@ ${typeLabels[type] || type}:`));
|
|
|
4657
4939
|
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4658
4940
|
}
|
|
4659
4941
|
|
|
4660
|
-
function displayQRCode(url) {
|
|
4661
|
-
console.log("=".repeat(80));
|
|
4662
|
-
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
4663
|
-
console.log("=".repeat(80));
|
|
4664
|
-
qrcode.generate(url, { small: true }, (qr) => {
|
|
4665
|
-
for (let l of qr.split("\n")) {
|
|
4666
|
-
console.log(" ".repeat(10) + l);
|
|
4667
|
-
}
|
|
4668
|
-
});
|
|
4669
|
-
console.log("=".repeat(80));
|
|
4670
|
-
}
|
|
4671
|
-
|
|
4672
|
-
function generateWebAuthUrl(publicKey) {
|
|
4673
|
-
const publicKeyBase64 = types.encodeBase64(publicKey, "base64url");
|
|
4674
|
-
const server = encodeURIComponent(types.configuration.serverUrl);
|
|
4675
|
-
const shouldAutoLogout = (() => {
|
|
4676
|
-
try {
|
|
4677
|
-
const serverUrl = new URL(types.configuration.serverUrl);
|
|
4678
|
-
const webUrl = new URL(types.configuration.webappUrl);
|
|
4679
|
-
const isLoopback = (host) => {
|
|
4680
|
-
const h = String(host || "").toLowerCase();
|
|
4681
|
-
return h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.endsWith(".localhost");
|
|
4682
|
-
};
|
|
4683
|
-
return isLoopback(serverUrl.hostname) && isLoopback(webUrl.hostname);
|
|
4684
|
-
} catch {
|
|
4685
|
-
return false;
|
|
4686
|
-
}
|
|
4687
|
-
})();
|
|
4688
|
-
return `${types.configuration.webappUrl}/terminal/connect#key=${publicKeyBase64}&server=${server}${shouldAutoLogout ? "&autologout=1" : ""}`;
|
|
4689
|
-
}
|
|
4690
|
-
|
|
4691
4942
|
async function openBrowser(url) {
|
|
4692
4943
|
try {
|
|
4693
4944
|
const forceOpen = process.env.FLOCKBAY_FORCE_BROWSER === "1" || process.env.FLOCKBAY_FORCE_BROWSER === "true";
|
|
@@ -4705,217 +4956,78 @@ async function openBrowser(url) {
|
|
|
4705
4956
|
}
|
|
4706
4957
|
}
|
|
4707
4958
|
|
|
4708
|
-
async function
|
|
4709
|
-
|
|
4710
|
-
const
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
} catch (error) {
|
|
4721
|
-
console.log(`[AUTH DEBUG] Failed to send auth request:`, error);
|
|
4722
|
-
console.log("Failed to create authentication request, please try again later.");
|
|
4723
|
-
return null;
|
|
4959
|
+
async function loginWithClerkAndPairMachine() {
|
|
4960
|
+
types.logger.debug("[AUTH] Starting Clerk-based CLI login + machine pairing");
|
|
4961
|
+
const settings = await types.updateSettings(async (s) => {
|
|
4962
|
+
const machineId2 = s.machineId || node_crypto.randomUUID();
|
|
4963
|
+
const serverUrl2 = s.serverUrl || types.configuration.serverUrl;
|
|
4964
|
+
const webappUrl = s.webappUrl || types.configuration.webappUrl;
|
|
4965
|
+
return { ...s, machineId: machineId2, serverUrl: serverUrl2, webappUrl };
|
|
4966
|
+
});
|
|
4967
|
+
const serverUrl = types.configuration.serverUrl.replace(/\/+$/, "");
|
|
4968
|
+
const machineId = String(settings.machineId || "").trim();
|
|
4969
|
+
if (!machineId) {
|
|
4970
|
+
throw new Error("Missing machineId (settings.json).");
|
|
4724
4971
|
}
|
|
4725
|
-
|
|
4726
|
-
}
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
const
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
console.log(`- Paste this URL into the Flockbay app: ${authUrl}`);
|
|
4735
|
-
console.log(`- Or open this link in a browser (works well on mobile web): ${generateWebAuthUrl(keypair.publicKey)}`);
|
|
4736
|
-
console.log("");
|
|
4737
|
-
return await waitForAuthentication(keypair);
|
|
4738
|
-
}
|
|
4739
|
-
async function doWebAuth(keypair) {
|
|
4740
|
-
console.clear();
|
|
4741
|
-
console.log("\nWeb Authentication\n");
|
|
4742
|
-
const webUrl = generateWebAuthUrl(keypair.publicKey);
|
|
4743
|
-
const localWebUrl = (() => {
|
|
4744
|
-
try {
|
|
4745
|
-
const u = new URL(webUrl);
|
|
4746
|
-
u.protocol = "http:";
|
|
4747
|
-
u.hostname = "localhost";
|
|
4748
|
-
u.port = "8081";
|
|
4749
|
-
return u.toString();
|
|
4750
|
-
} catch {
|
|
4751
|
-
return null;
|
|
4752
|
-
}
|
|
4753
|
-
})();
|
|
4754
|
-
const isLocalServer = (() => {
|
|
4755
|
-
try {
|
|
4756
|
-
const server = new URL(types.configuration.serverUrl);
|
|
4757
|
-
const host = server.hostname.toLowerCase();
|
|
4758
|
-
return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host.endsWith(".localhost");
|
|
4759
|
-
} catch {
|
|
4760
|
-
return false;
|
|
4761
|
-
}
|
|
4762
|
-
})();
|
|
4763
|
-
console.log("Opening your browser...");
|
|
4764
|
-
const primaryUrl = isLocalServer && localWebUrl ? localWebUrl : webUrl;
|
|
4765
|
-
const browserOpened = await openBrowser(primaryUrl);
|
|
4766
|
-
if (browserOpened) {
|
|
4767
|
-
console.log("\u2713 Browser opened\n");
|
|
4768
|
-
console.log("Complete authentication in your browser window.");
|
|
4769
|
-
} else {
|
|
4770
|
-
console.log("Could not open browser automatically.");
|
|
4771
|
-
}
|
|
4772
|
-
console.log("\nIf the browser did not open, please copy and paste this URL:");
|
|
4773
|
-
console.log(webUrl);
|
|
4774
|
-
if (localWebUrl && localWebUrl !== webUrl) {
|
|
4775
|
-
if (isLocalServer) {
|
|
4776
|
-
console.log("\nLocal dev URL (same auth key + server):");
|
|
4777
|
-
console.log(localWebUrl);
|
|
4778
|
-
} else {
|
|
4779
|
-
console.log("\nLocal dev note:");
|
|
4780
|
-
console.log(
|
|
4781
|
-
`- This terminal is waiting on ${types.configuration.serverUrl}, so opening the link on localhost will NOT complete auth.
|
|
4782
|
-
- To authenticate locally, re-run with:
|
|
4783
|
-
FLOCKBAY_SERVER_URL=http://localhost:3006 FLOCKBAY_WEBAPP_URL=http://localhost:8081`
|
|
4784
|
-
);
|
|
4785
|
-
}
|
|
4972
|
+
const res = await axios.post(
|
|
4973
|
+
`${serverUrl}/v1/cli/pair/request`,
|
|
4974
|
+
{ machineId },
|
|
4975
|
+
{ headers: { "Content-Type": "application/json" }, timeout: 3e4 }
|
|
4976
|
+
);
|
|
4977
|
+
const pairingId = String(res?.data?.pairingId || "").trim();
|
|
4978
|
+
const approveUrl = String(res?.data?.approveUrl || "").trim();
|
|
4979
|
+
if (!pairingId || !approveUrl) {
|
|
4980
|
+
throw new Error("Invalid pairing response from server.");
|
|
4786
4981
|
}
|
|
4787
4982
|
console.log("");
|
|
4788
|
-
|
|
4789
|
-
}
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
console.log("\n\nInvalid auth response (expected V2). Please update your app/CLI.");
|
|
4811
|
-
return null;
|
|
4812
|
-
}
|
|
4813
|
-
const credentials = {
|
|
4814
|
-
publicKey: decrypted.slice(1, 33),
|
|
4815
|
-
machineKey: node_crypto.randomBytes(32),
|
|
4816
|
-
token
|
|
4817
|
-
};
|
|
4818
|
-
await types.writeCredentialsDataKey(credentials);
|
|
4819
|
-
console.log("\n\n\u2713 Authentication successful\n");
|
|
4820
|
-
return {
|
|
4821
|
-
encryption: {
|
|
4822
|
-
type: "dataKey",
|
|
4823
|
-
publicKey: credentials.publicKey,
|
|
4824
|
-
machineKey: credentials.machineKey
|
|
4825
|
-
},
|
|
4826
|
-
token
|
|
4827
|
-
};
|
|
4828
|
-
} else {
|
|
4829
|
-
console.log("\n\nFailed to decrypt response. Please try again.");
|
|
4830
|
-
return null;
|
|
4831
|
-
}
|
|
4832
|
-
}
|
|
4833
|
-
} catch (error) {
|
|
4834
|
-
console.log("\n\nFailed to check authentication status. Please try again.");
|
|
4835
|
-
return null;
|
|
4983
|
+
console.log(chalk.bold("Flockbay CLI login"));
|
|
4984
|
+
console.log(chalk.gray(`Profile: ${types.configuration.profile}`));
|
|
4985
|
+
console.log(chalk.gray(`Server: ${types.configuration.serverUrl}`));
|
|
4986
|
+
console.log(chalk.gray(`Machine: ${machineId} (${os.hostname()})`));
|
|
4987
|
+
console.log("");
|
|
4988
|
+
console.log("Open this link to sign in with Clerk and approve this machine:");
|
|
4989
|
+
console.log(approveUrl);
|
|
4990
|
+
console.log("");
|
|
4991
|
+
void openBrowser(approveUrl).catch(() => null);
|
|
4992
|
+
const deadline = Date.now() + 10 * 6e4;
|
|
4993
|
+
while (Date.now() < deadline) {
|
|
4994
|
+
await types.delay(1e3);
|
|
4995
|
+
const status = await axios.get(`${serverUrl}/v1/cli/pair/status`, {
|
|
4996
|
+
params: { pairingId, consume: 1 },
|
|
4997
|
+
timeout: 15e3
|
|
4998
|
+
});
|
|
4999
|
+
const state = String(status?.data?.state || "").trim();
|
|
5000
|
+
if (state === "approved") {
|
|
5001
|
+
const machineToken = String(status?.data?.machineToken || "").trim();
|
|
5002
|
+
const orgId = String(status?.data?.orgId || "").trim();
|
|
5003
|
+
if (!machineToken || !orgId) {
|
|
5004
|
+
throw new Error("Pairing approved, but missing token/orgId.");
|
|
4836
5005
|
}
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
5006
|
+
const auth = { machineToken, orgId, createdAtMs: Date.now() };
|
|
5007
|
+
await types.writeCredentials(auth);
|
|
5008
|
+
console.log(chalk.green("\u2713 Machine paired to workspace"));
|
|
5009
|
+
console.log(chalk.gray(`Workspace: ${orgId}`));
|
|
5010
|
+
return auth;
|
|
5011
|
+
}
|
|
5012
|
+
if (state === "expired") throw new Error("Pairing expired. Re-run `flockbay login`.");
|
|
5013
|
+
if (state === "consumed") {
|
|
5014
|
+
throw new Error("Pairing token already consumed. If this is unexpected, re-run `flockbay login`.");
|
|
4840
5015
|
}
|
|
4841
|
-
} finally {
|
|
4842
|
-
process.off("SIGINT", handleInterrupt);
|
|
4843
|
-
}
|
|
4844
|
-
return null;
|
|
4845
|
-
}
|
|
4846
|
-
function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
4847
|
-
const ephemeralPublicKey = encryptedBundle.slice(0, 32);
|
|
4848
|
-
const nonce = encryptedBundle.slice(32, 32 + tweetnacl.box.nonceLength);
|
|
4849
|
-
const encrypted = encryptedBundle.slice(32 + tweetnacl.box.nonceLength);
|
|
4850
|
-
const decrypted = tweetnacl.box.open(encrypted, nonce, ephemeralPublicKey, recipientSecretKey);
|
|
4851
|
-
if (!decrypted) {
|
|
4852
|
-
return null;
|
|
4853
5016
|
}
|
|
4854
|
-
|
|
5017
|
+
throw new Error("Login timed out. Re-run `flockbay login`.");
|
|
4855
5018
|
}
|
|
4856
|
-
async function
|
|
4857
|
-
|
|
4858
|
-
const
|
|
4859
|
-
const
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
if (!allowAuthFlow) {
|
|
4863
|
-
throw new Error('Not authenticated. Run "flockbay auth login" first.');
|
|
4864
|
-
}
|
|
4865
|
-
types.logger.debug("[AUTH] No credentials found, starting authentication flow...");
|
|
4866
|
-
const authResult = await doAuth({ method: authMethod });
|
|
4867
|
-
if (!authResult) {
|
|
4868
|
-
throw new Error("Authentication failed or was cancelled");
|
|
4869
|
-
}
|
|
4870
|
-
credentials = authResult;
|
|
4871
|
-
} else {
|
|
4872
|
-
types.logger.debug("[AUTH] Using existing credentials");
|
|
5019
|
+
async function ensureMachineAuthOrLogin() {
|
|
5020
|
+
const existing = await types.readCredentials();
|
|
5021
|
+
const settings = await types.readSettings();
|
|
5022
|
+
const machineId = String(settings?.machineId || "").trim();
|
|
5023
|
+
if (existing && existing.machineToken && existing.orgId && machineId) {
|
|
5024
|
+
return { auth: existing, machineId };
|
|
4873
5025
|
}
|
|
4874
|
-
const
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
});
|
|
4880
|
-
const machines = Array.isArray(res.data) ? res.data : [];
|
|
4881
|
-
const host = os.hostname();
|
|
4882
|
-
const homeDir = os.homedir();
|
|
4883
|
-
const flockbayHomeDir = canonicalizePath(types.configuration.flockbayHomeDir);
|
|
4884
|
-
const matches = machines.map((m) => {
|
|
4885
|
-
const raw = m?.metadata;
|
|
4886
|
-
let metadata = raw;
|
|
4887
|
-
if (typeof raw === "string") {
|
|
4888
|
-
try {
|
|
4889
|
-
metadata = JSON.parse(raw);
|
|
4890
|
-
} catch {
|
|
4891
|
-
metadata = null;
|
|
4892
|
-
}
|
|
4893
|
-
}
|
|
4894
|
-
return { machine: m, metadata };
|
|
4895
|
-
}).filter(({ machine, metadata }) => {
|
|
4896
|
-
if (!machine?.id || !metadata) return false;
|
|
4897
|
-
if (String(metadata.host || "") !== host) return false;
|
|
4898
|
-
if (String(metadata.homeDir || "") !== homeDir) return false;
|
|
4899
|
-
if (canonicalizePath(String(metadata.flockbayHomeDir || "")) !== flockbayHomeDir) return false;
|
|
4900
|
-
return true;
|
|
4901
|
-
}).map(({ machine }) => machine);
|
|
4902
|
-
if (!matches.length) return null;
|
|
4903
|
-
matches.sort((a, b) => (Number(a.createdAt) || 0) - (Number(b.createdAt) || 0));
|
|
4904
|
-
return String(matches[0].id || "").trim() || null;
|
|
4905
|
-
} catch {
|
|
4906
|
-
return null;
|
|
4907
|
-
}
|
|
4908
|
-
};
|
|
4909
|
-
const existingSettings = await types.readSettings();
|
|
4910
|
-
const preferredMachineId = !existingSettings.machineId ? await findMatchingExistingMachineId() : null;
|
|
4911
|
-
const settings = await types.updateSettings(async (s) => {
|
|
4912
|
-
const machineId = s.machineId || preferredMachineId || node_crypto.randomUUID();
|
|
4913
|
-
const serverUrl = s.serverUrl || types.configuration.serverUrl;
|
|
4914
|
-
const webappUrl = s.webappUrl || types.configuration.webappUrl;
|
|
4915
|
-
return { ...s, machineId, serverUrl, webappUrl };
|
|
4916
|
-
});
|
|
4917
|
-
types.logger.debug(`[AUTH] Machine ID: ${settings.machineId}`);
|
|
4918
|
-
return { credentials, machineId: settings.machineId };
|
|
5026
|
+
const auth = await loginWithClerkAndPairMachine();
|
|
5027
|
+
const updated = await types.readSettings();
|
|
5028
|
+
const mid = String(updated?.machineId || "").trim();
|
|
5029
|
+
if (!mid) throw new Error("Missing machineId after login.");
|
|
5030
|
+
return { auth, machineId: mid };
|
|
4919
5031
|
}
|
|
4920
5032
|
|
|
4921
5033
|
function resolveTsxImportArgs(projectRoot) {
|
|
@@ -4995,6 +5107,7 @@ Error: ${message}` };
|
|
|
4995
5107
|
|
|
4996
5108
|
function startDaemonControlServer({
|
|
4997
5109
|
getChildren,
|
|
5110
|
+
getStatus,
|
|
4998
5111
|
stopSession,
|
|
4999
5112
|
spawnSession,
|
|
5000
5113
|
requestShutdown,
|
|
@@ -5050,6 +5163,15 @@ function startDaemonControlServer({
|
|
|
5050
5163
|
}))
|
|
5051
5164
|
};
|
|
5052
5165
|
});
|
|
5166
|
+
typed.post("/status", {
|
|
5167
|
+
schema: {
|
|
5168
|
+
response: {
|
|
5169
|
+
200: z.z.any()
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
}, async () => {
|
|
5173
|
+
return getStatus();
|
|
5174
|
+
});
|
|
5053
5175
|
typed.post("/stop-session", {
|
|
5054
5176
|
schema: {
|
|
5055
5177
|
body: z.z.object({
|
|
@@ -5189,6 +5311,26 @@ function startDaemonControlServer({
|
|
|
5189
5311
|
});
|
|
5190
5312
|
}
|
|
5191
5313
|
|
|
5314
|
+
function shouldAutoRestart(params) {
|
|
5315
|
+
const { exit, stopRequested } = params;
|
|
5316
|
+
if (stopRequested) return false;
|
|
5317
|
+
const { code, signal } = exit;
|
|
5318
|
+
const cleanExit = code === 0 && !signal;
|
|
5319
|
+
if (cleanExit) return false;
|
|
5320
|
+
const expectedSignal = signal === "SIGTERM" || signal === "SIGINT";
|
|
5321
|
+
if (expectedSignal) return false;
|
|
5322
|
+
return true;
|
|
5323
|
+
}
|
|
5324
|
+
function computeNextAutoRestart(params) {
|
|
5325
|
+
const { nowMs, prev, config } = params;
|
|
5326
|
+
const withinWindow = prev && nowMs - prev.firstFailureAtMs <= config.windowMs;
|
|
5327
|
+
const state = withinWindow ? { ...prev } : { attempts: 0, firstFailureAtMs: nowMs };
|
|
5328
|
+
state.attempts += 1;
|
|
5329
|
+
if (state.attempts > config.maxAttempts) return { action: "give_up" };
|
|
5330
|
+
const delayMs = Math.min(config.baseDelayMs * 2 ** (state.attempts - 1), config.maxDelayMs);
|
|
5331
|
+
return { action: "schedule", delayMs, state };
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5192
5334
|
async function pathExists(p) {
|
|
5193
5335
|
try {
|
|
5194
5336
|
await fs$2.access(p);
|
|
@@ -5259,15 +5401,23 @@ const initialMachineMetadata = {
|
|
|
5259
5401
|
flockbayLibDir: types.projectPath()
|
|
5260
5402
|
};
|
|
5261
5403
|
async function startDaemon() {
|
|
5404
|
+
let startupCompleted = false;
|
|
5405
|
+
let startupForceExitTimer = null;
|
|
5262
5406
|
let requestShutdown;
|
|
5263
5407
|
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
5264
5408
|
requestShutdown = (source, errorMessage) => {
|
|
5265
5409
|
types.logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5410
|
+
if (!startupCompleted && !startupForceExitTimer) {
|
|
5411
|
+
startupForceExitTimer = setTimeout(async () => {
|
|
5412
|
+
types.logger.debug("[DAEMON RUN] Startup malfunctioned, forcing exit with code 1");
|
|
5413
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
5414
|
+
process.exit(1);
|
|
5415
|
+
}, 1e4);
|
|
5416
|
+
try {
|
|
5417
|
+
startupForceExitTimer.unref();
|
|
5418
|
+
} catch {
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5271
5421
|
resolve({ source, errorMessage });
|
|
5272
5422
|
};
|
|
5273
5423
|
});
|
|
@@ -5321,7 +5471,7 @@ async function startDaemon() {
|
|
|
5321
5471
|
if (caffeinateStarted) {
|
|
5322
5472
|
types.logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
5323
5473
|
}
|
|
5324
|
-
const { credentials, machineId } = await
|
|
5474
|
+
const { auth: credentials, machineId } = await ensureMachineAuthOrLogin();
|
|
5325
5475
|
types.logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
5326
5476
|
const shouldStartUnrealMcp = String(process.env.FLOCKBAY_UNREAL_MCP_ENABLED || "").trim() === "1";
|
|
5327
5477
|
if (shouldStartUnrealMcp) {
|
|
@@ -5334,10 +5484,55 @@ async function startDaemon() {
|
|
|
5334
5484
|
}
|
|
5335
5485
|
}
|
|
5336
5486
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
5487
|
+
const stopRequestedSessionIds = /* @__PURE__ */ new Set();
|
|
5488
|
+
const sessionAutoRestart = /* @__PURE__ */ new Map();
|
|
5489
|
+
const autoRestartConfig = {
|
|
5490
|
+
maxAttempts: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_MAX_ATTEMPTS || "5", 10),
|
|
5491
|
+
windowMs: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_WINDOW_MS || String(10 * 6e4), 10),
|
|
5492
|
+
baseDelayMs: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_BASE_DELAY_MS || "1000", 10),
|
|
5493
|
+
maxDelayMs: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_MAX_DELAY_MS || "30000", 10)
|
|
5494
|
+
};
|
|
5337
5495
|
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
5338
5496
|
let machineRef = null;
|
|
5339
|
-
let
|
|
5497
|
+
let apiMachine = null;
|
|
5340
5498
|
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
5499
|
+
const findLatestLogForPid = async (pid) => {
|
|
5500
|
+
const suffix = `-pid-${pid}.log`;
|
|
5501
|
+
try {
|
|
5502
|
+
const files = await fs$1.readdir(types.configuration.logsDir);
|
|
5503
|
+
const matches = files.filter((f) => f.endsWith(suffix));
|
|
5504
|
+
if (matches.length === 0) return null;
|
|
5505
|
+
let best = null;
|
|
5506
|
+
for (const file of matches) {
|
|
5507
|
+
try {
|
|
5508
|
+
const st = await fs$1.stat(path$1.join(types.configuration.logsDir, file));
|
|
5509
|
+
const mtimeMs = Number(st.mtimeMs || 0);
|
|
5510
|
+
if (!best || mtimeMs > best.mtimeMs) best = { file, mtimeMs };
|
|
5511
|
+
} catch {
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
return best ? path$1.join(types.configuration.logsDir, best.file) : null;
|
|
5515
|
+
} catch {
|
|
5516
|
+
return null;
|
|
5517
|
+
}
|
|
5518
|
+
};
|
|
5519
|
+
const readLogTail = async (path, maxBytes) => {
|
|
5520
|
+
try {
|
|
5521
|
+
const st = await fs$1.stat(path);
|
|
5522
|
+
const size = Number(st.size || 0);
|
|
5523
|
+
const offset = Math.max(0, size - maxBytes);
|
|
5524
|
+
const handle = await fs$1.open(path, "r");
|
|
5525
|
+
try {
|
|
5526
|
+
const buf = Buffer.alloc(Math.min(maxBytes, size));
|
|
5527
|
+
const { bytesRead } = await handle.read(buf, 0, buf.length, offset);
|
|
5528
|
+
return buf.subarray(0, bytesRead).toString("utf8");
|
|
5529
|
+
} finally {
|
|
5530
|
+
await handle.close();
|
|
5531
|
+
}
|
|
5532
|
+
} catch {
|
|
5533
|
+
return "";
|
|
5534
|
+
}
|
|
5535
|
+
};
|
|
5341
5536
|
const onSessionWebhook = (sessionId, sessionMetadata) => {
|
|
5342
5537
|
types.logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
|
|
5343
5538
|
const pid = sessionMetadata.hostPid;
|
|
@@ -5351,6 +5546,9 @@ async function startDaemon() {
|
|
|
5351
5546
|
if (existingSession && existingSession.startedBy === "daemon") {
|
|
5352
5547
|
existingSession.serverSessionId = sessionId;
|
|
5353
5548
|
existingSession.serverSessionMetadataFromLocalWebhook = sessionMetadata;
|
|
5549
|
+
if (existingSession.spawnOptions) {
|
|
5550
|
+
existingSession.spawnOptions = { ...existingSession.spawnOptions, sessionId };
|
|
5551
|
+
}
|
|
5354
5552
|
types.logger.debug(`[DAEMON RUN] Updated daemon-spawned session ${sessionId} with metadata`);
|
|
5355
5553
|
const awaiter = pidToAwaiter.get(pid);
|
|
5356
5554
|
if (awaiter) {
|
|
@@ -5389,16 +5587,7 @@ async function startDaemon() {
|
|
|
5389
5587
|
const refreshBypassFromServerIfNeeded = async () => {
|
|
5390
5588
|
if (bypassUeGates) return;
|
|
5391
5589
|
if (envBypassUeGates) return;
|
|
5392
|
-
|
|
5393
|
-
const targetMachineId = requestedMachineId || machineRef?.id;
|
|
5394
|
-
if (!targetMachineId) return;
|
|
5395
|
-
try {
|
|
5396
|
-
const latest = await api.getMachine(targetMachineId);
|
|
5397
|
-
machineRef = latest;
|
|
5398
|
-
bypassUeGates = envBypassUeGates || Boolean(latest?.metadata?.flockbayDevBypassUeGates);
|
|
5399
|
-
} catch (err) {
|
|
5400
|
-
bypassRefreshError = err instanceof Error ? err.message : String(err);
|
|
5401
|
-
}
|
|
5590
|
+
bypassRefreshError = "refresh_not_supported";
|
|
5402
5591
|
};
|
|
5403
5592
|
const detection = await detectUnrealProject(directory);
|
|
5404
5593
|
if (!detection.ok && !bypassUeGates) {
|
|
@@ -5419,7 +5608,7 @@ async function startDaemon() {
|
|
|
5419
5608
|
const res = await fetch(url, {
|
|
5420
5609
|
method: "POST",
|
|
5421
5610
|
headers: {
|
|
5422
|
-
Authorization: `
|
|
5611
|
+
Authorization: `Machine ${credentials.machineToken}`,
|
|
5423
5612
|
"Content-Type": "application/json"
|
|
5424
5613
|
},
|
|
5425
5614
|
body: JSON.stringify(body ?? {})
|
|
@@ -5620,11 +5809,16 @@ Fix: restart session creation and re-select the project.`
|
|
|
5620
5809
|
}
|
|
5621
5810
|
const args = [
|
|
5622
5811
|
agentCommand,
|
|
5812
|
+
"--profile",
|
|
5813
|
+
types.configuration.profile,
|
|
5623
5814
|
"--flockbay-starting-mode",
|
|
5624
5815
|
"remote",
|
|
5625
5816
|
"--started-by",
|
|
5626
5817
|
"daemon"
|
|
5627
5818
|
];
|
|
5819
|
+
if (options.sessionId) {
|
|
5820
|
+
args.push("--flockbay-session-id", String(options.sessionId));
|
|
5821
|
+
}
|
|
5628
5822
|
const captureChildOutput = Boolean(process.env.DEBUG);
|
|
5629
5823
|
const spawnEnv = {
|
|
5630
5824
|
...process.env,
|
|
@@ -5667,24 +5861,30 @@ Fix: restart session creation and re-select the project.`
|
|
|
5667
5861
|
};
|
|
5668
5862
|
}
|
|
5669
5863
|
types.logger.debug(`[DAEMON RUN] Spawned process with PID ${sessionProcess.pid}`);
|
|
5864
|
+
const spawnOptionsForTracking = {
|
|
5865
|
+
...options,
|
|
5866
|
+
approvedNewDirectoryCreation: true,
|
|
5867
|
+
coordination: coordinationForSpawn && typeof coordinationForSpawn === "object" ? coordinationForSpawn : options.coordination ?? null
|
|
5868
|
+
};
|
|
5670
5869
|
const trackedSession = {
|
|
5671
5870
|
startedBy: "daemon",
|
|
5672
5871
|
pid: sessionProcess.pid,
|
|
5673
5872
|
childProcess: sessionProcess,
|
|
5674
5873
|
directoryCreated,
|
|
5874
|
+
spawnOptions: spawnOptionsForTracking,
|
|
5675
5875
|
message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0
|
|
5676
5876
|
};
|
|
5677
5877
|
pidToTrackedSession.set(sessionProcess.pid, trackedSession);
|
|
5678
5878
|
sessionProcess.on("exit", (code, signal) => {
|
|
5679
5879
|
types.logger.debug(`[DAEMON RUN] Child PID ${sessionProcess.pid} exited with code ${code}, signal ${signal}`);
|
|
5680
5880
|
if (sessionProcess.pid) {
|
|
5681
|
-
onChildExited(sessionProcess.pid);
|
|
5881
|
+
onChildExited(sessionProcess.pid, { code, signal });
|
|
5682
5882
|
}
|
|
5683
5883
|
});
|
|
5684
5884
|
sessionProcess.on("error", (error) => {
|
|
5685
5885
|
types.logger.debug(`[DAEMON RUN] Child process error:`, error);
|
|
5686
5886
|
if (sessionProcess.pid) {
|
|
5687
|
-
onChildExited(sessionProcess.pid);
|
|
5887
|
+
onChildExited(sessionProcess.pid, { code: null, signal: null });
|
|
5688
5888
|
}
|
|
5689
5889
|
});
|
|
5690
5890
|
types.logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${sessionProcess.pid}`);
|
|
@@ -5707,17 +5907,25 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5707
5907
|
}, 3e4);
|
|
5708
5908
|
const onEarlyExit = (code, signal) => {
|
|
5709
5909
|
clearTimeout(timeout);
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5910
|
+
void (async () => {
|
|
5911
|
+
const logPath = await findLatestLogForPid(pid);
|
|
5912
|
+
settleOnce({
|
|
5913
|
+
type: "error",
|
|
5914
|
+
errorMessage: `Session process exited before webhook (PID ${pid}, code ${code ?? "unknown"}, signal ${signal ?? "none"}).
|
|
5915
|
+
Log: ${logPath || `not found (check ${types.configuration.logsDir})`}`
|
|
5916
|
+
});
|
|
5917
|
+
})();
|
|
5714
5918
|
};
|
|
5715
5919
|
const onEarlyError = (error) => {
|
|
5716
5920
|
clearTimeout(timeout);
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5921
|
+
void (async () => {
|
|
5922
|
+
const logPath = await findLatestLogForPid(pid);
|
|
5923
|
+
settleOnce({
|
|
5924
|
+
type: "error",
|
|
5925
|
+
errorMessage: `Session process error before webhook (PID ${pid}): ${error.message}.
|
|
5926
|
+
Log: ${logPath || `not found (check ${types.configuration.logsDir})`}`
|
|
5927
|
+
});
|
|
5928
|
+
})();
|
|
5721
5929
|
};
|
|
5722
5930
|
sessionProcess.once("exit", onEarlyExit);
|
|
5723
5931
|
sessionProcess.once("error", onEarlyError);
|
|
@@ -5767,8 +5975,13 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5767
5975
|
};
|
|
5768
5976
|
const stopSession = (sessionId) => {
|
|
5769
5977
|
types.logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
5978
|
+
if (sessionId && !sessionId.startsWith("PID-")) {
|
|
5979
|
+
stopRequestedSessionIds.add(sessionId);
|
|
5980
|
+
}
|
|
5770
5981
|
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
5771
5982
|
if (session.serverSessionId === sessionId || sessionId.startsWith("PID-") && pid === parseInt(sessionId.replace("PID-", ""))) {
|
|
5983
|
+
session.stopRequested = true;
|
|
5984
|
+
if (session.serverSessionId) stopRequestedSessionIds.add(session.serverSessionId);
|
|
5772
5985
|
if (session.startedBy === "daemon" && session.childProcess) {
|
|
5773
5986
|
try {
|
|
5774
5987
|
session.childProcess.kill("SIGTERM");
|
|
@@ -5784,20 +5997,144 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5784
5997
|
types.logger.debug(`[DAEMON RUN] Failed to kill external session PID ${pid}:`, error);
|
|
5785
5998
|
}
|
|
5786
5999
|
}
|
|
5787
|
-
|
|
5788
|
-
types.logger.debug(`[DAEMON RUN] Removed session ${sessionId} from tracking`);
|
|
6000
|
+
types.logger.debug(`[DAEMON RUN] Stop requested for session ${sessionId}; waiting for exit to remove tracking`);
|
|
5789
6001
|
return true;
|
|
5790
6002
|
}
|
|
5791
6003
|
}
|
|
5792
6004
|
types.logger.debug(`[DAEMON RUN] Session ${sessionId} not found`);
|
|
5793
6005
|
return false;
|
|
5794
6006
|
};
|
|
5795
|
-
const
|
|
6007
|
+
const scheduleAutoRestart = (params) => {
|
|
6008
|
+
const { sessionId, spawnOptions, reason } = params;
|
|
6009
|
+
if (stopRequestedSessionIds.has(sessionId)) {
|
|
6010
|
+
types.logger.debug(`[DAEMON RUN] Auto-restart suppressed (stop requested) for session ${sessionId}`);
|
|
6011
|
+
return;
|
|
6012
|
+
}
|
|
6013
|
+
const now = Date.now();
|
|
6014
|
+
const existing = sessionAutoRestart.get(sessionId);
|
|
6015
|
+
if (existing?.timer) {
|
|
6016
|
+
types.logger.debug(`[DAEMON RUN] Auto-restart already scheduled for session ${sessionId}`);
|
|
6017
|
+
return;
|
|
6018
|
+
}
|
|
6019
|
+
const next = computeNextAutoRestart({
|
|
6020
|
+
nowMs: now,
|
|
6021
|
+
prev: existing?.state ?? null,
|
|
6022
|
+
config: autoRestartConfig
|
|
6023
|
+
});
|
|
6024
|
+
if (next.action === "give_up") {
|
|
6025
|
+
types.logger.debug(
|
|
6026
|
+
`[DAEMON RUN] Auto-restart giving up for session ${sessionId} after ${autoRestartConfig.maxAttempts} attempt(s) within ${autoRestartConfig.windowMs}ms (last reason: ${reason})`
|
|
6027
|
+
);
|
|
6028
|
+
sessionAutoRestart.delete(sessionId);
|
|
6029
|
+
return;
|
|
6030
|
+
}
|
|
6031
|
+
const delay = next.delayMs;
|
|
6032
|
+
types.logger.debug(
|
|
6033
|
+
`[DAEMON RUN] Auto-restarting session ${sessionId} in ${delay}ms (attempt ${next.state.attempts}/${autoRestartConfig.maxAttempts}, reason: ${reason})`
|
|
6034
|
+
);
|
|
6035
|
+
const entry = { state: next.state, timer: void 0 };
|
|
6036
|
+
entry.timer = setTimeout(() => {
|
|
6037
|
+
entry.timer = void 0;
|
|
6038
|
+
sessionAutoRestart.set(sessionId, entry);
|
|
6039
|
+
if (stopRequestedSessionIds.has(sessionId)) {
|
|
6040
|
+
types.logger.debug(`[DAEMON RUN] Auto-restart canceled (stop requested) for session ${sessionId}`);
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
void (async () => {
|
|
6044
|
+
try {
|
|
6045
|
+
const result = await spawnSession({
|
|
6046
|
+
...spawnOptions,
|
|
6047
|
+
approvedNewDirectoryCreation: true,
|
|
6048
|
+
sessionId
|
|
6049
|
+
});
|
|
6050
|
+
if (result.type === "success") {
|
|
6051
|
+
types.logger.debug(`[DAEMON RUN] Auto-restart succeeded for session ${sessionId}`);
|
|
6052
|
+
sessionAutoRestart.delete(sessionId);
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
types.logger.debug(
|
|
6056
|
+
`[DAEMON RUN] Auto-restart failed for session ${sessionId} (type=${result.type}): ${result.type === "error" ? result.errorMessage : "needs-user-approval"}`
|
|
6057
|
+
);
|
|
6058
|
+
} catch (err) {
|
|
6059
|
+
types.logger.debug(`[DAEMON RUN] Auto-restart threw for session ${sessionId}:`, err);
|
|
6060
|
+
}
|
|
6061
|
+
scheduleAutoRestart({ sessionId, spawnOptions, reason: "restart_failed" });
|
|
6062
|
+
})();
|
|
6063
|
+
}, delay);
|
|
6064
|
+
try {
|
|
6065
|
+
entry.timer.unref();
|
|
6066
|
+
} catch {
|
|
6067
|
+
}
|
|
6068
|
+
sessionAutoRestart.set(sessionId, entry);
|
|
6069
|
+
};
|
|
6070
|
+
const onChildExited = (pid, exit) => {
|
|
6071
|
+
const tracked = pidToTrackedSession.get(pid);
|
|
5796
6072
|
types.logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
5797
6073
|
pidToTrackedSession.delete(pid);
|
|
6074
|
+
if (!tracked) return;
|
|
6075
|
+
tracked.lastExit = {
|
|
6076
|
+
code: exit?.code ?? null,
|
|
6077
|
+
signal: exit?.signal ?? null,
|
|
6078
|
+
atMs: Date.now()
|
|
6079
|
+
};
|
|
6080
|
+
const sessionId = String(tracked.serverSessionId || "").trim();
|
|
6081
|
+
if (!sessionId) return;
|
|
6082
|
+
if (tracked.stopRequested || stopRequestedSessionIds.has(sessionId)) {
|
|
6083
|
+
types.logger.debug(`[DAEMON RUN] Not auto-restarting session ${sessionId} (stop requested)`);
|
|
6084
|
+
sessionAutoRestart.delete(sessionId);
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
const code = tracked.lastExit.code;
|
|
6088
|
+
const signal = tracked.lastExit.signal;
|
|
6089
|
+
if (!shouldAutoRestart({
|
|
6090
|
+
exit: { code, signal },
|
|
6091
|
+
stopRequested: Boolean(tracked.stopRequested || stopRequestedSessionIds.has(sessionId))
|
|
6092
|
+
})) {
|
|
6093
|
+
types.logger.debug(`[DAEMON RUN] Not auto-restarting session ${sessionId} (clean exit)`);
|
|
6094
|
+
sessionAutoRestart.delete(sessionId);
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
6097
|
+
if (!tracked.spawnOptions) {
|
|
6098
|
+
types.logger.debug(`[DAEMON RUN] Not auto-restarting session ${sessionId} (missing spawn options)`);
|
|
6099
|
+
return;
|
|
6100
|
+
}
|
|
6101
|
+
void (async () => {
|
|
6102
|
+
const logPath = await findLatestLogForPid(pid);
|
|
6103
|
+
if (!logPath) return;
|
|
6104
|
+
const tail = await readLogTail(logPath, 16384);
|
|
6105
|
+
if (!tail.trim()) return;
|
|
6106
|
+
types.logger.debug(
|
|
6107
|
+
`[DAEMON RUN] Session runtime log tail for ${sessionId} (pid=${pid}, log=${path$1.basename(logPath)}):
|
|
6108
|
+
` + tail
|
|
6109
|
+
);
|
|
6110
|
+
})();
|
|
6111
|
+
scheduleAutoRestart({
|
|
6112
|
+
sessionId,
|
|
6113
|
+
spawnOptions: tracked.spawnOptions,
|
|
6114
|
+
reason: `exit(code=${code ?? "null"},signal=${signal ?? "null"})`
|
|
6115
|
+
});
|
|
5798
6116
|
};
|
|
5799
6117
|
const { port: controlPort, stop } = await startDaemonControlServer({
|
|
5800
6118
|
getChildren: getCurrentChildren,
|
|
6119
|
+
getStatus: () => ({
|
|
6120
|
+
profile: types.configuration.profile,
|
|
6121
|
+
serverUrl: types.configuration.serverUrl,
|
|
6122
|
+
webappUrl: types.configuration.webappUrl,
|
|
6123
|
+
orgId: credentials?.orgId || null,
|
|
6124
|
+
machineId,
|
|
6125
|
+
daemonPid: process.pid,
|
|
6126
|
+
daemonHttpPort: controlPort,
|
|
6127
|
+
startedWithCliVersion: types.packageJson.version,
|
|
6128
|
+
machine: machineRef ? { id: machineRef.id, seq: machineRef.seq || 0 } : null,
|
|
6129
|
+
connection: apiMachine ? apiMachine.getStatusSnapshot() : {
|
|
6130
|
+
connected: false,
|
|
6131
|
+
lastConnectError: null,
|
|
6132
|
+
lastDisconnectReason: null,
|
|
6133
|
+
lastHttpUpsertError: null,
|
|
6134
|
+
lastHttpUpsertStatus: null,
|
|
6135
|
+
lastHttpUpsertAt: null
|
|
6136
|
+
}
|
|
6137
|
+
}),
|
|
5801
6138
|
stopSession,
|
|
5802
6139
|
spawnSession,
|
|
5803
6140
|
requestShutdown: () => requestShutdown("flockbay-cli"),
|
|
@@ -5819,93 +6156,89 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5819
6156
|
httpPort: controlPort,
|
|
5820
6157
|
startedAt: Date.now()
|
|
5821
6158
|
};
|
|
5822
|
-
const
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
} catch (error) {
|
|
5833
|
-
const status = error?.response?.status;
|
|
5834
|
-
if (status === 404) {
|
|
5835
|
-
machine = await apiClient.getOrCreateMachine({
|
|
5836
|
-
machineId,
|
|
5837
|
-
metadata: initialMachineMetadata,
|
|
5838
|
-
daemonState: initialDaemonState
|
|
5839
|
-
});
|
|
5840
|
-
} else {
|
|
5841
|
-
throw error;
|
|
5842
|
-
}
|
|
5843
|
-
}
|
|
5844
|
-
types.logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
|
6159
|
+
const machine = {
|
|
6160
|
+
id: machineId,
|
|
6161
|
+
seq: 0,
|
|
6162
|
+
active: false,
|
|
6163
|
+
activeAt: null,
|
|
6164
|
+
createdAt: null,
|
|
6165
|
+
updatedAt: null,
|
|
6166
|
+
metadata: { ...initialMachineMetadata },
|
|
6167
|
+
daemonState: initialDaemonState
|
|
6168
|
+
};
|
|
5845
6169
|
machineRef = machine;
|
|
5846
|
-
|
|
6170
|
+
apiMachine = new types.ApiMachineClient(credentials.machineToken, machine);
|
|
5847
6171
|
apiMachine.setRPCHandlers({
|
|
5848
6172
|
spawnSession,
|
|
5849
6173
|
stopSession,
|
|
5850
6174
|
requestShutdown: () => requestShutdown("flockbay-app")
|
|
5851
6175
|
});
|
|
5852
|
-
|
|
6176
|
+
try {
|
|
6177
|
+
apiMachine.connect();
|
|
6178
|
+
} catch (err) {
|
|
6179
|
+
types.logger.debug("[DAEMON RUN] Failed to connect machine socket (will remain offline until restart):", err);
|
|
6180
|
+
}
|
|
5853
6181
|
const heartbeatIntervalMs = parseInt(process.env.FLOCKBAY_DAEMON_HEARTBEAT_INTERVAL || "60000");
|
|
5854
6182
|
let heartbeatRunning = false;
|
|
5855
6183
|
const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
|
|
5856
|
-
if (heartbeatRunning)
|
|
5857
|
-
return;
|
|
5858
|
-
}
|
|
6184
|
+
if (heartbeatRunning) return;
|
|
5859
6185
|
heartbeatRunning = true;
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
6186
|
+
try {
|
|
6187
|
+
if (process.env.DEBUG) {
|
|
6188
|
+
types.logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
6189
|
+
}
|
|
6190
|
+
for (const [pid, _] of pidToTrackedSession.entries()) {
|
|
6191
|
+
try {
|
|
6192
|
+
process.kill(pid, 0);
|
|
6193
|
+
} catch (error) {
|
|
6194
|
+
types.logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
|
|
6195
|
+
pidToTrackedSession.delete(pid);
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
5864
6198
|
try {
|
|
5865
|
-
|
|
6199
|
+
const projectVersion = JSON.parse(fs$3.readFileSync(path$1.join(types.projectPath(), "package.json"), "utf-8")).version;
|
|
6200
|
+
if (projectVersion !== types.configuration.currentCliVersion) {
|
|
6201
|
+
types.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
6202
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
6203
|
+
try {
|
|
6204
|
+
spawnFlockbayCLI(["daemon", "start"], {
|
|
6205
|
+
detached: true,
|
|
6206
|
+
stdio: "ignore"
|
|
6207
|
+
});
|
|
6208
|
+
} catch (error) {
|
|
6209
|
+
types.logger.debug("[DAEMON RUN] Failed to spawn new daemon, this can happen during integration tests", error);
|
|
6210
|
+
}
|
|
6211
|
+
types.logger.debug("[DAEMON RUN] Hanging briefly - waiting for CLI to kill us due to stale version");
|
|
6212
|
+
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
6213
|
+
process.exit(0);
|
|
6214
|
+
}
|
|
5866
6215
|
} catch (error) {
|
|
5867
|
-
types.logger.debug(
|
|
5868
|
-
|
|
6216
|
+
types.logger.debug("[DAEMON RUN] Failed to check CLI version during health check", error);
|
|
6217
|
+
}
|
|
6218
|
+
const daemonState = await types.readDaemonState();
|
|
6219
|
+
if (daemonState && daemonState.pid !== process.pid) {
|
|
6220
|
+
types.logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
|
|
6221
|
+
requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
|
|
5869
6222
|
}
|
|
5870
|
-
}
|
|
5871
|
-
const projectVersion = JSON.parse(fs$3.readFileSync(path$1.join(types.projectPath(), "package.json"), "utf-8")).version;
|
|
5872
|
-
if (projectVersion !== types.configuration.currentCliVersion) {
|
|
5873
|
-
types.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
5874
|
-
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
5875
6223
|
try {
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
6224
|
+
const updatedState = {
|
|
6225
|
+
pid: process.pid,
|
|
6226
|
+
httpPort: controlPort,
|
|
6227
|
+
startTime: fileState.startTime,
|
|
6228
|
+
startedWithCliVersion: types.packageJson.version,
|
|
6229
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
6230
|
+
daemonLogPath: fileState.daemonLogPath
|
|
6231
|
+
};
|
|
6232
|
+
types.writeDaemonState(updatedState);
|
|
6233
|
+
if (process.env.DEBUG) {
|
|
6234
|
+
types.logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
|
|
6235
|
+
}
|
|
5880
6236
|
} catch (error) {
|
|
5881
|
-
types.logger.debug("[DAEMON RUN] Failed to
|
|
5882
|
-
}
|
|
5883
|
-
types.logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
|
|
5884
|
-
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
5885
|
-
process.exit(0);
|
|
5886
|
-
}
|
|
5887
|
-
const daemonState = await types.readDaemonState();
|
|
5888
|
-
if (daemonState && daemonState.pid !== process.pid) {
|
|
5889
|
-
types.logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
|
|
5890
|
-
requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
|
|
5891
|
-
}
|
|
5892
|
-
try {
|
|
5893
|
-
const updatedState = {
|
|
5894
|
-
pid: process.pid,
|
|
5895
|
-
httpPort: controlPort,
|
|
5896
|
-
startTime: fileState.startTime,
|
|
5897
|
-
startedWithCliVersion: types.packageJson.version,
|
|
5898
|
-
lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
5899
|
-
daemonLogPath: fileState.daemonLogPath
|
|
5900
|
-
};
|
|
5901
|
-
types.writeDaemonState(updatedState);
|
|
5902
|
-
if (process.env.DEBUG) {
|
|
5903
|
-
types.logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
|
|
6237
|
+
types.logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
|
|
5904
6238
|
}
|
|
5905
|
-
}
|
|
5906
|
-
|
|
6239
|
+
} finally {
|
|
6240
|
+
heartbeatRunning = false;
|
|
5907
6241
|
}
|
|
5908
|
-
heartbeatRunning = false;
|
|
5909
6242
|
}, heartbeatIntervalMs);
|
|
5910
6243
|
const cleanupAndShutdown = async (source, errorMessage) => {
|
|
5911
6244
|
types.logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
|
|
@@ -5914,13 +6247,15 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5914
6247
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
5915
6248
|
types.logger.debug("[DAEMON RUN] Health check interval cleared");
|
|
5916
6249
|
}
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
6250
|
+
try {
|
|
6251
|
+
await apiMachine?.updateDaemonStateOnce?.((state) => ({
|
|
6252
|
+
...state,
|
|
6253
|
+
status: "shutting-down",
|
|
6254
|
+
shutdownRequestedAt: Date.now(),
|
|
6255
|
+
shutdownSource: source
|
|
6256
|
+
}));
|
|
6257
|
+
} catch {
|
|
6258
|
+
}
|
|
5924
6259
|
if (killSessionsOnShutdown) {
|
|
5925
6260
|
try {
|
|
5926
6261
|
const tracked = Array.from(pidToTrackedSession.entries());
|
|
@@ -5971,7 +6306,7 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5971
6306
|
} else {
|
|
5972
6307
|
types.logger.debug("[DAEMON RUN] Preserving session processes across daemon shutdown");
|
|
5973
6308
|
}
|
|
5974
|
-
apiMachine
|
|
6309
|
+
apiMachine?.shutdown();
|
|
5975
6310
|
try {
|
|
5976
6311
|
unrealMcpChild?.kill();
|
|
5977
6312
|
} catch {
|
|
@@ -5986,7 +6321,12 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5986
6321
|
process.exit(0);
|
|
5987
6322
|
};
|
|
5988
6323
|
types.logger.debug("[DAEMON RUN] Daemon started successfully, waiting for shutdown request");
|
|
6324
|
+
startupCompleted = true;
|
|
5989
6325
|
const shutdownRequest = await resolvesWhenShutdownRequested;
|
|
6326
|
+
if (startupForceExitTimer) {
|
|
6327
|
+
clearTimeout(startupForceExitTimer);
|
|
6328
|
+
startupForceExitTimer = null;
|
|
6329
|
+
}
|
|
5990
6330
|
await cleanupAndShutdown(shutdownRequest.source, shutdownRequest.errorMessage);
|
|
5991
6331
|
} catch (error) {
|
|
5992
6332
|
types.logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
|
|
@@ -6029,12 +6369,259 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
6029
6369
|
async function runMechanicRun(_input) {
|
|
6030
6370
|
return {
|
|
6031
6371
|
ok: false,
|
|
6032
|
-
errorMessage: "Mechanic runs are disabled
|
|
6372
|
+
errorMessage: "Mechanic runs are disabled.",
|
|
6033
6373
|
hint: "Remove reliance on `unreal_mechanic_run`, or re-introduce this tool as a future feature built on project-local tooling.",
|
|
6034
6374
|
artifactsDir: null
|
|
6035
6375
|
};
|
|
6036
6376
|
}
|
|
6037
6377
|
|
|
6378
|
+
function targetPlatform() {
|
|
6379
|
+
if (process.platform === "win32") return "Win64";
|
|
6380
|
+
if (process.platform === "darwin") return "Mac";
|
|
6381
|
+
return "Linux";
|
|
6382
|
+
}
|
|
6383
|
+
function buildScriptPath(engineRoot) {
|
|
6384
|
+
if (process.platform === "win32") {
|
|
6385
|
+
return path.join(engineRoot, "Engine", "Build", "BatchFiles", "Build.bat");
|
|
6386
|
+
}
|
|
6387
|
+
return path.join(engineRoot, "Engine", "Build", "BatchFiles", "Build.sh");
|
|
6388
|
+
}
|
|
6389
|
+
function parseBuildIssuesFromText(text, limit) {
|
|
6390
|
+
const issues = [];
|
|
6391
|
+
let errors = 0;
|
|
6392
|
+
let warnings = 0;
|
|
6393
|
+
let truncated = false;
|
|
6394
|
+
const clangRe = /^(.+?):(\d+):(\d+):\s+(warning|error):\s+(.*)$/;
|
|
6395
|
+
const msvcRe = /^(.+?)\((\d+)\):\s+(warning|error)\s+([A-Z]+\d+):\s+(.*)$/;
|
|
6396
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
6397
|
+
for (const line of lines) {
|
|
6398
|
+
let m = clangRe.exec(line);
|
|
6399
|
+
if (m) {
|
|
6400
|
+
const severity = m[4] === "warning" ? "warning" : "error";
|
|
6401
|
+
if (severity === "error") errors += 1;
|
|
6402
|
+
else warnings += 1;
|
|
6403
|
+
if (issues.length < limit) {
|
|
6404
|
+
issues.push({
|
|
6405
|
+
file: m[1] ? String(m[1]) : null,
|
|
6406
|
+
line: Number(m[2]) || null,
|
|
6407
|
+
column: Number(m[3]) || null,
|
|
6408
|
+
severity,
|
|
6409
|
+
code: null,
|
|
6410
|
+
message: String(m[5] ?? "").trim()
|
|
6411
|
+
});
|
|
6412
|
+
} else {
|
|
6413
|
+
truncated = true;
|
|
6414
|
+
}
|
|
6415
|
+
continue;
|
|
6416
|
+
}
|
|
6417
|
+
m = msvcRe.exec(line);
|
|
6418
|
+
if (m) {
|
|
6419
|
+
const severity = m[3] === "warning" ? "warning" : "error";
|
|
6420
|
+
if (severity === "error") errors += 1;
|
|
6421
|
+
else warnings += 1;
|
|
6422
|
+
if (issues.length < limit) {
|
|
6423
|
+
issues.push({
|
|
6424
|
+
file: m[1] ? String(m[1]) : null,
|
|
6425
|
+
line: Number(m[2]) || null,
|
|
6426
|
+
column: null,
|
|
6427
|
+
severity,
|
|
6428
|
+
code: m[4] ? String(m[4]) : null,
|
|
6429
|
+
message: String(m[5] ?? "").trim()
|
|
6430
|
+
});
|
|
6431
|
+
} else {
|
|
6432
|
+
truncated = true;
|
|
6433
|
+
}
|
|
6434
|
+
continue;
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
return { issues, errors, warnings, truncated };
|
|
6438
|
+
}
|
|
6439
|
+
async function buildUnrealProject(args) {
|
|
6440
|
+
const uprojectPath = args.uprojectPath;
|
|
6441
|
+
const engineRoot = args.engineRoot;
|
|
6442
|
+
const configuration = args.configuration ?? "Development";
|
|
6443
|
+
const target = args.target ?? "Editor";
|
|
6444
|
+
const timeoutMs = Math.max(3e4, args.timeoutMs ?? 20 * 6e4);
|
|
6445
|
+
const issuesLimit = Math.max(1, Math.min(2e3, args.issuesLimit ?? 250));
|
|
6446
|
+
if (!uprojectPath || !path.isAbsolute(uprojectPath) || !uprojectPath.toLowerCase().endsWith(".uproject")) {
|
|
6447
|
+
return {
|
|
6448
|
+
ok: false,
|
|
6449
|
+
exitCode: null,
|
|
6450
|
+
logPath: "",
|
|
6451
|
+
issues: [],
|
|
6452
|
+
errors: 0,
|
|
6453
|
+
warnings: 0,
|
|
6454
|
+
truncated: false,
|
|
6455
|
+
errorMessage: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}`
|
|
6456
|
+
};
|
|
6457
|
+
}
|
|
6458
|
+
if (!fs.existsSync(uprojectPath)) {
|
|
6459
|
+
return {
|
|
6460
|
+
ok: false,
|
|
6461
|
+
exitCode: null,
|
|
6462
|
+
logPath: "",
|
|
6463
|
+
issues: [],
|
|
6464
|
+
errors: 0,
|
|
6465
|
+
warnings: 0,
|
|
6466
|
+
truncated: false,
|
|
6467
|
+
errorMessage: `uprojectPath not found: ${uprojectPath}`
|
|
6468
|
+
};
|
|
6469
|
+
}
|
|
6470
|
+
const script = buildScriptPath(engineRoot);
|
|
6471
|
+
if (!fs.existsSync(script)) {
|
|
6472
|
+
return {
|
|
6473
|
+
ok: false,
|
|
6474
|
+
exitCode: null,
|
|
6475
|
+
logPath: "",
|
|
6476
|
+
issues: [],
|
|
6477
|
+
errors: 0,
|
|
6478
|
+
warnings: 0,
|
|
6479
|
+
truncated: false,
|
|
6480
|
+
errorMessage: `Missing Unreal build script: ${script}`
|
|
6481
|
+
};
|
|
6482
|
+
}
|
|
6483
|
+
const projectName = path.basename(uprojectPath).replace(/\.uproject$/i, "");
|
|
6484
|
+
const targetName = target === "Editor" ? `${projectName}Editor` : projectName;
|
|
6485
|
+
const platform = targetPlatform();
|
|
6486
|
+
const logDir = args.logDir ?? path.join(path.dirname(uprojectPath), "Saved", "Logs", "Flockbay");
|
|
6487
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
6488
|
+
const logPath = path.join(logDir, `flockbay_build_${Date.now()}.log`);
|
|
6489
|
+
const buildArgs = [
|
|
6490
|
+
targetName,
|
|
6491
|
+
platform,
|
|
6492
|
+
configuration,
|
|
6493
|
+
`-Project=${uprojectPath}`,
|
|
6494
|
+
"-WaitMutex",
|
|
6495
|
+
"-NoHotReload"
|
|
6496
|
+
];
|
|
6497
|
+
const logStream = fs.createWriteStream(logPath, { flags: "a" });
|
|
6498
|
+
logStream.write(`engineRoot: ${engineRoot}
|
|
6499
|
+
`);
|
|
6500
|
+
logStream.write(`uprojectPath: ${uprojectPath}
|
|
6501
|
+
`);
|
|
6502
|
+
logStream.write(`script: ${script}
|
|
6503
|
+
`);
|
|
6504
|
+
logStream.write(`args: ${JSON.stringify(buildArgs)}
|
|
6505
|
+
|
|
6506
|
+
`);
|
|
6507
|
+
const child = process.platform === "win32" ? node_child_process.spawn("cmd.exe", ["/c", script, ...buildArgs], { stdio: ["ignore", "pipe", "pipe"] }) : node_child_process.spawn(script, buildArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
6508
|
+
let combinedTail = "";
|
|
6509
|
+
const appendTail = (chunk) => {
|
|
6510
|
+
combinedTail += chunk.toString("utf8");
|
|
6511
|
+
if (combinedTail.length > 2e6) combinedTail = combinedTail.slice(-4e5);
|
|
6512
|
+
};
|
|
6513
|
+
child.stdout?.on("data", (chunk) => {
|
|
6514
|
+
logStream.write(chunk);
|
|
6515
|
+
appendTail(chunk);
|
|
6516
|
+
});
|
|
6517
|
+
child.stderr?.on("data", (chunk) => {
|
|
6518
|
+
logStream.write(chunk);
|
|
6519
|
+
appendTail(chunk);
|
|
6520
|
+
});
|
|
6521
|
+
const exitCode = await new Promise((resolve) => {
|
|
6522
|
+
const timer = setTimeout(() => {
|
|
6523
|
+
try {
|
|
6524
|
+
child.kill("SIGKILL");
|
|
6525
|
+
} catch {
|
|
6526
|
+
}
|
|
6527
|
+
resolve(null);
|
|
6528
|
+
}, timeoutMs);
|
|
6529
|
+
child.on("close", (code) => {
|
|
6530
|
+
clearTimeout(timer);
|
|
6531
|
+
resolve(code);
|
|
6532
|
+
});
|
|
6533
|
+
child.on("error", () => {
|
|
6534
|
+
clearTimeout(timer);
|
|
6535
|
+
resolve(null);
|
|
6536
|
+
});
|
|
6537
|
+
});
|
|
6538
|
+
logStream.end(`
|
|
6539
|
+
exitCode: ${exitCode}
|
|
6540
|
+
`);
|
|
6541
|
+
const parsed = parseBuildIssuesFromText(combinedTail, issuesLimit);
|
|
6542
|
+
const ok = exitCode === 0;
|
|
6543
|
+
if (!ok) {
|
|
6544
|
+
return {
|
|
6545
|
+
ok: false,
|
|
6546
|
+
exitCode,
|
|
6547
|
+
logPath,
|
|
6548
|
+
...parsed,
|
|
6549
|
+
errorMessage: exitCode === null ? `Build timed out after ${timeoutMs}ms. Log: ${logPath}` : `Build failed (exitCode=${exitCode}). Log: ${logPath}`
|
|
6550
|
+
};
|
|
6551
|
+
}
|
|
6552
|
+
return {
|
|
6553
|
+
ok: true,
|
|
6554
|
+
exitCode: exitCode ?? 0,
|
|
6555
|
+
logPath,
|
|
6556
|
+
...parsed
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
|
|
6560
|
+
function stampForFilename() {
|
|
6561
|
+
const d = /* @__PURE__ */ new Date();
|
|
6562
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
6563
|
+
const yyyy = d.getFullYear();
|
|
6564
|
+
const mm = pad(d.getMonth() + 1);
|
|
6565
|
+
const dd = pad(d.getDate());
|
|
6566
|
+
const hh = pad(d.getHours());
|
|
6567
|
+
const mi = pad(d.getMinutes());
|
|
6568
|
+
const ss = pad(d.getSeconds());
|
|
6569
|
+
return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
|
|
6570
|
+
}
|
|
6571
|
+
async function runUnrealSmokeTest(args) {
|
|
6572
|
+
const startedAtMs = Date.now();
|
|
6573
|
+
const timeoutMs = Math.max(5e3, args.timeoutMs ?? 3e4);
|
|
6574
|
+
const stabilizeMs = Math.max(250, args.stabilizeMs ?? 1500);
|
|
6575
|
+
const stopIfPlaying = args.stopIfPlaying ?? true;
|
|
6576
|
+
const screenshotDir = path.join(path.dirname(args.uprojectPath), "Saved", "Screenshots", "Flockbay");
|
|
6577
|
+
await fs$2.mkdir(screenshotDir, { recursive: true });
|
|
6578
|
+
const screenshotPath = path.join(screenshotDir, `Flockbay_smoke_test_${stampForFilename()}.png`);
|
|
6579
|
+
let started = false;
|
|
6580
|
+
let stopped = false;
|
|
6581
|
+
try {
|
|
6582
|
+
const status = await types.sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", params: {}, timeoutMs: Math.min(timeoutMs, 5e3) });
|
|
6583
|
+
const result = status?.result && typeof status.result === "object" ? status.result : status;
|
|
6584
|
+
const isPlaying = typeof result?.isPlaying === "boolean" ? result.isPlaying : null;
|
|
6585
|
+
if (isPlaying) {
|
|
6586
|
+
if (!stopIfPlaying) {
|
|
6587
|
+
return {
|
|
6588
|
+
ok: false,
|
|
6589
|
+
durationMs: Date.now() - startedAtMs,
|
|
6590
|
+
started: false,
|
|
6591
|
+
stopped: false,
|
|
6592
|
+
screenshotPath: null,
|
|
6593
|
+
errorMessage: "PIE is currently running. Stop PIE first or re-run with stopIfPlaying=true."
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
await types.sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", params: {}, timeoutMs: Math.min(timeoutMs, 1e4) });
|
|
6597
|
+
}
|
|
6598
|
+
const playRes = await types.sendUnrealMcpTcpCommand({ type: "play_in_editor_windowed", params: {}, timeoutMs: Math.min(timeoutMs, 1e4) });
|
|
6599
|
+
const playResult = playRes?.result && typeof playRes.result === "object" ? playRes.result : playRes;
|
|
6600
|
+
started = typeof playResult?.started === "boolean" ? playResult.started : true;
|
|
6601
|
+
await new Promise((r) => setTimeout(r, stabilizeMs));
|
|
6602
|
+
await types.sendUnrealMcpTcpCommand({ type: "take_screenshot", params: { filepath: screenshotPath }, timeoutMs: Math.min(timeoutMs, 2e4) });
|
|
6603
|
+
await types.sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", params: {}, timeoutMs: Math.min(timeoutMs, 1e4) });
|
|
6604
|
+
stopped = true;
|
|
6605
|
+
return {
|
|
6606
|
+
ok: true,
|
|
6607
|
+
durationMs: Date.now() - startedAtMs,
|
|
6608
|
+
started,
|
|
6609
|
+
stopped,
|
|
6610
|
+
screenshotPath
|
|
6611
|
+
};
|
|
6612
|
+
} catch (err) {
|
|
6613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6614
|
+
return {
|
|
6615
|
+
ok: false,
|
|
6616
|
+
durationMs: Date.now() - startedAtMs,
|
|
6617
|
+
started,
|
|
6618
|
+
stopped,
|
|
6619
|
+
screenshotPath: screenshotPath || null,
|
|
6620
|
+
errorMessage: message
|
|
6621
|
+
};
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
|
|
6038
6625
|
function safeJsonParse(value) {
|
|
6039
6626
|
try {
|
|
6040
6627
|
return JSON.parse(value);
|
|
@@ -6251,12 +6838,172 @@ class ElicitationHub {
|
|
|
6251
6838
|
}
|
|
6252
6839
|
}
|
|
6253
6840
|
|
|
6841
|
+
function encodeBase64(buffer, variant = "base64") {
|
|
6842
|
+
if (variant === "base64url") {
|
|
6843
|
+
return encodeBase64Url(buffer);
|
|
6844
|
+
}
|
|
6845
|
+
return Buffer.from(buffer).toString("base64");
|
|
6846
|
+
}
|
|
6847
|
+
function encodeBase64Url(buffer) {
|
|
6848
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
6849
|
+
}
|
|
6850
|
+
function getRandomBytes(size) {
|
|
6851
|
+
return new Uint8Array(node_crypto.randomBytes(size));
|
|
6852
|
+
}
|
|
6853
|
+
function encryptWithDataKey(data, dataKey) {
|
|
6854
|
+
const nonce = getRandomBytes(12);
|
|
6855
|
+
const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
6856
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
6857
|
+
const encrypted = Buffer.concat([
|
|
6858
|
+
cipher.update(plaintext),
|
|
6859
|
+
cipher.final()
|
|
6860
|
+
]);
|
|
6861
|
+
const authTag = cipher.getAuthTag();
|
|
6862
|
+
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
6863
|
+
bundle.set([0], 0);
|
|
6864
|
+
bundle.set(nonce, 1);
|
|
6865
|
+
bundle.set(new Uint8Array(encrypted), 13);
|
|
6866
|
+
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
6867
|
+
return bundle;
|
|
6868
|
+
}
|
|
6869
|
+
function encrypt(key, data) {
|
|
6870
|
+
return encryptWithDataKey(data, key);
|
|
6871
|
+
}
|
|
6872
|
+
|
|
6254
6873
|
function deriveScreenshotViewIdFromFilename(name) {
|
|
6255
6874
|
const base = name.replace(/\.[^.]+$/, "");
|
|
6256
6875
|
const prefixed = /^Flockbay_(.+)$/.exec(base);
|
|
6257
6876
|
const raw = (prefixed ? prefixed[1] : base).trim();
|
|
6258
6877
|
return raw.replace(/_\d{8}-\d{6}$/, "").trim() || base;
|
|
6259
6878
|
}
|
|
6879
|
+
async function readJsonFile(filePath) {
|
|
6880
|
+
const raw = await fs$2.readFile(filePath, "utf8");
|
|
6881
|
+
return JSON.parse(raw);
|
|
6882
|
+
}
|
|
6883
|
+
function parseMajorMinorOrNull(raw) {
|
|
6884
|
+
if (typeof raw !== "string") return null;
|
|
6885
|
+
const m = raw.trim().match(/(\d+)\.(\d+)/);
|
|
6886
|
+
if (!m) return null;
|
|
6887
|
+
const major = Number(m[1]);
|
|
6888
|
+
const minor = Number(m[2]);
|
|
6889
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
|
|
6890
|
+
return { major, minor };
|
|
6891
|
+
}
|
|
6892
|
+
async function readUprojectEngineAssociationOrNull(uprojectPath) {
|
|
6893
|
+
try {
|
|
6894
|
+
const json = await readJsonFile(uprojectPath);
|
|
6895
|
+
return parseMajorMinorOrNull(json?.EngineAssociation);
|
|
6896
|
+
} catch {
|
|
6897
|
+
return null;
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
function isValidEngineRoot(engineRoot) {
|
|
6901
|
+
if (!engineRoot) return false;
|
|
6902
|
+
if (!fs.existsSync(engineRoot)) return false;
|
|
6903
|
+
if (!fs.existsSync(path.join(engineRoot, "Engine"))) return false;
|
|
6904
|
+
const buildVersion = path.join(engineRoot, "Engine", "Build", "Build.version");
|
|
6905
|
+
if (fs.existsSync(buildVersion)) return true;
|
|
6906
|
+
const editorCmd = process.platform === "darwin" ? path.join(engineRoot, "Engine", "Binaries", "Mac", "UnrealEditor-Cmd") : process.platform === "win32" ? path.join(engineRoot, "Engine", "Binaries", "Win64", "UnrealEditor-Cmd.exe") : process.platform === "linux" ? path.join(engineRoot, "Engine", "Binaries", "Linux", "UnrealEditor-Cmd") : "";
|
|
6907
|
+
if (editorCmd && fs.existsSync(editorCmd)) return true;
|
|
6908
|
+
return false;
|
|
6909
|
+
}
|
|
6910
|
+
function engineRootCandidatesForVersion(platform, version) {
|
|
6911
|
+
const suffix = `${version.major}.${version.minor}`;
|
|
6912
|
+
if (platform === "darwin") {
|
|
6913
|
+
return [
|
|
6914
|
+
`/Users/Shared/Epic Games/UE_${suffix}`,
|
|
6915
|
+
`/Applications/Epic Games/UE_${suffix}`,
|
|
6916
|
+
`/Applications/Unreal Engine/UE_${suffix}`
|
|
6917
|
+
];
|
|
6918
|
+
}
|
|
6919
|
+
if (platform === "win32") {
|
|
6920
|
+
return [
|
|
6921
|
+
`C:\\\\Program Files\\\\Epic Games\\\\UE_${suffix}`
|
|
6922
|
+
];
|
|
6923
|
+
}
|
|
6924
|
+
return [];
|
|
6925
|
+
}
|
|
6926
|
+
async function readEngineRootMajorMinorOrNull(engineRoot) {
|
|
6927
|
+
const buildVersionPath = path.join(engineRoot, "Engine", "Build", "Build.version");
|
|
6928
|
+
try {
|
|
6929
|
+
if (fs.existsSync(buildVersionPath)) {
|
|
6930
|
+
const json = await readJsonFile(buildVersionPath);
|
|
6931
|
+
const major2 = Number(json?.MajorVersion);
|
|
6932
|
+
const minor2 = Number(json?.MinorVersion);
|
|
6933
|
+
if (Number.isFinite(major2) && Number.isFinite(minor2)) return { major: major2, minor: minor2 };
|
|
6934
|
+
}
|
|
6935
|
+
} catch {
|
|
6936
|
+
}
|
|
6937
|
+
const m = engineRoot.match(/UE_(\d+)\.(\d+)/);
|
|
6938
|
+
if (!m) return null;
|
|
6939
|
+
const major = Number(m[1]);
|
|
6940
|
+
const minor = Number(m[2]);
|
|
6941
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
|
|
6942
|
+
return { major, minor };
|
|
6943
|
+
}
|
|
6944
|
+
async function assertEngineRootMatchesUprojectOrThrow(args) {
|
|
6945
|
+
const engineAssociation = await readUprojectEngineAssociationOrNull(args.uprojectPath);
|
|
6946
|
+
if (!engineAssociation) return;
|
|
6947
|
+
const engineRootVersion = await readEngineRootMajorMinorOrNull(args.engineRoot);
|
|
6948
|
+
if (!engineRootVersion) return;
|
|
6949
|
+
if (engineRootVersion.major !== engineAssociation.major || engineRootVersion.minor !== engineAssociation.minor) {
|
|
6950
|
+
const candidates = engineRootCandidatesForVersion(process.platform, engineAssociation);
|
|
6951
|
+
const suggestedEngineRoot = candidates.find((c) => isValidEngineRoot(c)) || candidates[0] || null;
|
|
6952
|
+
throw new Error(
|
|
6953
|
+
[
|
|
6954
|
+
`Engine mismatch: this project targets UE ${engineAssociation.major}.${engineAssociation.minor} (EngineAssociation),`,
|
|
6955
|
+
`but the provided engineRoot is UE ${engineRootVersion.major}.${engineRootVersion.minor}: ${args.engineRoot}`,
|
|
6956
|
+
"",
|
|
6957
|
+
args.source === "env" ? `Fix: update ENGINE_ROOT to a matching UE install (or unset it and let the tool infer from the .uproject).` : `Fix: pass a matching engineRoot (or omit it and let the tool infer from the .uproject).`,
|
|
6958
|
+
...suggestedEngineRoot ? [`Example engineRoot: ${suggestedEngineRoot}`] : []
|
|
6959
|
+
].join("\n")
|
|
6960
|
+
);
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
async function resolveEngineRootForUproject(args) {
|
|
6964
|
+
const fromArg = (args.engineRootArg || "").trim();
|
|
6965
|
+
if (fromArg) {
|
|
6966
|
+
if (!isValidEngineRoot(fromArg)) {
|
|
6967
|
+
return { ok: false, errorMessage: `Invalid engineRoot (expected a UE install root containing Engine/\u2026): ${fromArg}`, suggestedEngineRoot: null };
|
|
6968
|
+
}
|
|
6969
|
+
try {
|
|
6970
|
+
await assertEngineRootMatchesUprojectOrThrow({ uprojectPath: args.uprojectPath, engineRoot: fromArg, source: "arg" });
|
|
6971
|
+
} catch (e) {
|
|
6972
|
+
return { ok: false, errorMessage: e instanceof Error ? e.message : String(e), suggestedEngineRoot: null };
|
|
6973
|
+
}
|
|
6974
|
+
return { ok: true, engineRoot: fromArg, source: "arg", engineAssociation: await readUprojectEngineAssociationOrNull(args.uprojectPath) };
|
|
6975
|
+
}
|
|
6976
|
+
const fromEnv = (process.env.UE_ENGINE_ROOT || process.env.ENGINE_ROOT || "").trim();
|
|
6977
|
+
if (fromEnv) {
|
|
6978
|
+
if (!isValidEngineRoot(fromEnv)) {
|
|
6979
|
+
return { ok: false, errorMessage: `ENGINE_ROOT is set but invalid (expected a UE install root containing Engine/\u2026): ${fromEnv}`, suggestedEngineRoot: null };
|
|
6980
|
+
}
|
|
6981
|
+
try {
|
|
6982
|
+
await assertEngineRootMatchesUprojectOrThrow({ uprojectPath: args.uprojectPath, engineRoot: fromEnv, source: "env" });
|
|
6983
|
+
} catch (e) {
|
|
6984
|
+
return { ok: false, errorMessage: e instanceof Error ? e.message : String(e), suggestedEngineRoot: null };
|
|
6985
|
+
}
|
|
6986
|
+
return { ok: true, engineRoot: fromEnv, source: "env", engineAssociation: await readUprojectEngineAssociationOrNull(args.uprojectPath) };
|
|
6987
|
+
}
|
|
6988
|
+
const engineAssociation = await readUprojectEngineAssociationOrNull(args.uprojectPath);
|
|
6989
|
+
if (!engineAssociation) {
|
|
6990
|
+
return {
|
|
6991
|
+
ok: false,
|
|
6992
|
+
errorMessage: "Missing `engineRoot` (and UE_ENGINE_ROOT / ENGINE_ROOT not set). Unable to infer the engine version from the .uproject EngineAssociation.",
|
|
6993
|
+
suggestedEngineRoot: null
|
|
6994
|
+
};
|
|
6995
|
+
}
|
|
6996
|
+
const candidates = engineRootCandidatesForVersion(process.platform, engineAssociation);
|
|
6997
|
+
const valid = candidates.filter((c) => isValidEngineRoot(c));
|
|
6998
|
+
if (valid.length > 0) {
|
|
6999
|
+
return { ok: true, engineRoot: valid[0], source: "uproject", engineAssociation };
|
|
7000
|
+
}
|
|
7001
|
+
return {
|
|
7002
|
+
ok: false,
|
|
7003
|
+
errorMessage: `Missing \`engineRoot\` (and UE_ENGINE_ROOT / ENGINE_ROOT not set). This project targets UE ${engineAssociation.major}.${engineAssociation.minor} but no standard engine install was found.`,
|
|
7004
|
+
suggestedEngineRoot: candidates[0] || null
|
|
7005
|
+
};
|
|
7006
|
+
}
|
|
6260
7007
|
async function runCmdAndCapture(args) {
|
|
6261
7008
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
6262
7009
|
const child = node_child_process.spawn(args.cmd, args.cmdArgs, { stdio: ["ignore", "pipe", "pipe"], cwd: args.cwd, env: args.env });
|
|
@@ -6302,7 +7049,9 @@ async function uploadScreenshotViewsForSession(args) {
|
|
|
6302
7049
|
const res = await fetch(endpoint, {
|
|
6303
7050
|
method: "POST",
|
|
6304
7051
|
headers: {
|
|
6305
|
-
|
|
7052
|
+
// This tool runs inside the CLI/daemon context, so we authenticate as the machine.
|
|
7053
|
+
// The backend accepts `Machine <token>` for machine-scoped auth.
|
|
7054
|
+
Authorization: `Machine ${args.token}`
|
|
6306
7055
|
},
|
|
6307
7056
|
body: form
|
|
6308
7057
|
});
|
|
@@ -6338,11 +7087,14 @@ async function startFlockbayServer(client, options) {
|
|
|
6338
7087
|
const handler = async (title) => {
|
|
6339
7088
|
types.logger.debug("[flockbayMCP] Changing title to:", title);
|
|
6340
7089
|
try {
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
7090
|
+
const trimmed = String(title || "").trim();
|
|
7091
|
+
if (!trimmed) {
|
|
7092
|
+
return { success: false, error: "Missing title" };
|
|
7093
|
+
}
|
|
7094
|
+
client.updateMetadata((current) => ({
|
|
7095
|
+
...current || {},
|
|
7096
|
+
name: trimmed
|
|
7097
|
+
}));
|
|
6346
7098
|
return { success: true };
|
|
6347
7099
|
} catch (error) {
|
|
6348
7100
|
return { success: false, error: String(error) };
|
|
@@ -6371,7 +7123,7 @@ async function startFlockbayServer(client, options) {
|
|
|
6371
7123
|
const res = await fetch(`${types.configuration.serverUrl.replace(/\/+$/, "")}${pathname}`, {
|
|
6372
7124
|
method: "POST",
|
|
6373
7125
|
headers: {
|
|
6374
|
-
Authorization: `
|
|
7126
|
+
Authorization: `Machine ${client.getAuthToken()}`,
|
|
6375
7127
|
"Content-Type": "application/json"
|
|
6376
7128
|
},
|
|
6377
7129
|
body: JSON.stringify(body ?? {})
|
|
@@ -6396,6 +7148,166 @@ async function startFlockbayServer(client, options) {
|
|
|
6396
7148
|
const meta = client?.metadata;
|
|
6397
7149
|
return String(meta?.path || "").trim() || process.cwd();
|
|
6398
7150
|
};
|
|
7151
|
+
const unrealIssueEmitter = new node_events.EventEmitter();
|
|
7152
|
+
const unrealEditorSupervisor = (() => {
|
|
7153
|
+
const state = {
|
|
7154
|
+
lastActivityAtMs: 0,
|
|
7155
|
+
lastReachableAtMs: 0,
|
|
7156
|
+
lastIssueAtMs: 0,
|
|
7157
|
+
lastIssueKey: "",
|
|
7158
|
+
launched: null
|
|
7159
|
+
};
|
|
7160
|
+
const emitIssue = (event) => {
|
|
7161
|
+
const key = `${event.kind}:${event.severity}:${event.message}`;
|
|
7162
|
+
const now = event.detectedAtMs;
|
|
7163
|
+
if (state.lastIssueKey === key && now - state.lastIssueAtMs < 15e3) return;
|
|
7164
|
+
state.lastIssueKey = key;
|
|
7165
|
+
state.lastIssueAtMs = now;
|
|
7166
|
+
unrealIssueEmitter.emit("issue", event);
|
|
7167
|
+
if (unrealIssueEmitter.listenerCount("issue") === 0) {
|
|
7168
|
+
try {
|
|
7169
|
+
const msg = event.kind === "process_exit" ? `Unreal Editor crashed. ${event.message}` : `Unreal Editor is not reachable. ${event.message}`;
|
|
7170
|
+
client.sendSessionEvent({ type: "message", message: msg });
|
|
7171
|
+
const socket = client?.socket;
|
|
7172
|
+
const keyBytes = client?.encryptionKey;
|
|
7173
|
+
if (socket && keyBytes) {
|
|
7174
|
+
const params = encodeBase64(encrypt(keyBytes, {}));
|
|
7175
|
+
socket.emit("rpc-call", { method: `${client.sessionId}:abort`, params }, () => {
|
|
7176
|
+
});
|
|
7177
|
+
}
|
|
7178
|
+
} catch (err) {
|
|
7179
|
+
types.logger.debug("[flockbayMCP] Failed to auto-abort after Unreal issue", err);
|
|
7180
|
+
}
|
|
7181
|
+
}
|
|
7182
|
+
};
|
|
7183
|
+
const getUnrealEditorExe = (engineRoot) => {
|
|
7184
|
+
const root = engineRoot.trim().replace(/[\\/]+$/, "");
|
|
7185
|
+
if (process.platform === "darwin") {
|
|
7186
|
+
return path.join(root, "Engine", "Binaries", "Mac", "UnrealEditor.app", "Contents", "MacOS", "UnrealEditor");
|
|
7187
|
+
}
|
|
7188
|
+
if (process.platform === "win32") {
|
|
7189
|
+
return path.join(root, "Engine", "Binaries", "Win64", "UnrealEditor.exe");
|
|
7190
|
+
}
|
|
7191
|
+
if (process.platform === "linux") {
|
|
7192
|
+
return path.join(root, "Engine", "Binaries", "Linux", "UnrealEditor");
|
|
7193
|
+
}
|
|
7194
|
+
return "";
|
|
7195
|
+
};
|
|
7196
|
+
const runCommandCapture = async (cmd, args) => {
|
|
7197
|
+
return new Promise((resolve) => {
|
|
7198
|
+
const child = node_child_process.spawn(cmd, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
7199
|
+
let stdout = "";
|
|
7200
|
+
let stderr = "";
|
|
7201
|
+
child.stdout?.on("data", (c) => stdout += c.toString("utf8"));
|
|
7202
|
+
child.stderr?.on("data", (c) => stderr += c.toString("utf8"));
|
|
7203
|
+
child.on("error", (e) => resolve({ ok: false, stdout, stderr: e instanceof Error ? e.message : String(e) }));
|
|
7204
|
+
child.on("close", (code) => resolve({ ok: code === 0, stdout, stderr }));
|
|
7205
|
+
});
|
|
7206
|
+
};
|
|
7207
|
+
const isUnrealEditorProcessRunningBestEffort = async () => {
|
|
7208
|
+
if (process.platform === "win32") {
|
|
7209
|
+
const res2 = await runCommandCapture("tasklist", ["/FI", "IMAGENAME eq UnrealEditor.exe", "/FO", "CSV", "/NH"]);
|
|
7210
|
+
const out2 = `${res2.stdout}
|
|
7211
|
+
${res2.stderr}`.toLowerCase();
|
|
7212
|
+
return out2.includes("unrealeditor.exe");
|
|
7213
|
+
}
|
|
7214
|
+
const res = await runCommandCapture("ps", ["-A", "-o", "comm="]);
|
|
7215
|
+
const out = `${res.stdout}
|
|
7216
|
+
${res.stderr}`;
|
|
7217
|
+
return /\bUnrealEditor\b/.test(out) || /\bUE4Editor\b/.test(out) || /\bUE5Editor\b/.test(out);
|
|
7218
|
+
};
|
|
7219
|
+
const tick = async () => {
|
|
7220
|
+
const now = Date.now();
|
|
7221
|
+
const activeWindowMs = 10 * 6e4;
|
|
7222
|
+
const isActive = state.lastActivityAtMs > 0 && now - state.lastActivityAtMs < activeWindowMs || Boolean(state.launched);
|
|
7223
|
+
if (!isActive) return;
|
|
7224
|
+
try {
|
|
7225
|
+
await types.sendUnrealMcpTcpCommand({ type: "ping", params: {}, timeoutMs: 750 });
|
|
7226
|
+
state.lastReachableAtMs = now;
|
|
7227
|
+
return;
|
|
7228
|
+
} catch {
|
|
7229
|
+
}
|
|
7230
|
+
if (state.launched) return;
|
|
7231
|
+
if (!state.lastReachableAtMs || now - state.lastReachableAtMs > activeWindowMs) return;
|
|
7232
|
+
const running = await isUnrealEditorProcessRunningBestEffort().catch(() => true);
|
|
7233
|
+
if (running) return;
|
|
7234
|
+
emitIssue({
|
|
7235
|
+
kind: "unreachable",
|
|
7236
|
+
severity: "warning",
|
|
7237
|
+
detectedAtMs: now,
|
|
7238
|
+
message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.",
|
|
7239
|
+
detail: {
|
|
7240
|
+
lastReachableAtMs: state.lastReachableAtMs
|
|
7241
|
+
}
|
|
7242
|
+
});
|
|
7243
|
+
};
|
|
7244
|
+
const interval = setInterval(() => {
|
|
7245
|
+
void tick();
|
|
7246
|
+
}, 2e3);
|
|
7247
|
+
interval.unref();
|
|
7248
|
+
const noteUnrealActivity = () => {
|
|
7249
|
+
state.lastActivityAtMs = Date.now();
|
|
7250
|
+
};
|
|
7251
|
+
const noteUnrealReachable = () => {
|
|
7252
|
+
state.lastReachableAtMs = Date.now();
|
|
7253
|
+
};
|
|
7254
|
+
const launchEditor = async (params) => {
|
|
7255
|
+
const uprojectPath = params.uprojectPath;
|
|
7256
|
+
const engineRoot = params.engineRoot;
|
|
7257
|
+
const extraArgs = Array.isArray(params.extraArgs) ? params.extraArgs.filter((a) => typeof a === "string" && a.trim()) : [];
|
|
7258
|
+
const exe = getUnrealEditorExe(engineRoot);
|
|
7259
|
+
if (!exe) throw new Error(`Unsupported platform for Unreal Editor launch: ${process.platform}`);
|
|
7260
|
+
if (!fs.existsSync(exe)) throw new Error(`Unreal Editor binary not found: ${exe}`);
|
|
7261
|
+
const child = node_child_process.spawn(exe, [uprojectPath, ...extraArgs], { detached: true, stdio: "ignore" });
|
|
7262
|
+
child.unref();
|
|
7263
|
+
const pid = typeof child.pid === "number" ? child.pid : 0;
|
|
7264
|
+
state.launched = {
|
|
7265
|
+
pid,
|
|
7266
|
+
uprojectPath,
|
|
7267
|
+
engineRoot,
|
|
7268
|
+
startedAtMs: Date.now()
|
|
7269
|
+
};
|
|
7270
|
+
child.on("exit", (code, signal) => {
|
|
7271
|
+
const now = Date.now();
|
|
7272
|
+
const exitCode = typeof code === "number" ? code : null;
|
|
7273
|
+
const sig = typeof signal === "string" ? signal : null;
|
|
7274
|
+
const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
|
|
7275
|
+
state.launched = null;
|
|
7276
|
+
if (!isCrash) return;
|
|
7277
|
+
emitIssue({
|
|
7278
|
+
kind: "process_exit",
|
|
7279
|
+
severity: "crash",
|
|
7280
|
+
detectedAtMs: now,
|
|
7281
|
+
message: `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
|
|
7282
|
+
detail: { exitCode, signal: sig, pid, uprojectPath }
|
|
7283
|
+
});
|
|
7284
|
+
});
|
|
7285
|
+
child.on("error", (err) => {
|
|
7286
|
+
const now = Date.now();
|
|
7287
|
+
state.launched = null;
|
|
7288
|
+
emitIssue({
|
|
7289
|
+
kind: "process_exit",
|
|
7290
|
+
severity: "crash",
|
|
7291
|
+
detectedAtMs: now,
|
|
7292
|
+
message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}`,
|
|
7293
|
+
detail: { pid, uprojectPath }
|
|
7294
|
+
});
|
|
7295
|
+
});
|
|
7296
|
+
return { pid, exePath: exe };
|
|
7297
|
+
};
|
|
7298
|
+
return {
|
|
7299
|
+
noteUnrealActivity,
|
|
7300
|
+
noteUnrealReachable,
|
|
7301
|
+
launchEditor,
|
|
7302
|
+
stop: () => {
|
|
7303
|
+
try {
|
|
7304
|
+
interval.unref();
|
|
7305
|
+
clearInterval(interval);
|
|
7306
|
+
} catch {
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
};
|
|
7310
|
+
})();
|
|
6399
7311
|
const runGit = async (cmdArgs, cwd, timeoutMs) => {
|
|
6400
7312
|
const res = await runCmdAndCapture({
|
|
6401
7313
|
cmd: "git",
|
|
@@ -7103,7 +8015,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7103
8015
|
const url = `${types.configuration.serverUrl.replace(/\/+$/, "")}/v1/sessions/${encodeURIComponent(client.sessionId)}/evidence-artifacts?limit=${limit}`;
|
|
7104
8016
|
const res = await fetch(url, {
|
|
7105
8017
|
method: "GET",
|
|
7106
|
-
headers: { Authorization: `
|
|
8018
|
+
headers: { Authorization: `Machine ${client.getAuthToken()}`, accept: "application/json" }
|
|
7107
8019
|
});
|
|
7108
8020
|
const data = await res.json().catch(() => null);
|
|
7109
8021
|
if (!res.ok) {
|
|
@@ -7128,7 +8040,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7128
8040
|
const url = `${types.configuration.serverUrl.replace(/\/+$/, "")}/v1/sessions/${encodeURIComponent(client.sessionId)}/evidence-artifacts/${encodeURIComponent(id)}`;
|
|
7129
8041
|
const res = await fetch(url, {
|
|
7130
8042
|
method: "GET",
|
|
7131
|
-
headers: { Authorization: `
|
|
8043
|
+
headers: { Authorization: `Machine ${client.getAuthToken()}`, accept: "application/json" }
|
|
7132
8044
|
});
|
|
7133
8045
|
const data = await res.json().catch(() => null);
|
|
7134
8046
|
if (!res.ok) {
|
|
@@ -7684,6 +8596,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7684
8596
|
}
|
|
7685
8597
|
return {
|
|
7686
8598
|
content,
|
|
8599
|
+
views: viewsPayload,
|
|
7687
8600
|
isError: false
|
|
7688
8601
|
};
|
|
7689
8602
|
} catch (error) {
|
|
@@ -7698,8 +8611,8 @@ ${String(st.stdout || "").trim()}`
|
|
|
7698
8611
|
})
|
|
7699
8612
|
);
|
|
7700
8613
|
mcp.registerTool("unreal_latest_screenshots", {
|
|
7701
|
-
title: "Latest Unreal Screenshots",
|
|
7702
|
-
description: "Fetch the latest PNG screenshots from `Saved/Screenshots/Flockbay/` and return a `{ views: [...] }` payload so the app can display them.",
|
|
8614
|
+
title: "Latest Unreal Screenshots (Validation)",
|
|
8615
|
+
description: "Fetch the latest PNG screenshots from `Saved/Screenshots/Flockbay/` (for validation) and return a `{ views: [...] }` payload so the app can display them.",
|
|
7703
8616
|
inputSchema: {
|
|
7704
8617
|
uprojectPath: z.z.string().describe("Absolute path to the .uproject file."),
|
|
7705
8618
|
limit: z.z.number().int().positive().optional().describe("Max number of screenshots to return (default 12)."),
|
|
@@ -7784,6 +8697,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7784
8697
|
{ type: "text", text: `Found ${views.length} screenshot${views.length === 1 ? "" : "s"} in: ${outDir}` },
|
|
7785
8698
|
{ type: "text", text: JSON.stringify({ views }, null, 2) }
|
|
7786
8699
|
],
|
|
8700
|
+
views,
|
|
7787
8701
|
isError: false
|
|
7788
8702
|
};
|
|
7789
8703
|
} catch (error) {
|
|
@@ -7819,6 +8733,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7819
8733
|
const name = typeof pluginInfo?.name === "string" ? pluginInfo.name : "UnrealMCP";
|
|
7820
8734
|
const versionName = typeof pluginInfo?.versionName === "string" ? pluginInfo.versionName : null;
|
|
7821
8735
|
const baseDir = typeof pluginInfo?.baseDir === "string" ? pluginInfo.baseDir : null;
|
|
8736
|
+
const schemaVersion = typeof pluginInfo?.schemaVersion === "number" ? pluginInfo.schemaVersion : null;
|
|
7822
8737
|
const commands = Array.isArray(pluginInfo?.commands) ? pluginInfo.commands.filter((c) => typeof c === "string") : [];
|
|
7823
8738
|
const head = [name, versionName ? `v${versionName}` : null].filter(Boolean).join(" ");
|
|
7824
8739
|
const cmdText = commands.length > 0 ? commands.slice(0, 60).join(", ") : "(no commands reported)";
|
|
@@ -7826,10 +8741,126 @@ ${String(st.stdout || "").trim()}`
|
|
|
7826
8741
|
return [
|
|
7827
8742
|
`UnrealMCP capabilities detected: ${head}`,
|
|
7828
8743
|
baseDir ? `Plugin path: ${baseDir}` : null,
|
|
8744
|
+
schemaVersion !== null ? `Schema version: ${schemaVersion}` : null,
|
|
7829
8745
|
`Supported commands: ${cmdText}${truncated}`,
|
|
7830
|
-
`Guidance: avoid guessing; prefer using
|
|
8746
|
+
`Guidance: avoid guessing; prefer using get_command_schema / list_capabilities when unsure about params.`
|
|
7831
8747
|
].filter(Boolean).join("\n");
|
|
7832
8748
|
}
|
|
8749
|
+
mcp.registerTool("unreal_editor_launch", {
|
|
8750
|
+
title: "Unreal Editor: Launch Project",
|
|
8751
|
+
description: "Launch Unreal Editor for a given .uproject (no auto-restart). If the editor later crashes or becomes unreachable, Flockbay will abort the current agent run and report it in the chat.",
|
|
8752
|
+
inputSchema: {
|
|
8753
|
+
uprojectPath: z.z.string().describe("Absolute path to the .uproject file."),
|
|
8754
|
+
engineRoot: z.z.string().optional().describe("Optional Unreal Engine install root. Defaults to ENGINE_ROOT / UE_ENGINE_ROOT, or inferred from EngineAssociation when possible."),
|
|
8755
|
+
extraArgs: z.z.array(z.z.string()).optional().describe("Additional UnrealEditor command-line args (advanced).")
|
|
8756
|
+
}
|
|
8757
|
+
}, async (args) => runWithMcpToolCard("unreal_editor_launch", args, async () => {
|
|
8758
|
+
const uprojectPath = typeof args?.uprojectPath === "string" ? String(args.uprojectPath).trim() : "";
|
|
8759
|
+
const engineRootArg = typeof args?.engineRoot === "string" ? String(args.engineRoot).trim() : "";
|
|
8760
|
+
const extraArgs = Array.isArray(args?.extraArgs) ? args.extraArgs : [];
|
|
8761
|
+
if (!uprojectPath || !uprojectPath.toLowerCase().endsWith(".uproject") || !path.isAbsolute(uprojectPath)) {
|
|
8762
|
+
return {
|
|
8763
|
+
content: [{ type: "text", text: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}` }],
|
|
8764
|
+
isError: true
|
|
8765
|
+
};
|
|
8766
|
+
}
|
|
8767
|
+
if (!fs.existsSync(uprojectPath)) {
|
|
8768
|
+
return { content: [{ type: "text", text: `uprojectPath not found: ${uprojectPath}` }], isError: true };
|
|
8769
|
+
}
|
|
8770
|
+
const resolved = await resolveEngineRootForUproject({ uprojectPath, engineRootArg: engineRootArg || null });
|
|
8771
|
+
if (!resolved.ok) {
|
|
8772
|
+
return {
|
|
8773
|
+
content: [
|
|
8774
|
+
{ type: "text", text: `Unable to resolve engineRoot for this project.` },
|
|
8775
|
+
{ type: "text", text: resolved.errorMessage },
|
|
8776
|
+
...resolved.suggestedEngineRoot ? [{ type: "text", text: `Suggested engineRoot: ${resolved.suggestedEngineRoot}` }] : []
|
|
8777
|
+
],
|
|
8778
|
+
isError: true
|
|
8779
|
+
};
|
|
8780
|
+
}
|
|
8781
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8782
|
+
const launched = await unrealEditorSupervisor.launchEditor({
|
|
8783
|
+
uprojectPath,
|
|
8784
|
+
engineRoot: resolved.engineRoot,
|
|
8785
|
+
extraArgs
|
|
8786
|
+
});
|
|
8787
|
+
return {
|
|
8788
|
+
content: [
|
|
8789
|
+
{ type: "text", text: `Launched Unreal Editor for: ${uprojectPath}` },
|
|
8790
|
+
{
|
|
8791
|
+
type: "text",
|
|
8792
|
+
text: JSON.stringify(
|
|
8793
|
+
{
|
|
8794
|
+
engineRoot: resolved.engineRoot,
|
|
8795
|
+
engineRootSource: resolved.source,
|
|
8796
|
+
pid: launched.pid,
|
|
8797
|
+
exePath: launched.exePath
|
|
8798
|
+
},
|
|
8799
|
+
null,
|
|
8800
|
+
2
|
|
8801
|
+
)
|
|
8802
|
+
},
|
|
8803
|
+
{ type: "text", text: "Next: wait for the editor to finish loading, then use UnrealMCP tools (ping / get_plugin_info / play_in_editor / etc)." }
|
|
8804
|
+
],
|
|
8805
|
+
isError: false
|
|
8806
|
+
};
|
|
8807
|
+
}));
|
|
8808
|
+
mcp.registerTool("unreal_build_project", {
|
|
8809
|
+
title: "Unreal Build Project (UBT)",
|
|
8810
|
+
description: "Build the project via Unreal Build Tool (via Engine/Build/BatchFiles/Build.*). Returns structured errors (file/line) and a log path for deep debugging.",
|
|
8811
|
+
inputSchema: {
|
|
8812
|
+
uprojectPath: z.z.string().describe("Absolute path to the .uproject file."),
|
|
8813
|
+
engineRoot: z.z.string().optional().describe("Optional Unreal Engine install root. Defaults to ENGINE_ROOT / UE_ENGINE_ROOT, or inferred from EngineAssociation when possible."),
|
|
8814
|
+
configuration: z.z.enum(["Debug", "DebugGame", "Development", "Shipping"]).optional().describe("Build configuration (default Development)."),
|
|
8815
|
+
target: z.z.enum(["Editor", "Game"]).optional().describe("Build target (default Editor)."),
|
|
8816
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Timeout in ms (default 20m)."),
|
|
8817
|
+
issuesLimit: z.z.number().int().positive().optional().describe("Max issues to return (default 250, max 2000).")
|
|
8818
|
+
}
|
|
8819
|
+
}, async (args) => runWithMcpToolCard("unreal_build_project", args, async () => {
|
|
8820
|
+
const uprojectPath = typeof args?.uprojectPath === "string" ? String(args.uprojectPath).trim() : "";
|
|
8821
|
+
const engineRootArg = typeof args?.engineRoot === "string" ? String(args.engineRoot).trim() : "";
|
|
8822
|
+
const timeoutMs = typeof args?.timeoutMs === "number" ? args.timeoutMs : void 0;
|
|
8823
|
+
if (!uprojectPath || !uprojectPath.toLowerCase().endsWith(".uproject") || !path.isAbsolute(uprojectPath)) {
|
|
8824
|
+
return {
|
|
8825
|
+
content: [{ type: "text", text: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}` }],
|
|
8826
|
+
isError: true
|
|
8827
|
+
};
|
|
8828
|
+
}
|
|
8829
|
+
if (!fs.existsSync(uprojectPath)) {
|
|
8830
|
+
return { content: [{ type: "text", text: `uprojectPath not found: ${uprojectPath}` }], isError: true };
|
|
8831
|
+
}
|
|
8832
|
+
const resolved = await resolveEngineRootForUproject({ uprojectPath, engineRootArg: engineRootArg || null });
|
|
8833
|
+
if (!resolved.ok) {
|
|
8834
|
+
return {
|
|
8835
|
+
content: [
|
|
8836
|
+
{ type: "text", text: `Unable to resolve engineRoot for this project.` },
|
|
8837
|
+
{ type: "text", text: resolved.errorMessage },
|
|
8838
|
+
...resolved.suggestedEngineRoot ? [{ type: "text", text: `Suggested engineRoot: ${resolved.suggestedEngineRoot}` }] : []
|
|
8839
|
+
],
|
|
8840
|
+
isError: true
|
|
8841
|
+
};
|
|
8842
|
+
}
|
|
8843
|
+
const configuration2 = args?.configuration ?? "Development";
|
|
8844
|
+
const target = args?.target ?? "Editor";
|
|
8845
|
+
const issuesLimit = typeof args?.issuesLimit === "number" ? args.issuesLimit : void 0;
|
|
8846
|
+
const result = await buildUnrealProject({
|
|
8847
|
+
uprojectPath,
|
|
8848
|
+
engineRoot: resolved.engineRoot,
|
|
8849
|
+
configuration: configuration2,
|
|
8850
|
+
target,
|
|
8851
|
+
timeoutMs,
|
|
8852
|
+
issuesLimit
|
|
8853
|
+
});
|
|
8854
|
+
const payload = { ...result, engineRoot: resolved.engineRoot, engineRootSource: resolved.source };
|
|
8855
|
+
return {
|
|
8856
|
+
content: [
|
|
8857
|
+
{ type: "text", text: result.ok ? "Build succeeded." : "Build failed." },
|
|
8858
|
+
...result.logPath ? [{ type: "text", text: `Log: ${result.logPath}` }] : [],
|
|
8859
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
8860
|
+
],
|
|
8861
|
+
isError: !result.ok
|
|
8862
|
+
};
|
|
8863
|
+
}));
|
|
7833
8864
|
mcp.registerTool("unreal_mcp_command", {
|
|
7834
8865
|
title: "Unreal Editor Command (UnrealMCP)",
|
|
7835
8866
|
description: "Send a single UnrealMCP command to the running Unreal Editor (engine plugin) and return the JSON response. Requires Unreal Editor running with the UnrealMCP plugin enabled.",
|
|
@@ -7842,10 +8873,12 @@ ${String(st.stdout || "").trim()}`
|
|
|
7842
8873
|
const type = String(args.type || "").trim();
|
|
7843
8874
|
const params = args.params && typeof args.params === "object" ? args.params : {};
|
|
7844
8875
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
8876
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7845
8877
|
try {
|
|
7846
8878
|
const pluginInfoWasCached = Boolean(unrealMcpPluginInfoCache);
|
|
7847
8879
|
const pluginInfo = type !== "get_plugin_info" ? await getUnrealMcpPluginInfoBestEffort(timeoutMs) : null;
|
|
7848
8880
|
const response = await types.sendUnrealMcpTcpCommand({ type, params, timeoutMs });
|
|
8881
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7849
8882
|
return {
|
|
7850
8883
|
content: [
|
|
7851
8884
|
{ type: "text", text: `UnrealMCP command ok: ${type}` },
|
|
@@ -7868,16 +8901,928 @@ ${String(st.stdout || "").trim()}`
|
|
|
7868
8901
|
};
|
|
7869
8902
|
}
|
|
7870
8903
|
}));
|
|
7871
|
-
mcp.registerTool("
|
|
7872
|
-
title: "Unreal
|
|
7873
|
-
description: "
|
|
8904
|
+
mcp.registerTool("unreal_mcp_list_capabilities", {
|
|
8905
|
+
title: "Unreal MCP: Capabilities",
|
|
8906
|
+
description: "Query the running UnrealMCP plugin for its build info, supported command list, and capability flags. Use this to avoid guessing tool availability/behavior.",
|
|
8907
|
+
inputSchema: {
|
|
8908
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 2000).")
|
|
8909
|
+
}
|
|
8910
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_list_capabilities", args, async () => {
|
|
8911
|
+
const timeoutMs = args?.timeoutMs ?? 2e3;
|
|
8912
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8913
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "list_capabilities", params: {}, timeoutMs });
|
|
8914
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8915
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8916
|
+
}));
|
|
8917
|
+
mcp.registerTool("unreal_mcp_get_command_schema", {
|
|
8918
|
+
title: "Unreal MCP: Command Schema",
|
|
8919
|
+
description: "Fetch a per-command schema from the running UnrealMCP plugin (required params, aliases, examples). If command is omitted, returns all known schemas (can be large).",
|
|
7874
8920
|
inputSchema: {
|
|
8921
|
+
command: z.z.string().optional().describe('Command type to fetch schema for (e.g. "create_blueprint").'),
|
|
8922
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 2000).")
|
|
8923
|
+
}
|
|
8924
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_command_schema", args, async () => {
|
|
8925
|
+
const timeoutMs = args?.timeoutMs ?? 2e3;
|
|
8926
|
+
const command = typeof args?.command === "string" ? String(args.command).trim() : "";
|
|
8927
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8928
|
+
const response = await types.sendUnrealMcpTcpCommand({
|
|
8929
|
+
type: "get_command_schema",
|
|
8930
|
+
params: command ? { command } : {},
|
|
8931
|
+
timeoutMs
|
|
8932
|
+
});
|
|
8933
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8934
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8935
|
+
}));
|
|
8936
|
+
mcp.registerTool("unreal_mcp_search_blueprint_functions", {
|
|
8937
|
+
title: "Unreal MCP: Search Blueprint Functions",
|
|
8938
|
+
description: "Search for Blueprint-callable functions and return candidates (owner class path + signature hints) to use with add_blueprint_function_node.",
|
|
8939
|
+
inputSchema: {
|
|
8940
|
+
query: z.z.string().describe('Substring to search for (e.g. "MakeVector", "SetRelativeLocation").'),
|
|
8941
|
+
target: z.z.string().optional().describe('Optional target class constraint (e.g. "KismetMathLibrary").'),
|
|
8942
|
+
limit: z.z.number().int().positive().optional().describe("Max results (default 25)."),
|
|
7875
8943
|
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
7876
8944
|
}
|
|
7877
|
-
}, async (args) => runWithMcpToolCard("
|
|
8945
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_search_blueprint_functions", args, async () => {
|
|
8946
|
+
const timeoutMs = args?.timeoutMs ?? 5e3;
|
|
8947
|
+
const query = String(args.query || "").trim();
|
|
8948
|
+
const target = typeof args?.target === "string" ? String(args.target).trim() : "";
|
|
8949
|
+
const limit = typeof args?.limit === "number" ? args.limit : void 0;
|
|
8950
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8951
|
+
const params = { query };
|
|
8952
|
+
if (target) params.target = target;
|
|
8953
|
+
if (typeof limit === "number") params.limit = limit;
|
|
8954
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "search_blueprint_functions", params, timeoutMs });
|
|
8955
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8956
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8957
|
+
}));
|
|
8958
|
+
mcp.registerTool("unreal_mcp_resolve_blueprint_function", {
|
|
8959
|
+
title: "Unreal MCP: Resolve Blueprint Function",
|
|
8960
|
+
description: 'Resolve a Blueprint-callable function to an explicit owner class path usable with add_blueprint_function_node (reduces "Function not found" guesswork).',
|
|
8961
|
+
inputSchema: {
|
|
8962
|
+
functionName: z.z.string().describe('Function name to resolve (e.g. "MakeVector").'),
|
|
8963
|
+
target: z.z.string().optional().describe("Optional target class constraint."),
|
|
8964
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
8965
|
+
}
|
|
8966
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_resolve_blueprint_function", args, async () => {
|
|
8967
|
+
const timeoutMs = args?.timeoutMs ?? 5e3;
|
|
8968
|
+
const functionName = String(args.functionName || "").trim();
|
|
8969
|
+
const target = typeof args?.target === "string" ? String(args.target).trim() : "";
|
|
8970
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8971
|
+
const params = { function_name: functionName };
|
|
8972
|
+
if (target) params.target = target;
|
|
8973
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "resolve_blueprint_function", params, timeoutMs });
|
|
8974
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8975
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8976
|
+
}));
|
|
8977
|
+
mcp.registerTool("unreal_mcp_search_assets", {
|
|
8978
|
+
title: "Unreal Search Assets (UnrealMCP)",
|
|
8979
|
+
description: "Search for assets in the current Unreal project via the UnrealMCP engine plugin (AssetRegistry-backed). Returns object paths suitable for place_asset/spawn_actor.",
|
|
8980
|
+
inputSchema: {
|
|
8981
|
+
query: z.z.string().optional().describe("Fuzzy search string (name/path)."),
|
|
8982
|
+
class: z.z.union([z.z.string(), z.z.array(z.z.string())]).optional().describe('Optional asset class filter (e.g. "StaticMesh", "Blueprint", "Material").'),
|
|
8983
|
+
root: z.z.string().optional().describe('Root path to search (default "/Game/").'),
|
|
8984
|
+
limit: z.z.number().int().positive().optional().describe("Max results (default 25, max 200)."),
|
|
8985
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
8986
|
+
}
|
|
8987
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_search_assets", args, async () => {
|
|
8988
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
8989
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8990
|
+
try {
|
|
8991
|
+
const params = {};
|
|
8992
|
+
if (typeof args.query === "string" && args.query.trim()) params.query = args.query.trim();
|
|
8993
|
+
if (typeof args.root === "string" && args.root.trim()) params.root = args.root.trim();
|
|
8994
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
8995
|
+
if (typeof args.class === "string" && args.class.trim()) params.class = args.class.trim();
|
|
8996
|
+
if (Array.isArray(args.class)) params.class = args.class;
|
|
8997
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "search_assets", params, timeoutMs });
|
|
8998
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8999
|
+
return {
|
|
9000
|
+
content: [
|
|
9001
|
+
{ type: "text", text: "Unreal assets search ok." },
|
|
9002
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9003
|
+
],
|
|
9004
|
+
isError: false
|
|
9005
|
+
};
|
|
9006
|
+
} catch (error) {
|
|
9007
|
+
return {
|
|
9008
|
+
content: [
|
|
9009
|
+
{ type: "text", text: "Unreal assets search failed." },
|
|
9010
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9011
|
+
],
|
|
9012
|
+
isError: true
|
|
9013
|
+
};
|
|
9014
|
+
}
|
|
9015
|
+
}));
|
|
9016
|
+
mcp.registerTool("unreal_mcp_get_asset_info", {
|
|
9017
|
+
title: "Unreal Asset Info (UnrealMCP)",
|
|
9018
|
+
description: "Get metadata for a single Unreal asset (class/path + optional dependencies) via the UnrealMCP engine plugin.",
|
|
9019
|
+
inputSchema: {
|
|
9020
|
+
objectPath: z.z.string().describe('Unreal asset object path (e.g. "/Game/MyPack/MyMesh.MyMesh").'),
|
|
9021
|
+
includeDependencies: z.z.boolean().optional().describe("Include package dependencies (default false)."),
|
|
9022
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9023
|
+
}
|
|
9024
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_asset_info", args, async () => {
|
|
9025
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9026
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9027
|
+
try {
|
|
9028
|
+
const params = { objectPath: String(args.objectPath || "").trim() };
|
|
9029
|
+
if (typeof args.includeDependencies === "boolean") params.includeDependencies = args.includeDependencies;
|
|
9030
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "get_asset_info", params, timeoutMs });
|
|
9031
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9032
|
+
return {
|
|
9033
|
+
content: [
|
|
9034
|
+
{ type: "text", text: "Unreal asset info ok." },
|
|
9035
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9036
|
+
],
|
|
9037
|
+
isError: false
|
|
9038
|
+
};
|
|
9039
|
+
} catch (error) {
|
|
9040
|
+
return {
|
|
9041
|
+
content: [
|
|
9042
|
+
{ type: "text", text: "Unreal asset info failed." },
|
|
9043
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9044
|
+
],
|
|
9045
|
+
isError: true
|
|
9046
|
+
};
|
|
9047
|
+
}
|
|
9048
|
+
}));
|
|
9049
|
+
mcp.registerTool("unreal_mcp_list_asset_packs", {
|
|
9050
|
+
title: "Unreal List Asset Packs (UnrealMCP)",
|
|
9051
|
+
description: 'List top-level /Game content folders (best-effort "asset packs") via the UnrealMCP engine plugin.',
|
|
9052
|
+
inputSchema: {
|
|
9053
|
+
limit: z.z.number().int().positive().optional().describe("Max number of packs (default 200)."),
|
|
9054
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9055
|
+
}
|
|
9056
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_list_asset_packs", args, async () => {
|
|
9057
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9058
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9059
|
+
try {
|
|
9060
|
+
const params = {};
|
|
9061
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
9062
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "list_asset_packs", params, timeoutMs });
|
|
9063
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9064
|
+
return {
|
|
9065
|
+
content: [
|
|
9066
|
+
{ type: "text", text: "Unreal asset packs list ok." },
|
|
9067
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9068
|
+
],
|
|
9069
|
+
isError: false
|
|
9070
|
+
};
|
|
9071
|
+
} catch (error) {
|
|
9072
|
+
return {
|
|
9073
|
+
content: [
|
|
9074
|
+
{ type: "text", text: "Unreal asset packs list failed." },
|
|
9075
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9076
|
+
],
|
|
9077
|
+
isError: true
|
|
9078
|
+
};
|
|
9079
|
+
}
|
|
9080
|
+
}));
|
|
9081
|
+
mcp.registerTool("unreal_mcp_place_asset", {
|
|
9082
|
+
title: "Unreal Place Asset (UnrealMCP)",
|
|
9083
|
+
description: "Place an asset into the current Unreal level by object path (StaticMesh/Blueprint). Fails if PIE is running to avoid crashes.",
|
|
9084
|
+
inputSchema: {
|
|
9085
|
+
objectPath: z.z.string().describe('Unreal asset object path (e.g. "/Game/MyPack/MyMesh.MyMesh").'),
|
|
9086
|
+
name: z.z.string().optional().describe("Optional actor name (default: derived from asset name)."),
|
|
9087
|
+
location: z.z.array(z.z.number()).length(3).optional().describe("Spawn location [X,Y,Z] in cm."),
|
|
9088
|
+
rotation: z.z.array(z.z.number()).length(3).optional().describe("Spawn rotation [Pitch,Yaw,Roll] in degrees."),
|
|
9089
|
+
scale: z.z.array(z.z.number()).length(3).optional().describe("Spawn scale [X,Y,Z]."),
|
|
9090
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9091
|
+
}
|
|
9092
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_place_asset", args, async () => {
|
|
9093
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9094
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9095
|
+
try {
|
|
9096
|
+
const params = { objectPath: String(args.objectPath || "").trim() };
|
|
9097
|
+
if (typeof args.name === "string" && args.name.trim()) params.name = args.name.trim();
|
|
9098
|
+
if (Array.isArray(args.location)) params.location = args.location;
|
|
9099
|
+
if (Array.isArray(args.rotation)) params.rotation = args.rotation;
|
|
9100
|
+
if (Array.isArray(args.scale)) params.scale = args.scale;
|
|
9101
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "place_asset", params, timeoutMs });
|
|
9102
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9103
|
+
return {
|
|
9104
|
+
content: [
|
|
9105
|
+
{ type: "text", text: "Placed asset into Unreal level." },
|
|
9106
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9107
|
+
],
|
|
9108
|
+
isError: false
|
|
9109
|
+
};
|
|
9110
|
+
} catch (error) {
|
|
9111
|
+
return {
|
|
9112
|
+
content: [
|
|
9113
|
+
{ type: "text", text: "Failed to place asset into Unreal level." },
|
|
9114
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9115
|
+
{ type: "text", text: "Tip: use unreal_mcp_get_play_in_editor_status and stop PIE before placing assets." }
|
|
9116
|
+
],
|
|
9117
|
+
isError: true
|
|
9118
|
+
};
|
|
9119
|
+
}
|
|
9120
|
+
}));
|
|
9121
|
+
mcp.registerTool("unreal_mcp_get_editor_context", {
|
|
9122
|
+
title: "Unreal Editor Context (UnrealMCP)",
|
|
9123
|
+
description: "Return a compact snapshot of editor context: map, selection, and active viewport camera (if any).",
|
|
9124
|
+
inputSchema: {
|
|
9125
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9126
|
+
}
|
|
9127
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_editor_context", args, async () => {
|
|
9128
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9129
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9130
|
+
try {
|
|
9131
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs });
|
|
9132
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9133
|
+
return {
|
|
9134
|
+
content: [
|
|
9135
|
+
{ type: "text", text: "Fetched Unreal Editor context." },
|
|
9136
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9137
|
+
],
|
|
9138
|
+
isError: false
|
|
9139
|
+
};
|
|
9140
|
+
} catch (error) {
|
|
9141
|
+
return {
|
|
9142
|
+
content: [
|
|
9143
|
+
{ type: "text", text: "Failed to fetch Unreal Editor context." },
|
|
9144
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9145
|
+
],
|
|
9146
|
+
isError: true
|
|
9147
|
+
};
|
|
9148
|
+
}
|
|
9149
|
+
}));
|
|
9150
|
+
mcp.registerTool("unreal_mcp_get_player_context", {
|
|
9151
|
+
title: "Unreal Player Context (PIE)",
|
|
9152
|
+
description: "Return a compact snapshot of runtime player context (PIE): isPlaying, pawn transform, and camera transform.",
|
|
9153
|
+
inputSchema: {
|
|
9154
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9155
|
+
}
|
|
9156
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_player_context", args, async () => {
|
|
9157
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9158
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9159
|
+
try {
|
|
9160
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs });
|
|
9161
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9162
|
+
return {
|
|
9163
|
+
content: [
|
|
9164
|
+
{ type: "text", text: "Fetched player context." },
|
|
9165
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9166
|
+
],
|
|
9167
|
+
isError: false
|
|
9168
|
+
};
|
|
9169
|
+
} catch (error) {
|
|
9170
|
+
return {
|
|
9171
|
+
content: [
|
|
9172
|
+
{ type: "text", text: "Failed to fetch player context." },
|
|
9173
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9174
|
+
],
|
|
9175
|
+
isError: true
|
|
9176
|
+
};
|
|
9177
|
+
}
|
|
9178
|
+
}));
|
|
9179
|
+
mcp.registerTool("unreal_mcp_raycast_from_camera", {
|
|
9180
|
+
title: "Unreal Raycast From Camera (Editor/PIE)",
|
|
9181
|
+
description: "Raycast from the editor viewport camera or the PIE player camera to turn \u201Cthat surface\u201D into an exact hit location and normal.",
|
|
9182
|
+
inputSchema: {
|
|
9183
|
+
source: z.z.enum(["editor", "pie"]).optional().describe("Camera source (default editor)."),
|
|
9184
|
+
maxDistance: z.z.number().positive().optional().describe("Max distance in cm (default 10000)."),
|
|
9185
|
+
ignoreActors: z.z.array(z.z.string()).optional().describe("Actor names/labels to ignore (best-effort)."),
|
|
9186
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9187
|
+
}
|
|
9188
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_raycast_from_camera", args, async () => {
|
|
9189
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9190
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9191
|
+
try {
|
|
9192
|
+
const params = {};
|
|
9193
|
+
if (args.source) params.source = args.source;
|
|
9194
|
+
if (typeof args.maxDistance === "number" && Number.isFinite(args.maxDistance)) params.maxDistance = args.maxDistance;
|
|
9195
|
+
if (Array.isArray(args.ignoreActors)) params.ignoreActors = args.ignoreActors;
|
|
9196
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "raycast_from_camera", params, timeoutMs });
|
|
9197
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9198
|
+
return {
|
|
9199
|
+
content: [
|
|
9200
|
+
{ type: "text", text: "Raycast completed." },
|
|
9201
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9202
|
+
],
|
|
9203
|
+
isError: false
|
|
9204
|
+
};
|
|
9205
|
+
} catch (error) {
|
|
9206
|
+
return {
|
|
9207
|
+
content: [
|
|
9208
|
+
{ type: "text", text: "Raycast failed." },
|
|
9209
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9210
|
+
],
|
|
9211
|
+
isError: true
|
|
9212
|
+
};
|
|
9213
|
+
}
|
|
9214
|
+
}));
|
|
9215
|
+
mcp.registerTool("unreal_mcp_raycast_down", {
|
|
9216
|
+
title: "Unreal Raycast Down (Drop To Ground)",
|
|
9217
|
+
description: "Raycast straight down from a given startLocation to find ground/surface beneath. Useful for drop-to-ground placement.",
|
|
9218
|
+
inputSchema: {
|
|
9219
|
+
source: z.z.enum(["editor", "pie"]).optional().describe("World source (default editor)."),
|
|
9220
|
+
startLocation: z.z.array(z.z.number()).length(3).describe("Start location [X,Y,Z] in cm."),
|
|
9221
|
+
maxDistance: z.z.number().positive().optional().describe("Max distance in cm (default 100000)."),
|
|
9222
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9223
|
+
}
|
|
9224
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_raycast_down", args, async () => {
|
|
9225
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9226
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9227
|
+
try {
|
|
9228
|
+
const params = { startLocation: args.startLocation };
|
|
9229
|
+
if (args.source) params.source = args.source;
|
|
9230
|
+
if (typeof args.maxDistance === "number" && Number.isFinite(args.maxDistance)) params.maxDistance = args.maxDistance;
|
|
9231
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "raycast_down", params, timeoutMs });
|
|
9232
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9233
|
+
return {
|
|
9234
|
+
content: [
|
|
9235
|
+
{ type: "text", text: "Raycast-down completed." },
|
|
9236
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9237
|
+
],
|
|
9238
|
+
isError: false
|
|
9239
|
+
};
|
|
9240
|
+
} catch (error) {
|
|
9241
|
+
return {
|
|
9242
|
+
content: [
|
|
9243
|
+
{ type: "text", text: "Raycast-down failed." },
|
|
9244
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9245
|
+
],
|
|
9246
|
+
isError: true
|
|
9247
|
+
};
|
|
9248
|
+
}
|
|
9249
|
+
}));
|
|
9250
|
+
mcp.registerTool("unreal_mcp_get_actor_transform", {
|
|
9251
|
+
title: "Unreal Actor Transform (UnrealMCP)",
|
|
9252
|
+
description: "Get a single actor transform (location/rotation/scale) by actor name or label (best-effort).",
|
|
9253
|
+
inputSchema: {
|
|
9254
|
+
name: z.z.string().describe("Actor name or label."),
|
|
9255
|
+
source: z.z.enum(["editor", "pie"]).optional().describe("World source (default editor)."),
|
|
9256
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9257
|
+
}
|
|
9258
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_actor_transform", args, async () => {
|
|
9259
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9260
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9261
|
+
try {
|
|
9262
|
+
const params = { name: String(args.name || "").trim() };
|
|
9263
|
+
if (args.source) params.source = args.source;
|
|
9264
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "get_actor_transform", params, timeoutMs });
|
|
9265
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9266
|
+
return {
|
|
9267
|
+
content: [
|
|
9268
|
+
{ type: "text", text: "Fetched actor transform." },
|
|
9269
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9270
|
+
],
|
|
9271
|
+
isError: false
|
|
9272
|
+
};
|
|
9273
|
+
} catch (error) {
|
|
9274
|
+
return {
|
|
9275
|
+
content: [
|
|
9276
|
+
{ type: "text", text: "Failed to fetch actor transform." },
|
|
9277
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9278
|
+
],
|
|
9279
|
+
isError: true
|
|
9280
|
+
};
|
|
9281
|
+
}
|
|
9282
|
+
}));
|
|
9283
|
+
mcp.registerTool("unreal_mcp_get_actor_bounds", {
|
|
9284
|
+
title: "Unreal Actor Bounds (UnrealMCP)",
|
|
9285
|
+
description: "Get a single actor bounds (origin/extent + min/max) by actor name or label (best-effort).",
|
|
9286
|
+
inputSchema: {
|
|
9287
|
+
name: z.z.string().describe("Actor name or label."),
|
|
9288
|
+
source: z.z.enum(["editor", "pie"]).optional().describe("World source (default editor)."),
|
|
9289
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9290
|
+
}
|
|
9291
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_actor_bounds", args, async () => {
|
|
9292
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9293
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9294
|
+
try {
|
|
9295
|
+
const params = { name: String(args.name || "").trim() };
|
|
9296
|
+
if (args.source) params.source = args.source;
|
|
9297
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "get_actor_bounds", params, timeoutMs });
|
|
9298
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9299
|
+
return {
|
|
9300
|
+
content: [
|
|
9301
|
+
{ type: "text", text: "Fetched actor bounds." },
|
|
9302
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9303
|
+
],
|
|
9304
|
+
isError: false
|
|
9305
|
+
};
|
|
9306
|
+
} catch (error) {
|
|
9307
|
+
return {
|
|
9308
|
+
content: [
|
|
9309
|
+
{ type: "text", text: "Failed to fetch actor bounds." },
|
|
9310
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9311
|
+
],
|
|
9312
|
+
isError: true
|
|
9313
|
+
};
|
|
9314
|
+
}
|
|
9315
|
+
}));
|
|
9316
|
+
mcp.registerTool("unreal_mcp_create_landscape", {
|
|
9317
|
+
title: "Unreal Create Landscape (UnrealMCP)",
|
|
9318
|
+
description: "Create a new Landscape in the current editor level via the UnrealMCP engine plugin. Requires Unreal Editor running with the UnrealMCP plugin enabled. Fails if PIE is running.",
|
|
9319
|
+
inputSchema: {
|
|
9320
|
+
name: z.z.string().describe("New landscape actor name (must be unique)."),
|
|
9321
|
+
componentCountX: z.z.number().int().positive().optional().describe("Number of components in X (default 8)."),
|
|
9322
|
+
componentCountY: z.z.number().int().positive().optional().describe("Number of components in Y (default 8)."),
|
|
9323
|
+
sectionsPerComponent: z.z.number().int().optional().describe("Sections per component (1 or 2, default 1)."),
|
|
9324
|
+
quadsPerSection: z.z.number().int().positive().optional().describe("Quads per section (default 63)."),
|
|
9325
|
+
location: z.z.array(z.z.number()).length(3).optional().describe("Center location [X,Y,Z] in cm (default [0,0,0])."),
|
|
9326
|
+
rotation: z.z.array(z.z.number()).length(3).optional().describe("Rotation [Pitch,Yaw,Roll] in degrees (default [0,0,0])."),
|
|
9327
|
+
scale: z.z.array(z.z.number()).length(3).optional().describe("Scale [X,Y,Z] (default [100,100,100])."),
|
|
9328
|
+
materialPath: z.z.string().optional().describe('Optional landscape material object path (e.g. "/Game/\u2026/M_Landscape.M_Landscape").'),
|
|
9329
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 30000).")
|
|
9330
|
+
}
|
|
9331
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_create_landscape", args, async () => {
|
|
9332
|
+
const timeoutMs = args?.timeoutMs ?? 3e4;
|
|
9333
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9334
|
+
try {
|
|
9335
|
+
const params = {
|
|
9336
|
+
name: String(args?.name || "").trim()
|
|
9337
|
+
};
|
|
9338
|
+
if (!params.name) throw new Error("name is required.");
|
|
9339
|
+
if (typeof args?.componentCountX === "number") params.componentCountX = args.componentCountX;
|
|
9340
|
+
if (typeof args?.componentCountY === "number") params.componentCountY = args.componentCountY;
|
|
9341
|
+
if (typeof args?.sectionsPerComponent === "number") params.sectionsPerComponent = args.sectionsPerComponent;
|
|
9342
|
+
if (typeof args?.quadsPerSection === "number") params.quadsPerSection = args.quadsPerSection;
|
|
9343
|
+
if (Array.isArray(args?.location)) params.location = args.location;
|
|
9344
|
+
if (Array.isArray(args?.rotation)) params.rotation = args.rotation;
|
|
9345
|
+
if (Array.isArray(args?.scale)) params.scale = args.scale;
|
|
9346
|
+
if (typeof args?.materialPath === "string" && args.materialPath.trim()) {
|
|
9347
|
+
params.materialPath = args.materialPath.trim();
|
|
9348
|
+
}
|
|
9349
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "create_landscape", params, timeoutMs });
|
|
9350
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9351
|
+
return {
|
|
9352
|
+
content: [
|
|
9353
|
+
{ type: "text", text: "Landscape created." },
|
|
9354
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9355
|
+
],
|
|
9356
|
+
isError: false
|
|
9357
|
+
};
|
|
9358
|
+
} catch (error) {
|
|
9359
|
+
return {
|
|
9360
|
+
content: [
|
|
9361
|
+
{ type: "text", text: "Failed to create landscape." },
|
|
9362
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9363
|
+
{ type: "text", text: "Tip: stop PIE before creating a landscape." }
|
|
9364
|
+
],
|
|
9365
|
+
isError: true
|
|
9366
|
+
};
|
|
9367
|
+
}
|
|
9368
|
+
}));
|
|
9369
|
+
mcp.registerTool("unreal_mcp_map_check", {
|
|
9370
|
+
title: "Unreal Map Check (UnrealMCP)",
|
|
9371
|
+
description: "Run Unreal Map Check and return structured issues. Uses Unreal\u2019s Message Log to capture results. Fails if PIE is running to avoid editor instability.",
|
|
9372
|
+
inputSchema: {
|
|
9373
|
+
includeWarnings: z.z.boolean().optional().describe("Include warnings in the issues list (default true)."),
|
|
9374
|
+
limit: z.z.number().int().positive().optional().describe("Max number of issues to return (default 200, max 1000)."),
|
|
9375
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 15000).")
|
|
9376
|
+
}
|
|
9377
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_map_check", args, async () => {
|
|
9378
|
+
const timeoutMs = args.timeoutMs ?? 15e3;
|
|
9379
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9380
|
+
try {
|
|
9381
|
+
const params = {};
|
|
9382
|
+
if (typeof args.includeWarnings === "boolean") params.includeWarnings = args.includeWarnings;
|
|
9383
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
9384
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "map_check", params, timeoutMs });
|
|
9385
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9386
|
+
return {
|
|
9387
|
+
content: [
|
|
9388
|
+
{ type: "text", text: "Map Check completed." },
|
|
9389
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9390
|
+
],
|
|
9391
|
+
isError: false
|
|
9392
|
+
};
|
|
9393
|
+
} catch (error) {
|
|
9394
|
+
return {
|
|
9395
|
+
content: [
|
|
9396
|
+
{ type: "text", text: "Map Check failed." },
|
|
9397
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9398
|
+
{ type: "text", text: "Tip: stop PIE before running map_check." }
|
|
9399
|
+
],
|
|
9400
|
+
isError: true
|
|
9401
|
+
};
|
|
9402
|
+
}
|
|
9403
|
+
}));
|
|
9404
|
+
mcp.registerTool("unreal_mcp_compile_blueprints_all", {
|
|
9405
|
+
title: "Unreal Compile All Blueprints (UnrealMCP)",
|
|
9406
|
+
description: "Compile all Blueprints (optionally under specific content paths) and return a structured list of compile messages. Fails if PIE is running.",
|
|
9407
|
+
inputSchema: {
|
|
9408
|
+
paths: z.z.array(z.z.string()).optional().describe('Root content paths to search (default ["/Game"]).'),
|
|
9409
|
+
includeWarnings: z.z.boolean().optional().describe("Include warnings in the per-blueprint messages list (default true)."),
|
|
9410
|
+
limit: z.z.number().int().positive().optional().describe("Max number of blueprints to compile (default 500, max 10000)."),
|
|
9411
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 60000).")
|
|
9412
|
+
}
|
|
9413
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_compile_blueprints_all", args, async () => {
|
|
9414
|
+
const timeoutMs = args.timeoutMs ?? 6e4;
|
|
9415
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9416
|
+
try {
|
|
9417
|
+
const params = {};
|
|
9418
|
+
if (Array.isArray(args.paths)) params.paths = args.paths;
|
|
9419
|
+
if (typeof args.includeWarnings === "boolean") params.includeWarnings = args.includeWarnings;
|
|
9420
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
9421
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "compile_blueprints_all", params, timeoutMs });
|
|
9422
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9423
|
+
return {
|
|
9424
|
+
content: [
|
|
9425
|
+
{ type: "text", text: "Blueprint compile completed." },
|
|
9426
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9427
|
+
],
|
|
9428
|
+
isError: false
|
|
9429
|
+
};
|
|
9430
|
+
} catch (error) {
|
|
9431
|
+
return {
|
|
9432
|
+
content: [
|
|
9433
|
+
{ type: "text", text: "Blueprint compile failed." },
|
|
9434
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9435
|
+
{ type: "text", text: "Tip: stop PIE before compiling Blueprints." }
|
|
9436
|
+
],
|
|
9437
|
+
isError: true
|
|
9438
|
+
};
|
|
9439
|
+
}
|
|
9440
|
+
}));
|
|
9441
|
+
mcp.registerTool("unreal_mcp_save_all", {
|
|
9442
|
+
title: "Unreal Save All (UnrealMCP)",
|
|
9443
|
+
description: "Save all dirty packages (maps + content) in the running Unreal Editor without prompting. Intended to prevent leaving editor changes unsaved. Fails if PIE is running.",
|
|
9444
|
+
inputSchema: {
|
|
9445
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 60000).")
|
|
9446
|
+
}
|
|
9447
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_save_all", args, async () => {
|
|
9448
|
+
const timeoutMs = args?.timeoutMs ?? 6e4;
|
|
9449
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9450
|
+
try {
|
|
9451
|
+
const response = await types.sendUnrealMcpTcpCommand({ type: "save_all", params: {}, timeoutMs });
|
|
9452
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9453
|
+
return {
|
|
9454
|
+
content: [
|
|
9455
|
+
{ type: "text", text: "Save all completed." },
|
|
9456
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9457
|
+
],
|
|
9458
|
+
isError: false
|
|
9459
|
+
};
|
|
9460
|
+
} catch (error) {
|
|
9461
|
+
return {
|
|
9462
|
+
content: [
|
|
9463
|
+
{ type: "text", text: "Save all failed." },
|
|
9464
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9465
|
+
{ type: "text", text: "Tip: stop PIE, resolve any editor save prompts/errors (Save As, checkout), then retry." }
|
|
9466
|
+
],
|
|
9467
|
+
isError: true
|
|
9468
|
+
};
|
|
9469
|
+
}
|
|
9470
|
+
}));
|
|
9471
|
+
const vec = {
|
|
9472
|
+
add: (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]],
|
|
9473
|
+
sub: (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]],
|
|
9474
|
+
mul: (a, s) => [a[0] * s, a[1] * s, a[2] * s],
|
|
9475
|
+
dot: (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
|
|
9476
|
+
len: (a) => Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]),
|
|
9477
|
+
norm: (a) => {
|
|
9478
|
+
const l = Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
|
|
9479
|
+
if (!Number.isFinite(l) || l <= 1e-8) return [0, 0, 0];
|
|
9480
|
+
return [a[0] / l, a[1] / l, a[2] / l];
|
|
9481
|
+
},
|
|
9482
|
+
cross: (a, b) => [
|
|
9483
|
+
a[1] * b[2] - a[2] * b[1],
|
|
9484
|
+
a[2] * b[0] - a[0] * b[2],
|
|
9485
|
+
a[0] * b[1] - a[1] * b[0]
|
|
9486
|
+
],
|
|
9487
|
+
isFinite3: (a) => Array.isArray(a) && a.length === 3 && a.every((x) => typeof x === "number" && Number.isFinite(x))
|
|
9488
|
+
};
|
|
9489
|
+
function degToRad(deg) {
|
|
9490
|
+
return deg * Math.PI / 180;
|
|
9491
|
+
}
|
|
9492
|
+
function radToDeg(rad) {
|
|
9493
|
+
return rad * 180 / Math.PI;
|
|
9494
|
+
}
|
|
9495
|
+
function forwardRightUpFromRotator(rot) {
|
|
9496
|
+
const pitch = degToRad(rot[0]);
|
|
9497
|
+
const yaw = degToRad(rot[1]);
|
|
9498
|
+
const roll = degToRad(rot[2]);
|
|
9499
|
+
const cp = Math.cos(pitch);
|
|
9500
|
+
const sp = Math.sin(pitch);
|
|
9501
|
+
const cy = Math.cos(yaw);
|
|
9502
|
+
const sy = Math.sin(yaw);
|
|
9503
|
+
const cr = Math.cos(roll);
|
|
9504
|
+
const sr = Math.sin(roll);
|
|
9505
|
+
const forward = [cp * cy, cp * sy, sp];
|
|
9506
|
+
const right = [
|
|
9507
|
+
sr * sp * cy + cr * -sy,
|
|
9508
|
+
sr * sp * sy + cr * cy,
|
|
9509
|
+
-sr * cp
|
|
9510
|
+
];
|
|
9511
|
+
const up = [
|
|
9512
|
+
cr * sp * cy + -sr * -sy,
|
|
9513
|
+
cr * sp * sy + -sr * cy,
|
|
9514
|
+
cr * cp
|
|
9515
|
+
];
|
|
9516
|
+
return { forward: vec.norm(forward), right: vec.norm(right), up: vec.norm(up) };
|
|
9517
|
+
}
|
|
9518
|
+
function rotatorFromBasis(args) {
|
|
9519
|
+
const f = vec.norm(args.forward);
|
|
9520
|
+
const r = vec.norm(args.right);
|
|
9521
|
+
const u = vec.norm(args.up);
|
|
9522
|
+
const yaw = radToDeg(Math.atan2(f[1], f[0]));
|
|
9523
|
+
const pitch = radToDeg(Math.atan2(f[2], Math.sqrt(f[0] * f[0] + f[1] * f[1])));
|
|
9524
|
+
const roll = radToDeg(Math.atan2(r[2], u[2]));
|
|
9525
|
+
return [pitch, yaw, roll];
|
|
9526
|
+
}
|
|
9527
|
+
function unwrapUnrealMcpResult(raw) {
|
|
9528
|
+
if (raw?.result && typeof raw.result === "object") return raw.result;
|
|
9529
|
+
return raw;
|
|
9530
|
+
}
|
|
9531
|
+
mcp.registerTool("unreal_place_asset_relative", {
|
|
9532
|
+
title: "Unreal Place Asset Relative (Context-Aware)",
|
|
9533
|
+
description: "High-level helper built from context/raycast primitives: place an asset relative to selection/camera/player/hit result, with optional align-to-surface and drop-to-ground.",
|
|
9534
|
+
inputSchema: {
|
|
9535
|
+
objectPath: z.z.string().describe('Unreal asset object path (e.g. "/Game/MyPack/MyMesh.MyMesh").'),
|
|
9536
|
+
name: z.z.string().optional().describe("Optional actor name (default: derived from asset name)."),
|
|
9537
|
+
anchor: z.z.enum(["selection", "editor_camera", "player", "player_camera", "hit_result", "actor"]).describe("Reference anchor for placement."),
|
|
9538
|
+
actorName: z.z.string().optional().describe('When anchor="actor", the actor name/label to anchor to.'),
|
|
9539
|
+
offset: z.z.array(z.z.number()).length(3).optional().describe("Offset in anchor local axes [forwardCm, rightCm, upCm] (default [200,0,0])."),
|
|
9540
|
+
alignToSurface: z.z.boolean().optional().describe("When using hit_result or drop-to-ground hits, align actor up to hit normal (default false)."),
|
|
9541
|
+
dropToGround: z.z.boolean().optional().describe("After computing target location, raycast down and snap to the hit location (default false)."),
|
|
9542
|
+
raySource: z.z.enum(["editor", "pie"]).optional().describe('When anchor="hit_result", which camera to raycast from (default editor).'),
|
|
9543
|
+
maxDistance: z.z.number().positive().optional().describe("Raycast max distance in cm (default 10000)."),
|
|
9544
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 8000).")
|
|
9545
|
+
}
|
|
9546
|
+
}, async (args) => runWithMcpToolCard("unreal_place_asset_relative", args, async () => {
|
|
9547
|
+
const timeoutMs = args.timeoutMs ?? 8e3;
|
|
9548
|
+
const offset = Array.isArray(args.offset) && args.offset.length === 3 ? args.offset : [200, 0, 0];
|
|
9549
|
+
const alignToSurface = args.alignToSurface ?? false;
|
|
9550
|
+
const dropToGround = args.dropToGround ?? false;
|
|
9551
|
+
const maxDistance = typeof args.maxDistance === "number" ? args.maxDistance : 1e4;
|
|
9552
|
+
const raySource = args.raySource ?? "editor";
|
|
9553
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9554
|
+
try {
|
|
9555
|
+
const anchorRaw = String(args.anchor || "").trim();
|
|
9556
|
+
const anchor = anchorRaw === "player" ? "player_camera" : anchorRaw;
|
|
9557
|
+
const objectPath = String(args.objectPath || "").trim();
|
|
9558
|
+
if (!objectPath) throw new Error("Missing objectPath.");
|
|
9559
|
+
if (!vec.isFinite3(offset)) throw new Error("Invalid offset (expected [forward,right,up] numbers).");
|
|
9560
|
+
let origin = null;
|
|
9561
|
+
let rot = null;
|
|
9562
|
+
let surfaceNormal = null;
|
|
9563
|
+
if (anchor === "selection" || anchor === "editor_camera") {
|
|
9564
|
+
const ctxRaw = await types.sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs });
|
|
9565
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9566
|
+
const ctx = unwrapUnrealMcpResult(ctxRaw);
|
|
9567
|
+
if (anchor === "selection") {
|
|
9568
|
+
const sel = Array.isArray(ctx?.selection) ? ctx.selection : [];
|
|
9569
|
+
if (sel.length === 0) throw new Error("No selected actors in Unreal Editor. Select an actor and retry.");
|
|
9570
|
+
const first = sel[0];
|
|
9571
|
+
origin = Array.isArray(first?.location) ? first.location : null;
|
|
9572
|
+
rot = Array.isArray(first?.rotation) ? first.rotation : null;
|
|
9573
|
+
} else {
|
|
9574
|
+
const cam = ctx?.viewportCamera;
|
|
9575
|
+
if (!cam || cam === null) throw new Error("No active viewport camera. Click the viewport and retry.");
|
|
9576
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9577
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9578
|
+
}
|
|
9579
|
+
} else if (anchor === "player_camera") {
|
|
9580
|
+
const pcRaw = await types.sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs });
|
|
9581
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9582
|
+
const pc = unwrapUnrealMcpResult(pcRaw);
|
|
9583
|
+
if (!pc?.isPlaying) throw new Error("PIE is not running. Start PIE and retry (or use editor_camera/selection anchors).");
|
|
9584
|
+
const cam = pc?.camera;
|
|
9585
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9586
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9587
|
+
} else if (anchor === "actor") {
|
|
9588
|
+
const actorName = String(args.actorName || "").trim();
|
|
9589
|
+
if (!actorName) throw new Error('anchor="actor" requires actorName.');
|
|
9590
|
+
const tRaw = await types.sendUnrealMcpTcpCommand({ type: "get_actor_transform", params: { name: actorName, source: "editor" }, timeoutMs });
|
|
9591
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9592
|
+
const t = unwrapUnrealMcpResult(tRaw);
|
|
9593
|
+
origin = Array.isArray(t?.location) ? t.location : null;
|
|
9594
|
+
rot = Array.isArray(t?.rotation) ? t.rotation : null;
|
|
9595
|
+
} else if (anchor === "hit_result") {
|
|
9596
|
+
const hitRaw = await types.sendUnrealMcpTcpCommand({
|
|
9597
|
+
type: "raycast_from_camera",
|
|
9598
|
+
params: { source: raySource, maxDistance },
|
|
9599
|
+
timeoutMs
|
|
9600
|
+
});
|
|
9601
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9602
|
+
const hit = unwrapUnrealMcpResult(hitRaw);
|
|
9603
|
+
if (!hit?.hit) throw new Error("Raycast did not hit anything.");
|
|
9604
|
+
origin = Array.isArray(hit?.location) ? hit.location : null;
|
|
9605
|
+
surfaceNormal = Array.isArray(hit?.normal) ? hit.normal : null;
|
|
9606
|
+
const ctxRot = raySource === "pie" ? unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs }))?.camera?.rotation : unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs }))?.viewportCamera?.rotation;
|
|
9607
|
+
rot = Array.isArray(ctxRot) ? ctxRot : [0, 0, 0];
|
|
9608
|
+
} else {
|
|
9609
|
+
throw new Error(`Unknown anchor: ${anchor}`);
|
|
9610
|
+
}
|
|
9611
|
+
if (!vec.isFinite3(origin)) throw new Error("Anchor origin is missing or invalid.");
|
|
9612
|
+
if (!rot || !vec.isFinite3(rot)) throw new Error("Anchor rotation is missing or invalid.");
|
|
9613
|
+
const basis = forwardRightUpFromRotator(rot);
|
|
9614
|
+
let target = vec.add(origin, vec.add(vec.mul(basis.forward, offset[0]), vec.add(vec.mul(basis.right, offset[1]), vec.mul(basis.up, offset[2]))));
|
|
9615
|
+
let finalNormal = surfaceNormal;
|
|
9616
|
+
if (dropToGround) {
|
|
9617
|
+
const start = vec.add(target, [0, 0, 1e4]);
|
|
9618
|
+
const downRaw = await types.sendUnrealMcpTcpCommand({
|
|
9619
|
+
type: "raycast_down",
|
|
9620
|
+
params: { source: "editor", startLocation: start, maxDistance: 2e5 },
|
|
9621
|
+
timeoutMs
|
|
9622
|
+
});
|
|
9623
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9624
|
+
const down = unwrapUnrealMcpResult(downRaw);
|
|
9625
|
+
if (down?.hit && Array.isArray(down?.location)) {
|
|
9626
|
+
target = down.location;
|
|
9627
|
+
if (Array.isArray(down?.normal)) finalNormal = down.normal;
|
|
9628
|
+
} else {
|
|
9629
|
+
throw new Error("dropToGround=true but raycast_down did not hit anything.");
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9632
|
+
let outRot = rot;
|
|
9633
|
+
if (alignToSurface && vec.isFinite3(finalNormal)) {
|
|
9634
|
+
const up = vec.norm(finalNormal);
|
|
9635
|
+
let fwd = basis.forward;
|
|
9636
|
+
fwd = vec.sub(fwd, vec.mul(up, vec.dot(fwd, up)));
|
|
9637
|
+
if (vec.len(fwd) <= 1e-4) {
|
|
9638
|
+
fwd = vec.cross([0, 1, 0], up);
|
|
9639
|
+
}
|
|
9640
|
+
fwd = vec.norm(fwd);
|
|
9641
|
+
const right = vec.norm(vec.cross(up, fwd));
|
|
9642
|
+
const fixedFwd = vec.norm(vec.cross(right, up));
|
|
9643
|
+
outRot = rotatorFromBasis({ forward: fixedFwd, right, up });
|
|
9644
|
+
}
|
|
9645
|
+
const params = {
|
|
9646
|
+
objectPath,
|
|
9647
|
+
...typeof args.name === "string" && args.name.trim() ? { name: args.name.trim() } : {},
|
|
9648
|
+
location: target,
|
|
9649
|
+
rotation: outRot
|
|
9650
|
+
};
|
|
9651
|
+
const placed = await types.sendUnrealMcpTcpCommand({ type: "place_asset", params, timeoutMs: Math.max(timeoutMs, 1e4) });
|
|
9652
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9653
|
+
const payload = {
|
|
9654
|
+
anchor,
|
|
9655
|
+
origin,
|
|
9656
|
+
offset,
|
|
9657
|
+
location: target,
|
|
9658
|
+
rotation: outRot,
|
|
9659
|
+
...finalNormal ? { normal: finalNormal } : {},
|
|
9660
|
+
placed
|
|
9661
|
+
};
|
|
9662
|
+
return {
|
|
9663
|
+
content: [
|
|
9664
|
+
{ type: "text", text: "Placed asset relative to context." },
|
|
9665
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
9666
|
+
],
|
|
9667
|
+
isError: false
|
|
9668
|
+
};
|
|
9669
|
+
} catch (error) {
|
|
9670
|
+
return {
|
|
9671
|
+
content: [
|
|
9672
|
+
{ type: "text", text: "Failed to place asset relative to context." },
|
|
9673
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9674
|
+
],
|
|
9675
|
+
isError: true
|
|
9676
|
+
};
|
|
9677
|
+
}
|
|
9678
|
+
}));
|
|
9679
|
+
mcp.registerTool("unreal_spawn_actor_relative", {
|
|
9680
|
+
title: "Unreal Spawn Actor Relative (Context-Aware)",
|
|
9681
|
+
description: "High-level helper built from context/raycast primitives: spawn an actor type relative to selection/camera/player/hit result.",
|
|
9682
|
+
inputSchema: {
|
|
9683
|
+
type: z.z.string().describe('Actor class/type (e.g. "PointLight", "CameraActor", "StaticMeshActor").'),
|
|
9684
|
+
name: z.z.string().describe("Actor name."),
|
|
9685
|
+
anchor: z.z.enum(["selection", "editor_camera", "player", "player_camera", "hit_result", "actor"]).describe("Reference anchor for placement."),
|
|
9686
|
+
actorName: z.z.string().optional().describe('When anchor="actor", the actor name/label to anchor to.'),
|
|
9687
|
+
offset: z.z.array(z.z.number()).length(3).optional().describe("Offset in anchor local axes [forwardCm, rightCm, upCm] (default [200,0,0])."),
|
|
9688
|
+
alignToSurface: z.z.boolean().optional().describe("When using hit_result or drop-to-ground hits, align actor up to hit normal (default false)."),
|
|
9689
|
+
dropToGround: z.z.boolean().optional().describe("After computing target location, raycast down and snap to the hit location (default false)."),
|
|
9690
|
+
raySource: z.z.enum(["editor", "pie"]).optional().describe('When anchor="hit_result", which camera to raycast from (default editor).'),
|
|
9691
|
+
maxDistance: z.z.number().positive().optional().describe("Raycast max distance in cm (default 10000)."),
|
|
9692
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 8000).")
|
|
9693
|
+
}
|
|
9694
|
+
}, async (args) => runWithMcpToolCard("unreal_spawn_actor_relative", args, async () => {
|
|
9695
|
+
const timeoutMs = args.timeoutMs ?? 8e3;
|
|
9696
|
+
const offset = Array.isArray(args.offset) && args.offset.length === 3 ? args.offset : [200, 0, 0];
|
|
9697
|
+
const alignToSurface = args.alignToSurface ?? false;
|
|
9698
|
+
const dropToGround = args.dropToGround ?? false;
|
|
9699
|
+
const maxDistance = typeof args.maxDistance === "number" ? args.maxDistance : 1e4;
|
|
9700
|
+
const raySource = args.raySource ?? "editor";
|
|
9701
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9702
|
+
try {
|
|
9703
|
+
const type = String(args.type || "").trim();
|
|
9704
|
+
const name = String(args.name || "").trim();
|
|
9705
|
+
const anchorRaw = String(args.anchor || "").trim();
|
|
9706
|
+
const anchor = anchorRaw === "player" ? "player_camera" : anchorRaw;
|
|
9707
|
+
if (!type) throw new Error("Missing type.");
|
|
9708
|
+
if (!name) throw new Error("Missing name.");
|
|
9709
|
+
if (!vec.isFinite3(offset)) throw new Error("Invalid offset (expected [forward,right,up] numbers).");
|
|
9710
|
+
let origin = null;
|
|
9711
|
+
let rot = null;
|
|
9712
|
+
let surfaceNormal = null;
|
|
9713
|
+
if (anchor === "selection" || anchor === "editor_camera") {
|
|
9714
|
+
const ctx = unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs }));
|
|
9715
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9716
|
+
if (anchor === "selection") {
|
|
9717
|
+
const sel = Array.isArray(ctx?.selection) ? ctx.selection : [];
|
|
9718
|
+
if (sel.length === 0) throw new Error("No selected actors in Unreal Editor. Select an actor and retry.");
|
|
9719
|
+
const first = sel[0];
|
|
9720
|
+
origin = Array.isArray(first?.location) ? first.location : null;
|
|
9721
|
+
rot = Array.isArray(first?.rotation) ? first.rotation : null;
|
|
9722
|
+
} else {
|
|
9723
|
+
const cam = ctx?.viewportCamera;
|
|
9724
|
+
if (!cam || cam === null) throw new Error("No active viewport camera. Click the viewport and retry.");
|
|
9725
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9726
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9727
|
+
}
|
|
9728
|
+
} else if (anchor === "player_camera") {
|
|
9729
|
+
const pc = unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs }));
|
|
9730
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9731
|
+
if (!pc?.isPlaying) throw new Error("PIE is not running. Start PIE and retry (or use editor_camera/selection anchors).");
|
|
9732
|
+
const cam = pc?.camera;
|
|
9733
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9734
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9735
|
+
} else if (anchor === "actor") {
|
|
9736
|
+
const actorName = String(args.actorName || "").trim();
|
|
9737
|
+
if (!actorName) throw new Error('anchor="actor" requires actorName.');
|
|
9738
|
+
const t = unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "get_actor_transform", params: { name: actorName, source: "editor" }, timeoutMs }));
|
|
9739
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9740
|
+
origin = Array.isArray(t?.location) ? t.location : null;
|
|
9741
|
+
rot = Array.isArray(t?.rotation) ? t.rotation : null;
|
|
9742
|
+
} else if (anchor === "hit_result") {
|
|
9743
|
+
const hit = unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "raycast_from_camera", params: { source: raySource, maxDistance }, timeoutMs }));
|
|
9744
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9745
|
+
if (!hit?.hit) throw new Error("Raycast did not hit anything.");
|
|
9746
|
+
origin = Array.isArray(hit?.location) ? hit.location : null;
|
|
9747
|
+
surfaceNormal = Array.isArray(hit?.normal) ? hit.normal : null;
|
|
9748
|
+
const ctxRot = raySource === "pie" ? unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs }))?.camera?.rotation : unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs }))?.viewportCamera?.rotation;
|
|
9749
|
+
rot = Array.isArray(ctxRot) ? ctxRot : [0, 0, 0];
|
|
9750
|
+
} else {
|
|
9751
|
+
throw new Error(`Unknown anchor: ${anchor}`);
|
|
9752
|
+
}
|
|
9753
|
+
if (!vec.isFinite3(origin)) throw new Error("Anchor origin is missing or invalid.");
|
|
9754
|
+
if (!rot || !vec.isFinite3(rot)) throw new Error("Anchor rotation is missing or invalid.");
|
|
9755
|
+
const basis = forwardRightUpFromRotator(rot);
|
|
9756
|
+
let target = vec.add(origin, vec.add(vec.mul(basis.forward, offset[0]), vec.add(vec.mul(basis.right, offset[1]), vec.mul(basis.up, offset[2]))));
|
|
9757
|
+
let finalNormal = surfaceNormal;
|
|
9758
|
+
if (dropToGround) {
|
|
9759
|
+
const start = vec.add(target, [0, 0, 1e4]);
|
|
9760
|
+
const down = unwrapUnrealMcpResult(await types.sendUnrealMcpTcpCommand({ type: "raycast_down", params: { source: "editor", startLocation: start, maxDistance: 2e5 }, timeoutMs }));
|
|
9761
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9762
|
+
if (down?.hit && Array.isArray(down?.location)) {
|
|
9763
|
+
target = down.location;
|
|
9764
|
+
if (Array.isArray(down?.normal)) finalNormal = down.normal;
|
|
9765
|
+
} else {
|
|
9766
|
+
throw new Error("dropToGround=true but raycast_down did not hit anything.");
|
|
9767
|
+
}
|
|
9768
|
+
}
|
|
9769
|
+
let outRot = rot;
|
|
9770
|
+
if (alignToSurface && vec.isFinite3(finalNormal)) {
|
|
9771
|
+
const up = vec.norm(finalNormal);
|
|
9772
|
+
let fwd = basis.forward;
|
|
9773
|
+
fwd = vec.sub(fwd, vec.mul(up, vec.dot(fwd, up)));
|
|
9774
|
+
if (vec.len(fwd) <= 1e-4) {
|
|
9775
|
+
fwd = vec.cross([0, 1, 0], up);
|
|
9776
|
+
}
|
|
9777
|
+
fwd = vec.norm(fwd);
|
|
9778
|
+
const right = vec.norm(vec.cross(up, fwd));
|
|
9779
|
+
const fixedFwd = vec.norm(vec.cross(right, up));
|
|
9780
|
+
outRot = rotatorFromBasis({ forward: fixedFwd, right, up });
|
|
9781
|
+
}
|
|
9782
|
+
const spawned = await types.sendUnrealMcpTcpCommand({
|
|
9783
|
+
type: "spawn_actor",
|
|
9784
|
+
params: { type, name, location: target, rotation: outRot },
|
|
9785
|
+
timeoutMs: Math.max(timeoutMs, 1e4)
|
|
9786
|
+
});
|
|
9787
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9788
|
+
const payload = {
|
|
9789
|
+
anchor,
|
|
9790
|
+
origin,
|
|
9791
|
+
offset,
|
|
9792
|
+
location: target,
|
|
9793
|
+
rotation: outRot,
|
|
9794
|
+
...finalNormal ? { normal: finalNormal } : {},
|
|
9795
|
+
spawned
|
|
9796
|
+
};
|
|
9797
|
+
return {
|
|
9798
|
+
content: [
|
|
9799
|
+
{ type: "text", text: "Spawned actor relative to context." },
|
|
9800
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
9801
|
+
],
|
|
9802
|
+
isError: false
|
|
9803
|
+
};
|
|
9804
|
+
} catch (error) {
|
|
9805
|
+
return {
|
|
9806
|
+
content: [
|
|
9807
|
+
{ type: "text", text: "Failed to spawn actor relative to context." },
|
|
9808
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9809
|
+
],
|
|
9810
|
+
isError: true
|
|
9811
|
+
};
|
|
9812
|
+
}
|
|
9813
|
+
}));
|
|
9814
|
+
mcp.registerTool("unreal_mcp_get_actors_in_level", {
|
|
9815
|
+
title: "Unreal Actors In Level (UnrealMCP)",
|
|
9816
|
+
description: "List all actors in the current Unreal Editor level via the UnrealMCP engine plugin. Requires Unreal Editor running with the UnrealMCP plugin enabled.",
|
|
9817
|
+
inputSchema: {
|
|
9818
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9819
|
+
}
|
|
9820
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_actors_in_level", args, async () => {
|
|
7878
9821
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9822
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7879
9823
|
try {
|
|
7880
9824
|
const response = await types.sendUnrealMcpTcpCommand({ type: "get_actors_in_level", params: {}, timeoutMs });
|
|
9825
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7881
9826
|
const actors = Array.isArray(response?.actors) ? response.actors : Array.isArray(response?.result?.actors) ? response.result.actors : null;
|
|
7882
9827
|
if (!actors) {
|
|
7883
9828
|
return {
|
|
@@ -7914,8 +9859,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
7914
9859
|
}
|
|
7915
9860
|
}, async (args) => runWithMcpToolCard("unreal_mcp_get_play_in_editor_status", args, async () => {
|
|
7916
9861
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9862
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7917
9863
|
try {
|
|
7918
9864
|
const response = await types.sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", params: {}, timeoutMs });
|
|
9865
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7919
9866
|
return {
|
|
7920
9867
|
content: [
|
|
7921
9868
|
{ type: "text", text: "Fetched Play-In-Editor status." },
|
|
@@ -7942,6 +9889,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7942
9889
|
}
|
|
7943
9890
|
}, async (args) => runWithMcpToolCard("unreal_mcp_editor_health", args, async () => {
|
|
7944
9891
|
const timeoutMs = args.timeoutMs ?? 1500;
|
|
9892
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7945
9893
|
const classify = (message) => {
|
|
7946
9894
|
const m = message.toLowerCase();
|
|
7947
9895
|
if (m.includes("econnrefused")) return { state: "offline", hint: "UnrealMCP TCP port is not accepting connections (Unreal Editor likely closed/crashed or plugin not running)." };
|
|
@@ -7951,10 +9899,12 @@ ${String(st.stdout || "").trim()}`
|
|
|
7951
9899
|
};
|
|
7952
9900
|
try {
|
|
7953
9901
|
const ping = await types.sendUnrealMcpTcpCommand({ type: "ping", params: {}, timeoutMs });
|
|
9902
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7954
9903
|
const pluginInfo = await getUnrealMcpPluginInfoBestEffort(timeoutMs);
|
|
7955
9904
|
let pieStatus = null;
|
|
7956
9905
|
try {
|
|
7957
9906
|
pieStatus = await types.sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", params: {}, timeoutMs });
|
|
9907
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7958
9908
|
} catch {
|
|
7959
9909
|
pieStatus = null;
|
|
7960
9910
|
}
|
|
@@ -7994,8 +9944,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
7994
9944
|
}
|
|
7995
9945
|
}, async (args) => runWithMcpToolCard("unreal_mcp_play_in_editor", args, async () => {
|
|
7996
9946
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9947
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7997
9948
|
try {
|
|
7998
9949
|
const response = await types.sendUnrealMcpTcpCommand({ type: "play_in_editor", params: {}, timeoutMs });
|
|
9950
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7999
9951
|
return {
|
|
8000
9952
|
content: [
|
|
8001
9953
|
{ type: "text", text: "Requested Play In Editor (PIE) in active viewport." },
|
|
@@ -8022,8 +9974,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
8022
9974
|
}
|
|
8023
9975
|
}, async (args) => runWithMcpToolCard("unreal_mcp_play_in_editor_windowed", args, async () => {
|
|
8024
9976
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9977
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8025
9978
|
try {
|
|
8026
9979
|
const response = await types.sendUnrealMcpTcpCommand({ type: "play_in_editor_windowed", params: {}, timeoutMs });
|
|
9980
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8027
9981
|
return {
|
|
8028
9982
|
content: [
|
|
8029
9983
|
{ type: "text", text: "Requested Play In Editor (PIE) in a new editor window." },
|
|
@@ -8051,8 +10005,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
8051
10005
|
}
|
|
8052
10006
|
}, async (args) => runWithMcpToolCard("unreal_mcp_stop_play_in_editor", args, async () => {
|
|
8053
10007
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
10008
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8054
10009
|
try {
|
|
8055
10010
|
const response = await types.sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", params: {}, timeoutMs });
|
|
10011
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8056
10012
|
return {
|
|
8057
10013
|
content: [
|
|
8058
10014
|
{ type: "text", text: "Requested Stop Play In Editor (PIE)." },
|
|
@@ -8070,6 +10026,48 @@ ${String(st.stdout || "").trim()}`
|
|
|
8070
10026
|
};
|
|
8071
10027
|
}
|
|
8072
10028
|
}));
|
|
10029
|
+
mcp.registerTool("unreal_smoke_test", {
|
|
10030
|
+
title: "Unreal Smoke Test (PIE + Screenshot Validation)",
|
|
10031
|
+
description: 'Evidence-first "did it run?": ensures PIE is stopped, starts PIE in a new window, waits briefly, captures a validation screenshot to Saved/Screenshots/Flockbay, stops PIE, then uploads the screenshot to the current session.',
|
|
10032
|
+
inputSchema: {
|
|
10033
|
+
uprojectPath: z.z.string().describe("Absolute path to the .uproject file (used to locate Saved/Screenshots/Flockbay for evidence upload)."),
|
|
10034
|
+
stabilizeMs: z.z.number().int().positive().optional().describe("Time to wait before taking screenshot (default 1500)."),
|
|
10035
|
+
timeoutMs: z.z.number().int().positive().optional().describe("Socket timeout in ms for each step (default 30000)."),
|
|
10036
|
+
stopIfPlaying: z.z.boolean().optional().describe("If PIE is already running, stop it first (default true).")
|
|
10037
|
+
}
|
|
10038
|
+
}, async (args) => runWithMcpToolCard("unreal_smoke_test", args, async () => {
|
|
10039
|
+
const uprojectPath = typeof args?.uprojectPath === "string" ? String(args.uprojectPath).trim() : "";
|
|
10040
|
+
const stabilizeMs = typeof args?.stabilizeMs === "number" ? args.stabilizeMs : void 0;
|
|
10041
|
+
const timeoutMs = typeof args?.timeoutMs === "number" ? args.timeoutMs : void 0;
|
|
10042
|
+
const stopIfPlaying = typeof args?.stopIfPlaying === "boolean" ? args.stopIfPlaying : void 0;
|
|
10043
|
+
if (!uprojectPath || !uprojectPath.toLowerCase().endsWith(".uproject") || !path.isAbsolute(uprojectPath)) {
|
|
10044
|
+
return {
|
|
10045
|
+
content: [{ type: "text", text: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}` }],
|
|
10046
|
+
isError: true
|
|
10047
|
+
};
|
|
10048
|
+
}
|
|
10049
|
+
if (!fs.existsSync(uprojectPath)) {
|
|
10050
|
+
return { content: [{ type: "text", text: `uprojectPath not found: ${uprojectPath}` }], isError: true };
|
|
10051
|
+
}
|
|
10052
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
10053
|
+
const result = await runUnrealSmokeTest({ uprojectPath, stabilizeMs, timeoutMs, stopIfPlaying });
|
|
10054
|
+
let viewsPayload = [];
|
|
10055
|
+
if (result.screenshotPath && typeof result.screenshotPath === "string" && fs.existsSync(result.screenshotPath)) {
|
|
10056
|
+
const uploaded = await uploadScreenshotViewsForSession({
|
|
10057
|
+
sessionId: client.sessionId,
|
|
10058
|
+
token: client.getAuthToken(),
|
|
10059
|
+
views: [{ id: "smoke_test", path: result.screenshotPath }]
|
|
10060
|
+
});
|
|
10061
|
+
viewsPayload = uploaded.map((u) => ({ ...u, id: u.id || "smoke_test" }));
|
|
10062
|
+
}
|
|
10063
|
+
const payload = { ...result, ...viewsPayload.length ? { views: viewsPayload } : {} };
|
|
10064
|
+
const content = [
|
|
10065
|
+
{ type: "text", text: result.ok ? "Smoke test succeeded." : "Smoke test failed." },
|
|
10066
|
+
...result.screenshotPath ? [{ type: "text", text: `Screenshot: ${result.screenshotPath}` }] : [],
|
|
10067
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
10068
|
+
];
|
|
10069
|
+
return { content, ...viewsPayload.length ? { views: viewsPayload } : {}, isError: !result.ok };
|
|
10070
|
+
}));
|
|
8073
10071
|
mcp.registerTool("unreal_mechanic_run", {
|
|
8074
10072
|
title: "Unreal Mechanic Builder (Dash MVP)",
|
|
8075
10073
|
description: "Create a project-safe mechanic test map (Parallel mode) and prove it works with Flockbay screenshot evidence. V1 supports dash only.",
|
|
@@ -8231,13 +10229,48 @@ Fix: ${res.hint}` : "";
|
|
|
8231
10229
|
"read_images",
|
|
8232
10230
|
"unreal_headless_screenshot",
|
|
8233
10231
|
"unreal_latest_screenshots",
|
|
10232
|
+
"unreal_editor_launch",
|
|
10233
|
+
"unreal_build_project",
|
|
8234
10234
|
"unreal_mcp_command",
|
|
10235
|
+
"unreal_mcp_list_capabilities",
|
|
10236
|
+
"unreal_mcp_get_command_schema",
|
|
10237
|
+
"unreal_mcp_search_blueprint_functions",
|
|
10238
|
+
"unreal_mcp_resolve_blueprint_function",
|
|
10239
|
+
"unreal_mcp_search_assets",
|
|
10240
|
+
"unreal_mcp_get_asset_info",
|
|
10241
|
+
"unreal_mcp_list_asset_packs",
|
|
10242
|
+
"unreal_mcp_place_asset",
|
|
10243
|
+
"unreal_mcp_get_editor_context",
|
|
10244
|
+
"unreal_mcp_get_player_context",
|
|
10245
|
+
"unreal_mcp_raycast_from_camera",
|
|
10246
|
+
"unreal_mcp_raycast_down",
|
|
10247
|
+
"unreal_mcp_get_actor_transform",
|
|
10248
|
+
"unreal_mcp_get_actor_bounds",
|
|
10249
|
+
"unreal_mcp_create_landscape",
|
|
10250
|
+
"unreal_mcp_map_check",
|
|
10251
|
+
"unreal_mcp_compile_blueprints_all",
|
|
10252
|
+
"unreal_mcp_save_all",
|
|
10253
|
+
"unreal_place_asset_relative",
|
|
10254
|
+
"unreal_spawn_actor_relative",
|
|
8235
10255
|
"unreal_mcp_get_actors_in_level",
|
|
10256
|
+
"unreal_mcp_get_play_in_editor_status",
|
|
10257
|
+
"unreal_mcp_editor_health",
|
|
10258
|
+
"unreal_mcp_play_in_editor",
|
|
10259
|
+
"unreal_mcp_play_in_editor_windowed",
|
|
10260
|
+
"unreal_mcp_stop_play_in_editor",
|
|
10261
|
+
"unreal_smoke_test",
|
|
8236
10262
|
"unreal_fast_preview",
|
|
8237
10263
|
"unreal_mechanic_run"
|
|
8238
10264
|
],
|
|
10265
|
+
unreal: {
|
|
10266
|
+
onIssue: (handler2) => {
|
|
10267
|
+
unrealIssueEmitter.on("issue", handler2);
|
|
10268
|
+
return () => unrealIssueEmitter.off("issue", handler2);
|
|
10269
|
+
}
|
|
10270
|
+
},
|
|
8239
10271
|
stop: () => {
|
|
8240
10272
|
types.logger.debug("[flockbayMCP] Stopping server");
|
|
10273
|
+
unrealEditorSupervisor.stop();
|
|
8241
10274
|
mcp.close();
|
|
8242
10275
|
server.close();
|
|
8243
10276
|
}
|
|
@@ -8462,8 +10495,20 @@ async function runClaude(credentials, options = {}) {
|
|
|
8462
10495
|
lifecycleStateSince: Date.now(),
|
|
8463
10496
|
flavor: "claude"
|
|
8464
10497
|
};
|
|
8465
|
-
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
8466
|
-
types.logger.debug(
|
|
10498
|
+
const response = options.sessionId ? await api.getSessionById(options.sessionId) : await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
10499
|
+
types.logger.debug(`${options.sessionId ? "Session attached" : "Session created"}: ${response.id}`);
|
|
10500
|
+
const session = api.sessionSyncClient(response);
|
|
10501
|
+
if (options.sessionId) {
|
|
10502
|
+
session.updateMetadata((currentMetadata) => ({
|
|
10503
|
+
...currentMetadata,
|
|
10504
|
+
...metadata,
|
|
10505
|
+
// Preserve user-facing fields set by other clients.
|
|
10506
|
+
name: currentMetadata?.name,
|
|
10507
|
+
summary: currentMetadata?.summary
|
|
10508
|
+
}));
|
|
10509
|
+
}
|
|
10510
|
+
await session.connectAndWait(15e3);
|
|
10511
|
+
session.keepAlive(false, options.startingMode === "remote" ? "remote" : "local");
|
|
8467
10512
|
try {
|
|
8468
10513
|
types.logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
8469
10514
|
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
|
@@ -8478,7 +10523,7 @@ async function runClaude(credentials, options = {}) {
|
|
|
8478
10523
|
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
8479
10524
|
types.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
8480
10525
|
try {
|
|
8481
|
-
|
|
10526
|
+
session.updateMetadata((currentMetadata) => ({
|
|
8482
10527
|
...currentMetadata,
|
|
8483
10528
|
tools: sdkMetadata.tools,
|
|
8484
10529
|
slashCommands: sdkMetadata.slashCommands
|
|
@@ -8488,7 +10533,6 @@ async function runClaude(credentials, options = {}) {
|
|
|
8488
10533
|
types.logger.debug("[start] Failed to update session metadata:", error);
|
|
8489
10534
|
}
|
|
8490
10535
|
});
|
|
8491
|
-
const session = api.sessionSyncClient(response);
|
|
8492
10536
|
const elicitationHub = new ElicitationHub();
|
|
8493
10537
|
const flockbayServer = await startFlockbayServer(session, { elicitationHub });
|
|
8494
10538
|
types.logger.debug(`[START] Flockbay MCP server started at ${flockbayServer.url}`);
|
|
@@ -8840,20 +10884,45 @@ async function handleAuthCommand(args) {
|
|
|
8840
10884
|
return;
|
|
8841
10885
|
}
|
|
8842
10886
|
switch (subcommand) {
|
|
8843
|
-
case "login":
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
await
|
|
8856
|
-
|
|
10887
|
+
case "login": {
|
|
10888
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
10889
|
+
if (force) {
|
|
10890
|
+
try {
|
|
10891
|
+
await stopDaemon();
|
|
10892
|
+
} catch {
|
|
10893
|
+
}
|
|
10894
|
+
await types.clearCredentials();
|
|
10895
|
+
if (args.includes("--clear-machine-id")) {
|
|
10896
|
+
await types.clearMachineId();
|
|
10897
|
+
}
|
|
10898
|
+
}
|
|
10899
|
+
await loginWithClerkAndPairMachine();
|
|
10900
|
+
return;
|
|
10901
|
+
}
|
|
10902
|
+
case "logout": {
|
|
10903
|
+
try {
|
|
10904
|
+
await stopDaemon();
|
|
10905
|
+
} catch {
|
|
10906
|
+
}
|
|
10907
|
+
await types.clearCredentials();
|
|
10908
|
+
await types.clearMachineId();
|
|
10909
|
+
console.log(chalk.green("\u2713 Logged out (profile reset)"));
|
|
10910
|
+
return;
|
|
10911
|
+
}
|
|
10912
|
+
case "status": {
|
|
10913
|
+
const auth = await types.readCredentials();
|
|
10914
|
+
const settings = await types.readSettings();
|
|
10915
|
+
const daemon = await types.readDaemonState();
|
|
10916
|
+
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
10917
|
+
console.log(chalk.gray(`Profile: ${types.configuration.profile}`));
|
|
10918
|
+
console.log(chalk.gray(`Server: ${types.configuration.serverUrl}`));
|
|
10919
|
+
console.log(chalk.gray(`Web app: ${types.configuration.webappUrl}`));
|
|
10920
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
10921
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
10922
|
+
console.log(chalk.gray(`Auth: ${auth?.machineToken ? "paired" : "not paired"}`));
|
|
10923
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
10924
|
+
return;
|
|
10925
|
+
}
|
|
8857
10926
|
default:
|
|
8858
10927
|
console.error(chalk.red(`Unknown auth subcommand: ${subcommand}`));
|
|
8859
10928
|
showAuthHelp();
|
|
@@ -8862,186 +10931,14 @@ async function handleAuthCommand(args) {
|
|
|
8862
10931
|
}
|
|
8863
10932
|
function showAuthHelp() {
|
|
8864
10933
|
console.log(`
|
|
8865
|
-
${chalk.bold("flockbay auth")} - Authentication management
|
|
10934
|
+
${chalk.bold("flockbay auth")} - Authentication management (Clerk-based)
|
|
8866
10935
|
|
|
8867
10936
|
${chalk.bold("Usage:")}
|
|
8868
|
-
flockbay auth login [--force] [--
|
|
8869
|
-
flockbay auth logout
|
|
8870
|
-
flockbay auth status
|
|
8871
|
-
flockbay auth show-backup Display backup key for mobile/web clients
|
|
8872
|
-
flockbay auth help Show this help message
|
|
8873
|
-
|
|
8874
|
-
${chalk.bold("Options:")}
|
|
8875
|
-
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
8876
|
-
--mobile Authenticate by scanning a QR code (advanced)
|
|
8877
|
-
--web Authenticate via browser login (default)
|
|
8878
|
-
`);
|
|
8879
|
-
}
|
|
8880
|
-
async function handleAuthLogin(args) {
|
|
8881
|
-
const forceAuth = args.includes("--force") || args.includes("-f");
|
|
8882
|
-
const quiet = args.includes("--quiet") || args.includes("--no-summary");
|
|
8883
|
-
const preferMobile = args.includes("--mobile");
|
|
8884
|
-
const preferWeb = args.includes("--web");
|
|
8885
|
-
if (preferMobile && preferWeb) {
|
|
8886
|
-
console.error(chalk.red("Choose only one authentication method: --mobile or --web"));
|
|
8887
|
-
process.exit(1);
|
|
8888
|
-
}
|
|
8889
|
-
const authMethod = preferMobile ? "mobile" : "web";
|
|
8890
|
-
if (forceAuth) {
|
|
8891
|
-
if (!quiet) {
|
|
8892
|
-
console.log(chalk.yellow("Force authentication requested."));
|
|
8893
|
-
console.log(chalk.gray("This will:"));
|
|
8894
|
-
console.log(chalk.gray(" \u2022 Clear existing credentials"));
|
|
8895
|
-
console.log(chalk.gray(" \u2022 Clear machine ID"));
|
|
8896
|
-
console.log(chalk.gray(" \u2022 Stop daemon if running"));
|
|
8897
|
-
console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
|
|
8898
|
-
}
|
|
8899
|
-
try {
|
|
8900
|
-
types.logger.debug("Stopping daemon for force auth...");
|
|
8901
|
-
await stopDaemon();
|
|
8902
|
-
if (!quiet) console.log(chalk.gray("\u2713 Stopped daemon"));
|
|
8903
|
-
} catch (error) {
|
|
8904
|
-
types.logger.debug("Daemon was not running or failed to stop:", error);
|
|
8905
|
-
}
|
|
8906
|
-
await types.clearCredentials();
|
|
8907
|
-
if (!quiet) console.log(chalk.gray("\u2713 Cleared credentials"));
|
|
8908
|
-
await types.clearMachineId();
|
|
8909
|
-
if (!quiet) console.log(chalk.gray("\u2713 Cleared machine ID"));
|
|
8910
|
-
if (!quiet) console.log("");
|
|
8911
|
-
}
|
|
8912
|
-
if (!forceAuth) {
|
|
8913
|
-
const existingCreds = await types.readCredentials();
|
|
8914
|
-
const settings = await types.readSettings();
|
|
8915
|
-
if (existingCreds && settings?.machineId) {
|
|
8916
|
-
if (!quiet) {
|
|
8917
|
-
console.log(chalk.green("\u2713 Already authenticated"));
|
|
8918
|
-
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
8919
|
-
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
8920
|
-
console.log(chalk.gray(` Use 'flockbay auth login --force' to re-authenticate`));
|
|
8921
|
-
}
|
|
8922
|
-
return;
|
|
8923
|
-
} else if (existingCreds && !settings?.machineId) {
|
|
8924
|
-
if (!quiet) {
|
|
8925
|
-
console.log(chalk.yellow("\u26A0\uFE0F Credentials exist but machine ID is missing"));
|
|
8926
|
-
console.log(chalk.gray(" This can happen if --auth flag was used previously"));
|
|
8927
|
-
console.log(chalk.gray(" Fixing by setting up machine...\n"));
|
|
8928
|
-
}
|
|
8929
|
-
}
|
|
8930
|
-
}
|
|
8931
|
-
try {
|
|
8932
|
-
const result = await authAndSetupMachineIfNeeded({ authMethod });
|
|
8933
|
-
if (!quiet) {
|
|
8934
|
-
console.log(chalk.green("\n\u2713 Authentication successful"));
|
|
8935
|
-
console.log(chalk.gray(` Machine ID: ${result.machineId}`));
|
|
8936
|
-
console.log(chalk.gray(` Server: ${types.configuration.serverUrl}`));
|
|
8937
|
-
console.log(chalk.gray(` Web app: ${types.configuration.webappUrl}`));
|
|
8938
|
-
console.log(
|
|
8939
|
-
chalk.gray(
|
|
8940
|
-
`
|
|
8941
|
-
Next: run ${chalk.bold("flockbay start")} to start the daemon (this is what makes your computer appear as connected in the app).`
|
|
8942
|
-
)
|
|
8943
|
-
);
|
|
8944
|
-
}
|
|
8945
|
-
} catch (error) {
|
|
8946
|
-
console.error(chalk.red("Authentication failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
8947
|
-
process.exit(1);
|
|
8948
|
-
}
|
|
8949
|
-
}
|
|
8950
|
-
async function handleAuthLogout() {
|
|
8951
|
-
const dataDir = types.configuration.flockbayHomeDir;
|
|
8952
|
-
const credentials = await types.readCredentials();
|
|
8953
|
-
if (!credentials) {
|
|
8954
|
-
console.log(chalk.yellow("Not currently authenticated"));
|
|
8955
|
-
return;
|
|
8956
|
-
}
|
|
8957
|
-
console.log(chalk.blue("This will log you out of Flockbay"));
|
|
8958
|
-
console.log(chalk.yellow("\u26A0\uFE0F You will need to re-authenticate to use Flockbay again"));
|
|
8959
|
-
const rl = node_readline.createInterface({
|
|
8960
|
-
input: process.stdin,
|
|
8961
|
-
output: process.stdout
|
|
8962
|
-
});
|
|
8963
|
-
const answer = await new Promise((resolve) => {
|
|
8964
|
-
rl.question(chalk.yellow("Are you sure you want to log out? (y/N): "), resolve);
|
|
8965
|
-
});
|
|
8966
|
-
rl.close();
|
|
8967
|
-
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
8968
|
-
try {
|
|
8969
|
-
try {
|
|
8970
|
-
await stopDaemon();
|
|
8971
|
-
console.log(chalk.gray("Stopped daemon"));
|
|
8972
|
-
} catch (err) {
|
|
8973
|
-
console.error("[auth logout] Failed to stop daemon during logout", err);
|
|
8974
|
-
console.log(chalk.yellow("Warning: failed to stop daemon (continuing logout)."));
|
|
8975
|
-
}
|
|
8976
|
-
if (fs.existsSync(dataDir)) {
|
|
8977
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
8978
|
-
}
|
|
8979
|
-
console.log(chalk.green("\u2713 Successfully logged out"));
|
|
8980
|
-
console.log(chalk.gray(' Run "flockbay auth login" to authenticate again'));
|
|
8981
|
-
} catch (error) {
|
|
8982
|
-
throw new Error(`Failed to logout: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
8983
|
-
}
|
|
8984
|
-
} else {
|
|
8985
|
-
console.log(chalk.blue("Logout cancelled"));
|
|
8986
|
-
}
|
|
8987
|
-
}
|
|
8988
|
-
async function handleAuthShowBackup() {
|
|
8989
|
-
const accessKeyPath = types.configuration.privateKeyFile;
|
|
8990
|
-
if (!fs.existsSync(accessKeyPath)) {
|
|
8991
|
-
console.log(chalk.red("\u2717 Not authenticated"));
|
|
8992
|
-
console.log(chalk.gray(' Run "flockbay auth login" to authenticate'));
|
|
8993
|
-
process.exit(1);
|
|
8994
|
-
}
|
|
8995
|
-
let parsed;
|
|
8996
|
-
try {
|
|
8997
|
-
parsed = JSON.parse(fs.readFileSync(accessKeyPath, "utf8"));
|
|
8998
|
-
} catch {
|
|
8999
|
-
console.error(chalk.red("Backup key unavailable: failed to parse key file."));
|
|
9000
|
-
process.exit(1);
|
|
9001
|
-
}
|
|
9002
|
-
const machineKeyB64 = parsed?.encryption?.machineKey;
|
|
9003
|
-
if (!machineKeyB64 || typeof machineKeyB64 !== "string") {
|
|
9004
|
-
console.error(chalk.red("Backup key unavailable: encryption.machineKey missing in key file."));
|
|
9005
|
-
process.exit(1);
|
|
9006
|
-
}
|
|
9007
|
-
const buf = Buffer.from(machineKeyB64, "base64");
|
|
9008
|
-
const key = buf.toString("base64url");
|
|
9009
|
-
process.stdout.write(`${key}
|
|
10937
|
+
flockbay auth login [--force] [--clear-machine-id] Sign in and pair this machine to a workspace
|
|
10938
|
+
flockbay auth logout Clear local auth + machine id for this profile
|
|
10939
|
+
flockbay auth status Show current status
|
|
9010
10940
|
`);
|
|
9011
10941
|
}
|
|
9012
|
-
async function handleAuthStatus() {
|
|
9013
|
-
const credentials = await types.readCredentials();
|
|
9014
|
-
const settings = await types.readSettings();
|
|
9015
|
-
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
9016
|
-
if (!credentials) {
|
|
9017
|
-
console.log(chalk.red("\u2717 Not authenticated"));
|
|
9018
|
-
console.log(chalk.gray(' Run "flockbay auth login" to authenticate'));
|
|
9019
|
-
return;
|
|
9020
|
-
}
|
|
9021
|
-
console.log(chalk.green("\u2713 Authenticated"));
|
|
9022
|
-
const tokenPreview = credentials.token.substring(0, 30) + "...";
|
|
9023
|
-
console.log(chalk.gray(` Token: ${tokenPreview}`));
|
|
9024
|
-
if (settings?.machineId) {
|
|
9025
|
-
console.log(chalk.green("\u2713 Machine registered"));
|
|
9026
|
-
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
9027
|
-
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
9028
|
-
} else {
|
|
9029
|
-
console.log(chalk.yellow("\u26A0\uFE0F Machine not registered"));
|
|
9030
|
-
console.log(chalk.gray(' Run "flockbay auth login --force" to fix this'));
|
|
9031
|
-
}
|
|
9032
|
-
console.log(chalk.gray(`
|
|
9033
|
-
Data directory: ${types.configuration.flockbayHomeDir}`));
|
|
9034
|
-
try {
|
|
9035
|
-
const running = await checkIfDaemonRunningAndCleanupStaleState();
|
|
9036
|
-
if (running) {
|
|
9037
|
-
console.log(chalk.green("\u2713 Daemon running"));
|
|
9038
|
-
} else {
|
|
9039
|
-
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
9040
|
-
}
|
|
9041
|
-
} catch {
|
|
9042
|
-
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
9043
|
-
}
|
|
9044
|
-
}
|
|
9045
10942
|
|
|
9046
10943
|
const CLIENT_ID$2 = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
9047
10944
|
const AUTH_BASE_URL = "https://auth.openai.com";
|
|
@@ -9667,6 +11564,15 @@ function looksLikeMachineAuthMismatch(logTail) {
|
|
|
9667
11564
|
const hasMachinesEndpoint = /\/v1\/machines\b/i.test(t);
|
|
9668
11565
|
return has401 && hasMachinesEndpoint;
|
|
9669
11566
|
}
|
|
11567
|
+
function looksLikeServerUnreachable(logTail) {
|
|
11568
|
+
const t = String(logTail || "");
|
|
11569
|
+
if (!t) return null;
|
|
11570
|
+
const codeMatch = t.match(/\b(ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT)\b/) || t.match(/connect\s+(ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT)\b/i);
|
|
11571
|
+
const code = codeMatch?.[1] || codeMatch?.[0] || null;
|
|
11572
|
+
if (!code) return null;
|
|
11573
|
+
const url = t.match(/"url"\s*:\s*"([^"]+)"/)?.[1] ?? null;
|
|
11574
|
+
return { code: String(code), url };
|
|
11575
|
+
}
|
|
9670
11576
|
async function promptYesNo(question, { defaultYes }) {
|
|
9671
11577
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
9672
11578
|
if (!isInteractive) return false;
|
|
@@ -9689,10 +11595,135 @@ async function reauthForCurrentServerKeepingMachineId() {
|
|
|
9689
11595
|
} catch {
|
|
9690
11596
|
}
|
|
9691
11597
|
await types.clearCredentials();
|
|
9692
|
-
await
|
|
11598
|
+
await loginWithClerkAndPairMachine();
|
|
11599
|
+
}
|
|
11600
|
+
function openUrlBestEffort(url) {
|
|
11601
|
+
const u = String(url || "").trim();
|
|
11602
|
+
if (!u) return;
|
|
11603
|
+
if (process.env.FLOCKBAY_NO_OPEN === "1") return;
|
|
11604
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
11605
|
+
if (!isInteractive) return;
|
|
11606
|
+
try {
|
|
11607
|
+
if (process.platform === "darwin") {
|
|
11608
|
+
const p2 = node_child_process.spawn("open", [u], { detached: true, stdio: "ignore" });
|
|
11609
|
+
p2.unref();
|
|
11610
|
+
return;
|
|
11611
|
+
}
|
|
11612
|
+
if (process.platform === "win32") {
|
|
11613
|
+
const p2 = node_child_process.spawn("cmd", ["/c", "start", "", u], { detached: true, stdio: "ignore" });
|
|
11614
|
+
p2.unref();
|
|
11615
|
+
return;
|
|
11616
|
+
}
|
|
11617
|
+
const p = node_child_process.spawn("xdg-open", [u], { detached: true, stdio: "ignore" });
|
|
11618
|
+
p.unref();
|
|
11619
|
+
} catch {
|
|
11620
|
+
}
|
|
11621
|
+
}
|
|
11622
|
+
async function fetchDaemonStatus() {
|
|
11623
|
+
const state = await types.readDaemonState().catch(() => null);
|
|
11624
|
+
if (!state?.httpPort || !state?.pid) return null;
|
|
11625
|
+
try {
|
|
11626
|
+
process.kill(state.pid, 0);
|
|
11627
|
+
} catch {
|
|
11628
|
+
return null;
|
|
11629
|
+
}
|
|
11630
|
+
try {
|
|
11631
|
+
const res = await fetch(`http://127.0.0.1:${state.httpPort}/status`, {
|
|
11632
|
+
method: "POST",
|
|
11633
|
+
headers: { "Content-Type": "application/json" },
|
|
11634
|
+
body: "{}",
|
|
11635
|
+
signal: AbortSignal.timeout(1500)
|
|
11636
|
+
});
|
|
11637
|
+
if (!res.ok) return null;
|
|
11638
|
+
return await res.json().catch(() => null);
|
|
11639
|
+
} catch {
|
|
11640
|
+
return null;
|
|
11641
|
+
}
|
|
11642
|
+
}
|
|
11643
|
+
async function waitForDaemonConnected(timeoutMs) {
|
|
11644
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
11645
|
+
while (Date.now() < deadline) {
|
|
11646
|
+
const status = await fetchDaemonStatus();
|
|
11647
|
+
const connected = Boolean(status?.connection?.connected);
|
|
11648
|
+
if (connected) return status;
|
|
11649
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
11650
|
+
}
|
|
11651
|
+
return fetchDaemonStatus();
|
|
9693
11652
|
}
|
|
9694
11653
|
async function startDaemonDetachedOrExit(opts) {
|
|
9695
|
-
const
|
|
11654
|
+
const alreadyRunning = await checkIfDaemonRunningAndCleanupStaleState().catch(() => false);
|
|
11655
|
+
if (alreadyRunning) {
|
|
11656
|
+
const versionMatches = await isDaemonRunningCurrentCliVersion().catch(() => true);
|
|
11657
|
+
if (!versionMatches) {
|
|
11658
|
+
await stopDaemon();
|
|
11659
|
+
} else {
|
|
11660
|
+
const status = await waitForDaemonConnected(5e3);
|
|
11661
|
+
const connected = Boolean(status?.connection?.connected);
|
|
11662
|
+
const lastUpsertStatus = Number.isFinite(Number(status?.connection?.lastHttpUpsertStatus)) ? Number(status.connection.lastHttpUpsertStatus) : null;
|
|
11663
|
+
const lastConnectError = typeof status?.connection?.lastConnectError === "string" ? status.connection.lastConnectError : "";
|
|
11664
|
+
if (connected) {
|
|
11665
|
+
const auth = await types.readCredentials().catch(() => null);
|
|
11666
|
+
const settings = await types.readSettings().catch(() => null);
|
|
11667
|
+
const daemon = await types.readDaemonState().catch(() => null);
|
|
11668
|
+
const desiredOrgId = auth?.orgId ? String(auth.orgId).trim() : "";
|
|
11669
|
+
const desiredMachineId = settings?.machineId ? String(settings.machineId).trim() : "";
|
|
11670
|
+
const daemonOrgId = typeof status?.orgId === "string" ? status.orgId.trim() : "";
|
|
11671
|
+
const daemonMachineId = typeof status?.machineId === "string" ? status.machineId.trim() : "";
|
|
11672
|
+
const daemonServerUrl = typeof status?.serverUrl === "string" ? status.serverUrl.trim() : "";
|
|
11673
|
+
const desiredServerUrl = String(types.configuration.serverUrl || "").trim();
|
|
11674
|
+
const orgMismatch = desiredOrgId && daemonOrgId && desiredOrgId !== daemonOrgId;
|
|
11675
|
+
const machineMismatch = desiredMachineId && daemonMachineId && desiredMachineId !== daemonMachineId;
|
|
11676
|
+
const serverMismatch = desiredServerUrl && daemonServerUrl && desiredServerUrl !== daemonServerUrl;
|
|
11677
|
+
if ((orgMismatch || machineMismatch || serverMismatch) && !opts?.restartAttempted) {
|
|
11678
|
+
console.error("");
|
|
11679
|
+
console.error(chalk.yellow("Daemon is running with different settings than the current CLI profile."));
|
|
11680
|
+
if (serverMismatch) console.error(chalk.gray(`Daemon server: ${daemonServerUrl || "unknown"}`));
|
|
11681
|
+
if (desiredServerUrl) console.error(chalk.gray(`CLI server: ${desiredServerUrl}`));
|
|
11682
|
+
if (orgMismatch) console.error(chalk.gray(`Daemon org: ${daemonOrgId || "unknown"}`));
|
|
11683
|
+
if (desiredOrgId) console.error(chalk.gray(`CLI org: ${desiredOrgId}`));
|
|
11684
|
+
if (machineMismatch) console.error(chalk.gray(`Daemon machine: ${daemonMachineId || "unknown"}`));
|
|
11685
|
+
if (desiredMachineId) console.error(chalk.gray(`CLI machine: ${desiredMachineId}`));
|
|
11686
|
+
console.error(chalk.gray("Restarting daemon to apply current login/pairing..."));
|
|
11687
|
+
await stopDaemon();
|
|
11688
|
+
await startDaemonDetachedOrExit({ ...opts, restartAttempted: true });
|
|
11689
|
+
return;
|
|
11690
|
+
}
|
|
11691
|
+
console.log(chalk.bold("\nFlockbay ready\n"));
|
|
11692
|
+
console.log(chalk.gray(`Profile: ${types.configuration.profile}`));
|
|
11693
|
+
console.log(chalk.gray(`Server: ${types.configuration.serverUrl}`));
|
|
11694
|
+
console.log(chalk.gray(`Web app: ${types.configuration.webappUrl}`));
|
|
11695
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
11696
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
11697
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
11698
|
+
console.log("");
|
|
11699
|
+
openUrlBestEffort(types.configuration.webappUrl);
|
|
11700
|
+
process.exit(0);
|
|
11701
|
+
}
|
|
11702
|
+
const authMismatch = lastUpsertStatus === 401 || /unauthorized/i.test(lastConnectError);
|
|
11703
|
+
if (authMismatch && !opts?.reauthAttempted) {
|
|
11704
|
+
console.error("");
|
|
11705
|
+
console.error(chalk.yellow("Your saved CLI token was rejected by the server (401/unauthorized)."));
|
|
11706
|
+
console.error(chalk.gray("This is common in local dev if the backend store was reset/rebuilt."));
|
|
11707
|
+
console.error("");
|
|
11708
|
+
const shouldReauth = await promptYesNo("Re-authenticate now and retry?", { defaultYes: true });
|
|
11709
|
+
if (shouldReauth) {
|
|
11710
|
+
await reauthForCurrentServerKeepingMachineId();
|
|
11711
|
+
await startDaemonDetachedOrExit({ reauthAttempted: true });
|
|
11712
|
+
return;
|
|
11713
|
+
}
|
|
11714
|
+
process.exit(1);
|
|
11715
|
+
}
|
|
11716
|
+
console.error("");
|
|
11717
|
+
console.error(chalk.red("Daemon is running but not connected to the server."));
|
|
11718
|
+
if (typeof status?.connection?.lastHttpUpsertError === "string" && status.connection.lastHttpUpsertError.trim()) {
|
|
11719
|
+
console.error(chalk.gray(`Last upsert error: ${status.connection.lastHttpUpsertError.trim()}`));
|
|
11720
|
+
}
|
|
11721
|
+
if (lastConnectError) console.error(chalk.gray(`Last connect error: ${lastConnectError}`));
|
|
11722
|
+
console.error(chalk.gray("Tip: if the backend is restarting, wait a moment and re-run `flockbay start`."));
|
|
11723
|
+
process.exit(1);
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
const child = spawnFlockbayCLI(["daemon", "start-sync", "--profile", types.configuration.profile], {
|
|
9696
11727
|
detached: true,
|
|
9697
11728
|
stdio: "ignore",
|
|
9698
11729
|
env: process.env
|
|
@@ -9707,7 +11738,28 @@ async function startDaemonDetachedOrExit(opts) {
|
|
|
9707
11738
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
9708
11739
|
}
|
|
9709
11740
|
if (started) {
|
|
9710
|
-
|
|
11741
|
+
const status = await waitForDaemonConnected(5e3);
|
|
11742
|
+
if (!Boolean(status?.connection?.connected)) {
|
|
11743
|
+
const lastUpsertError = typeof status?.connection?.lastHttpUpsertError === "string" ? status.connection.lastHttpUpsertError.trim() : "";
|
|
11744
|
+
const lastConnectError = typeof status?.connection?.lastConnectError === "string" ? status.connection.lastConnectError.trim() : "";
|
|
11745
|
+
console.error(chalk.red("Daemon started, but failed to connect to the server."));
|
|
11746
|
+
if (lastUpsertError) console.error(chalk.gray(`Last upsert error: ${lastUpsertError}`));
|
|
11747
|
+
if (lastConnectError) console.error(chalk.gray(`Last connect error: ${lastConnectError}`));
|
|
11748
|
+
console.error(chalk.gray("Tip: if the backend is restarting, wait a moment and re-run `flockbay start`."));
|
|
11749
|
+
process.exit(1);
|
|
11750
|
+
}
|
|
11751
|
+
const auth = await types.readCredentials().catch(() => null);
|
|
11752
|
+
const settings = await types.readSettings().catch(() => null);
|
|
11753
|
+
const daemon = await types.readDaemonState().catch(() => null);
|
|
11754
|
+
console.log(chalk.bold("\nFlockbay ready\n"));
|
|
11755
|
+
console.log(chalk.gray(`Profile: ${types.configuration.profile}`));
|
|
11756
|
+
console.log(chalk.gray(`Server: ${types.configuration.serverUrl}`));
|
|
11757
|
+
console.log(chalk.gray(`Web app: ${types.configuration.webappUrl}`));
|
|
11758
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
11759
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
11760
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
11761
|
+
console.log("");
|
|
11762
|
+
openUrlBestEffort(types.configuration.webappUrl);
|
|
9711
11763
|
process.exit(0);
|
|
9712
11764
|
} else {
|
|
9713
11765
|
const latest = await types.getLatestDaemonLog();
|
|
@@ -9728,6 +11780,19 @@ async function startDaemonDetachedOrExit(opts) {
|
|
|
9728
11780
|
console.error(chalk.gray("Fix: run `flockbay start` to re-authenticate and restart the daemon."));
|
|
9729
11781
|
process.exit(1);
|
|
9730
11782
|
}
|
|
11783
|
+
const unreachable = looksLikeServerUnreachable(logTail);
|
|
11784
|
+
if (unreachable) {
|
|
11785
|
+
console.error("");
|
|
11786
|
+
console.error(chalk.red(`Cannot reach server while starting daemon (${unreachable.code}).`));
|
|
11787
|
+
console.error(chalk.gray(`Server: ${types.configuration.serverUrl}`));
|
|
11788
|
+
if (unreachable.url) console.error(chalk.gray(`Failed request: ${unreachable.url}`));
|
|
11789
|
+
if (/^https?:\/\/(127\.0\.0\.1|localhost)\b/i.test(types.configuration.serverUrl)) {
|
|
11790
|
+
console.error("");
|
|
11791
|
+
console.error(chalk.gray("Local dev fix: start the backend first, then re-run `flockbay start`:"));
|
|
11792
|
+
console.error(chalk.gray(" cd backend && npm run dev"));
|
|
11793
|
+
}
|
|
11794
|
+
process.exit(1);
|
|
11795
|
+
}
|
|
9731
11796
|
console.error("Failed to start daemon");
|
|
9732
11797
|
console.error("Tip: run `flockbay auth status` to confirm you are logged in.");
|
|
9733
11798
|
process.exit(1);
|
|
@@ -9738,10 +11803,10 @@ function showStartHelp() {
|
|
|
9738
11803
|
${chalk.bold("flockbay start")} - One-command setup
|
|
9739
11804
|
|
|
9740
11805
|
${chalk.bold("Usage:")}
|
|
9741
|
-
flockbay start [--engine-root <path>]
|
|
11806
|
+
flockbay start [--engine-root <path>] [--profile <name>] Ensure login + daemon running
|
|
9742
11807
|
|
|
9743
11808
|
${chalk.bold("Options:")}
|
|
9744
|
-
|
|
11809
|
+
--profile <name> CLI profile (isolates server/workspace/machine state)
|
|
9745
11810
|
--engine-root <path> Unreal Engine install folder (UE_5.5\u2013UE_5.7)
|
|
9746
11811
|
--skip-unreal Skip Unreal bridge install
|
|
9747
11812
|
`);
|
|
@@ -9753,6 +11818,10 @@ function readArgValue(args, key) {
|
|
|
9753
11818
|
if (!value || value.startsWith("-")) return null;
|
|
9754
11819
|
return value;
|
|
9755
11820
|
}
|
|
11821
|
+
async function authAndSetupMachineIfNeeded() {
|
|
11822
|
+
const { auth, machineId } = await ensureMachineAuthOrLogin();
|
|
11823
|
+
return { credentials: auth, machineId };
|
|
11824
|
+
}
|
|
9756
11825
|
(async () => {
|
|
9757
11826
|
const invokedCwd = process.env.FLOCKBAY_INVOKED_CWD;
|
|
9758
11827
|
if (invokedCwd?.trim()) {
|
|
@@ -9773,6 +11842,9 @@ function readArgValue(args, key) {
|
|
|
9773
11842
|
args = args.slice(1);
|
|
9774
11843
|
subcommand = args[0];
|
|
9775
11844
|
}
|
|
11845
|
+
if (subcommand === "setup") {
|
|
11846
|
+
subcommand = "start";
|
|
11847
|
+
}
|
|
9776
11848
|
if (!args.includes("--version")) ;
|
|
9777
11849
|
if (subcommand === "doctor") {
|
|
9778
11850
|
if (args[1] === "clean") {
|
|
@@ -9796,6 +11868,43 @@ function readArgValue(args, key) {
|
|
|
9796
11868
|
process.exit(1);
|
|
9797
11869
|
}
|
|
9798
11870
|
return;
|
|
11871
|
+
} else if (subcommand === "login") {
|
|
11872
|
+
try {
|
|
11873
|
+
await loginWithClerkAndPairMachine();
|
|
11874
|
+
} catch (error) {
|
|
11875
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
11876
|
+
if (process.env.DEBUG) console.error(error);
|
|
11877
|
+
process.exit(1);
|
|
11878
|
+
}
|
|
11879
|
+
return;
|
|
11880
|
+
} else if (subcommand === "status") {
|
|
11881
|
+
const auth = await types.readCredentials();
|
|
11882
|
+
const settings = await types.readSettings();
|
|
11883
|
+
const daemon = await types.readDaemonState();
|
|
11884
|
+
console.log(chalk.bold("\nFlockbay Status\n"));
|
|
11885
|
+
console.log(chalk.gray(`Profile: ${types.configuration.profile}`));
|
|
11886
|
+
console.log(chalk.gray(`Server: ${types.configuration.serverUrl}`));
|
|
11887
|
+
console.log(chalk.gray(`Web app: ${types.configuration.webappUrl}`));
|
|
11888
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
11889
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
11890
|
+
console.log(chalk.gray(`Auth: ${auth?.machineToken ? "paired" : "not paired"}`));
|
|
11891
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
11892
|
+
process.exit(0);
|
|
11893
|
+
} else if (subcommand === "reset") {
|
|
11894
|
+
const force = args.includes("--force");
|
|
11895
|
+
if (!force) {
|
|
11896
|
+
console.error(chalk.red("Refusing to reset without --force"));
|
|
11897
|
+
console.error(chalk.gray("This clears local auth + machine binding for this profile."));
|
|
11898
|
+
process.exit(1);
|
|
11899
|
+
}
|
|
11900
|
+
try {
|
|
11901
|
+
await stopDaemon();
|
|
11902
|
+
} catch {
|
|
11903
|
+
}
|
|
11904
|
+
await types.clearCredentials();
|
|
11905
|
+
await types.clearMachineId();
|
|
11906
|
+
console.log("Reset complete");
|
|
11907
|
+
process.exit(0);
|
|
9799
11908
|
} else if (subcommand === "connect") {
|
|
9800
11909
|
try {
|
|
9801
11910
|
await handleConnectCommand(args.slice(1));
|
|
@@ -9807,22 +11916,14 @@ function readArgValue(args, key) {
|
|
|
9807
11916
|
process.exit(1);
|
|
9808
11917
|
}
|
|
9809
11918
|
return;
|
|
9810
|
-
} else if (subcommand === "start"
|
|
9811
|
-
if (subcommand === "setup") {
|
|
9812
|
-
console.log(chalk.yellow("Note: `flockbay setup` was renamed to `flockbay start`."));
|
|
9813
|
-
console.log(chalk.gray("Running `flockbay start`...\n"));
|
|
9814
|
-
}
|
|
11919
|
+
} else if (subcommand === "start") {
|
|
9815
11920
|
const startArgs = args.slice(1);
|
|
9816
11921
|
if (startArgs.includes("--help") || startArgs.includes("-h") || startArgs.includes("help")) {
|
|
9817
11922
|
showStartHelp();
|
|
9818
11923
|
return;
|
|
9819
11924
|
}
|
|
9820
11925
|
try {
|
|
9821
|
-
|
|
9822
|
-
await checkIfDaemonRunningAndCleanupStaleState();
|
|
9823
|
-
await stopDaemon();
|
|
9824
|
-
} catch {
|
|
9825
|
-
}
|
|
11926
|
+
await ensureMachineAuthOrLogin();
|
|
9826
11927
|
const skipUnreal = startArgs.includes("--skip-unreal");
|
|
9827
11928
|
if (!skipUnreal) {
|
|
9828
11929
|
const engineRoot = readArgValue(startArgs, "--engine-root") || (process.env.UE_ENGINE_ROOT || "").trim() || (process.env.ENGINE_ROOT || "").trim() || null;
|
|
@@ -9844,7 +11945,6 @@ ${engineRoot}`, {
|
|
|
9844
11945
|
}
|
|
9845
11946
|
}
|
|
9846
11947
|
}
|
|
9847
|
-
await handleAuthCommand(["login", "--force", "--quiet"]);
|
|
9848
11948
|
await startDaemonDetachedOrExit();
|
|
9849
11949
|
} catch (error) {
|
|
9850
11950
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -9857,17 +11957,20 @@ ${engineRoot}`, {
|
|
|
9857
11957
|
} else if (subcommand === "codex") {
|
|
9858
11958
|
try {
|
|
9859
11959
|
await chdirToNearestUprojectRootIfPresent();
|
|
9860
|
-
const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-
|
|
11960
|
+
const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-DuCGwO2K.cjs'); });
|
|
9861
11961
|
let startedBy = void 0;
|
|
11962
|
+
let sessionId = void 0;
|
|
9862
11963
|
for (let i = 1; i < args.length; i++) {
|
|
9863
11964
|
if (args[i] === "--started-by") {
|
|
9864
11965
|
startedBy = args[++i];
|
|
11966
|
+
} else if (args[i] === "--flockbay-session-id") {
|
|
11967
|
+
sessionId = args[++i];
|
|
9865
11968
|
}
|
|
9866
11969
|
}
|
|
9867
11970
|
const {
|
|
9868
11971
|
credentials
|
|
9869
11972
|
} = await authAndSetupMachineIfNeeded();
|
|
9870
|
-
await runCodex({ credentials, startedBy });
|
|
11973
|
+
await runCodex({ credentials, startedBy, sessionId });
|
|
9871
11974
|
} catch (error) {
|
|
9872
11975
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
9873
11976
|
if (process.env.DEBUG) {
|
|
@@ -9949,11 +12052,14 @@ ${engineRoot}`, {
|
|
|
9949
12052
|
}
|
|
9950
12053
|
try {
|
|
9951
12054
|
await chdirToNearestUprojectRootIfPresent();
|
|
9952
|
-
const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-
|
|
12055
|
+
const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-B25LZ4Cw.cjs'); });
|
|
9953
12056
|
let startedBy = void 0;
|
|
12057
|
+
let sessionId = void 0;
|
|
9954
12058
|
for (let i = 1; i < args.length; i++) {
|
|
9955
12059
|
if (args[i] === "--started-by") {
|
|
9956
12060
|
startedBy = args[++i];
|
|
12061
|
+
} else if (args[i] === "--flockbay-session-id") {
|
|
12062
|
+
sessionId = args[++i];
|
|
9957
12063
|
}
|
|
9958
12064
|
}
|
|
9959
12065
|
const {
|
|
@@ -9970,19 +12076,7 @@ ${engineRoot}`, {
|
|
|
9970
12076
|
daemonProcess.unref();
|
|
9971
12077
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
9972
12078
|
}
|
|
9973
|
-
await runGemini({ credentials, startedBy });
|
|
9974
|
-
} catch (error) {
|
|
9975
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
9976
|
-
if (process.env.DEBUG) {
|
|
9977
|
-
console.error(error);
|
|
9978
|
-
}
|
|
9979
|
-
process.exit(1);
|
|
9980
|
-
}
|
|
9981
|
-
return;
|
|
9982
|
-
} else if (subcommand === "logout") {
|
|
9983
|
-
console.log(chalk.yellow('Note: "logout" is deprecated. Use "flockbay auth logout" instead.\n'));
|
|
9984
|
-
try {
|
|
9985
|
-
await handleAuthCommand(["logout"]);
|
|
12079
|
+
await runGemini({ credentials, startedBy, sessionId });
|
|
9986
12080
|
} catch (error) {
|
|
9987
12081
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
9988
12082
|
if (process.env.DEBUG) {
|
|
@@ -10111,6 +12205,8 @@ ${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("flockbay docto
|
|
|
10111
12205
|
unknownArgs.push("--dangerously-skip-permissions");
|
|
10112
12206
|
} else if (arg === "--started-by") {
|
|
10113
12207
|
options.startedBy = args[++i];
|
|
12208
|
+
} else if (arg === "--flockbay-session-id") {
|
|
12209
|
+
options.sessionId = args[++i];
|
|
10114
12210
|
} else if (arg === "--claude-env") {
|
|
10115
12211
|
const envArg = args[++i];
|
|
10116
12212
|
if (envArg && envArg.includes("=")) {
|
|
@@ -10295,6 +12391,7 @@ exports.applyCoordinationSideEffectsFromMcpToolResult = applyCoordinationSideEff
|
|
|
10295
12391
|
exports.autoFinalizeCoordinationWorkItem = autoFinalizeCoordinationWorkItem;
|
|
10296
12392
|
exports.buildProjectCapsule = buildProjectCapsule;
|
|
10297
12393
|
exports.consumeToolQuota = consumeToolQuota;
|
|
12394
|
+
exports.detectScreenshotsForGate = detectScreenshotsForGate;
|
|
10298
12395
|
exports.detectUnrealProject = detectUnrealProject;
|
|
10299
12396
|
exports.extractUserImagesMarker = extractUserImagesMarker;
|
|
10300
12397
|
exports.formatQuotaDeniedReason = formatQuotaDeniedReason;
|