flockbay 0.10.15 → 0.10.16
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-Cau-_Qvn.cjs} +2683 -609
- package/dist/{index-CUp3juDS.mjs → index-DtmFQzXY.mjs} +2684 -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-o6PCbHQ7.mjs → runCodex-Di9eHddq.mjs} +263 -42
- package/dist/{runCodex-D3eT-TvB.cjs → runCodex-DzP3VUa-.cjs} +264 -43
- package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-BS6sBU_V.mjs} +63 -28
- package/dist/{runGemini-CBxZp6I7.cjs → runGemini-CpmehDQ2.cjs} +64 -29
- package/dist/{types-DGd6ea2Z.mjs → types-CwzNqYEx.mjs} +465 -1142
- package/dist/{types-C-jnUdn_.cjs → types-SUAKq-K0.cjs} +466 -1146
- 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
|
@@ -1,33 +1,30 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
|
|
2
2
|
import os, { homedir } from 'node:os';
|
|
3
|
-
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
|
-
import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema,
|
|
3
|
+
import { randomUUID, createCipheriv, randomBytes } from 'node:crypto';
|
|
4
|
+
import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as readDaemonState, g as clearDaemonState, b as packageJson, r as readSettings, h as readCredentials, u as updateSettings, w as writeCredentials, i as unrealMcpPythonDir, j as acquireDaemonLock, k as writeDaemonState, m as ApiMachineClient, n as releaseDaemonLock, s as sendUnrealMcpTcpCommand, A as ApiClient, o as clearCredentials, q as clearMachineId, t as installUnrealMcpPluginToEngine, v as getLatestDaemonLog } from './types-CwzNqYEx.mjs';
|
|
5
5
|
import { spawn, execFileSync, execSync } from 'node:child_process';
|
|
6
6
|
import path, { resolve, join, dirname } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
|
-
import fs__default, { existsSync, readFileSync, mkdirSync, readdirSync, accessSync, constants, statSync, createReadStream, writeFileSync, unlinkSync
|
|
9
|
+
import fs__default, { existsSync, readFileSync, mkdirSync, readdirSync, accessSync, constants, statSync, createReadStream, writeFileSync, unlinkSync } from 'node:fs';
|
|
10
10
|
import process$1 from 'node:process';
|
|
11
|
-
import fs$1, { readFile, access as access$1,
|
|
11
|
+
import fs$1, { readFile, access as access$1, mkdir, stat, readdir } from 'node:fs/promises';
|
|
12
12
|
import fs$2, { watch, access } from 'fs/promises';
|
|
13
13
|
import { useStdout, useInput, Box, Text, render } from 'ink';
|
|
14
14
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import axios from 'axios';
|
|
17
|
-
import 'node:events';
|
|
17
|
+
import { EventEmitter } from 'node:events';
|
|
18
18
|
import 'socket.io-client';
|
|
19
|
-
import tweetnacl from 'tweetnacl';
|
|
20
19
|
import { spawn as spawn$1, execSync as execSync$1, exec } from 'child_process';
|
|
21
20
|
import { createHash, randomBytes as randomBytes$1 } from 'crypto';
|
|
22
21
|
import { join as join$1, basename } from 'path';
|
|
23
22
|
import os$1 from 'os';
|
|
24
23
|
import 'node:net';
|
|
25
|
-
import 'expo-server-sdk';
|
|
26
24
|
import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync as writeFileSync$1, chmodSync, unlinkSync as unlinkSync$1 } from 'fs';
|
|
27
25
|
import psList from 'ps-list';
|
|
28
26
|
import spawn$2 from 'cross-spawn';
|
|
29
27
|
import * as tmp from 'tmp';
|
|
30
|
-
import qrcode from 'qrcode-terminal';
|
|
31
28
|
import open from 'open';
|
|
32
29
|
import fastify from 'fastify';
|
|
33
30
|
import { z } from 'zod';
|
|
@@ -35,6 +32,7 @@ import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod
|
|
|
35
32
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
36
33
|
import { createServer } from 'node:http';
|
|
37
34
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
35
|
+
import 'tweetnacl';
|
|
38
36
|
import { createServer as createServer$1 } from 'http';
|
|
39
37
|
import { promisify } from 'util';
|
|
40
38
|
|
|
@@ -276,27 +274,68 @@ const PLATFORM_SYSTEM_PROMPT = trimIdent(`
|
|
|
276
274
|
|
|
277
275
|
UnrealMCP capability hygiene (avoid guessing):
|
|
278
276
|
- UnrealMCP command sets can differ by plugin build/version.
|
|
279
|
-
- When starting UnrealMCP work in a session (or after any "Unknown command" error),
|
|
277
|
+
- When starting UnrealMCP work in a session (or after any "Unknown command" error), prefer:
|
|
278
|
+
- \`mcp__flockbay__unreal_mcp_list_capabilities\` (fast: build info + supported commands + capability flags)
|
|
279
|
+
- or \`mcp__flockbay__unreal_mcp_command\` with \`type: "get_plugin_info"\` (supported commands list)
|
|
280
|
+
- When you're unsure about required params or accepted aliases for a command, use:
|
|
281
|
+
- \`mcp__flockbay__unreal_mcp_get_command_schema\` (returns required keys + examples for the running plugin build)
|
|
280
282
|
- 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.
|
|
281
283
|
|
|
284
|
+
UnrealMCP asset placement (avoid guessing \`/Game/...\` paths):
|
|
285
|
+
- If the user asks to place assets from the project\u2019s \`/Content\` folder, do NOT guess object paths.
|
|
286
|
+
- 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.
|
|
287
|
+
- Place it via \`mcp__flockbay__unreal_mcp_place_asset\` (supports StaticMesh + Blueprint assets).
|
|
288
|
+
- 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.
|
|
289
|
+
|
|
290
|
+
Unreal spatial intent (stop guessing \u201Chere/there\u201D):
|
|
291
|
+
- For \u201Cput X here/there / in front of me / on that surface\u201D, prefer these primitives:
|
|
292
|
+
- \`mcp__flockbay__unreal_mcp_get_editor_context\` (map + selection + editor camera)
|
|
293
|
+
- \`mcp__flockbay__unreal_mcp_get_player_context\` (PIE player pawn + camera)
|
|
294
|
+
- \`mcp__flockbay__unreal_mcp_raycast_from_camera\` (turn \u201Cthat surface\u201D into hit location + normal)
|
|
295
|
+
- \`mcp__flockbay__unreal_mcp_raycast_down\` (drop-to-ground)
|
|
296
|
+
- \`mcp__flockbay__unreal_mcp_get_actor_transform\` / \`mcp__flockbay__unreal_mcp_get_actor_bounds\`
|
|
297
|
+
- If the user wants a one-call helper, use:
|
|
298
|
+
- \`mcp__flockbay__unreal_place_asset_relative\` (place asset relative to selection/camera/player/hit)
|
|
299
|
+
- \`mcp__flockbay__unreal_spawn_actor_relative\` (spawn actor relative to selection/camera/player/hit)
|
|
300
|
+
|
|
282
301
|
UnrealMCP PIE safety (avoid editor crashes):
|
|
283
302
|
- Before running editor-world mutation commands (spawn/delete actors, set properties/transforms, spawn Blueprint actors), check \`get_play_in_editor_status\`.
|
|
284
303
|
- If PIE is running/queued, stop it (\`stop_play_in_editor\`) before retrying the mutation.
|
|
285
304
|
|
|
305
|
+
Unreal verify + diagnose (recommended validation after changes):
|
|
306
|
+
- After making changes (especially content/Blueprint changes), prefer this sequence:
|
|
307
|
+
1) \`mcp__flockbay__unreal_mcp_compile_blueprints_all\`
|
|
308
|
+
2) \`mcp__flockbay__unreal_mcp_map_check\`
|
|
309
|
+
3) If C++ exists or was edited: \`mcp__flockbay__unreal_build_project\`
|
|
310
|
+
4) \`mcp__flockbay__unreal_smoke_test\` (PIE windowed + screenshot validation)
|
|
311
|
+
- If any step fails: stop, report the structured failures, and propose the smallest fix.
|
|
312
|
+
- After the change is validated and before concluding, save editor changes:
|
|
313
|
+
- Prefer \`mcp__flockbay__unreal_mcp_save_all\` to save all dirty packages.
|
|
314
|
+
- Or use \`mcp__flockbay__unreal_mcp_command\` with \`type: "save_all"\` to save all dirty packages.
|
|
315
|
+
- If \`save_all\` reports still-dirty packages, do not end the request; resolve the save issue and retry.
|
|
316
|
+
|
|
286
317
|
UnrealMCP editor health (detect crashes/offline):
|
|
287
318
|
- If UnrealMCP requests fail to connect or time out, run the health check tool (if available) to determine whether the editor is reachable.
|
|
288
319
|
- 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.
|
|
289
320
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
321
|
+
Unreal Editor relaunch (manual, never auto-restart):
|
|
322
|
+
- The platform may detect that Unreal Editor crashed/unreachable and will automatically abort your current run and post a chat message.
|
|
323
|
+
- When that happens: STOP UnrealMCP calls, fix the project as needed, then (only when ready) relaunch Unreal Editor with \`mcp__flockbay__unreal_editor_launch\`.
|
|
324
|
+
- Do not \u201Cauto-restart\u201D in a loop. Relaunch is a deliberate action after fixes.
|
|
325
|
+
|
|
326
|
+
## B) Screenshots (via UnrealMCP)
|
|
327
|
+
Use UnrealMCP when the user asks for:
|
|
328
|
+
- a screenshot of the editor viewport
|
|
329
|
+
- visual validation of an in-editor change
|
|
294
330
|
|
|
295
331
|
How:
|
|
296
332
|
- Use \`mcp__flockbay__unreal_mcp_command\` with \`type: "take_screenshot"\`.
|
|
297
333
|
- Default save path is \`<Project>/Saved/Screenshots/Flockbay/\` (auto-generated filename). Optional params: \`filepath\` (absolute or relative filename) and/or \`filename\` (base name).
|
|
298
334
|
|
|
299
335
|
Do not use screenshots as a default fallback for editor queries (e.g. \u201Clist actors\u201D).
|
|
336
|
+
Do not treat \u201Ctook a screenshot\u201D as \u201Cdone\u201D:
|
|
337
|
+
- Screenshots are primarily for YOU (the agent) to validate the change.
|
|
338
|
+
- If the screenshot does not clearly confirm the requirement, continue iterating (or run a different verify tool).
|
|
300
339
|
|
|
301
340
|
# Non\u2011negotiables
|
|
302
341
|
|
|
@@ -315,9 +354,10 @@ const PLATFORM_SYSTEM_PROMPT = trimIdent(`
|
|
|
315
354
|
# Evidence strategy (how to validate)
|
|
316
355
|
|
|
317
356
|
- Choose the lightest evidence that answers the user\u2019s request.
|
|
318
|
-
- Prefer \u201Cone
|
|
357
|
+
- Prefer \u201Cone validation\u201D over \u201Cmany validations\u201D.
|
|
319
358
|
- If you need visual confirmation, ask for or produce a screenshot explicitly (don\u2019t sneak it in).
|
|
320
|
-
- Always view the resulting image
|
|
359
|
+
- Always view the resulting image after creating one, and judge the result for yourself (not just for the user).
|
|
360
|
+
- If the image is ambiguous or doesn\u2019t show the requirement, say what\u2019s missing and keep going (don\u2019t end the request prematurely).
|
|
321
361
|
|
|
322
362
|
# Session hygiene
|
|
323
363
|
|
|
@@ -836,7 +876,7 @@ async function claudeLocalLauncher(session) {
|
|
|
836
876
|
}
|
|
837
877
|
await abort();
|
|
838
878
|
}
|
|
839
|
-
session.client.rpcHandlerManager.registerHandler("
|
|
879
|
+
session.client.rpcHandlerManager.registerHandler("cancel-generation", doAbort);
|
|
840
880
|
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
|
|
841
881
|
session.queue.setOnMessage((message, mode) => {
|
|
842
882
|
doSwitch();
|
|
@@ -884,8 +924,6 @@ async function claudeLocalLauncher(session) {
|
|
|
884
924
|
}
|
|
885
925
|
} finally {
|
|
886
926
|
exutFuture.resolve(void 0);
|
|
887
|
-
session.client.rpcHandlerManager.registerHandler("abort", async () => {
|
|
888
|
-
});
|
|
889
927
|
session.client.rpcHandlerManager.registerHandler("switch", async () => {
|
|
890
928
|
});
|
|
891
929
|
session.queue.setOnMessage(null);
|
|
@@ -1891,6 +1929,139 @@ function getLatestUserImages() {
|
|
|
1891
1929
|
return latestUserImages;
|
|
1892
1930
|
}
|
|
1893
1931
|
|
|
1932
|
+
function uniqPush(into, seen, value) {
|
|
1933
|
+
const v = value.trim();
|
|
1934
|
+
if (!v) return;
|
|
1935
|
+
if (seen.has(v)) return;
|
|
1936
|
+
seen.add(v);
|
|
1937
|
+
into.push(v);
|
|
1938
|
+
}
|
|
1939
|
+
function normalizeFilePathToken(token) {
|
|
1940
|
+
return token.trim().replace(/^['"`]+/, "").replace(/['"`]+$/, "").replace(/[),.;:'"`]+$/, "").trim();
|
|
1941
|
+
}
|
|
1942
|
+
function resolveCandidatePath(candidate, cwd) {
|
|
1943
|
+
const raw = normalizeFilePathToken(candidate);
|
|
1944
|
+
if (!raw) return raw;
|
|
1945
|
+
if (raw.startsWith("~/")) {
|
|
1946
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1947
|
+
if (home) return path.join(home, raw.slice(2));
|
|
1948
|
+
}
|
|
1949
|
+
return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
1950
|
+
}
|
|
1951
|
+
function isImageBlock(block) {
|
|
1952
|
+
if (!block || typeof block !== "object") return false;
|
|
1953
|
+
if (block.type !== "image") return false;
|
|
1954
|
+
if (typeof block.data === "string" && block.data.length > 0) return true;
|
|
1955
|
+
if (block.source && typeof block.source === "object" && typeof block.source.data === "string" && block.source.data.length > 0) return true;
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1958
|
+
function tryParseJsonObjectWithViews(text) {
|
|
1959
|
+
const trimmed = text.trim();
|
|
1960
|
+
if (!trimmed) return null;
|
|
1961
|
+
try {
|
|
1962
|
+
const direct = JSON.parse(trimmed);
|
|
1963
|
+
if (direct && typeof direct === "object" && Array.isArray(direct.views)) return direct;
|
|
1964
|
+
} catch {
|
|
1965
|
+
}
|
|
1966
|
+
for (let i = trimmed.length - 1; i >= 0; i -= 1) {
|
|
1967
|
+
if (trimmed[i] !== "{") continue;
|
|
1968
|
+
const candidate = trimmed.slice(i);
|
|
1969
|
+
try {
|
|
1970
|
+
const parsed = JSON.parse(candidate);
|
|
1971
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.views)) return parsed;
|
|
1972
|
+
} catch {
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
function extractScreenshotViewsFromText(text, cwd) {
|
|
1978
|
+
const out = [];
|
|
1979
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1980
|
+
const re = /(?:^|[\s"'(])(?:-\s*)?([^\s"'()]*Saved[\\/]+Screenshots[\\/]+Flockbay[\\/]+[^\s"'()]+\.(?:png|jpg|jpeg))/gi;
|
|
1981
|
+
for (const match of text.matchAll(re)) {
|
|
1982
|
+
const token = String(match[1] || "");
|
|
1983
|
+
const resolved = resolveCandidatePath(token, cwd);
|
|
1984
|
+
if (!resolved) continue;
|
|
1985
|
+
if (!resolved.includes(`${path.sep}Saved${path.sep}Screenshots${path.sep}Flockbay${path.sep}`) && !resolved.includes("Saved/Screenshots/Flockbay/") && !resolved.includes("Saved\\Screenshots\\Flockbay\\")) {
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
uniqPush(out, seen, resolved);
|
|
1989
|
+
}
|
|
1990
|
+
if (out.length === 0 && (text.includes("Saved/Screenshots/Flockbay") || text.includes("Saved\\Screenshots\\Flockbay"))) {
|
|
1991
|
+
const filenameRe = /(?:^|[\s"'(\\/])(?:-\s*)?(Flockbay_[A-Za-z0-9_.-]+\.(?:png|jpg|jpeg))/gi;
|
|
1992
|
+
for (const match of text.matchAll(filenameRe)) {
|
|
1993
|
+
const filename = normalizeFilePathToken(String(match[1] || ""));
|
|
1994
|
+
if (!filename) continue;
|
|
1995
|
+
const rel = path.join("Saved", "Screenshots", "Flockbay", filename);
|
|
1996
|
+
const resolved = resolveCandidatePath(rel, cwd);
|
|
1997
|
+
uniqPush(out, seen, resolved);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return out;
|
|
2001
|
+
}
|
|
2002
|
+
function extractViewsFromParsedJson(parsed, cwd) {
|
|
2003
|
+
const out = [];
|
|
2004
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2005
|
+
const views = Array.isArray(parsed?.views) ? parsed.views : [];
|
|
2006
|
+
for (const v of views) {
|
|
2007
|
+
const p = typeof v?.path === "string" ? v.path : "";
|
|
2008
|
+
if (!p.trim()) continue;
|
|
2009
|
+
uniqPush(out, seen, resolveCandidatePath(p, cwd));
|
|
2010
|
+
}
|
|
2011
|
+
return out;
|
|
2012
|
+
}
|
|
2013
|
+
function extractFromContentBlocks(content, cwd) {
|
|
2014
|
+
const texts = [];
|
|
2015
|
+
let hasImages = false;
|
|
2016
|
+
for (const block of content) {
|
|
2017
|
+
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
2018
|
+
texts.push(block.text);
|
|
2019
|
+
}
|
|
2020
|
+
if (!hasImages && isImageBlock(block)) hasImages = true;
|
|
2021
|
+
}
|
|
2022
|
+
const text = texts.join("\n");
|
|
2023
|
+
const parsed = tryParseJsonObjectWithViews(text);
|
|
2024
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
2025
|
+
const pathMatches = extractScreenshotViewsFromText(text, cwd);
|
|
2026
|
+
const combined = [];
|
|
2027
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2028
|
+
for (const p of jsonPaths) uniqPush(combined, seen, p);
|
|
2029
|
+
for (const p of pathMatches) uniqPush(combined, seen, p);
|
|
2030
|
+
return { paths: combined, hasImages };
|
|
2031
|
+
}
|
|
2032
|
+
function detectScreenshotsForGate(args) {
|
|
2033
|
+
const cwd = args.cwd && args.cwd.trim().length > 0 ? args.cwd : process.cwd();
|
|
2034
|
+
const output = args.output;
|
|
2035
|
+
if (Array.isArray(output)) {
|
|
2036
|
+
const extracted = extractFromContentBlocks(output, cwd);
|
|
2037
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
2038
|
+
}
|
|
2039
|
+
if (output && typeof output === "object" && Array.isArray(output.content)) {
|
|
2040
|
+
const extracted = extractFromContentBlocks(output.content, cwd);
|
|
2041
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
2042
|
+
}
|
|
2043
|
+
if (output && typeof output === "object" && Array.isArray(output.views)) {
|
|
2044
|
+
return { paths: extractViewsFromParsedJson(output, cwd), hasImageBlocks: false };
|
|
2045
|
+
}
|
|
2046
|
+
const candidates = [
|
|
2047
|
+
typeof output === "string" ? output : null,
|
|
2048
|
+
typeof output?.stdout === "string" ? output.stdout : null,
|
|
2049
|
+
typeof output?.stderr === "string" ? output.stderr : null,
|
|
2050
|
+
typeof output?.output === "string" ? output.output : null,
|
|
2051
|
+
typeof output?.message === "string" ? output.message : null
|
|
2052
|
+
];
|
|
2053
|
+
const combinedText = candidates.filter((v) => typeof v === "string" && v.trim().length > 0).join("\n");
|
|
2054
|
+
if (!combinedText) return { paths: [], hasImageBlocks: false };
|
|
2055
|
+
const parsed = tryParseJsonObjectWithViews(combinedText);
|
|
2056
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
2057
|
+
const pathMatches = extractScreenshotViewsFromText(combinedText, cwd);
|
|
2058
|
+
const out = [];
|
|
2059
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2060
|
+
for (const p of jsonPaths) uniqPush(out, seen, p);
|
|
2061
|
+
for (const p of pathMatches) uniqPush(out, seen, p);
|
|
2062
|
+
return { paths: out, hasImageBlocks: false };
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1894
2065
|
function buildClaudeUserContent(text, images) {
|
|
1895
2066
|
const cleanText = String(text ?? "").trim();
|
|
1896
2067
|
if (images.length === 0) return cleanText;
|
|
@@ -2001,6 +2172,61 @@ async function claudeRemote(opts) {
|
|
|
2001
2172
|
}
|
|
2002
2173
|
};
|
|
2003
2174
|
let messages = new PushableAsyncIterable();
|
|
2175
|
+
const screenshotGate = {
|
|
2176
|
+
paths: [],
|
|
2177
|
+
seen: /* @__PURE__ */ new Set(),
|
|
2178
|
+
hasImageBlocks: false,
|
|
2179
|
+
inAutoReview: false
|
|
2180
|
+
};
|
|
2181
|
+
const resetScreenshotGateForTurn = () => {
|
|
2182
|
+
screenshotGate.paths = [];
|
|
2183
|
+
screenshotGate.seen.clear();
|
|
2184
|
+
screenshotGate.hasImageBlocks = false;
|
|
2185
|
+
screenshotGate.inAutoReview = false;
|
|
2186
|
+
};
|
|
2187
|
+
const collectScreenshotsForGate = (output) => {
|
|
2188
|
+
if (!output) return;
|
|
2189
|
+
const detected = detectScreenshotsForGate({ output, cwd: opts.path });
|
|
2190
|
+
if (detected.paths.length === 0) return;
|
|
2191
|
+
if (detected.hasImageBlocks) screenshotGate.hasImageBlocks = true;
|
|
2192
|
+
for (const p of detected.paths) {
|
|
2193
|
+
const trimmed = String(p || "").trim();
|
|
2194
|
+
if (!trimmed) continue;
|
|
2195
|
+
if (screenshotGate.seen.has(trimmed)) continue;
|
|
2196
|
+
screenshotGate.seen.add(trimmed);
|
|
2197
|
+
screenshotGate.paths.push(trimmed);
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
const buildScreenshotAutoReviewPrompt = (paths) => {
|
|
2201
|
+
const unique = Array.from(new Set(paths.map((p) => String(p || "").trim()).filter(Boolean)));
|
|
2202
|
+
const toolArgs = JSON.stringify(
|
|
2203
|
+
{
|
|
2204
|
+
paths: unique,
|
|
2205
|
+
limit: unique.length,
|
|
2206
|
+
upload: false,
|
|
2207
|
+
includeToolImages: true
|
|
2208
|
+
},
|
|
2209
|
+
null,
|
|
2210
|
+
2
|
|
2211
|
+
);
|
|
2212
|
+
return trimIdent(`
|
|
2213
|
+
You just generated ${unique.length} screenshot${unique.length === 1 ? "" : "s"}.
|
|
2214
|
+
|
|
2215
|
+
Before doing anything else, call \`mcp__flockbay__read_images\` with:
|
|
2216
|
+
${toolArgs}
|
|
2217
|
+
|
|
2218
|
+
Then visually inspect EVERY screenshot and report (this is for YOUR validation, not ceremony):
|
|
2219
|
+
1) List 2\u20135 concrete acceptance criteria for the user's request.
|
|
2220
|
+
2) One short bullet per image ("Image N: ...") describing what you see.
|
|
2221
|
+
3) For each acceptance criterion: mark it as Verified / Not visible / Failed based ONLY on the screenshots.
|
|
2222
|
+
- If "Not visible", say what evidence is missing.
|
|
2223
|
+
- If "Failed", say what is wrong.
|
|
2224
|
+
4) If anything is not Verified: state the next tool/action you will take to fix or validate it (do not claim completion yet).
|
|
2225
|
+
|
|
2226
|
+
Do not run any other tools in this step.
|
|
2227
|
+
`);
|
|
2228
|
+
};
|
|
2229
|
+
resetScreenshotGateForTurn();
|
|
2004
2230
|
const initialParsed = extractUserImagesMarker(initial.message);
|
|
2005
2231
|
const initialImages = initialParsed.hasImages ? getLatestUserImages().slice(0, initialParsed.count) : [];
|
|
2006
2232
|
messages.push({
|
|
@@ -2075,6 +2301,17 @@ async function claudeRemote(opts) {
|
|
|
2075
2301
|
if (message.type === "result") {
|
|
2076
2302
|
updateThinking(false);
|
|
2077
2303
|
logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
2304
|
+
if (!screenshotGate.inAutoReview && screenshotGate.paths.length > 0 && !screenshotGate.hasImageBlocks) {
|
|
2305
|
+
screenshotGate.inAutoReview = true;
|
|
2306
|
+
const reviewPrompt = buildScreenshotAutoReviewPrompt(screenshotGate.paths);
|
|
2307
|
+
messages.push({
|
|
2308
|
+
type: "user",
|
|
2309
|
+
message: { role: "user", content: reviewPrompt }
|
|
2310
|
+
});
|
|
2311
|
+
updateThinking(true);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
screenshotGate.inAutoReview = false;
|
|
2078
2315
|
if (isCompactCommand) {
|
|
2079
2316
|
logger.debug("[claudeRemote] Compaction completed");
|
|
2080
2317
|
if (opts.onCompletionEvent) {
|
|
@@ -2089,14 +2326,19 @@ async function claudeRemote(opts) {
|
|
|
2089
2326
|
return;
|
|
2090
2327
|
}
|
|
2091
2328
|
mode = next.mode;
|
|
2329
|
+
resetScreenshotGateForTurn();
|
|
2092
2330
|
const parsed = extractUserImagesMarker(next.message);
|
|
2093
2331
|
const nextImages = parsed.hasImages ? getLatestUserImages().slice(0, parsed.count) : [];
|
|
2094
2332
|
messages.push({ type: "user", message: { role: "user", content: buildClaudeUserContent(parsed.text, nextImages) } });
|
|
2333
|
+
updateThinking(true);
|
|
2095
2334
|
}
|
|
2096
2335
|
if (message.type === "user") {
|
|
2097
2336
|
const msg = message;
|
|
2098
2337
|
if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
|
|
2099
2338
|
for (let c of msg.message.content) {
|
|
2339
|
+
if (c.type === "tool_result") {
|
|
2340
|
+
collectScreenshotsForGate(c.content ?? c);
|
|
2341
|
+
}
|
|
2100
2342
|
if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
|
|
2101
2343
|
logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
|
|
2102
2344
|
return;
|
|
@@ -2643,11 +2885,14 @@ ${next}`
|
|
|
2643
2885
|
}
|
|
2644
2886
|
this.pendingRequests.clear();
|
|
2645
2887
|
this.session.client.updateAgentState((currentState) => {
|
|
2646
|
-
const
|
|
2647
|
-
const
|
|
2888
|
+
const pendingRaw = currentState?.requests;
|
|
2889
|
+
const pendingRequests = pendingRaw && typeof pendingRaw === "object" ? pendingRaw : {};
|
|
2890
|
+
const completedRaw = currentState?.completedRequests;
|
|
2891
|
+
const completedRequests = completedRaw && typeof completedRaw === "object" ? { ...completedRaw } : {};
|
|
2648
2892
|
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
2893
|
+
const reqObj = request && typeof request === "object" ? request : { value: request };
|
|
2649
2894
|
completedRequests[id] = {
|
|
2650
|
-
...
|
|
2895
|
+
...reqObj,
|
|
2651
2896
|
completedAt: Date.now(),
|
|
2652
2897
|
status: "canceled",
|
|
2653
2898
|
reason: "Session switched to local mode"
|
|
@@ -3058,6 +3303,40 @@ class SDKToLogConverter {
|
|
|
3058
3303
|
}
|
|
3059
3304
|
}
|
|
3060
3305
|
|
|
3306
|
+
class AsyncLock {
|
|
3307
|
+
permits = 1;
|
|
3308
|
+
promiseResolverQueue = [];
|
|
3309
|
+
async inLock(func) {
|
|
3310
|
+
try {
|
|
3311
|
+
await this.lock();
|
|
3312
|
+
return await func();
|
|
3313
|
+
} finally {
|
|
3314
|
+
this.unlock();
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async lock() {
|
|
3318
|
+
if (this.permits > 0) {
|
|
3319
|
+
this.permits = this.permits - 1;
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
|
|
3323
|
+
}
|
|
3324
|
+
unlock() {
|
|
3325
|
+
this.permits += 1;
|
|
3326
|
+
if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
|
|
3327
|
+
throw new Error("this.permits should never be > 0 when there is someone waiting.");
|
|
3328
|
+
} else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
|
|
3329
|
+
this.permits -= 1;
|
|
3330
|
+
const nextResolver = this.promiseResolverQueue.shift();
|
|
3331
|
+
if (nextResolver) {
|
|
3332
|
+
setTimeout(() => {
|
|
3333
|
+
nextResolver(true);
|
|
3334
|
+
}, 0);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3061
3340
|
class OutgoingMessageQueue {
|
|
3062
3341
|
constructor(sendFunction) {
|
|
3063
3342
|
this.sendFunction = sendFunction;
|
|
@@ -3249,7 +3528,7 @@ async function postJson(path, token, body) {
|
|
|
3249
3528
|
const res = await fetch(`${base}${path}`, {
|
|
3250
3529
|
method: "POST",
|
|
3251
3530
|
headers: {
|
|
3252
|
-
Authorization: `
|
|
3531
|
+
Authorization: `Machine ${token}`,
|
|
3253
3532
|
"Content-Type": "application/json"
|
|
3254
3533
|
},
|
|
3255
3534
|
body: JSON.stringify(body ?? {})
|
|
@@ -3391,7 +3670,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
3391
3670
|
}
|
|
3392
3671
|
await abort();
|
|
3393
3672
|
}
|
|
3394
|
-
session.client.rpcHandlerManager.registerHandler("
|
|
3673
|
+
session.client.rpcHandlerManager.registerHandler("cancel-generation", doAbort);
|
|
3395
3674
|
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
|
|
3396
3675
|
const permissionHandler = new PermissionHandler(session);
|
|
3397
3676
|
const messageQueue = new OutgoingMessageQueue(
|
|
@@ -4247,7 +4526,9 @@ async function daemonPost(path, body) {
|
|
|
4247
4526
|
}
|
|
4248
4527
|
try {
|
|
4249
4528
|
const timeoutRaw = process.env.FLOCKBAY_DAEMON_HTTP_TIMEOUT;
|
|
4250
|
-
const
|
|
4529
|
+
const fallbackTimeout = path === "/spawn-session" ? 6e4 : 1e4;
|
|
4530
|
+
const parsedTimeout = timeoutRaw ? Number.parseInt(timeoutRaw, 10) : NaN;
|
|
4531
|
+
const timeout = Number.isFinite(parsedTimeout) ? parsedTimeout : fallbackTimeout;
|
|
4251
4532
|
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
4252
4533
|
method: "POST",
|
|
4253
4534
|
headers: { "Content-Type": "application/json" },
|
|
@@ -4341,6 +4622,7 @@ async function stopDaemon() {
|
|
|
4341
4622
|
try {
|
|
4342
4623
|
await stopDaemonHttp();
|
|
4343
4624
|
await waitForProcessDeath(state.pid, 2e3);
|
|
4625
|
+
await cleanupDaemonState();
|
|
4344
4626
|
logger.debug("Daemon stopped gracefully via HTTP");
|
|
4345
4627
|
return;
|
|
4346
4628
|
} catch (error) {
|
|
@@ -4635,37 +4917,6 @@ ${typeLabels[type] || type}:`));
|
|
|
4635
4917
|
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4636
4918
|
}
|
|
4637
4919
|
|
|
4638
|
-
function displayQRCode(url) {
|
|
4639
|
-
console.log("=".repeat(80));
|
|
4640
|
-
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
4641
|
-
console.log("=".repeat(80));
|
|
4642
|
-
qrcode.generate(url, { small: true }, (qr) => {
|
|
4643
|
-
for (let l of qr.split("\n")) {
|
|
4644
|
-
console.log(" ".repeat(10) + l);
|
|
4645
|
-
}
|
|
4646
|
-
});
|
|
4647
|
-
console.log("=".repeat(80));
|
|
4648
|
-
}
|
|
4649
|
-
|
|
4650
|
-
function generateWebAuthUrl(publicKey) {
|
|
4651
|
-
const publicKeyBase64 = encodeBase64(publicKey, "base64url");
|
|
4652
|
-
const server = encodeURIComponent(configuration.serverUrl);
|
|
4653
|
-
const shouldAutoLogout = (() => {
|
|
4654
|
-
try {
|
|
4655
|
-
const serverUrl = new URL(configuration.serverUrl);
|
|
4656
|
-
const webUrl = new URL(configuration.webappUrl);
|
|
4657
|
-
const isLoopback = (host) => {
|
|
4658
|
-
const h = String(host || "").toLowerCase();
|
|
4659
|
-
return h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.endsWith(".localhost");
|
|
4660
|
-
};
|
|
4661
|
-
return isLoopback(serverUrl.hostname) && isLoopback(webUrl.hostname);
|
|
4662
|
-
} catch {
|
|
4663
|
-
return false;
|
|
4664
|
-
}
|
|
4665
|
-
})();
|
|
4666
|
-
return `${configuration.webappUrl}/terminal/connect#key=${publicKeyBase64}&server=${server}${shouldAutoLogout ? "&autologout=1" : ""}`;
|
|
4667
|
-
}
|
|
4668
|
-
|
|
4669
4920
|
async function openBrowser(url) {
|
|
4670
4921
|
try {
|
|
4671
4922
|
const forceOpen = process.env.FLOCKBAY_FORCE_BROWSER === "1" || process.env.FLOCKBAY_FORCE_BROWSER === "true";
|
|
@@ -4683,217 +4934,78 @@ async function openBrowser(url) {
|
|
|
4683
4934
|
}
|
|
4684
4935
|
}
|
|
4685
4936
|
|
|
4686
|
-
async function
|
|
4687
|
-
|
|
4688
|
-
const
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
} catch (error) {
|
|
4699
|
-
console.log(`[AUTH DEBUG] Failed to send auth request:`, error);
|
|
4700
|
-
console.log("Failed to create authentication request, please try again later.");
|
|
4701
|
-
return null;
|
|
4937
|
+
async function loginWithClerkAndPairMachine() {
|
|
4938
|
+
logger.debug("[AUTH] Starting Clerk-based CLI login + machine pairing");
|
|
4939
|
+
const settings = await updateSettings(async (s) => {
|
|
4940
|
+
const machineId2 = s.machineId || randomUUID();
|
|
4941
|
+
const serverUrl2 = s.serverUrl || configuration.serverUrl;
|
|
4942
|
+
const webappUrl = s.webappUrl || configuration.webappUrl;
|
|
4943
|
+
return { ...s, machineId: machineId2, serverUrl: serverUrl2, webappUrl };
|
|
4944
|
+
});
|
|
4945
|
+
const serverUrl = configuration.serverUrl.replace(/\/+$/, "");
|
|
4946
|
+
const machineId = String(settings.machineId || "").trim();
|
|
4947
|
+
if (!machineId) {
|
|
4948
|
+
throw new Error("Missing machineId (settings.json).");
|
|
4702
4949
|
}
|
|
4703
|
-
|
|
4704
|
-
}
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
const
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
console.log(`- Paste this URL into the Flockbay app: ${authUrl}`);
|
|
4713
|
-
console.log(`- Or open this link in a browser (works well on mobile web): ${generateWebAuthUrl(keypair.publicKey)}`);
|
|
4714
|
-
console.log("");
|
|
4715
|
-
return await waitForAuthentication(keypair);
|
|
4716
|
-
}
|
|
4717
|
-
async function doWebAuth(keypair) {
|
|
4718
|
-
console.clear();
|
|
4719
|
-
console.log("\nWeb Authentication\n");
|
|
4720
|
-
const webUrl = generateWebAuthUrl(keypair.publicKey);
|
|
4721
|
-
const localWebUrl = (() => {
|
|
4722
|
-
try {
|
|
4723
|
-
const u = new URL(webUrl);
|
|
4724
|
-
u.protocol = "http:";
|
|
4725
|
-
u.hostname = "localhost";
|
|
4726
|
-
u.port = "8081";
|
|
4727
|
-
return u.toString();
|
|
4728
|
-
} catch {
|
|
4729
|
-
return null;
|
|
4730
|
-
}
|
|
4731
|
-
})();
|
|
4732
|
-
const isLocalServer = (() => {
|
|
4733
|
-
try {
|
|
4734
|
-
const server = new URL(configuration.serverUrl);
|
|
4735
|
-
const host = server.hostname.toLowerCase();
|
|
4736
|
-
return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host.endsWith(".localhost");
|
|
4737
|
-
} catch {
|
|
4738
|
-
return false;
|
|
4739
|
-
}
|
|
4740
|
-
})();
|
|
4741
|
-
console.log("Opening your browser...");
|
|
4742
|
-
const primaryUrl = isLocalServer && localWebUrl ? localWebUrl : webUrl;
|
|
4743
|
-
const browserOpened = await openBrowser(primaryUrl);
|
|
4744
|
-
if (browserOpened) {
|
|
4745
|
-
console.log("\u2713 Browser opened\n");
|
|
4746
|
-
console.log("Complete authentication in your browser window.");
|
|
4747
|
-
} else {
|
|
4748
|
-
console.log("Could not open browser automatically.");
|
|
4749
|
-
}
|
|
4750
|
-
console.log("\nIf the browser did not open, please copy and paste this URL:");
|
|
4751
|
-
console.log(webUrl);
|
|
4752
|
-
if (localWebUrl && localWebUrl !== webUrl) {
|
|
4753
|
-
if (isLocalServer) {
|
|
4754
|
-
console.log("\nLocal dev URL (same auth key + server):");
|
|
4755
|
-
console.log(localWebUrl);
|
|
4756
|
-
} else {
|
|
4757
|
-
console.log("\nLocal dev note:");
|
|
4758
|
-
console.log(
|
|
4759
|
-
`- This terminal is waiting on ${configuration.serverUrl}, so opening the link on localhost will NOT complete auth.
|
|
4760
|
-
- To authenticate locally, re-run with:
|
|
4761
|
-
FLOCKBAY_SERVER_URL=http://localhost:3006 FLOCKBAY_WEBAPP_URL=http://localhost:8081`
|
|
4762
|
-
);
|
|
4763
|
-
}
|
|
4950
|
+
const res = await axios.post(
|
|
4951
|
+
`${serverUrl}/v1/cli/pair/request`,
|
|
4952
|
+
{ machineId },
|
|
4953
|
+
{ headers: { "Content-Type": "application/json" }, timeout: 3e4 }
|
|
4954
|
+
);
|
|
4955
|
+
const pairingId = String(res?.data?.pairingId || "").trim();
|
|
4956
|
+
const approveUrl = String(res?.data?.approveUrl || "").trim();
|
|
4957
|
+
if (!pairingId || !approveUrl) {
|
|
4958
|
+
throw new Error("Invalid pairing response from server.");
|
|
4764
4959
|
}
|
|
4765
4960
|
console.log("");
|
|
4766
|
-
|
|
4767
|
-
}
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
console.log("\n\nInvalid auth response (expected V2). Please update your app/CLI.");
|
|
4789
|
-
return null;
|
|
4790
|
-
}
|
|
4791
|
-
const credentials = {
|
|
4792
|
-
publicKey: decrypted.slice(1, 33),
|
|
4793
|
-
machineKey: randomBytes(32),
|
|
4794
|
-
token
|
|
4795
|
-
};
|
|
4796
|
-
await writeCredentialsDataKey(credentials);
|
|
4797
|
-
console.log("\n\n\u2713 Authentication successful\n");
|
|
4798
|
-
return {
|
|
4799
|
-
encryption: {
|
|
4800
|
-
type: "dataKey",
|
|
4801
|
-
publicKey: credentials.publicKey,
|
|
4802
|
-
machineKey: credentials.machineKey
|
|
4803
|
-
},
|
|
4804
|
-
token
|
|
4805
|
-
};
|
|
4806
|
-
} else {
|
|
4807
|
-
console.log("\n\nFailed to decrypt response. Please try again.");
|
|
4808
|
-
return null;
|
|
4809
|
-
}
|
|
4810
|
-
}
|
|
4811
|
-
} catch (error) {
|
|
4812
|
-
console.log("\n\nFailed to check authentication status. Please try again.");
|
|
4813
|
-
return null;
|
|
4961
|
+
console.log(chalk.bold("Flockbay CLI login"));
|
|
4962
|
+
console.log(chalk.gray(`Profile: ${configuration.profile}`));
|
|
4963
|
+
console.log(chalk.gray(`Server: ${configuration.serverUrl}`));
|
|
4964
|
+
console.log(chalk.gray(`Machine: ${machineId} (${os.hostname()})`));
|
|
4965
|
+
console.log("");
|
|
4966
|
+
console.log("Open this link to sign in with Clerk and approve this machine:");
|
|
4967
|
+
console.log(approveUrl);
|
|
4968
|
+
console.log("");
|
|
4969
|
+
void openBrowser(approveUrl).catch(() => null);
|
|
4970
|
+
const deadline = Date.now() + 10 * 6e4;
|
|
4971
|
+
while (Date.now() < deadline) {
|
|
4972
|
+
await delay(1e3);
|
|
4973
|
+
const status = await axios.get(`${serverUrl}/v1/cli/pair/status`, {
|
|
4974
|
+
params: { pairingId, consume: 1 },
|
|
4975
|
+
timeout: 15e3
|
|
4976
|
+
});
|
|
4977
|
+
const state = String(status?.data?.state || "").trim();
|
|
4978
|
+
if (state === "approved") {
|
|
4979
|
+
const machineToken = String(status?.data?.machineToken || "").trim();
|
|
4980
|
+
const orgId = String(status?.data?.orgId || "").trim();
|
|
4981
|
+
if (!machineToken || !orgId) {
|
|
4982
|
+
throw new Error("Pairing approved, but missing token/orgId.");
|
|
4814
4983
|
}
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4984
|
+
const auth = { machineToken, orgId, createdAtMs: Date.now() };
|
|
4985
|
+
await writeCredentials(auth);
|
|
4986
|
+
console.log(chalk.green("\u2713 Machine paired to workspace"));
|
|
4987
|
+
console.log(chalk.gray(`Workspace: ${orgId}`));
|
|
4988
|
+
return auth;
|
|
4989
|
+
}
|
|
4990
|
+
if (state === "expired") throw new Error("Pairing expired. Re-run `flockbay login`.");
|
|
4991
|
+
if (state === "consumed") {
|
|
4992
|
+
throw new Error("Pairing token already consumed. If this is unexpected, re-run `flockbay login`.");
|
|
4818
4993
|
}
|
|
4819
|
-
} finally {
|
|
4820
|
-
process.off("SIGINT", handleInterrupt);
|
|
4821
|
-
}
|
|
4822
|
-
return null;
|
|
4823
|
-
}
|
|
4824
|
-
function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
4825
|
-
const ephemeralPublicKey = encryptedBundle.slice(0, 32);
|
|
4826
|
-
const nonce = encryptedBundle.slice(32, 32 + tweetnacl.box.nonceLength);
|
|
4827
|
-
const encrypted = encryptedBundle.slice(32 + tweetnacl.box.nonceLength);
|
|
4828
|
-
const decrypted = tweetnacl.box.open(encrypted, nonce, ephemeralPublicKey, recipientSecretKey);
|
|
4829
|
-
if (!decrypted) {
|
|
4830
|
-
return null;
|
|
4831
4994
|
}
|
|
4832
|
-
|
|
4995
|
+
throw new Error("Login timed out. Re-run `flockbay login`.");
|
|
4833
4996
|
}
|
|
4834
|
-
async function
|
|
4835
|
-
|
|
4836
|
-
const
|
|
4837
|
-
const
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
if (!allowAuthFlow) {
|
|
4841
|
-
throw new Error('Not authenticated. Run "flockbay auth login" first.');
|
|
4842
|
-
}
|
|
4843
|
-
logger.debug("[AUTH] No credentials found, starting authentication flow...");
|
|
4844
|
-
const authResult = await doAuth({ method: authMethod });
|
|
4845
|
-
if (!authResult) {
|
|
4846
|
-
throw new Error("Authentication failed or was cancelled");
|
|
4847
|
-
}
|
|
4848
|
-
credentials = authResult;
|
|
4849
|
-
} else {
|
|
4850
|
-
logger.debug("[AUTH] Using existing credentials");
|
|
4997
|
+
async function ensureMachineAuthOrLogin() {
|
|
4998
|
+
const existing = await readCredentials();
|
|
4999
|
+
const settings = await readSettings();
|
|
5000
|
+
const machineId = String(settings?.machineId || "").trim();
|
|
5001
|
+
if (existing && existing.machineToken && existing.orgId && machineId) {
|
|
5002
|
+
return { auth: existing, machineId };
|
|
4851
5003
|
}
|
|
4852
|
-
const
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
});
|
|
4858
|
-
const machines = Array.isArray(res.data) ? res.data : [];
|
|
4859
|
-
const host = os.hostname();
|
|
4860
|
-
const homeDir = os.homedir();
|
|
4861
|
-
const flockbayHomeDir = canonicalizePath(configuration.flockbayHomeDir);
|
|
4862
|
-
const matches = machines.map((m) => {
|
|
4863
|
-
const raw = m?.metadata;
|
|
4864
|
-
let metadata = raw;
|
|
4865
|
-
if (typeof raw === "string") {
|
|
4866
|
-
try {
|
|
4867
|
-
metadata = JSON.parse(raw);
|
|
4868
|
-
} catch {
|
|
4869
|
-
metadata = null;
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
return { machine: m, metadata };
|
|
4873
|
-
}).filter(({ machine, metadata }) => {
|
|
4874
|
-
if (!machine?.id || !metadata) return false;
|
|
4875
|
-
if (String(metadata.host || "") !== host) return false;
|
|
4876
|
-
if (String(metadata.homeDir || "") !== homeDir) return false;
|
|
4877
|
-
if (canonicalizePath(String(metadata.flockbayHomeDir || "")) !== flockbayHomeDir) return false;
|
|
4878
|
-
return true;
|
|
4879
|
-
}).map(({ machine }) => machine);
|
|
4880
|
-
if (!matches.length) return null;
|
|
4881
|
-
matches.sort((a, b) => (Number(a.createdAt) || 0) - (Number(b.createdAt) || 0));
|
|
4882
|
-
return String(matches[0].id || "").trim() || null;
|
|
4883
|
-
} catch {
|
|
4884
|
-
return null;
|
|
4885
|
-
}
|
|
4886
|
-
};
|
|
4887
|
-
const existingSettings = await readSettings();
|
|
4888
|
-
const preferredMachineId = !existingSettings.machineId ? await findMatchingExistingMachineId() : null;
|
|
4889
|
-
const settings = await updateSettings(async (s) => {
|
|
4890
|
-
const machineId = s.machineId || preferredMachineId || randomUUID();
|
|
4891
|
-
const serverUrl = s.serverUrl || configuration.serverUrl;
|
|
4892
|
-
const webappUrl = s.webappUrl || configuration.webappUrl;
|
|
4893
|
-
return { ...s, machineId, serverUrl, webappUrl };
|
|
4894
|
-
});
|
|
4895
|
-
logger.debug(`[AUTH] Machine ID: ${settings.machineId}`);
|
|
4896
|
-
return { credentials, machineId: settings.machineId };
|
|
5004
|
+
const auth = await loginWithClerkAndPairMachine();
|
|
5005
|
+
const updated = await readSettings();
|
|
5006
|
+
const mid = String(updated?.machineId || "").trim();
|
|
5007
|
+
if (!mid) throw new Error("Missing machineId after login.");
|
|
5008
|
+
return { auth, machineId: mid };
|
|
4897
5009
|
}
|
|
4898
5010
|
|
|
4899
5011
|
function resolveTsxImportArgs(projectRoot) {
|
|
@@ -4973,6 +5085,7 @@ Error: ${message}` };
|
|
|
4973
5085
|
|
|
4974
5086
|
function startDaemonControlServer({
|
|
4975
5087
|
getChildren,
|
|
5088
|
+
getStatus,
|
|
4976
5089
|
stopSession,
|
|
4977
5090
|
spawnSession,
|
|
4978
5091
|
requestShutdown,
|
|
@@ -5028,6 +5141,15 @@ function startDaemonControlServer({
|
|
|
5028
5141
|
}))
|
|
5029
5142
|
};
|
|
5030
5143
|
});
|
|
5144
|
+
typed.post("/status", {
|
|
5145
|
+
schema: {
|
|
5146
|
+
response: {
|
|
5147
|
+
200: z.any()
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
}, async () => {
|
|
5151
|
+
return getStatus();
|
|
5152
|
+
});
|
|
5031
5153
|
typed.post("/stop-session", {
|
|
5032
5154
|
schema: {
|
|
5033
5155
|
body: z.object({
|
|
@@ -5167,6 +5289,26 @@ function startDaemonControlServer({
|
|
|
5167
5289
|
});
|
|
5168
5290
|
}
|
|
5169
5291
|
|
|
5292
|
+
function shouldAutoRestart(params) {
|
|
5293
|
+
const { exit, stopRequested } = params;
|
|
5294
|
+
if (stopRequested) return false;
|
|
5295
|
+
const { code, signal } = exit;
|
|
5296
|
+
const cleanExit = code === 0 && !signal;
|
|
5297
|
+
if (cleanExit) return false;
|
|
5298
|
+
const expectedSignal = signal === "SIGTERM" || signal === "SIGINT";
|
|
5299
|
+
if (expectedSignal) return false;
|
|
5300
|
+
return true;
|
|
5301
|
+
}
|
|
5302
|
+
function computeNextAutoRestart(params) {
|
|
5303
|
+
const { nowMs, prev, config } = params;
|
|
5304
|
+
const withinWindow = prev && nowMs - prev.firstFailureAtMs <= config.windowMs;
|
|
5305
|
+
const state = withinWindow ? { ...prev } : { attempts: 0, firstFailureAtMs: nowMs };
|
|
5306
|
+
state.attempts += 1;
|
|
5307
|
+
if (state.attempts > config.maxAttempts) return { action: "give_up" };
|
|
5308
|
+
const delayMs = Math.min(config.baseDelayMs * 2 ** (state.attempts - 1), config.maxDelayMs);
|
|
5309
|
+
return { action: "schedule", delayMs, state };
|
|
5310
|
+
}
|
|
5311
|
+
|
|
5170
5312
|
async function pathExists(p) {
|
|
5171
5313
|
try {
|
|
5172
5314
|
await fs$1.access(p);
|
|
@@ -5237,15 +5379,23 @@ const initialMachineMetadata = {
|
|
|
5237
5379
|
flockbayLibDir: projectPath()
|
|
5238
5380
|
};
|
|
5239
5381
|
async function startDaemon() {
|
|
5382
|
+
let startupCompleted = false;
|
|
5383
|
+
let startupForceExitTimer = null;
|
|
5240
5384
|
let requestShutdown;
|
|
5241
5385
|
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
5242
5386
|
requestShutdown = (source, errorMessage) => {
|
|
5243
5387
|
logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5388
|
+
if (!startupCompleted && !startupForceExitTimer) {
|
|
5389
|
+
startupForceExitTimer = setTimeout(async () => {
|
|
5390
|
+
logger.debug("[DAEMON RUN] Startup malfunctioned, forcing exit with code 1");
|
|
5391
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
5392
|
+
process.exit(1);
|
|
5393
|
+
}, 1e4);
|
|
5394
|
+
try {
|
|
5395
|
+
startupForceExitTimer.unref();
|
|
5396
|
+
} catch {
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5249
5399
|
resolve({ source, errorMessage });
|
|
5250
5400
|
};
|
|
5251
5401
|
});
|
|
@@ -5299,7 +5449,7 @@ async function startDaemon() {
|
|
|
5299
5449
|
if (caffeinateStarted) {
|
|
5300
5450
|
logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
5301
5451
|
}
|
|
5302
|
-
const { credentials, machineId } = await
|
|
5452
|
+
const { auth: credentials, machineId } = await ensureMachineAuthOrLogin();
|
|
5303
5453
|
logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
5304
5454
|
const shouldStartUnrealMcp = String(process.env.FLOCKBAY_UNREAL_MCP_ENABLED || "").trim() === "1";
|
|
5305
5455
|
if (shouldStartUnrealMcp) {
|
|
@@ -5312,10 +5462,55 @@ async function startDaemon() {
|
|
|
5312
5462
|
}
|
|
5313
5463
|
}
|
|
5314
5464
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
5465
|
+
const stopRequestedSessionIds = /* @__PURE__ */ new Set();
|
|
5466
|
+
const sessionAutoRestart = /* @__PURE__ */ new Map();
|
|
5467
|
+
const autoRestartConfig = {
|
|
5468
|
+
maxAttempts: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_MAX_ATTEMPTS || "5", 10),
|
|
5469
|
+
windowMs: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_WINDOW_MS || String(10 * 6e4), 10),
|
|
5470
|
+
baseDelayMs: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_BASE_DELAY_MS || "1000", 10),
|
|
5471
|
+
maxDelayMs: parseInt(process.env.FLOCKBAY_DAEMON_AUTO_RESTART_MAX_DELAY_MS || "30000", 10)
|
|
5472
|
+
};
|
|
5315
5473
|
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
5316
5474
|
let machineRef = null;
|
|
5317
|
-
let
|
|
5475
|
+
let apiMachine = null;
|
|
5318
5476
|
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
5477
|
+
const findLatestLogForPid = async (pid) => {
|
|
5478
|
+
const suffix = `-pid-${pid}.log`;
|
|
5479
|
+
try {
|
|
5480
|
+
const files = await fs$2.readdir(configuration.logsDir);
|
|
5481
|
+
const matches = files.filter((f) => f.endsWith(suffix));
|
|
5482
|
+
if (matches.length === 0) return null;
|
|
5483
|
+
let best = null;
|
|
5484
|
+
for (const file of matches) {
|
|
5485
|
+
try {
|
|
5486
|
+
const st = await fs$2.stat(join$1(configuration.logsDir, file));
|
|
5487
|
+
const mtimeMs = Number(st.mtimeMs || 0);
|
|
5488
|
+
if (!best || mtimeMs > best.mtimeMs) best = { file, mtimeMs };
|
|
5489
|
+
} catch {
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
return best ? join$1(configuration.logsDir, best.file) : null;
|
|
5493
|
+
} catch {
|
|
5494
|
+
return null;
|
|
5495
|
+
}
|
|
5496
|
+
};
|
|
5497
|
+
const readLogTail = async (path, maxBytes) => {
|
|
5498
|
+
try {
|
|
5499
|
+
const st = await fs$2.stat(path);
|
|
5500
|
+
const size = Number(st.size || 0);
|
|
5501
|
+
const offset = Math.max(0, size - maxBytes);
|
|
5502
|
+
const handle = await fs$2.open(path, "r");
|
|
5503
|
+
try {
|
|
5504
|
+
const buf = Buffer.alloc(Math.min(maxBytes, size));
|
|
5505
|
+
const { bytesRead } = await handle.read(buf, 0, buf.length, offset);
|
|
5506
|
+
return buf.subarray(0, bytesRead).toString("utf8");
|
|
5507
|
+
} finally {
|
|
5508
|
+
await handle.close();
|
|
5509
|
+
}
|
|
5510
|
+
} catch {
|
|
5511
|
+
return "";
|
|
5512
|
+
}
|
|
5513
|
+
};
|
|
5319
5514
|
const onSessionWebhook = (sessionId, sessionMetadata) => {
|
|
5320
5515
|
logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
|
|
5321
5516
|
const pid = sessionMetadata.hostPid;
|
|
@@ -5329,6 +5524,9 @@ async function startDaemon() {
|
|
|
5329
5524
|
if (existingSession && existingSession.startedBy === "daemon") {
|
|
5330
5525
|
existingSession.serverSessionId = sessionId;
|
|
5331
5526
|
existingSession.serverSessionMetadataFromLocalWebhook = sessionMetadata;
|
|
5527
|
+
if (existingSession.spawnOptions) {
|
|
5528
|
+
existingSession.spawnOptions = { ...existingSession.spawnOptions, sessionId };
|
|
5529
|
+
}
|
|
5332
5530
|
logger.debug(`[DAEMON RUN] Updated daemon-spawned session ${sessionId} with metadata`);
|
|
5333
5531
|
const awaiter = pidToAwaiter.get(pid);
|
|
5334
5532
|
if (awaiter) {
|
|
@@ -5367,16 +5565,7 @@ async function startDaemon() {
|
|
|
5367
5565
|
const refreshBypassFromServerIfNeeded = async () => {
|
|
5368
5566
|
if (bypassUeGates) return;
|
|
5369
5567
|
if (envBypassUeGates) return;
|
|
5370
|
-
|
|
5371
|
-
const targetMachineId = requestedMachineId || machineRef?.id;
|
|
5372
|
-
if (!targetMachineId) return;
|
|
5373
|
-
try {
|
|
5374
|
-
const latest = await api.getMachine(targetMachineId);
|
|
5375
|
-
machineRef = latest;
|
|
5376
|
-
bypassUeGates = envBypassUeGates || Boolean(latest?.metadata?.flockbayDevBypassUeGates);
|
|
5377
|
-
} catch (err) {
|
|
5378
|
-
bypassRefreshError = err instanceof Error ? err.message : String(err);
|
|
5379
|
-
}
|
|
5568
|
+
bypassRefreshError = "refresh_not_supported";
|
|
5380
5569
|
};
|
|
5381
5570
|
const detection = await detectUnrealProject(directory);
|
|
5382
5571
|
if (!detection.ok && !bypassUeGates) {
|
|
@@ -5397,7 +5586,7 @@ async function startDaemon() {
|
|
|
5397
5586
|
const res = await fetch(url, {
|
|
5398
5587
|
method: "POST",
|
|
5399
5588
|
headers: {
|
|
5400
|
-
Authorization: `
|
|
5589
|
+
Authorization: `Machine ${credentials.machineToken}`,
|
|
5401
5590
|
"Content-Type": "application/json"
|
|
5402
5591
|
},
|
|
5403
5592
|
body: JSON.stringify(body ?? {})
|
|
@@ -5598,11 +5787,16 @@ Fix: restart session creation and re-select the project.`
|
|
|
5598
5787
|
}
|
|
5599
5788
|
const args = [
|
|
5600
5789
|
agentCommand,
|
|
5790
|
+
"--profile",
|
|
5791
|
+
configuration.profile,
|
|
5601
5792
|
"--flockbay-starting-mode",
|
|
5602
5793
|
"remote",
|
|
5603
5794
|
"--started-by",
|
|
5604
5795
|
"daemon"
|
|
5605
5796
|
];
|
|
5797
|
+
if (options.sessionId) {
|
|
5798
|
+
args.push("--flockbay-session-id", String(options.sessionId));
|
|
5799
|
+
}
|
|
5606
5800
|
const captureChildOutput = Boolean(process.env.DEBUG);
|
|
5607
5801
|
const spawnEnv = {
|
|
5608
5802
|
...process.env,
|
|
@@ -5645,24 +5839,30 @@ Fix: restart session creation and re-select the project.`
|
|
|
5645
5839
|
};
|
|
5646
5840
|
}
|
|
5647
5841
|
logger.debug(`[DAEMON RUN] Spawned process with PID ${sessionProcess.pid}`);
|
|
5842
|
+
const spawnOptionsForTracking = {
|
|
5843
|
+
...options,
|
|
5844
|
+
approvedNewDirectoryCreation: true,
|
|
5845
|
+
coordination: coordinationForSpawn && typeof coordinationForSpawn === "object" ? coordinationForSpawn : options.coordination ?? null
|
|
5846
|
+
};
|
|
5648
5847
|
const trackedSession = {
|
|
5649
5848
|
startedBy: "daemon",
|
|
5650
5849
|
pid: sessionProcess.pid,
|
|
5651
5850
|
childProcess: sessionProcess,
|
|
5652
5851
|
directoryCreated,
|
|
5852
|
+
spawnOptions: spawnOptionsForTracking,
|
|
5653
5853
|
message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0
|
|
5654
5854
|
};
|
|
5655
5855
|
pidToTrackedSession.set(sessionProcess.pid, trackedSession);
|
|
5656
5856
|
sessionProcess.on("exit", (code, signal) => {
|
|
5657
5857
|
logger.debug(`[DAEMON RUN] Child PID ${sessionProcess.pid} exited with code ${code}, signal ${signal}`);
|
|
5658
5858
|
if (sessionProcess.pid) {
|
|
5659
|
-
onChildExited(sessionProcess.pid);
|
|
5859
|
+
onChildExited(sessionProcess.pid, { code, signal });
|
|
5660
5860
|
}
|
|
5661
5861
|
});
|
|
5662
5862
|
sessionProcess.on("error", (error) => {
|
|
5663
5863
|
logger.debug(`[DAEMON RUN] Child process error:`, error);
|
|
5664
5864
|
if (sessionProcess.pid) {
|
|
5665
|
-
onChildExited(sessionProcess.pid);
|
|
5865
|
+
onChildExited(sessionProcess.pid, { code: null, signal: null });
|
|
5666
5866
|
}
|
|
5667
5867
|
});
|
|
5668
5868
|
logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${sessionProcess.pid}`);
|
|
@@ -5685,17 +5885,25 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5685
5885
|
}, 3e4);
|
|
5686
5886
|
const onEarlyExit = (code, signal) => {
|
|
5687
5887
|
clearTimeout(timeout);
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5888
|
+
void (async () => {
|
|
5889
|
+
const logPath = await findLatestLogForPid(pid);
|
|
5890
|
+
settleOnce({
|
|
5891
|
+
type: "error",
|
|
5892
|
+
errorMessage: `Session process exited before webhook (PID ${pid}, code ${code ?? "unknown"}, signal ${signal ?? "none"}).
|
|
5893
|
+
Log: ${logPath || `not found (check ${configuration.logsDir})`}`
|
|
5894
|
+
});
|
|
5895
|
+
})();
|
|
5692
5896
|
};
|
|
5693
5897
|
const onEarlyError = (error) => {
|
|
5694
5898
|
clearTimeout(timeout);
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5899
|
+
void (async () => {
|
|
5900
|
+
const logPath = await findLatestLogForPid(pid);
|
|
5901
|
+
settleOnce({
|
|
5902
|
+
type: "error",
|
|
5903
|
+
errorMessage: `Session process error before webhook (PID ${pid}): ${error.message}.
|
|
5904
|
+
Log: ${logPath || `not found (check ${configuration.logsDir})`}`
|
|
5905
|
+
});
|
|
5906
|
+
})();
|
|
5699
5907
|
};
|
|
5700
5908
|
sessionProcess.once("exit", onEarlyExit);
|
|
5701
5909
|
sessionProcess.once("error", onEarlyError);
|
|
@@ -5745,8 +5953,13 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5745
5953
|
};
|
|
5746
5954
|
const stopSession = (sessionId) => {
|
|
5747
5955
|
logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
5956
|
+
if (sessionId && !sessionId.startsWith("PID-")) {
|
|
5957
|
+
stopRequestedSessionIds.add(sessionId);
|
|
5958
|
+
}
|
|
5748
5959
|
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
5749
5960
|
if (session.serverSessionId === sessionId || sessionId.startsWith("PID-") && pid === parseInt(sessionId.replace("PID-", ""))) {
|
|
5961
|
+
session.stopRequested = true;
|
|
5962
|
+
if (session.serverSessionId) stopRequestedSessionIds.add(session.serverSessionId);
|
|
5750
5963
|
if (session.startedBy === "daemon" && session.childProcess) {
|
|
5751
5964
|
try {
|
|
5752
5965
|
session.childProcess.kill("SIGTERM");
|
|
@@ -5762,20 +5975,144 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5762
5975
|
logger.debug(`[DAEMON RUN] Failed to kill external session PID ${pid}:`, error);
|
|
5763
5976
|
}
|
|
5764
5977
|
}
|
|
5765
|
-
|
|
5766
|
-
logger.debug(`[DAEMON RUN] Removed session ${sessionId} from tracking`);
|
|
5978
|
+
logger.debug(`[DAEMON RUN] Stop requested for session ${sessionId}; waiting for exit to remove tracking`);
|
|
5767
5979
|
return true;
|
|
5768
5980
|
}
|
|
5769
5981
|
}
|
|
5770
5982
|
logger.debug(`[DAEMON RUN] Session ${sessionId} not found`);
|
|
5771
5983
|
return false;
|
|
5772
5984
|
};
|
|
5773
|
-
const
|
|
5985
|
+
const scheduleAutoRestart = (params) => {
|
|
5986
|
+
const { sessionId, spawnOptions, reason } = params;
|
|
5987
|
+
if (stopRequestedSessionIds.has(sessionId)) {
|
|
5988
|
+
logger.debug(`[DAEMON RUN] Auto-restart suppressed (stop requested) for session ${sessionId}`);
|
|
5989
|
+
return;
|
|
5990
|
+
}
|
|
5991
|
+
const now = Date.now();
|
|
5992
|
+
const existing = sessionAutoRestart.get(sessionId);
|
|
5993
|
+
if (existing?.timer) {
|
|
5994
|
+
logger.debug(`[DAEMON RUN] Auto-restart already scheduled for session ${sessionId}`);
|
|
5995
|
+
return;
|
|
5996
|
+
}
|
|
5997
|
+
const next = computeNextAutoRestart({
|
|
5998
|
+
nowMs: now,
|
|
5999
|
+
prev: existing?.state ?? null,
|
|
6000
|
+
config: autoRestartConfig
|
|
6001
|
+
});
|
|
6002
|
+
if (next.action === "give_up") {
|
|
6003
|
+
logger.debug(
|
|
6004
|
+
`[DAEMON RUN] Auto-restart giving up for session ${sessionId} after ${autoRestartConfig.maxAttempts} attempt(s) within ${autoRestartConfig.windowMs}ms (last reason: ${reason})`
|
|
6005
|
+
);
|
|
6006
|
+
sessionAutoRestart.delete(sessionId);
|
|
6007
|
+
return;
|
|
6008
|
+
}
|
|
6009
|
+
const delay = next.delayMs;
|
|
6010
|
+
logger.debug(
|
|
6011
|
+
`[DAEMON RUN] Auto-restarting session ${sessionId} in ${delay}ms (attempt ${next.state.attempts}/${autoRestartConfig.maxAttempts}, reason: ${reason})`
|
|
6012
|
+
);
|
|
6013
|
+
const entry = { state: next.state, timer: void 0 };
|
|
6014
|
+
entry.timer = setTimeout(() => {
|
|
6015
|
+
entry.timer = void 0;
|
|
6016
|
+
sessionAutoRestart.set(sessionId, entry);
|
|
6017
|
+
if (stopRequestedSessionIds.has(sessionId)) {
|
|
6018
|
+
logger.debug(`[DAEMON RUN] Auto-restart canceled (stop requested) for session ${sessionId}`);
|
|
6019
|
+
return;
|
|
6020
|
+
}
|
|
6021
|
+
void (async () => {
|
|
6022
|
+
try {
|
|
6023
|
+
const result = await spawnSession({
|
|
6024
|
+
...spawnOptions,
|
|
6025
|
+
approvedNewDirectoryCreation: true,
|
|
6026
|
+
sessionId
|
|
6027
|
+
});
|
|
6028
|
+
if (result.type === "success") {
|
|
6029
|
+
logger.debug(`[DAEMON RUN] Auto-restart succeeded for session ${sessionId}`);
|
|
6030
|
+
sessionAutoRestart.delete(sessionId);
|
|
6031
|
+
return;
|
|
6032
|
+
}
|
|
6033
|
+
logger.debug(
|
|
6034
|
+
`[DAEMON RUN] Auto-restart failed for session ${sessionId} (type=${result.type}): ${result.type === "error" ? result.errorMessage : "needs-user-approval"}`
|
|
6035
|
+
);
|
|
6036
|
+
} catch (err) {
|
|
6037
|
+
logger.debug(`[DAEMON RUN] Auto-restart threw for session ${sessionId}:`, err);
|
|
6038
|
+
}
|
|
6039
|
+
scheduleAutoRestart({ sessionId, spawnOptions, reason: "restart_failed" });
|
|
6040
|
+
})();
|
|
6041
|
+
}, delay);
|
|
6042
|
+
try {
|
|
6043
|
+
entry.timer.unref();
|
|
6044
|
+
} catch {
|
|
6045
|
+
}
|
|
6046
|
+
sessionAutoRestart.set(sessionId, entry);
|
|
6047
|
+
};
|
|
6048
|
+
const onChildExited = (pid, exit) => {
|
|
6049
|
+
const tracked = pidToTrackedSession.get(pid);
|
|
5774
6050
|
logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
5775
6051
|
pidToTrackedSession.delete(pid);
|
|
6052
|
+
if (!tracked) return;
|
|
6053
|
+
tracked.lastExit = {
|
|
6054
|
+
code: exit?.code ?? null,
|
|
6055
|
+
signal: exit?.signal ?? null,
|
|
6056
|
+
atMs: Date.now()
|
|
6057
|
+
};
|
|
6058
|
+
const sessionId = String(tracked.serverSessionId || "").trim();
|
|
6059
|
+
if (!sessionId) return;
|
|
6060
|
+
if (tracked.stopRequested || stopRequestedSessionIds.has(sessionId)) {
|
|
6061
|
+
logger.debug(`[DAEMON RUN] Not auto-restarting session ${sessionId} (stop requested)`);
|
|
6062
|
+
sessionAutoRestart.delete(sessionId);
|
|
6063
|
+
return;
|
|
6064
|
+
}
|
|
6065
|
+
const code = tracked.lastExit.code;
|
|
6066
|
+
const signal = tracked.lastExit.signal;
|
|
6067
|
+
if (!shouldAutoRestart({
|
|
6068
|
+
exit: { code, signal },
|
|
6069
|
+
stopRequested: Boolean(tracked.stopRequested || stopRequestedSessionIds.has(sessionId))
|
|
6070
|
+
})) {
|
|
6071
|
+
logger.debug(`[DAEMON RUN] Not auto-restarting session ${sessionId} (clean exit)`);
|
|
6072
|
+
sessionAutoRestart.delete(sessionId);
|
|
6073
|
+
return;
|
|
6074
|
+
}
|
|
6075
|
+
if (!tracked.spawnOptions) {
|
|
6076
|
+
logger.debug(`[DAEMON RUN] Not auto-restarting session ${sessionId} (missing spawn options)`);
|
|
6077
|
+
return;
|
|
6078
|
+
}
|
|
6079
|
+
void (async () => {
|
|
6080
|
+
const logPath = await findLatestLogForPid(pid);
|
|
6081
|
+
if (!logPath) return;
|
|
6082
|
+
const tail = await readLogTail(logPath, 16384);
|
|
6083
|
+
if (!tail.trim()) return;
|
|
6084
|
+
logger.debug(
|
|
6085
|
+
`[DAEMON RUN] Session runtime log tail for ${sessionId} (pid=${pid}, log=${basename(logPath)}):
|
|
6086
|
+
` + tail
|
|
6087
|
+
);
|
|
6088
|
+
})();
|
|
6089
|
+
scheduleAutoRestart({
|
|
6090
|
+
sessionId,
|
|
6091
|
+
spawnOptions: tracked.spawnOptions,
|
|
6092
|
+
reason: `exit(code=${code ?? "null"},signal=${signal ?? "null"})`
|
|
6093
|
+
});
|
|
5776
6094
|
};
|
|
5777
6095
|
const { port: controlPort, stop } = await startDaemonControlServer({
|
|
5778
6096
|
getChildren: getCurrentChildren,
|
|
6097
|
+
getStatus: () => ({
|
|
6098
|
+
profile: configuration.profile,
|
|
6099
|
+
serverUrl: configuration.serverUrl,
|
|
6100
|
+
webappUrl: configuration.webappUrl,
|
|
6101
|
+
orgId: credentials?.orgId || null,
|
|
6102
|
+
machineId,
|
|
6103
|
+
daemonPid: process.pid,
|
|
6104
|
+
daemonHttpPort: controlPort,
|
|
6105
|
+
startedWithCliVersion: packageJson.version,
|
|
6106
|
+
machine: machineRef ? { id: machineRef.id, seq: machineRef.seq || 0 } : null,
|
|
6107
|
+
connection: apiMachine ? apiMachine.getStatusSnapshot() : {
|
|
6108
|
+
connected: false,
|
|
6109
|
+
lastConnectError: null,
|
|
6110
|
+
lastDisconnectReason: null,
|
|
6111
|
+
lastHttpUpsertError: null,
|
|
6112
|
+
lastHttpUpsertStatus: null,
|
|
6113
|
+
lastHttpUpsertAt: null
|
|
6114
|
+
}
|
|
6115
|
+
}),
|
|
5779
6116
|
stopSession,
|
|
5780
6117
|
spawnSession,
|
|
5781
6118
|
requestShutdown: () => requestShutdown("flockbay-cli"),
|
|
@@ -5797,93 +6134,89 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5797
6134
|
httpPort: controlPort,
|
|
5798
6135
|
startedAt: Date.now()
|
|
5799
6136
|
};
|
|
5800
|
-
const
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
} catch (error) {
|
|
5811
|
-
const status = error?.response?.status;
|
|
5812
|
-
if (status === 404) {
|
|
5813
|
-
machine = await apiClient.getOrCreateMachine({
|
|
5814
|
-
machineId,
|
|
5815
|
-
metadata: initialMachineMetadata,
|
|
5816
|
-
daemonState: initialDaemonState
|
|
5817
|
-
});
|
|
5818
|
-
} else {
|
|
5819
|
-
throw error;
|
|
5820
|
-
}
|
|
5821
|
-
}
|
|
5822
|
-
logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
|
6137
|
+
const machine = {
|
|
6138
|
+
id: machineId,
|
|
6139
|
+
seq: 0,
|
|
6140
|
+
active: false,
|
|
6141
|
+
activeAt: null,
|
|
6142
|
+
createdAt: null,
|
|
6143
|
+
updatedAt: null,
|
|
6144
|
+
metadata: { ...initialMachineMetadata },
|
|
6145
|
+
daemonState: initialDaemonState
|
|
6146
|
+
};
|
|
5823
6147
|
machineRef = machine;
|
|
5824
|
-
|
|
6148
|
+
apiMachine = new ApiMachineClient(credentials.machineToken, machine);
|
|
5825
6149
|
apiMachine.setRPCHandlers({
|
|
5826
6150
|
spawnSession,
|
|
5827
6151
|
stopSession,
|
|
5828
6152
|
requestShutdown: () => requestShutdown("flockbay-app")
|
|
5829
6153
|
});
|
|
5830
|
-
|
|
6154
|
+
try {
|
|
6155
|
+
apiMachine.connect();
|
|
6156
|
+
} catch (err) {
|
|
6157
|
+
logger.debug("[DAEMON RUN] Failed to connect machine socket (will remain offline until restart):", err);
|
|
6158
|
+
}
|
|
5831
6159
|
const heartbeatIntervalMs = parseInt(process.env.FLOCKBAY_DAEMON_HEARTBEAT_INTERVAL || "60000");
|
|
5832
6160
|
let heartbeatRunning = false;
|
|
5833
6161
|
const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
|
|
5834
|
-
if (heartbeatRunning)
|
|
5835
|
-
return;
|
|
5836
|
-
}
|
|
6162
|
+
if (heartbeatRunning) return;
|
|
5837
6163
|
heartbeatRunning = true;
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
6164
|
+
try {
|
|
6165
|
+
if (process.env.DEBUG) {
|
|
6166
|
+
logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
6167
|
+
}
|
|
6168
|
+
for (const [pid, _] of pidToTrackedSession.entries()) {
|
|
6169
|
+
try {
|
|
6170
|
+
process.kill(pid, 0);
|
|
6171
|
+
} catch (error) {
|
|
6172
|
+
logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
|
|
6173
|
+
pidToTrackedSession.delete(pid);
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
5842
6176
|
try {
|
|
5843
|
-
|
|
6177
|
+
const projectVersion = JSON.parse(readFileSync$1(join$1(projectPath(), "package.json"), "utf-8")).version;
|
|
6178
|
+
if (projectVersion !== configuration.currentCliVersion) {
|
|
6179
|
+
logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
6180
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
6181
|
+
try {
|
|
6182
|
+
spawnFlockbayCLI(["daemon", "start"], {
|
|
6183
|
+
detached: true,
|
|
6184
|
+
stdio: "ignore"
|
|
6185
|
+
});
|
|
6186
|
+
} catch (error) {
|
|
6187
|
+
logger.debug("[DAEMON RUN] Failed to spawn new daemon, this can happen during integration tests", error);
|
|
6188
|
+
}
|
|
6189
|
+
logger.debug("[DAEMON RUN] Hanging briefly - waiting for CLI to kill us due to stale version");
|
|
6190
|
+
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
6191
|
+
process.exit(0);
|
|
6192
|
+
}
|
|
5844
6193
|
} catch (error) {
|
|
5845
|
-
logger.debug(
|
|
5846
|
-
|
|
6194
|
+
logger.debug("[DAEMON RUN] Failed to check CLI version during health check", error);
|
|
6195
|
+
}
|
|
6196
|
+
const daemonState = await readDaemonState();
|
|
6197
|
+
if (daemonState && daemonState.pid !== process.pid) {
|
|
6198
|
+
logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
|
|
6199
|
+
requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
|
|
5847
6200
|
}
|
|
5848
|
-
}
|
|
5849
|
-
const projectVersion = JSON.parse(readFileSync$1(join$1(projectPath(), "package.json"), "utf-8")).version;
|
|
5850
|
-
if (projectVersion !== configuration.currentCliVersion) {
|
|
5851
|
-
logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
5852
|
-
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
5853
6201
|
try {
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
6202
|
+
const updatedState = {
|
|
6203
|
+
pid: process.pid,
|
|
6204
|
+
httpPort: controlPort,
|
|
6205
|
+
startTime: fileState.startTime,
|
|
6206
|
+
startedWithCliVersion: packageJson.version,
|
|
6207
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
6208
|
+
daemonLogPath: fileState.daemonLogPath
|
|
6209
|
+
};
|
|
6210
|
+
writeDaemonState(updatedState);
|
|
6211
|
+
if (process.env.DEBUG) {
|
|
6212
|
+
logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
|
|
6213
|
+
}
|
|
5858
6214
|
} catch (error) {
|
|
5859
|
-
logger.debug("[DAEMON RUN] Failed to
|
|
5860
|
-
}
|
|
5861
|
-
logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
|
|
5862
|
-
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
5863
|
-
process.exit(0);
|
|
5864
|
-
}
|
|
5865
|
-
const daemonState = await readDaemonState();
|
|
5866
|
-
if (daemonState && daemonState.pid !== process.pid) {
|
|
5867
|
-
logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
|
|
5868
|
-
requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
|
|
5869
|
-
}
|
|
5870
|
-
try {
|
|
5871
|
-
const updatedState = {
|
|
5872
|
-
pid: process.pid,
|
|
5873
|
-
httpPort: controlPort,
|
|
5874
|
-
startTime: fileState.startTime,
|
|
5875
|
-
startedWithCliVersion: packageJson.version,
|
|
5876
|
-
lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
5877
|
-
daemonLogPath: fileState.daemonLogPath
|
|
5878
|
-
};
|
|
5879
|
-
writeDaemonState(updatedState);
|
|
5880
|
-
if (process.env.DEBUG) {
|
|
5881
|
-
logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
|
|
6215
|
+
logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
|
|
5882
6216
|
}
|
|
5883
|
-
}
|
|
5884
|
-
|
|
6217
|
+
} finally {
|
|
6218
|
+
heartbeatRunning = false;
|
|
5885
6219
|
}
|
|
5886
|
-
heartbeatRunning = false;
|
|
5887
6220
|
}, heartbeatIntervalMs);
|
|
5888
6221
|
const cleanupAndShutdown = async (source, errorMessage) => {
|
|
5889
6222
|
logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
|
|
@@ -5892,13 +6225,15 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5892
6225
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
5893
6226
|
logger.debug("[DAEMON RUN] Health check interval cleared");
|
|
5894
6227
|
}
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
6228
|
+
try {
|
|
6229
|
+
await apiMachine?.updateDaemonStateOnce?.((state) => ({
|
|
6230
|
+
...state,
|
|
6231
|
+
status: "shutting-down",
|
|
6232
|
+
shutdownRequestedAt: Date.now(),
|
|
6233
|
+
shutdownSource: source
|
|
6234
|
+
}));
|
|
6235
|
+
} catch {
|
|
6236
|
+
}
|
|
5902
6237
|
if (killSessionsOnShutdown) {
|
|
5903
6238
|
try {
|
|
5904
6239
|
const tracked = Array.from(pidToTrackedSession.entries());
|
|
@@ -5949,7 +6284,7 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5949
6284
|
} else {
|
|
5950
6285
|
logger.debug("[DAEMON RUN] Preserving session processes across daemon shutdown");
|
|
5951
6286
|
}
|
|
5952
|
-
apiMachine
|
|
6287
|
+
apiMachine?.shutdown();
|
|
5953
6288
|
try {
|
|
5954
6289
|
unrealMcpChild?.kill();
|
|
5955
6290
|
} catch {
|
|
@@ -5964,7 +6299,12 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
5964
6299
|
process.exit(0);
|
|
5965
6300
|
};
|
|
5966
6301
|
logger.debug("[DAEMON RUN] Daemon started successfully, waiting for shutdown request");
|
|
6302
|
+
startupCompleted = true;
|
|
5967
6303
|
const shutdownRequest = await resolvesWhenShutdownRequested;
|
|
6304
|
+
if (startupForceExitTimer) {
|
|
6305
|
+
clearTimeout(startupForceExitTimer);
|
|
6306
|
+
startupForceExitTimer = null;
|
|
6307
|
+
}
|
|
5968
6308
|
await cleanupAndShutdown(shutdownRequest.source, shutdownRequest.errorMessage);
|
|
5969
6309
|
} catch (error) {
|
|
5970
6310
|
logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
|
|
@@ -6007,12 +6347,259 @@ Fix: restart the daemon and try again (macOS: \`flockbay daemon stop\` then \`fl
|
|
|
6007
6347
|
async function runMechanicRun(_input) {
|
|
6008
6348
|
return {
|
|
6009
6349
|
ok: false,
|
|
6010
|
-
errorMessage: "Mechanic runs are disabled
|
|
6350
|
+
errorMessage: "Mechanic runs are disabled.",
|
|
6011
6351
|
hint: "Remove reliance on `unreal_mechanic_run`, or re-introduce this tool as a future feature built on project-local tooling.",
|
|
6012
6352
|
artifactsDir: null
|
|
6013
6353
|
};
|
|
6014
6354
|
}
|
|
6015
6355
|
|
|
6356
|
+
function targetPlatform() {
|
|
6357
|
+
if (process.platform === "win32") return "Win64";
|
|
6358
|
+
if (process.platform === "darwin") return "Mac";
|
|
6359
|
+
return "Linux";
|
|
6360
|
+
}
|
|
6361
|
+
function buildScriptPath(engineRoot) {
|
|
6362
|
+
if (process.platform === "win32") {
|
|
6363
|
+
return path.join(engineRoot, "Engine", "Build", "BatchFiles", "Build.bat");
|
|
6364
|
+
}
|
|
6365
|
+
return path.join(engineRoot, "Engine", "Build", "BatchFiles", "Build.sh");
|
|
6366
|
+
}
|
|
6367
|
+
function parseBuildIssuesFromText(text, limit) {
|
|
6368
|
+
const issues = [];
|
|
6369
|
+
let errors = 0;
|
|
6370
|
+
let warnings = 0;
|
|
6371
|
+
let truncated = false;
|
|
6372
|
+
const clangRe = /^(.+?):(\d+):(\d+):\s+(warning|error):\s+(.*)$/;
|
|
6373
|
+
const msvcRe = /^(.+?)\((\d+)\):\s+(warning|error)\s+([A-Z]+\d+):\s+(.*)$/;
|
|
6374
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
6375
|
+
for (const line of lines) {
|
|
6376
|
+
let m = clangRe.exec(line);
|
|
6377
|
+
if (m) {
|
|
6378
|
+
const severity = m[4] === "warning" ? "warning" : "error";
|
|
6379
|
+
if (severity === "error") errors += 1;
|
|
6380
|
+
else warnings += 1;
|
|
6381
|
+
if (issues.length < limit) {
|
|
6382
|
+
issues.push({
|
|
6383
|
+
file: m[1] ? String(m[1]) : null,
|
|
6384
|
+
line: Number(m[2]) || null,
|
|
6385
|
+
column: Number(m[3]) || null,
|
|
6386
|
+
severity,
|
|
6387
|
+
code: null,
|
|
6388
|
+
message: String(m[5] ?? "").trim()
|
|
6389
|
+
});
|
|
6390
|
+
} else {
|
|
6391
|
+
truncated = true;
|
|
6392
|
+
}
|
|
6393
|
+
continue;
|
|
6394
|
+
}
|
|
6395
|
+
m = msvcRe.exec(line);
|
|
6396
|
+
if (m) {
|
|
6397
|
+
const severity = m[3] === "warning" ? "warning" : "error";
|
|
6398
|
+
if (severity === "error") errors += 1;
|
|
6399
|
+
else warnings += 1;
|
|
6400
|
+
if (issues.length < limit) {
|
|
6401
|
+
issues.push({
|
|
6402
|
+
file: m[1] ? String(m[1]) : null,
|
|
6403
|
+
line: Number(m[2]) || null,
|
|
6404
|
+
column: null,
|
|
6405
|
+
severity,
|
|
6406
|
+
code: m[4] ? String(m[4]) : null,
|
|
6407
|
+
message: String(m[5] ?? "").trim()
|
|
6408
|
+
});
|
|
6409
|
+
} else {
|
|
6410
|
+
truncated = true;
|
|
6411
|
+
}
|
|
6412
|
+
continue;
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
return { issues, errors, warnings, truncated };
|
|
6416
|
+
}
|
|
6417
|
+
async function buildUnrealProject(args) {
|
|
6418
|
+
const uprojectPath = args.uprojectPath;
|
|
6419
|
+
const engineRoot = args.engineRoot;
|
|
6420
|
+
const configuration = args.configuration ?? "Development";
|
|
6421
|
+
const target = args.target ?? "Editor";
|
|
6422
|
+
const timeoutMs = Math.max(3e4, args.timeoutMs ?? 20 * 6e4);
|
|
6423
|
+
const issuesLimit = Math.max(1, Math.min(2e3, args.issuesLimit ?? 250));
|
|
6424
|
+
if (!uprojectPath || !path.isAbsolute(uprojectPath) || !uprojectPath.toLowerCase().endsWith(".uproject")) {
|
|
6425
|
+
return {
|
|
6426
|
+
ok: false,
|
|
6427
|
+
exitCode: null,
|
|
6428
|
+
logPath: "",
|
|
6429
|
+
issues: [],
|
|
6430
|
+
errors: 0,
|
|
6431
|
+
warnings: 0,
|
|
6432
|
+
truncated: false,
|
|
6433
|
+
errorMessage: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}`
|
|
6434
|
+
};
|
|
6435
|
+
}
|
|
6436
|
+
if (!fs__default.existsSync(uprojectPath)) {
|
|
6437
|
+
return {
|
|
6438
|
+
ok: false,
|
|
6439
|
+
exitCode: null,
|
|
6440
|
+
logPath: "",
|
|
6441
|
+
issues: [],
|
|
6442
|
+
errors: 0,
|
|
6443
|
+
warnings: 0,
|
|
6444
|
+
truncated: false,
|
|
6445
|
+
errorMessage: `uprojectPath not found: ${uprojectPath}`
|
|
6446
|
+
};
|
|
6447
|
+
}
|
|
6448
|
+
const script = buildScriptPath(engineRoot);
|
|
6449
|
+
if (!fs__default.existsSync(script)) {
|
|
6450
|
+
return {
|
|
6451
|
+
ok: false,
|
|
6452
|
+
exitCode: null,
|
|
6453
|
+
logPath: "",
|
|
6454
|
+
issues: [],
|
|
6455
|
+
errors: 0,
|
|
6456
|
+
warnings: 0,
|
|
6457
|
+
truncated: false,
|
|
6458
|
+
errorMessage: `Missing Unreal build script: ${script}`
|
|
6459
|
+
};
|
|
6460
|
+
}
|
|
6461
|
+
const projectName = path.basename(uprojectPath).replace(/\.uproject$/i, "");
|
|
6462
|
+
const targetName = target === "Editor" ? `${projectName}Editor` : projectName;
|
|
6463
|
+
const platform = targetPlatform();
|
|
6464
|
+
const logDir = args.logDir ?? path.join(path.dirname(uprojectPath), "Saved", "Logs", "Flockbay");
|
|
6465
|
+
fs__default.mkdirSync(logDir, { recursive: true });
|
|
6466
|
+
const logPath = path.join(logDir, `flockbay_build_${Date.now()}.log`);
|
|
6467
|
+
const buildArgs = [
|
|
6468
|
+
targetName,
|
|
6469
|
+
platform,
|
|
6470
|
+
configuration,
|
|
6471
|
+
`-Project=${uprojectPath}`,
|
|
6472
|
+
"-WaitMutex",
|
|
6473
|
+
"-NoHotReload"
|
|
6474
|
+
];
|
|
6475
|
+
const logStream = fs__default.createWriteStream(logPath, { flags: "a" });
|
|
6476
|
+
logStream.write(`engineRoot: ${engineRoot}
|
|
6477
|
+
`);
|
|
6478
|
+
logStream.write(`uprojectPath: ${uprojectPath}
|
|
6479
|
+
`);
|
|
6480
|
+
logStream.write(`script: ${script}
|
|
6481
|
+
`);
|
|
6482
|
+
logStream.write(`args: ${JSON.stringify(buildArgs)}
|
|
6483
|
+
|
|
6484
|
+
`);
|
|
6485
|
+
const child = process.platform === "win32" ? spawn("cmd.exe", ["/c", script, ...buildArgs], { stdio: ["ignore", "pipe", "pipe"] }) : spawn(script, buildArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
6486
|
+
let combinedTail = "";
|
|
6487
|
+
const appendTail = (chunk) => {
|
|
6488
|
+
combinedTail += chunk.toString("utf8");
|
|
6489
|
+
if (combinedTail.length > 2e6) combinedTail = combinedTail.slice(-4e5);
|
|
6490
|
+
};
|
|
6491
|
+
child.stdout?.on("data", (chunk) => {
|
|
6492
|
+
logStream.write(chunk);
|
|
6493
|
+
appendTail(chunk);
|
|
6494
|
+
});
|
|
6495
|
+
child.stderr?.on("data", (chunk) => {
|
|
6496
|
+
logStream.write(chunk);
|
|
6497
|
+
appendTail(chunk);
|
|
6498
|
+
});
|
|
6499
|
+
const exitCode = await new Promise((resolve) => {
|
|
6500
|
+
const timer = setTimeout(() => {
|
|
6501
|
+
try {
|
|
6502
|
+
child.kill("SIGKILL");
|
|
6503
|
+
} catch {
|
|
6504
|
+
}
|
|
6505
|
+
resolve(null);
|
|
6506
|
+
}, timeoutMs);
|
|
6507
|
+
child.on("close", (code) => {
|
|
6508
|
+
clearTimeout(timer);
|
|
6509
|
+
resolve(code);
|
|
6510
|
+
});
|
|
6511
|
+
child.on("error", () => {
|
|
6512
|
+
clearTimeout(timer);
|
|
6513
|
+
resolve(null);
|
|
6514
|
+
});
|
|
6515
|
+
});
|
|
6516
|
+
logStream.end(`
|
|
6517
|
+
exitCode: ${exitCode}
|
|
6518
|
+
`);
|
|
6519
|
+
const parsed = parseBuildIssuesFromText(combinedTail, issuesLimit);
|
|
6520
|
+
const ok = exitCode === 0;
|
|
6521
|
+
if (!ok) {
|
|
6522
|
+
return {
|
|
6523
|
+
ok: false,
|
|
6524
|
+
exitCode,
|
|
6525
|
+
logPath,
|
|
6526
|
+
...parsed,
|
|
6527
|
+
errorMessage: exitCode === null ? `Build timed out after ${timeoutMs}ms. Log: ${logPath}` : `Build failed (exitCode=${exitCode}). Log: ${logPath}`
|
|
6528
|
+
};
|
|
6529
|
+
}
|
|
6530
|
+
return {
|
|
6531
|
+
ok: true,
|
|
6532
|
+
exitCode: exitCode ?? 0,
|
|
6533
|
+
logPath,
|
|
6534
|
+
...parsed
|
|
6535
|
+
};
|
|
6536
|
+
}
|
|
6537
|
+
|
|
6538
|
+
function stampForFilename() {
|
|
6539
|
+
const d = /* @__PURE__ */ new Date();
|
|
6540
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
6541
|
+
const yyyy = d.getFullYear();
|
|
6542
|
+
const mm = pad(d.getMonth() + 1);
|
|
6543
|
+
const dd = pad(d.getDate());
|
|
6544
|
+
const hh = pad(d.getHours());
|
|
6545
|
+
const mi = pad(d.getMinutes());
|
|
6546
|
+
const ss = pad(d.getSeconds());
|
|
6547
|
+
return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
|
|
6548
|
+
}
|
|
6549
|
+
async function runUnrealSmokeTest(args) {
|
|
6550
|
+
const startedAtMs = Date.now();
|
|
6551
|
+
const timeoutMs = Math.max(5e3, args.timeoutMs ?? 3e4);
|
|
6552
|
+
const stabilizeMs = Math.max(250, args.stabilizeMs ?? 1500);
|
|
6553
|
+
const stopIfPlaying = args.stopIfPlaying ?? true;
|
|
6554
|
+
const screenshotDir = path.join(path.dirname(args.uprojectPath), "Saved", "Screenshots", "Flockbay");
|
|
6555
|
+
await mkdir(screenshotDir, { recursive: true });
|
|
6556
|
+
const screenshotPath = path.join(screenshotDir, `Flockbay_smoke_test_${stampForFilename()}.png`);
|
|
6557
|
+
let started = false;
|
|
6558
|
+
let stopped = false;
|
|
6559
|
+
try {
|
|
6560
|
+
const status = await sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", params: {}, timeoutMs: Math.min(timeoutMs, 5e3) });
|
|
6561
|
+
const result = status?.result && typeof status.result === "object" ? status.result : status;
|
|
6562
|
+
const isPlaying = typeof result?.isPlaying === "boolean" ? result.isPlaying : null;
|
|
6563
|
+
if (isPlaying) {
|
|
6564
|
+
if (!stopIfPlaying) {
|
|
6565
|
+
return {
|
|
6566
|
+
ok: false,
|
|
6567
|
+
durationMs: Date.now() - startedAtMs,
|
|
6568
|
+
started: false,
|
|
6569
|
+
stopped: false,
|
|
6570
|
+
screenshotPath: null,
|
|
6571
|
+
errorMessage: "PIE is currently running. Stop PIE first or re-run with stopIfPlaying=true."
|
|
6572
|
+
};
|
|
6573
|
+
}
|
|
6574
|
+
await sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", params: {}, timeoutMs: Math.min(timeoutMs, 1e4) });
|
|
6575
|
+
}
|
|
6576
|
+
const playRes = await sendUnrealMcpTcpCommand({ type: "play_in_editor_windowed", params: {}, timeoutMs: Math.min(timeoutMs, 1e4) });
|
|
6577
|
+
const playResult = playRes?.result && typeof playRes.result === "object" ? playRes.result : playRes;
|
|
6578
|
+
started = typeof playResult?.started === "boolean" ? playResult.started : true;
|
|
6579
|
+
await new Promise((r) => setTimeout(r, stabilizeMs));
|
|
6580
|
+
await sendUnrealMcpTcpCommand({ type: "take_screenshot", params: { filepath: screenshotPath }, timeoutMs: Math.min(timeoutMs, 2e4) });
|
|
6581
|
+
await sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", params: {}, timeoutMs: Math.min(timeoutMs, 1e4) });
|
|
6582
|
+
stopped = true;
|
|
6583
|
+
return {
|
|
6584
|
+
ok: true,
|
|
6585
|
+
durationMs: Date.now() - startedAtMs,
|
|
6586
|
+
started,
|
|
6587
|
+
stopped,
|
|
6588
|
+
screenshotPath
|
|
6589
|
+
};
|
|
6590
|
+
} catch (err) {
|
|
6591
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6592
|
+
return {
|
|
6593
|
+
ok: false,
|
|
6594
|
+
durationMs: Date.now() - startedAtMs,
|
|
6595
|
+
started,
|
|
6596
|
+
stopped,
|
|
6597
|
+
screenshotPath: screenshotPath || null,
|
|
6598
|
+
errorMessage: message
|
|
6599
|
+
};
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6016
6603
|
function safeJsonParse(value) {
|
|
6017
6604
|
try {
|
|
6018
6605
|
return JSON.parse(value);
|
|
@@ -6229,12 +6816,172 @@ class ElicitationHub {
|
|
|
6229
6816
|
}
|
|
6230
6817
|
}
|
|
6231
6818
|
|
|
6819
|
+
function encodeBase64(buffer, variant = "base64") {
|
|
6820
|
+
if (variant === "base64url") {
|
|
6821
|
+
return encodeBase64Url(buffer);
|
|
6822
|
+
}
|
|
6823
|
+
return Buffer.from(buffer).toString("base64");
|
|
6824
|
+
}
|
|
6825
|
+
function encodeBase64Url(buffer) {
|
|
6826
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
6827
|
+
}
|
|
6828
|
+
function getRandomBytes(size) {
|
|
6829
|
+
return new Uint8Array(randomBytes(size));
|
|
6830
|
+
}
|
|
6831
|
+
function encryptWithDataKey(data, dataKey) {
|
|
6832
|
+
const nonce = getRandomBytes(12);
|
|
6833
|
+
const cipher = createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
6834
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
6835
|
+
const encrypted = Buffer.concat([
|
|
6836
|
+
cipher.update(plaintext),
|
|
6837
|
+
cipher.final()
|
|
6838
|
+
]);
|
|
6839
|
+
const authTag = cipher.getAuthTag();
|
|
6840
|
+
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
6841
|
+
bundle.set([0], 0);
|
|
6842
|
+
bundle.set(nonce, 1);
|
|
6843
|
+
bundle.set(new Uint8Array(encrypted), 13);
|
|
6844
|
+
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
6845
|
+
return bundle;
|
|
6846
|
+
}
|
|
6847
|
+
function encrypt(key, data) {
|
|
6848
|
+
return encryptWithDataKey(data, key);
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6232
6851
|
function deriveScreenshotViewIdFromFilename(name) {
|
|
6233
6852
|
const base = name.replace(/\.[^.]+$/, "");
|
|
6234
6853
|
const prefixed = /^Flockbay_(.+)$/.exec(base);
|
|
6235
6854
|
const raw = (prefixed ? prefixed[1] : base).trim();
|
|
6236
6855
|
return raw.replace(/_\d{8}-\d{6}$/, "").trim() || base;
|
|
6237
6856
|
}
|
|
6857
|
+
async function readJsonFile(filePath) {
|
|
6858
|
+
const raw = await readFile(filePath, "utf8");
|
|
6859
|
+
return JSON.parse(raw);
|
|
6860
|
+
}
|
|
6861
|
+
function parseMajorMinorOrNull(raw) {
|
|
6862
|
+
if (typeof raw !== "string") return null;
|
|
6863
|
+
const m = raw.trim().match(/(\d+)\.(\d+)/);
|
|
6864
|
+
if (!m) return null;
|
|
6865
|
+
const major = Number(m[1]);
|
|
6866
|
+
const minor = Number(m[2]);
|
|
6867
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
|
|
6868
|
+
return { major, minor };
|
|
6869
|
+
}
|
|
6870
|
+
async function readUprojectEngineAssociationOrNull(uprojectPath) {
|
|
6871
|
+
try {
|
|
6872
|
+
const json = await readJsonFile(uprojectPath);
|
|
6873
|
+
return parseMajorMinorOrNull(json?.EngineAssociation);
|
|
6874
|
+
} catch {
|
|
6875
|
+
return null;
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
function isValidEngineRoot(engineRoot) {
|
|
6879
|
+
if (!engineRoot) return false;
|
|
6880
|
+
if (!existsSync(engineRoot)) return false;
|
|
6881
|
+
if (!existsSync(path.join(engineRoot, "Engine"))) return false;
|
|
6882
|
+
const buildVersion = path.join(engineRoot, "Engine", "Build", "Build.version");
|
|
6883
|
+
if (existsSync(buildVersion)) return true;
|
|
6884
|
+
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") : "";
|
|
6885
|
+
if (editorCmd && existsSync(editorCmd)) return true;
|
|
6886
|
+
return false;
|
|
6887
|
+
}
|
|
6888
|
+
function engineRootCandidatesForVersion(platform, version) {
|
|
6889
|
+
const suffix = `${version.major}.${version.minor}`;
|
|
6890
|
+
if (platform === "darwin") {
|
|
6891
|
+
return [
|
|
6892
|
+
`/Users/Shared/Epic Games/UE_${suffix}`,
|
|
6893
|
+
`/Applications/Epic Games/UE_${suffix}`,
|
|
6894
|
+
`/Applications/Unreal Engine/UE_${suffix}`
|
|
6895
|
+
];
|
|
6896
|
+
}
|
|
6897
|
+
if (platform === "win32") {
|
|
6898
|
+
return [
|
|
6899
|
+
`C:\\\\Program Files\\\\Epic Games\\\\UE_${suffix}`
|
|
6900
|
+
];
|
|
6901
|
+
}
|
|
6902
|
+
return [];
|
|
6903
|
+
}
|
|
6904
|
+
async function readEngineRootMajorMinorOrNull(engineRoot) {
|
|
6905
|
+
const buildVersionPath = path.join(engineRoot, "Engine", "Build", "Build.version");
|
|
6906
|
+
try {
|
|
6907
|
+
if (existsSync(buildVersionPath)) {
|
|
6908
|
+
const json = await readJsonFile(buildVersionPath);
|
|
6909
|
+
const major2 = Number(json?.MajorVersion);
|
|
6910
|
+
const minor2 = Number(json?.MinorVersion);
|
|
6911
|
+
if (Number.isFinite(major2) && Number.isFinite(minor2)) return { major: major2, minor: minor2 };
|
|
6912
|
+
}
|
|
6913
|
+
} catch {
|
|
6914
|
+
}
|
|
6915
|
+
const m = engineRoot.match(/UE_(\d+)\.(\d+)/);
|
|
6916
|
+
if (!m) return null;
|
|
6917
|
+
const major = Number(m[1]);
|
|
6918
|
+
const minor = Number(m[2]);
|
|
6919
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
|
|
6920
|
+
return { major, minor };
|
|
6921
|
+
}
|
|
6922
|
+
async function assertEngineRootMatchesUprojectOrThrow(args) {
|
|
6923
|
+
const engineAssociation = await readUprojectEngineAssociationOrNull(args.uprojectPath);
|
|
6924
|
+
if (!engineAssociation) return;
|
|
6925
|
+
const engineRootVersion = await readEngineRootMajorMinorOrNull(args.engineRoot);
|
|
6926
|
+
if (!engineRootVersion) return;
|
|
6927
|
+
if (engineRootVersion.major !== engineAssociation.major || engineRootVersion.minor !== engineAssociation.minor) {
|
|
6928
|
+
const candidates = engineRootCandidatesForVersion(process.platform, engineAssociation);
|
|
6929
|
+
const suggestedEngineRoot = candidates.find((c) => isValidEngineRoot(c)) || candidates[0] || null;
|
|
6930
|
+
throw new Error(
|
|
6931
|
+
[
|
|
6932
|
+
`Engine mismatch: this project targets UE ${engineAssociation.major}.${engineAssociation.minor} (EngineAssociation),`,
|
|
6933
|
+
`but the provided engineRoot is UE ${engineRootVersion.major}.${engineRootVersion.minor}: ${args.engineRoot}`,
|
|
6934
|
+
"",
|
|
6935
|
+
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).`,
|
|
6936
|
+
...suggestedEngineRoot ? [`Example engineRoot: ${suggestedEngineRoot}`] : []
|
|
6937
|
+
].join("\n")
|
|
6938
|
+
);
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6941
|
+
async function resolveEngineRootForUproject(args) {
|
|
6942
|
+
const fromArg = (args.engineRootArg || "").trim();
|
|
6943
|
+
if (fromArg) {
|
|
6944
|
+
if (!isValidEngineRoot(fromArg)) {
|
|
6945
|
+
return { ok: false, errorMessage: `Invalid engineRoot (expected a UE install root containing Engine/\u2026): ${fromArg}`, suggestedEngineRoot: null };
|
|
6946
|
+
}
|
|
6947
|
+
try {
|
|
6948
|
+
await assertEngineRootMatchesUprojectOrThrow({ uprojectPath: args.uprojectPath, engineRoot: fromArg, source: "arg" });
|
|
6949
|
+
} catch (e) {
|
|
6950
|
+
return { ok: false, errorMessage: e instanceof Error ? e.message : String(e), suggestedEngineRoot: null };
|
|
6951
|
+
}
|
|
6952
|
+
return { ok: true, engineRoot: fromArg, source: "arg", engineAssociation: await readUprojectEngineAssociationOrNull(args.uprojectPath) };
|
|
6953
|
+
}
|
|
6954
|
+
const fromEnv = (process.env.UE_ENGINE_ROOT || process.env.ENGINE_ROOT || "").trim();
|
|
6955
|
+
if (fromEnv) {
|
|
6956
|
+
if (!isValidEngineRoot(fromEnv)) {
|
|
6957
|
+
return { ok: false, errorMessage: `ENGINE_ROOT is set but invalid (expected a UE install root containing Engine/\u2026): ${fromEnv}`, suggestedEngineRoot: null };
|
|
6958
|
+
}
|
|
6959
|
+
try {
|
|
6960
|
+
await assertEngineRootMatchesUprojectOrThrow({ uprojectPath: args.uprojectPath, engineRoot: fromEnv, source: "env" });
|
|
6961
|
+
} catch (e) {
|
|
6962
|
+
return { ok: false, errorMessage: e instanceof Error ? e.message : String(e), suggestedEngineRoot: null };
|
|
6963
|
+
}
|
|
6964
|
+
return { ok: true, engineRoot: fromEnv, source: "env", engineAssociation: await readUprojectEngineAssociationOrNull(args.uprojectPath) };
|
|
6965
|
+
}
|
|
6966
|
+
const engineAssociation = await readUprojectEngineAssociationOrNull(args.uprojectPath);
|
|
6967
|
+
if (!engineAssociation) {
|
|
6968
|
+
return {
|
|
6969
|
+
ok: false,
|
|
6970
|
+
errorMessage: "Missing `engineRoot` (and UE_ENGINE_ROOT / ENGINE_ROOT not set). Unable to infer the engine version from the .uproject EngineAssociation.",
|
|
6971
|
+
suggestedEngineRoot: null
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
const candidates = engineRootCandidatesForVersion(process.platform, engineAssociation);
|
|
6975
|
+
const valid = candidates.filter((c) => isValidEngineRoot(c));
|
|
6976
|
+
if (valid.length > 0) {
|
|
6977
|
+
return { ok: true, engineRoot: valid[0], source: "uproject", engineAssociation };
|
|
6978
|
+
}
|
|
6979
|
+
return {
|
|
6980
|
+
ok: false,
|
|
6981
|
+
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.`,
|
|
6982
|
+
suggestedEngineRoot: candidates[0] || null
|
|
6983
|
+
};
|
|
6984
|
+
}
|
|
6238
6985
|
async function runCmdAndCapture(args) {
|
|
6239
6986
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
6240
6987
|
const child = spawn(args.cmd, args.cmdArgs, { stdio: ["ignore", "pipe", "pipe"], cwd: args.cwd, env: args.env });
|
|
@@ -6280,7 +7027,9 @@ async function uploadScreenshotViewsForSession(args) {
|
|
|
6280
7027
|
const res = await fetch(endpoint, {
|
|
6281
7028
|
method: "POST",
|
|
6282
7029
|
headers: {
|
|
6283
|
-
|
|
7030
|
+
// This tool runs inside the CLI/daemon context, so we authenticate as the machine.
|
|
7031
|
+
// The backend accepts `Machine <token>` for machine-scoped auth.
|
|
7032
|
+
Authorization: `Machine ${args.token}`
|
|
6284
7033
|
},
|
|
6285
7034
|
body: form
|
|
6286
7035
|
});
|
|
@@ -6316,11 +7065,14 @@ async function startFlockbayServer(client, options) {
|
|
|
6316
7065
|
const handler = async (title) => {
|
|
6317
7066
|
logger.debug("[flockbayMCP] Changing title to:", title);
|
|
6318
7067
|
try {
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
7068
|
+
const trimmed = String(title || "").trim();
|
|
7069
|
+
if (!trimmed) {
|
|
7070
|
+
return { success: false, error: "Missing title" };
|
|
7071
|
+
}
|
|
7072
|
+
client.updateMetadata((current) => ({
|
|
7073
|
+
...current || {},
|
|
7074
|
+
name: trimmed
|
|
7075
|
+
}));
|
|
6324
7076
|
return { success: true };
|
|
6325
7077
|
} catch (error) {
|
|
6326
7078
|
return { success: false, error: String(error) };
|
|
@@ -6349,7 +7101,7 @@ async function startFlockbayServer(client, options) {
|
|
|
6349
7101
|
const res = await fetch(`${configuration.serverUrl.replace(/\/+$/, "")}${pathname}`, {
|
|
6350
7102
|
method: "POST",
|
|
6351
7103
|
headers: {
|
|
6352
|
-
Authorization: `
|
|
7104
|
+
Authorization: `Machine ${client.getAuthToken()}`,
|
|
6353
7105
|
"Content-Type": "application/json"
|
|
6354
7106
|
},
|
|
6355
7107
|
body: JSON.stringify(body ?? {})
|
|
@@ -6374,6 +7126,166 @@ async function startFlockbayServer(client, options) {
|
|
|
6374
7126
|
const meta = client?.metadata;
|
|
6375
7127
|
return String(meta?.path || "").trim() || process.cwd();
|
|
6376
7128
|
};
|
|
7129
|
+
const unrealIssueEmitter = new EventEmitter();
|
|
7130
|
+
const unrealEditorSupervisor = (() => {
|
|
7131
|
+
const state = {
|
|
7132
|
+
lastActivityAtMs: 0,
|
|
7133
|
+
lastReachableAtMs: 0,
|
|
7134
|
+
lastIssueAtMs: 0,
|
|
7135
|
+
lastIssueKey: "",
|
|
7136
|
+
launched: null
|
|
7137
|
+
};
|
|
7138
|
+
const emitIssue = (event) => {
|
|
7139
|
+
const key = `${event.kind}:${event.severity}:${event.message}`;
|
|
7140
|
+
const now = event.detectedAtMs;
|
|
7141
|
+
if (state.lastIssueKey === key && now - state.lastIssueAtMs < 15e3) return;
|
|
7142
|
+
state.lastIssueKey = key;
|
|
7143
|
+
state.lastIssueAtMs = now;
|
|
7144
|
+
unrealIssueEmitter.emit("issue", event);
|
|
7145
|
+
if (unrealIssueEmitter.listenerCount("issue") === 0) {
|
|
7146
|
+
try {
|
|
7147
|
+
const msg = event.kind === "process_exit" ? `Unreal Editor crashed. ${event.message}` : `Unreal Editor is not reachable. ${event.message}`;
|
|
7148
|
+
client.sendSessionEvent({ type: "message", message: msg });
|
|
7149
|
+
const socket = client?.socket;
|
|
7150
|
+
const keyBytes = client?.encryptionKey;
|
|
7151
|
+
if (socket && keyBytes) {
|
|
7152
|
+
const params = encodeBase64(encrypt(keyBytes, {}));
|
|
7153
|
+
socket.emit("rpc-call", { method: `${client.sessionId}:abort`, params }, () => {
|
|
7154
|
+
});
|
|
7155
|
+
}
|
|
7156
|
+
} catch (err) {
|
|
7157
|
+
logger.debug("[flockbayMCP] Failed to auto-abort after Unreal issue", err);
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7160
|
+
};
|
|
7161
|
+
const getUnrealEditorExe = (engineRoot) => {
|
|
7162
|
+
const root = engineRoot.trim().replace(/[\\/]+$/, "");
|
|
7163
|
+
if (process.platform === "darwin") {
|
|
7164
|
+
return path.join(root, "Engine", "Binaries", "Mac", "UnrealEditor.app", "Contents", "MacOS", "UnrealEditor");
|
|
7165
|
+
}
|
|
7166
|
+
if (process.platform === "win32") {
|
|
7167
|
+
return path.join(root, "Engine", "Binaries", "Win64", "UnrealEditor.exe");
|
|
7168
|
+
}
|
|
7169
|
+
if (process.platform === "linux") {
|
|
7170
|
+
return path.join(root, "Engine", "Binaries", "Linux", "UnrealEditor");
|
|
7171
|
+
}
|
|
7172
|
+
return "";
|
|
7173
|
+
};
|
|
7174
|
+
const runCommandCapture = async (cmd, args) => {
|
|
7175
|
+
return new Promise((resolve) => {
|
|
7176
|
+
const child = spawn(cmd, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
7177
|
+
let stdout = "";
|
|
7178
|
+
let stderr = "";
|
|
7179
|
+
child.stdout?.on("data", (c) => stdout += c.toString("utf8"));
|
|
7180
|
+
child.stderr?.on("data", (c) => stderr += c.toString("utf8"));
|
|
7181
|
+
child.on("error", (e) => resolve({ ok: false, stdout, stderr: e instanceof Error ? e.message : String(e) }));
|
|
7182
|
+
child.on("close", (code) => resolve({ ok: code === 0, stdout, stderr }));
|
|
7183
|
+
});
|
|
7184
|
+
};
|
|
7185
|
+
const isUnrealEditorProcessRunningBestEffort = async () => {
|
|
7186
|
+
if (process.platform === "win32") {
|
|
7187
|
+
const res2 = await runCommandCapture("tasklist", ["/FI", "IMAGENAME eq UnrealEditor.exe", "/FO", "CSV", "/NH"]);
|
|
7188
|
+
const out2 = `${res2.stdout}
|
|
7189
|
+
${res2.stderr}`.toLowerCase();
|
|
7190
|
+
return out2.includes("unrealeditor.exe");
|
|
7191
|
+
}
|
|
7192
|
+
const res = await runCommandCapture("ps", ["-A", "-o", "comm="]);
|
|
7193
|
+
const out = `${res.stdout}
|
|
7194
|
+
${res.stderr}`;
|
|
7195
|
+
return /\bUnrealEditor\b/.test(out) || /\bUE4Editor\b/.test(out) || /\bUE5Editor\b/.test(out);
|
|
7196
|
+
};
|
|
7197
|
+
const tick = async () => {
|
|
7198
|
+
const now = Date.now();
|
|
7199
|
+
const activeWindowMs = 10 * 6e4;
|
|
7200
|
+
const isActive = state.lastActivityAtMs > 0 && now - state.lastActivityAtMs < activeWindowMs || Boolean(state.launched);
|
|
7201
|
+
if (!isActive) return;
|
|
7202
|
+
try {
|
|
7203
|
+
await sendUnrealMcpTcpCommand({ type: "ping", params: {}, timeoutMs: 750 });
|
|
7204
|
+
state.lastReachableAtMs = now;
|
|
7205
|
+
return;
|
|
7206
|
+
} catch {
|
|
7207
|
+
}
|
|
7208
|
+
if (state.launched) return;
|
|
7209
|
+
if (!state.lastReachableAtMs || now - state.lastReachableAtMs > activeWindowMs) return;
|
|
7210
|
+
const running = await isUnrealEditorProcessRunningBestEffort().catch(() => true);
|
|
7211
|
+
if (running) return;
|
|
7212
|
+
emitIssue({
|
|
7213
|
+
kind: "unreachable",
|
|
7214
|
+
severity: "warning",
|
|
7215
|
+
detectedAtMs: now,
|
|
7216
|
+
message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.",
|
|
7217
|
+
detail: {
|
|
7218
|
+
lastReachableAtMs: state.lastReachableAtMs
|
|
7219
|
+
}
|
|
7220
|
+
});
|
|
7221
|
+
};
|
|
7222
|
+
const interval = setInterval(() => {
|
|
7223
|
+
void tick();
|
|
7224
|
+
}, 2e3);
|
|
7225
|
+
interval.unref();
|
|
7226
|
+
const noteUnrealActivity = () => {
|
|
7227
|
+
state.lastActivityAtMs = Date.now();
|
|
7228
|
+
};
|
|
7229
|
+
const noteUnrealReachable = () => {
|
|
7230
|
+
state.lastReachableAtMs = Date.now();
|
|
7231
|
+
};
|
|
7232
|
+
const launchEditor = async (params) => {
|
|
7233
|
+
const uprojectPath = params.uprojectPath;
|
|
7234
|
+
const engineRoot = params.engineRoot;
|
|
7235
|
+
const extraArgs = Array.isArray(params.extraArgs) ? params.extraArgs.filter((a) => typeof a === "string" && a.trim()) : [];
|
|
7236
|
+
const exe = getUnrealEditorExe(engineRoot);
|
|
7237
|
+
if (!exe) throw new Error(`Unsupported platform for Unreal Editor launch: ${process.platform}`);
|
|
7238
|
+
if (!existsSync(exe)) throw new Error(`Unreal Editor binary not found: ${exe}`);
|
|
7239
|
+
const child = spawn(exe, [uprojectPath, ...extraArgs], { detached: true, stdio: "ignore" });
|
|
7240
|
+
child.unref();
|
|
7241
|
+
const pid = typeof child.pid === "number" ? child.pid : 0;
|
|
7242
|
+
state.launched = {
|
|
7243
|
+
pid,
|
|
7244
|
+
uprojectPath,
|
|
7245
|
+
engineRoot,
|
|
7246
|
+
startedAtMs: Date.now()
|
|
7247
|
+
};
|
|
7248
|
+
child.on("exit", (code, signal) => {
|
|
7249
|
+
const now = Date.now();
|
|
7250
|
+
const exitCode = typeof code === "number" ? code : null;
|
|
7251
|
+
const sig = typeof signal === "string" ? signal : null;
|
|
7252
|
+
const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
|
|
7253
|
+
state.launched = null;
|
|
7254
|
+
if (!isCrash) return;
|
|
7255
|
+
emitIssue({
|
|
7256
|
+
kind: "process_exit",
|
|
7257
|
+
severity: "crash",
|
|
7258
|
+
detectedAtMs: now,
|
|
7259
|
+
message: `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
|
|
7260
|
+
detail: { exitCode, signal: sig, pid, uprojectPath }
|
|
7261
|
+
});
|
|
7262
|
+
});
|
|
7263
|
+
child.on("error", (err) => {
|
|
7264
|
+
const now = Date.now();
|
|
7265
|
+
state.launched = null;
|
|
7266
|
+
emitIssue({
|
|
7267
|
+
kind: "process_exit",
|
|
7268
|
+
severity: "crash",
|
|
7269
|
+
detectedAtMs: now,
|
|
7270
|
+
message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}`,
|
|
7271
|
+
detail: { pid, uprojectPath }
|
|
7272
|
+
});
|
|
7273
|
+
});
|
|
7274
|
+
return { pid, exePath: exe };
|
|
7275
|
+
};
|
|
7276
|
+
return {
|
|
7277
|
+
noteUnrealActivity,
|
|
7278
|
+
noteUnrealReachable,
|
|
7279
|
+
launchEditor,
|
|
7280
|
+
stop: () => {
|
|
7281
|
+
try {
|
|
7282
|
+
interval.unref();
|
|
7283
|
+
clearInterval(interval);
|
|
7284
|
+
} catch {
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
};
|
|
7288
|
+
})();
|
|
6377
7289
|
const runGit = async (cmdArgs, cwd, timeoutMs) => {
|
|
6378
7290
|
const res = await runCmdAndCapture({
|
|
6379
7291
|
cmd: "git",
|
|
@@ -7081,7 +7993,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7081
7993
|
const url = `${configuration.serverUrl.replace(/\/+$/, "")}/v1/sessions/${encodeURIComponent(client.sessionId)}/evidence-artifacts?limit=${limit}`;
|
|
7082
7994
|
const res = await fetch(url, {
|
|
7083
7995
|
method: "GET",
|
|
7084
|
-
headers: { Authorization: `
|
|
7996
|
+
headers: { Authorization: `Machine ${client.getAuthToken()}`, accept: "application/json" }
|
|
7085
7997
|
});
|
|
7086
7998
|
const data = await res.json().catch(() => null);
|
|
7087
7999
|
if (!res.ok) {
|
|
@@ -7106,7 +8018,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7106
8018
|
const url = `${configuration.serverUrl.replace(/\/+$/, "")}/v1/sessions/${encodeURIComponent(client.sessionId)}/evidence-artifacts/${encodeURIComponent(id)}`;
|
|
7107
8019
|
const res = await fetch(url, {
|
|
7108
8020
|
method: "GET",
|
|
7109
|
-
headers: { Authorization: `
|
|
8021
|
+
headers: { Authorization: `Machine ${client.getAuthToken()}`, accept: "application/json" }
|
|
7110
8022
|
});
|
|
7111
8023
|
const data = await res.json().catch(() => null);
|
|
7112
8024
|
if (!res.ok) {
|
|
@@ -7662,6 +8574,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7662
8574
|
}
|
|
7663
8575
|
return {
|
|
7664
8576
|
content,
|
|
8577
|
+
views: viewsPayload,
|
|
7665
8578
|
isError: false
|
|
7666
8579
|
};
|
|
7667
8580
|
} catch (error) {
|
|
@@ -7676,8 +8589,8 @@ ${String(st.stdout || "").trim()}`
|
|
|
7676
8589
|
})
|
|
7677
8590
|
);
|
|
7678
8591
|
mcp.registerTool("unreal_latest_screenshots", {
|
|
7679
|
-
title: "Latest Unreal Screenshots",
|
|
7680
|
-
description: "Fetch the latest PNG screenshots from `Saved/Screenshots/Flockbay/` and return a `{ views: [...] }` payload so the app can display them.",
|
|
8592
|
+
title: "Latest Unreal Screenshots (Validation)",
|
|
8593
|
+
description: "Fetch the latest PNG screenshots from `Saved/Screenshots/Flockbay/` (for validation) and return a `{ views: [...] }` payload so the app can display them.",
|
|
7681
8594
|
inputSchema: {
|
|
7682
8595
|
uprojectPath: z.string().describe("Absolute path to the .uproject file."),
|
|
7683
8596
|
limit: z.number().int().positive().optional().describe("Max number of screenshots to return (default 12)."),
|
|
@@ -7762,6 +8675,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7762
8675
|
{ type: "text", text: `Found ${views.length} screenshot${views.length === 1 ? "" : "s"} in: ${outDir}` },
|
|
7763
8676
|
{ type: "text", text: JSON.stringify({ views }, null, 2) }
|
|
7764
8677
|
],
|
|
8678
|
+
views,
|
|
7765
8679
|
isError: false
|
|
7766
8680
|
};
|
|
7767
8681
|
} catch (error) {
|
|
@@ -7797,6 +8711,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7797
8711
|
const name = typeof pluginInfo?.name === "string" ? pluginInfo.name : "UnrealMCP";
|
|
7798
8712
|
const versionName = typeof pluginInfo?.versionName === "string" ? pluginInfo.versionName : null;
|
|
7799
8713
|
const baseDir = typeof pluginInfo?.baseDir === "string" ? pluginInfo.baseDir : null;
|
|
8714
|
+
const schemaVersion = typeof pluginInfo?.schemaVersion === "number" ? pluginInfo.schemaVersion : null;
|
|
7800
8715
|
const commands = Array.isArray(pluginInfo?.commands) ? pluginInfo.commands.filter((c) => typeof c === "string") : [];
|
|
7801
8716
|
const head = [name, versionName ? `v${versionName}` : null].filter(Boolean).join(" ");
|
|
7802
8717
|
const cmdText = commands.length > 0 ? commands.slice(0, 60).join(", ") : "(no commands reported)";
|
|
@@ -7804,10 +8719,126 @@ ${String(st.stdout || "").trim()}`
|
|
|
7804
8719
|
return [
|
|
7805
8720
|
`UnrealMCP capabilities detected: ${head}`,
|
|
7806
8721
|
baseDir ? `Plugin path: ${baseDir}` : null,
|
|
8722
|
+
schemaVersion !== null ? `Schema version: ${schemaVersion}` : null,
|
|
7807
8723
|
`Supported commands: ${cmdText}${truncated}`,
|
|
7808
|
-
`Guidance: avoid guessing; prefer using
|
|
8724
|
+
`Guidance: avoid guessing; prefer using get_command_schema / list_capabilities when unsure about params.`
|
|
7809
8725
|
].filter(Boolean).join("\n");
|
|
7810
8726
|
}
|
|
8727
|
+
mcp.registerTool("unreal_editor_launch", {
|
|
8728
|
+
title: "Unreal Editor: Launch Project",
|
|
8729
|
+
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.",
|
|
8730
|
+
inputSchema: {
|
|
8731
|
+
uprojectPath: z.string().describe("Absolute path to the .uproject file."),
|
|
8732
|
+
engineRoot: z.string().optional().describe("Optional Unreal Engine install root. Defaults to ENGINE_ROOT / UE_ENGINE_ROOT, or inferred from EngineAssociation when possible."),
|
|
8733
|
+
extraArgs: z.array(z.string()).optional().describe("Additional UnrealEditor command-line args (advanced).")
|
|
8734
|
+
}
|
|
8735
|
+
}, async (args) => runWithMcpToolCard("unreal_editor_launch", args, async () => {
|
|
8736
|
+
const uprojectPath = typeof args?.uprojectPath === "string" ? String(args.uprojectPath).trim() : "";
|
|
8737
|
+
const engineRootArg = typeof args?.engineRoot === "string" ? String(args.engineRoot).trim() : "";
|
|
8738
|
+
const extraArgs = Array.isArray(args?.extraArgs) ? args.extraArgs : [];
|
|
8739
|
+
if (!uprojectPath || !uprojectPath.toLowerCase().endsWith(".uproject") || !path.isAbsolute(uprojectPath)) {
|
|
8740
|
+
return {
|
|
8741
|
+
content: [{ type: "text", text: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}` }],
|
|
8742
|
+
isError: true
|
|
8743
|
+
};
|
|
8744
|
+
}
|
|
8745
|
+
if (!existsSync(uprojectPath)) {
|
|
8746
|
+
return { content: [{ type: "text", text: `uprojectPath not found: ${uprojectPath}` }], isError: true };
|
|
8747
|
+
}
|
|
8748
|
+
const resolved = await resolveEngineRootForUproject({ uprojectPath, engineRootArg: engineRootArg || null });
|
|
8749
|
+
if (!resolved.ok) {
|
|
8750
|
+
return {
|
|
8751
|
+
content: [
|
|
8752
|
+
{ type: "text", text: `Unable to resolve engineRoot for this project.` },
|
|
8753
|
+
{ type: "text", text: resolved.errorMessage },
|
|
8754
|
+
...resolved.suggestedEngineRoot ? [{ type: "text", text: `Suggested engineRoot: ${resolved.suggestedEngineRoot}` }] : []
|
|
8755
|
+
],
|
|
8756
|
+
isError: true
|
|
8757
|
+
};
|
|
8758
|
+
}
|
|
8759
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8760
|
+
const launched = await unrealEditorSupervisor.launchEditor({
|
|
8761
|
+
uprojectPath,
|
|
8762
|
+
engineRoot: resolved.engineRoot,
|
|
8763
|
+
extraArgs
|
|
8764
|
+
});
|
|
8765
|
+
return {
|
|
8766
|
+
content: [
|
|
8767
|
+
{ type: "text", text: `Launched Unreal Editor for: ${uprojectPath}` },
|
|
8768
|
+
{
|
|
8769
|
+
type: "text",
|
|
8770
|
+
text: JSON.stringify(
|
|
8771
|
+
{
|
|
8772
|
+
engineRoot: resolved.engineRoot,
|
|
8773
|
+
engineRootSource: resolved.source,
|
|
8774
|
+
pid: launched.pid,
|
|
8775
|
+
exePath: launched.exePath
|
|
8776
|
+
},
|
|
8777
|
+
null,
|
|
8778
|
+
2
|
|
8779
|
+
)
|
|
8780
|
+
},
|
|
8781
|
+
{ type: "text", text: "Next: wait for the editor to finish loading, then use UnrealMCP tools (ping / get_plugin_info / play_in_editor / etc)." }
|
|
8782
|
+
],
|
|
8783
|
+
isError: false
|
|
8784
|
+
};
|
|
8785
|
+
}));
|
|
8786
|
+
mcp.registerTool("unreal_build_project", {
|
|
8787
|
+
title: "Unreal Build Project (UBT)",
|
|
8788
|
+
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.",
|
|
8789
|
+
inputSchema: {
|
|
8790
|
+
uprojectPath: z.string().describe("Absolute path to the .uproject file."),
|
|
8791
|
+
engineRoot: z.string().optional().describe("Optional Unreal Engine install root. Defaults to ENGINE_ROOT / UE_ENGINE_ROOT, or inferred from EngineAssociation when possible."),
|
|
8792
|
+
configuration: z.enum(["Debug", "DebugGame", "Development", "Shipping"]).optional().describe("Build configuration (default Development)."),
|
|
8793
|
+
target: z.enum(["Editor", "Game"]).optional().describe("Build target (default Editor)."),
|
|
8794
|
+
timeoutMs: z.number().int().positive().optional().describe("Timeout in ms (default 20m)."),
|
|
8795
|
+
issuesLimit: z.number().int().positive().optional().describe("Max issues to return (default 250, max 2000).")
|
|
8796
|
+
}
|
|
8797
|
+
}, async (args) => runWithMcpToolCard("unreal_build_project", args, async () => {
|
|
8798
|
+
const uprojectPath = typeof args?.uprojectPath === "string" ? String(args.uprojectPath).trim() : "";
|
|
8799
|
+
const engineRootArg = typeof args?.engineRoot === "string" ? String(args.engineRoot).trim() : "";
|
|
8800
|
+
const timeoutMs = typeof args?.timeoutMs === "number" ? args.timeoutMs : void 0;
|
|
8801
|
+
if (!uprojectPath || !uprojectPath.toLowerCase().endsWith(".uproject") || !path.isAbsolute(uprojectPath)) {
|
|
8802
|
+
return {
|
|
8803
|
+
content: [{ type: "text", text: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}` }],
|
|
8804
|
+
isError: true
|
|
8805
|
+
};
|
|
8806
|
+
}
|
|
8807
|
+
if (!existsSync(uprojectPath)) {
|
|
8808
|
+
return { content: [{ type: "text", text: `uprojectPath not found: ${uprojectPath}` }], isError: true };
|
|
8809
|
+
}
|
|
8810
|
+
const resolved = await resolveEngineRootForUproject({ uprojectPath, engineRootArg: engineRootArg || null });
|
|
8811
|
+
if (!resolved.ok) {
|
|
8812
|
+
return {
|
|
8813
|
+
content: [
|
|
8814
|
+
{ type: "text", text: `Unable to resolve engineRoot for this project.` },
|
|
8815
|
+
{ type: "text", text: resolved.errorMessage },
|
|
8816
|
+
...resolved.suggestedEngineRoot ? [{ type: "text", text: `Suggested engineRoot: ${resolved.suggestedEngineRoot}` }] : []
|
|
8817
|
+
],
|
|
8818
|
+
isError: true
|
|
8819
|
+
};
|
|
8820
|
+
}
|
|
8821
|
+
const configuration2 = args?.configuration ?? "Development";
|
|
8822
|
+
const target = args?.target ?? "Editor";
|
|
8823
|
+
const issuesLimit = typeof args?.issuesLimit === "number" ? args.issuesLimit : void 0;
|
|
8824
|
+
const result = await buildUnrealProject({
|
|
8825
|
+
uprojectPath,
|
|
8826
|
+
engineRoot: resolved.engineRoot,
|
|
8827
|
+
configuration: configuration2,
|
|
8828
|
+
target,
|
|
8829
|
+
timeoutMs,
|
|
8830
|
+
issuesLimit
|
|
8831
|
+
});
|
|
8832
|
+
const payload = { ...result, engineRoot: resolved.engineRoot, engineRootSource: resolved.source };
|
|
8833
|
+
return {
|
|
8834
|
+
content: [
|
|
8835
|
+
{ type: "text", text: result.ok ? "Build succeeded." : "Build failed." },
|
|
8836
|
+
...result.logPath ? [{ type: "text", text: `Log: ${result.logPath}` }] : [],
|
|
8837
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
8838
|
+
],
|
|
8839
|
+
isError: !result.ok
|
|
8840
|
+
};
|
|
8841
|
+
}));
|
|
7811
8842
|
mcp.registerTool("unreal_mcp_command", {
|
|
7812
8843
|
title: "Unreal Editor Command (UnrealMCP)",
|
|
7813
8844
|
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.",
|
|
@@ -7820,10 +8851,12 @@ ${String(st.stdout || "").trim()}`
|
|
|
7820
8851
|
const type = String(args.type || "").trim();
|
|
7821
8852
|
const params = args.params && typeof args.params === "object" ? args.params : {};
|
|
7822
8853
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
8854
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7823
8855
|
try {
|
|
7824
8856
|
const pluginInfoWasCached = Boolean(unrealMcpPluginInfoCache);
|
|
7825
8857
|
const pluginInfo = type !== "get_plugin_info" ? await getUnrealMcpPluginInfoBestEffort(timeoutMs) : null;
|
|
7826
8858
|
const response = await sendUnrealMcpTcpCommand({ type, params, timeoutMs });
|
|
8859
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7827
8860
|
return {
|
|
7828
8861
|
content: [
|
|
7829
8862
|
{ type: "text", text: `UnrealMCP command ok: ${type}` },
|
|
@@ -7846,16 +8879,928 @@ ${String(st.stdout || "").trim()}`
|
|
|
7846
8879
|
};
|
|
7847
8880
|
}
|
|
7848
8881
|
}));
|
|
7849
|
-
mcp.registerTool("
|
|
7850
|
-
title: "Unreal
|
|
7851
|
-
description: "
|
|
8882
|
+
mcp.registerTool("unreal_mcp_list_capabilities", {
|
|
8883
|
+
title: "Unreal MCP: Capabilities",
|
|
8884
|
+
description: "Query the running UnrealMCP plugin for its build info, supported command list, and capability flags. Use this to avoid guessing tool availability/behavior.",
|
|
8885
|
+
inputSchema: {
|
|
8886
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 2000).")
|
|
8887
|
+
}
|
|
8888
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_list_capabilities", args, async () => {
|
|
8889
|
+
const timeoutMs = args?.timeoutMs ?? 2e3;
|
|
8890
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8891
|
+
const response = await sendUnrealMcpTcpCommand({ type: "list_capabilities", params: {}, timeoutMs });
|
|
8892
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8893
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8894
|
+
}));
|
|
8895
|
+
mcp.registerTool("unreal_mcp_get_command_schema", {
|
|
8896
|
+
title: "Unreal MCP: Command Schema",
|
|
8897
|
+
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).",
|
|
7852
8898
|
inputSchema: {
|
|
8899
|
+
command: z.string().optional().describe('Command type to fetch schema for (e.g. "create_blueprint").'),
|
|
8900
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 2000).")
|
|
8901
|
+
}
|
|
8902
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_command_schema", args, async () => {
|
|
8903
|
+
const timeoutMs = args?.timeoutMs ?? 2e3;
|
|
8904
|
+
const command = typeof args?.command === "string" ? String(args.command).trim() : "";
|
|
8905
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8906
|
+
const response = await sendUnrealMcpTcpCommand({
|
|
8907
|
+
type: "get_command_schema",
|
|
8908
|
+
params: command ? { command } : {},
|
|
8909
|
+
timeoutMs
|
|
8910
|
+
});
|
|
8911
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8912
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8913
|
+
}));
|
|
8914
|
+
mcp.registerTool("unreal_mcp_search_blueprint_functions", {
|
|
8915
|
+
title: "Unreal MCP: Search Blueprint Functions",
|
|
8916
|
+
description: "Search for Blueprint-callable functions and return candidates (owner class path + signature hints) to use with add_blueprint_function_node.",
|
|
8917
|
+
inputSchema: {
|
|
8918
|
+
query: z.string().describe('Substring to search for (e.g. "MakeVector", "SetRelativeLocation").'),
|
|
8919
|
+
target: z.string().optional().describe('Optional target class constraint (e.g. "KismetMathLibrary").'),
|
|
8920
|
+
limit: z.number().int().positive().optional().describe("Max results (default 25)."),
|
|
7853
8921
|
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
7854
8922
|
}
|
|
7855
|
-
}, async (args) => runWithMcpToolCard("
|
|
8923
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_search_blueprint_functions", args, async () => {
|
|
8924
|
+
const timeoutMs = args?.timeoutMs ?? 5e3;
|
|
8925
|
+
const query = String(args.query || "").trim();
|
|
8926
|
+
const target = typeof args?.target === "string" ? String(args.target).trim() : "";
|
|
8927
|
+
const limit = typeof args?.limit === "number" ? args.limit : void 0;
|
|
8928
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8929
|
+
const params = { query };
|
|
8930
|
+
if (target) params.target = target;
|
|
8931
|
+
if (typeof limit === "number") params.limit = limit;
|
|
8932
|
+
const response = await sendUnrealMcpTcpCommand({ type: "search_blueprint_functions", params, timeoutMs });
|
|
8933
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8934
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8935
|
+
}));
|
|
8936
|
+
mcp.registerTool("unreal_mcp_resolve_blueprint_function", {
|
|
8937
|
+
title: "Unreal MCP: Resolve Blueprint Function",
|
|
8938
|
+
description: 'Resolve a Blueprint-callable function to an explicit owner class path usable with add_blueprint_function_node (reduces "Function not found" guesswork).',
|
|
8939
|
+
inputSchema: {
|
|
8940
|
+
functionName: z.string().describe('Function name to resolve (e.g. "MakeVector").'),
|
|
8941
|
+
target: z.string().optional().describe("Optional target class constraint."),
|
|
8942
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
8943
|
+
}
|
|
8944
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_resolve_blueprint_function", args, async () => {
|
|
8945
|
+
const timeoutMs = args?.timeoutMs ?? 5e3;
|
|
8946
|
+
const functionName = String(args.functionName || "").trim();
|
|
8947
|
+
const target = typeof args?.target === "string" ? String(args.target).trim() : "";
|
|
8948
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8949
|
+
const params = { function_name: functionName };
|
|
8950
|
+
if (target) params.target = target;
|
|
8951
|
+
const response = await sendUnrealMcpTcpCommand({ type: "resolve_blueprint_function", params, timeoutMs });
|
|
8952
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8953
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false };
|
|
8954
|
+
}));
|
|
8955
|
+
mcp.registerTool("unreal_mcp_search_assets", {
|
|
8956
|
+
title: "Unreal Search Assets (UnrealMCP)",
|
|
8957
|
+
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.",
|
|
8958
|
+
inputSchema: {
|
|
8959
|
+
query: z.string().optional().describe("Fuzzy search string (name/path)."),
|
|
8960
|
+
class: z.union([z.string(), z.array(z.string())]).optional().describe('Optional asset class filter (e.g. "StaticMesh", "Blueprint", "Material").'),
|
|
8961
|
+
root: z.string().optional().describe('Root path to search (default "/Game/").'),
|
|
8962
|
+
limit: z.number().int().positive().optional().describe("Max results (default 25, max 200)."),
|
|
8963
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
8964
|
+
}
|
|
8965
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_search_assets", args, async () => {
|
|
8966
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
8967
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8968
|
+
try {
|
|
8969
|
+
const params = {};
|
|
8970
|
+
if (typeof args.query === "string" && args.query.trim()) params.query = args.query.trim();
|
|
8971
|
+
if (typeof args.root === "string" && args.root.trim()) params.root = args.root.trim();
|
|
8972
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
8973
|
+
if (typeof args.class === "string" && args.class.trim()) params.class = args.class.trim();
|
|
8974
|
+
if (Array.isArray(args.class)) params.class = args.class;
|
|
8975
|
+
const response = await sendUnrealMcpTcpCommand({ type: "search_assets", params, timeoutMs });
|
|
8976
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8977
|
+
return {
|
|
8978
|
+
content: [
|
|
8979
|
+
{ type: "text", text: "Unreal assets search ok." },
|
|
8980
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
8981
|
+
],
|
|
8982
|
+
isError: false
|
|
8983
|
+
};
|
|
8984
|
+
} catch (error) {
|
|
8985
|
+
return {
|
|
8986
|
+
content: [
|
|
8987
|
+
{ type: "text", text: "Unreal assets search failed." },
|
|
8988
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
8989
|
+
],
|
|
8990
|
+
isError: true
|
|
8991
|
+
};
|
|
8992
|
+
}
|
|
8993
|
+
}));
|
|
8994
|
+
mcp.registerTool("unreal_mcp_get_asset_info", {
|
|
8995
|
+
title: "Unreal Asset Info (UnrealMCP)",
|
|
8996
|
+
description: "Get metadata for a single Unreal asset (class/path + optional dependencies) via the UnrealMCP engine plugin.",
|
|
8997
|
+
inputSchema: {
|
|
8998
|
+
objectPath: z.string().describe('Unreal asset object path (e.g. "/Game/MyPack/MyMesh.MyMesh").'),
|
|
8999
|
+
includeDependencies: z.boolean().optional().describe("Include package dependencies (default false)."),
|
|
9000
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9001
|
+
}
|
|
9002
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_asset_info", args, async () => {
|
|
9003
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9004
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9005
|
+
try {
|
|
9006
|
+
const params = { objectPath: String(args.objectPath || "").trim() };
|
|
9007
|
+
if (typeof args.includeDependencies === "boolean") params.includeDependencies = args.includeDependencies;
|
|
9008
|
+
const response = await sendUnrealMcpTcpCommand({ type: "get_asset_info", params, timeoutMs });
|
|
9009
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9010
|
+
return {
|
|
9011
|
+
content: [
|
|
9012
|
+
{ type: "text", text: "Unreal asset info ok." },
|
|
9013
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9014
|
+
],
|
|
9015
|
+
isError: false
|
|
9016
|
+
};
|
|
9017
|
+
} catch (error) {
|
|
9018
|
+
return {
|
|
9019
|
+
content: [
|
|
9020
|
+
{ type: "text", text: "Unreal asset info failed." },
|
|
9021
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9022
|
+
],
|
|
9023
|
+
isError: true
|
|
9024
|
+
};
|
|
9025
|
+
}
|
|
9026
|
+
}));
|
|
9027
|
+
mcp.registerTool("unreal_mcp_list_asset_packs", {
|
|
9028
|
+
title: "Unreal List Asset Packs (UnrealMCP)",
|
|
9029
|
+
description: 'List top-level /Game content folders (best-effort "asset packs") via the UnrealMCP engine plugin.',
|
|
9030
|
+
inputSchema: {
|
|
9031
|
+
limit: z.number().int().positive().optional().describe("Max number of packs (default 200)."),
|
|
9032
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9033
|
+
}
|
|
9034
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_list_asset_packs", args, async () => {
|
|
9035
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9036
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9037
|
+
try {
|
|
9038
|
+
const params = {};
|
|
9039
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
9040
|
+
const response = await sendUnrealMcpTcpCommand({ type: "list_asset_packs", params, timeoutMs });
|
|
9041
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9042
|
+
return {
|
|
9043
|
+
content: [
|
|
9044
|
+
{ type: "text", text: "Unreal asset packs list ok." },
|
|
9045
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9046
|
+
],
|
|
9047
|
+
isError: false
|
|
9048
|
+
};
|
|
9049
|
+
} catch (error) {
|
|
9050
|
+
return {
|
|
9051
|
+
content: [
|
|
9052
|
+
{ type: "text", text: "Unreal asset packs list failed." },
|
|
9053
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9054
|
+
],
|
|
9055
|
+
isError: true
|
|
9056
|
+
};
|
|
9057
|
+
}
|
|
9058
|
+
}));
|
|
9059
|
+
mcp.registerTool("unreal_mcp_place_asset", {
|
|
9060
|
+
title: "Unreal Place Asset (UnrealMCP)",
|
|
9061
|
+
description: "Place an asset into the current Unreal level by object path (StaticMesh/Blueprint). Fails if PIE is running to avoid crashes.",
|
|
9062
|
+
inputSchema: {
|
|
9063
|
+
objectPath: z.string().describe('Unreal asset object path (e.g. "/Game/MyPack/MyMesh.MyMesh").'),
|
|
9064
|
+
name: z.string().optional().describe("Optional actor name (default: derived from asset name)."),
|
|
9065
|
+
location: z.array(z.number()).length(3).optional().describe("Spawn location [X,Y,Z] in cm."),
|
|
9066
|
+
rotation: z.array(z.number()).length(3).optional().describe("Spawn rotation [Pitch,Yaw,Roll] in degrees."),
|
|
9067
|
+
scale: z.array(z.number()).length(3).optional().describe("Spawn scale [X,Y,Z]."),
|
|
9068
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9069
|
+
}
|
|
9070
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_place_asset", args, async () => {
|
|
9071
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9072
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9073
|
+
try {
|
|
9074
|
+
const params = { objectPath: String(args.objectPath || "").trim() };
|
|
9075
|
+
if (typeof args.name === "string" && args.name.trim()) params.name = args.name.trim();
|
|
9076
|
+
if (Array.isArray(args.location)) params.location = args.location;
|
|
9077
|
+
if (Array.isArray(args.rotation)) params.rotation = args.rotation;
|
|
9078
|
+
if (Array.isArray(args.scale)) params.scale = args.scale;
|
|
9079
|
+
const response = await sendUnrealMcpTcpCommand({ type: "place_asset", params, timeoutMs });
|
|
9080
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9081
|
+
return {
|
|
9082
|
+
content: [
|
|
9083
|
+
{ type: "text", text: "Placed asset into Unreal level." },
|
|
9084
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9085
|
+
],
|
|
9086
|
+
isError: false
|
|
9087
|
+
};
|
|
9088
|
+
} catch (error) {
|
|
9089
|
+
return {
|
|
9090
|
+
content: [
|
|
9091
|
+
{ type: "text", text: "Failed to place asset into Unreal level." },
|
|
9092
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9093
|
+
{ type: "text", text: "Tip: use unreal_mcp_get_play_in_editor_status and stop PIE before placing assets." }
|
|
9094
|
+
],
|
|
9095
|
+
isError: true
|
|
9096
|
+
};
|
|
9097
|
+
}
|
|
9098
|
+
}));
|
|
9099
|
+
mcp.registerTool("unreal_mcp_get_editor_context", {
|
|
9100
|
+
title: "Unreal Editor Context (UnrealMCP)",
|
|
9101
|
+
description: "Return a compact snapshot of editor context: map, selection, and active viewport camera (if any).",
|
|
9102
|
+
inputSchema: {
|
|
9103
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9104
|
+
}
|
|
9105
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_editor_context", args, async () => {
|
|
9106
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9107
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9108
|
+
try {
|
|
9109
|
+
const response = await sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs });
|
|
9110
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9111
|
+
return {
|
|
9112
|
+
content: [
|
|
9113
|
+
{ type: "text", text: "Fetched Unreal Editor context." },
|
|
9114
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9115
|
+
],
|
|
9116
|
+
isError: false
|
|
9117
|
+
};
|
|
9118
|
+
} catch (error) {
|
|
9119
|
+
return {
|
|
9120
|
+
content: [
|
|
9121
|
+
{ type: "text", text: "Failed to fetch Unreal Editor context." },
|
|
9122
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9123
|
+
],
|
|
9124
|
+
isError: true
|
|
9125
|
+
};
|
|
9126
|
+
}
|
|
9127
|
+
}));
|
|
9128
|
+
mcp.registerTool("unreal_mcp_get_player_context", {
|
|
9129
|
+
title: "Unreal Player Context (PIE)",
|
|
9130
|
+
description: "Return a compact snapshot of runtime player context (PIE): isPlaying, pawn transform, and camera transform.",
|
|
9131
|
+
inputSchema: {
|
|
9132
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9133
|
+
}
|
|
9134
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_player_context", args, async () => {
|
|
9135
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9136
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9137
|
+
try {
|
|
9138
|
+
const response = await sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs });
|
|
9139
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9140
|
+
return {
|
|
9141
|
+
content: [
|
|
9142
|
+
{ type: "text", text: "Fetched player context." },
|
|
9143
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9144
|
+
],
|
|
9145
|
+
isError: false
|
|
9146
|
+
};
|
|
9147
|
+
} catch (error) {
|
|
9148
|
+
return {
|
|
9149
|
+
content: [
|
|
9150
|
+
{ type: "text", text: "Failed to fetch player context." },
|
|
9151
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9152
|
+
],
|
|
9153
|
+
isError: true
|
|
9154
|
+
};
|
|
9155
|
+
}
|
|
9156
|
+
}));
|
|
9157
|
+
mcp.registerTool("unreal_mcp_raycast_from_camera", {
|
|
9158
|
+
title: "Unreal Raycast From Camera (Editor/PIE)",
|
|
9159
|
+
description: "Raycast from the editor viewport camera or the PIE player camera to turn \u201Cthat surface\u201D into an exact hit location and normal.",
|
|
9160
|
+
inputSchema: {
|
|
9161
|
+
source: z.enum(["editor", "pie"]).optional().describe("Camera source (default editor)."),
|
|
9162
|
+
maxDistance: z.number().positive().optional().describe("Max distance in cm (default 10000)."),
|
|
9163
|
+
ignoreActors: z.array(z.string()).optional().describe("Actor names/labels to ignore (best-effort)."),
|
|
9164
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9165
|
+
}
|
|
9166
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_raycast_from_camera", args, async () => {
|
|
9167
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9168
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9169
|
+
try {
|
|
9170
|
+
const params = {};
|
|
9171
|
+
if (args.source) params.source = args.source;
|
|
9172
|
+
if (typeof args.maxDistance === "number" && Number.isFinite(args.maxDistance)) params.maxDistance = args.maxDistance;
|
|
9173
|
+
if (Array.isArray(args.ignoreActors)) params.ignoreActors = args.ignoreActors;
|
|
9174
|
+
const response = await sendUnrealMcpTcpCommand({ type: "raycast_from_camera", params, timeoutMs });
|
|
9175
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9176
|
+
return {
|
|
9177
|
+
content: [
|
|
9178
|
+
{ type: "text", text: "Raycast completed." },
|
|
9179
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9180
|
+
],
|
|
9181
|
+
isError: false
|
|
9182
|
+
};
|
|
9183
|
+
} catch (error) {
|
|
9184
|
+
return {
|
|
9185
|
+
content: [
|
|
9186
|
+
{ type: "text", text: "Raycast failed." },
|
|
9187
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9188
|
+
],
|
|
9189
|
+
isError: true
|
|
9190
|
+
};
|
|
9191
|
+
}
|
|
9192
|
+
}));
|
|
9193
|
+
mcp.registerTool("unreal_mcp_raycast_down", {
|
|
9194
|
+
title: "Unreal Raycast Down (Drop To Ground)",
|
|
9195
|
+
description: "Raycast straight down from a given startLocation to find ground/surface beneath. Useful for drop-to-ground placement.",
|
|
9196
|
+
inputSchema: {
|
|
9197
|
+
source: z.enum(["editor", "pie"]).optional().describe("World source (default editor)."),
|
|
9198
|
+
startLocation: z.array(z.number()).length(3).describe("Start location [X,Y,Z] in cm."),
|
|
9199
|
+
maxDistance: z.number().positive().optional().describe("Max distance in cm (default 100000)."),
|
|
9200
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9201
|
+
}
|
|
9202
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_raycast_down", args, async () => {
|
|
9203
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9204
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9205
|
+
try {
|
|
9206
|
+
const params = { startLocation: args.startLocation };
|
|
9207
|
+
if (args.source) params.source = args.source;
|
|
9208
|
+
if (typeof args.maxDistance === "number" && Number.isFinite(args.maxDistance)) params.maxDistance = args.maxDistance;
|
|
9209
|
+
const response = await sendUnrealMcpTcpCommand({ type: "raycast_down", params, timeoutMs });
|
|
9210
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9211
|
+
return {
|
|
9212
|
+
content: [
|
|
9213
|
+
{ type: "text", text: "Raycast-down completed." },
|
|
9214
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9215
|
+
],
|
|
9216
|
+
isError: false
|
|
9217
|
+
};
|
|
9218
|
+
} catch (error) {
|
|
9219
|
+
return {
|
|
9220
|
+
content: [
|
|
9221
|
+
{ type: "text", text: "Raycast-down failed." },
|
|
9222
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9223
|
+
],
|
|
9224
|
+
isError: true
|
|
9225
|
+
};
|
|
9226
|
+
}
|
|
9227
|
+
}));
|
|
9228
|
+
mcp.registerTool("unreal_mcp_get_actor_transform", {
|
|
9229
|
+
title: "Unreal Actor Transform (UnrealMCP)",
|
|
9230
|
+
description: "Get a single actor transform (location/rotation/scale) by actor name or label (best-effort).",
|
|
9231
|
+
inputSchema: {
|
|
9232
|
+
name: z.string().describe("Actor name or label."),
|
|
9233
|
+
source: z.enum(["editor", "pie"]).optional().describe("World source (default editor)."),
|
|
9234
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9235
|
+
}
|
|
9236
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_actor_transform", args, async () => {
|
|
9237
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9238
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9239
|
+
try {
|
|
9240
|
+
const params = { name: String(args.name || "").trim() };
|
|
9241
|
+
if (args.source) params.source = args.source;
|
|
9242
|
+
const response = await sendUnrealMcpTcpCommand({ type: "get_actor_transform", params, timeoutMs });
|
|
9243
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9244
|
+
return {
|
|
9245
|
+
content: [
|
|
9246
|
+
{ type: "text", text: "Fetched actor transform." },
|
|
9247
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9248
|
+
],
|
|
9249
|
+
isError: false
|
|
9250
|
+
};
|
|
9251
|
+
} catch (error) {
|
|
9252
|
+
return {
|
|
9253
|
+
content: [
|
|
9254
|
+
{ type: "text", text: "Failed to fetch actor transform." },
|
|
9255
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9256
|
+
],
|
|
9257
|
+
isError: true
|
|
9258
|
+
};
|
|
9259
|
+
}
|
|
9260
|
+
}));
|
|
9261
|
+
mcp.registerTool("unreal_mcp_get_actor_bounds", {
|
|
9262
|
+
title: "Unreal Actor Bounds (UnrealMCP)",
|
|
9263
|
+
description: "Get a single actor bounds (origin/extent + min/max) by actor name or label (best-effort).",
|
|
9264
|
+
inputSchema: {
|
|
9265
|
+
name: z.string().describe("Actor name or label."),
|
|
9266
|
+
source: z.enum(["editor", "pie"]).optional().describe("World source (default editor)."),
|
|
9267
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9268
|
+
}
|
|
9269
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_actor_bounds", args, async () => {
|
|
9270
|
+
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9271
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9272
|
+
try {
|
|
9273
|
+
const params = { name: String(args.name || "").trim() };
|
|
9274
|
+
if (args.source) params.source = args.source;
|
|
9275
|
+
const response = await sendUnrealMcpTcpCommand({ type: "get_actor_bounds", params, timeoutMs });
|
|
9276
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9277
|
+
return {
|
|
9278
|
+
content: [
|
|
9279
|
+
{ type: "text", text: "Fetched actor bounds." },
|
|
9280
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9281
|
+
],
|
|
9282
|
+
isError: false
|
|
9283
|
+
};
|
|
9284
|
+
} catch (error) {
|
|
9285
|
+
return {
|
|
9286
|
+
content: [
|
|
9287
|
+
{ type: "text", text: "Failed to fetch actor bounds." },
|
|
9288
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9289
|
+
],
|
|
9290
|
+
isError: true
|
|
9291
|
+
};
|
|
9292
|
+
}
|
|
9293
|
+
}));
|
|
9294
|
+
mcp.registerTool("unreal_mcp_create_landscape", {
|
|
9295
|
+
title: "Unreal Create Landscape (UnrealMCP)",
|
|
9296
|
+
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.",
|
|
9297
|
+
inputSchema: {
|
|
9298
|
+
name: z.string().describe("New landscape actor name (must be unique)."),
|
|
9299
|
+
componentCountX: z.number().int().positive().optional().describe("Number of components in X (default 8)."),
|
|
9300
|
+
componentCountY: z.number().int().positive().optional().describe("Number of components in Y (default 8)."),
|
|
9301
|
+
sectionsPerComponent: z.number().int().optional().describe("Sections per component (1 or 2, default 1)."),
|
|
9302
|
+
quadsPerSection: z.number().int().positive().optional().describe("Quads per section (default 63)."),
|
|
9303
|
+
location: z.array(z.number()).length(3).optional().describe("Center location [X,Y,Z] in cm (default [0,0,0])."),
|
|
9304
|
+
rotation: z.array(z.number()).length(3).optional().describe("Rotation [Pitch,Yaw,Roll] in degrees (default [0,0,0])."),
|
|
9305
|
+
scale: z.array(z.number()).length(3).optional().describe("Scale [X,Y,Z] (default [100,100,100])."),
|
|
9306
|
+
materialPath: z.string().optional().describe('Optional landscape material object path (e.g. "/Game/\u2026/M_Landscape.M_Landscape").'),
|
|
9307
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 30000).")
|
|
9308
|
+
}
|
|
9309
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_create_landscape", args, async () => {
|
|
9310
|
+
const timeoutMs = args?.timeoutMs ?? 3e4;
|
|
9311
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9312
|
+
try {
|
|
9313
|
+
const params = {
|
|
9314
|
+
name: String(args?.name || "").trim()
|
|
9315
|
+
};
|
|
9316
|
+
if (!params.name) throw new Error("name is required.");
|
|
9317
|
+
if (typeof args?.componentCountX === "number") params.componentCountX = args.componentCountX;
|
|
9318
|
+
if (typeof args?.componentCountY === "number") params.componentCountY = args.componentCountY;
|
|
9319
|
+
if (typeof args?.sectionsPerComponent === "number") params.sectionsPerComponent = args.sectionsPerComponent;
|
|
9320
|
+
if (typeof args?.quadsPerSection === "number") params.quadsPerSection = args.quadsPerSection;
|
|
9321
|
+
if (Array.isArray(args?.location)) params.location = args.location;
|
|
9322
|
+
if (Array.isArray(args?.rotation)) params.rotation = args.rotation;
|
|
9323
|
+
if (Array.isArray(args?.scale)) params.scale = args.scale;
|
|
9324
|
+
if (typeof args?.materialPath === "string" && args.materialPath.trim()) {
|
|
9325
|
+
params.materialPath = args.materialPath.trim();
|
|
9326
|
+
}
|
|
9327
|
+
const response = await sendUnrealMcpTcpCommand({ type: "create_landscape", params, timeoutMs });
|
|
9328
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9329
|
+
return {
|
|
9330
|
+
content: [
|
|
9331
|
+
{ type: "text", text: "Landscape created." },
|
|
9332
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9333
|
+
],
|
|
9334
|
+
isError: false
|
|
9335
|
+
};
|
|
9336
|
+
} catch (error) {
|
|
9337
|
+
return {
|
|
9338
|
+
content: [
|
|
9339
|
+
{ type: "text", text: "Failed to create landscape." },
|
|
9340
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9341
|
+
{ type: "text", text: "Tip: stop PIE before creating a landscape." }
|
|
9342
|
+
],
|
|
9343
|
+
isError: true
|
|
9344
|
+
};
|
|
9345
|
+
}
|
|
9346
|
+
}));
|
|
9347
|
+
mcp.registerTool("unreal_mcp_map_check", {
|
|
9348
|
+
title: "Unreal Map Check (UnrealMCP)",
|
|
9349
|
+
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.",
|
|
9350
|
+
inputSchema: {
|
|
9351
|
+
includeWarnings: z.boolean().optional().describe("Include warnings in the issues list (default true)."),
|
|
9352
|
+
limit: z.number().int().positive().optional().describe("Max number of issues to return (default 200, max 1000)."),
|
|
9353
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 15000).")
|
|
9354
|
+
}
|
|
9355
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_map_check", args, async () => {
|
|
9356
|
+
const timeoutMs = args.timeoutMs ?? 15e3;
|
|
9357
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9358
|
+
try {
|
|
9359
|
+
const params = {};
|
|
9360
|
+
if (typeof args.includeWarnings === "boolean") params.includeWarnings = args.includeWarnings;
|
|
9361
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
9362
|
+
const response = await sendUnrealMcpTcpCommand({ type: "map_check", params, timeoutMs });
|
|
9363
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9364
|
+
return {
|
|
9365
|
+
content: [
|
|
9366
|
+
{ type: "text", text: "Map Check completed." },
|
|
9367
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9368
|
+
],
|
|
9369
|
+
isError: false
|
|
9370
|
+
};
|
|
9371
|
+
} catch (error) {
|
|
9372
|
+
return {
|
|
9373
|
+
content: [
|
|
9374
|
+
{ type: "text", text: "Map Check failed." },
|
|
9375
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9376
|
+
{ type: "text", text: "Tip: stop PIE before running map_check." }
|
|
9377
|
+
],
|
|
9378
|
+
isError: true
|
|
9379
|
+
};
|
|
9380
|
+
}
|
|
9381
|
+
}));
|
|
9382
|
+
mcp.registerTool("unreal_mcp_compile_blueprints_all", {
|
|
9383
|
+
title: "Unreal Compile All Blueprints (UnrealMCP)",
|
|
9384
|
+
description: "Compile all Blueprints (optionally under specific content paths) and return a structured list of compile messages. Fails if PIE is running.",
|
|
9385
|
+
inputSchema: {
|
|
9386
|
+
paths: z.array(z.string()).optional().describe('Root content paths to search (default ["/Game"]).'),
|
|
9387
|
+
includeWarnings: z.boolean().optional().describe("Include warnings in the per-blueprint messages list (default true)."),
|
|
9388
|
+
limit: z.number().int().positive().optional().describe("Max number of blueprints to compile (default 500, max 10000)."),
|
|
9389
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 60000).")
|
|
9390
|
+
}
|
|
9391
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_compile_blueprints_all", args, async () => {
|
|
9392
|
+
const timeoutMs = args.timeoutMs ?? 6e4;
|
|
9393
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9394
|
+
try {
|
|
9395
|
+
const params = {};
|
|
9396
|
+
if (Array.isArray(args.paths)) params.paths = args.paths;
|
|
9397
|
+
if (typeof args.includeWarnings === "boolean") params.includeWarnings = args.includeWarnings;
|
|
9398
|
+
if (typeof args.limit === "number" && Number.isFinite(args.limit)) params.limit = args.limit;
|
|
9399
|
+
const response = await sendUnrealMcpTcpCommand({ type: "compile_blueprints_all", params, timeoutMs });
|
|
9400
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9401
|
+
return {
|
|
9402
|
+
content: [
|
|
9403
|
+
{ type: "text", text: "Blueprint compile completed." },
|
|
9404
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9405
|
+
],
|
|
9406
|
+
isError: false
|
|
9407
|
+
};
|
|
9408
|
+
} catch (error) {
|
|
9409
|
+
return {
|
|
9410
|
+
content: [
|
|
9411
|
+
{ type: "text", text: "Blueprint compile failed." },
|
|
9412
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9413
|
+
{ type: "text", text: "Tip: stop PIE before compiling Blueprints." }
|
|
9414
|
+
],
|
|
9415
|
+
isError: true
|
|
9416
|
+
};
|
|
9417
|
+
}
|
|
9418
|
+
}));
|
|
9419
|
+
mcp.registerTool("unreal_mcp_save_all", {
|
|
9420
|
+
title: "Unreal Save All (UnrealMCP)",
|
|
9421
|
+
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.",
|
|
9422
|
+
inputSchema: {
|
|
9423
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 60000).")
|
|
9424
|
+
}
|
|
9425
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_save_all", args, async () => {
|
|
9426
|
+
const timeoutMs = args?.timeoutMs ?? 6e4;
|
|
9427
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9428
|
+
try {
|
|
9429
|
+
const response = await sendUnrealMcpTcpCommand({ type: "save_all", params: {}, timeoutMs });
|
|
9430
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9431
|
+
return {
|
|
9432
|
+
content: [
|
|
9433
|
+
{ type: "text", text: "Save all completed." },
|
|
9434
|
+
{ type: "text", text: JSON.stringify(response, null, 2) }
|
|
9435
|
+
],
|
|
9436
|
+
isError: false
|
|
9437
|
+
};
|
|
9438
|
+
} catch (error) {
|
|
9439
|
+
return {
|
|
9440
|
+
content: [
|
|
9441
|
+
{ type: "text", text: "Save all failed." },
|
|
9442
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
9443
|
+
{ type: "text", text: "Tip: stop PIE, resolve any editor save prompts/errors (Save As, checkout), then retry." }
|
|
9444
|
+
],
|
|
9445
|
+
isError: true
|
|
9446
|
+
};
|
|
9447
|
+
}
|
|
9448
|
+
}));
|
|
9449
|
+
const vec = {
|
|
9450
|
+
add: (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]],
|
|
9451
|
+
sub: (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]],
|
|
9452
|
+
mul: (a, s) => [a[0] * s, a[1] * s, a[2] * s],
|
|
9453
|
+
dot: (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
|
|
9454
|
+
len: (a) => Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]),
|
|
9455
|
+
norm: (a) => {
|
|
9456
|
+
const l = Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
|
|
9457
|
+
if (!Number.isFinite(l) || l <= 1e-8) return [0, 0, 0];
|
|
9458
|
+
return [a[0] / l, a[1] / l, a[2] / l];
|
|
9459
|
+
},
|
|
9460
|
+
cross: (a, b) => [
|
|
9461
|
+
a[1] * b[2] - a[2] * b[1],
|
|
9462
|
+
a[2] * b[0] - a[0] * b[2],
|
|
9463
|
+
a[0] * b[1] - a[1] * b[0]
|
|
9464
|
+
],
|
|
9465
|
+
isFinite3: (a) => Array.isArray(a) && a.length === 3 && a.every((x) => typeof x === "number" && Number.isFinite(x))
|
|
9466
|
+
};
|
|
9467
|
+
function degToRad(deg) {
|
|
9468
|
+
return deg * Math.PI / 180;
|
|
9469
|
+
}
|
|
9470
|
+
function radToDeg(rad) {
|
|
9471
|
+
return rad * 180 / Math.PI;
|
|
9472
|
+
}
|
|
9473
|
+
function forwardRightUpFromRotator(rot) {
|
|
9474
|
+
const pitch = degToRad(rot[0]);
|
|
9475
|
+
const yaw = degToRad(rot[1]);
|
|
9476
|
+
const roll = degToRad(rot[2]);
|
|
9477
|
+
const cp = Math.cos(pitch);
|
|
9478
|
+
const sp = Math.sin(pitch);
|
|
9479
|
+
const cy = Math.cos(yaw);
|
|
9480
|
+
const sy = Math.sin(yaw);
|
|
9481
|
+
const cr = Math.cos(roll);
|
|
9482
|
+
const sr = Math.sin(roll);
|
|
9483
|
+
const forward = [cp * cy, cp * sy, sp];
|
|
9484
|
+
const right = [
|
|
9485
|
+
sr * sp * cy + cr * -sy,
|
|
9486
|
+
sr * sp * sy + cr * cy,
|
|
9487
|
+
-sr * cp
|
|
9488
|
+
];
|
|
9489
|
+
const up = [
|
|
9490
|
+
cr * sp * cy + -sr * -sy,
|
|
9491
|
+
cr * sp * sy + -sr * cy,
|
|
9492
|
+
cr * cp
|
|
9493
|
+
];
|
|
9494
|
+
return { forward: vec.norm(forward), right: vec.norm(right), up: vec.norm(up) };
|
|
9495
|
+
}
|
|
9496
|
+
function rotatorFromBasis(args) {
|
|
9497
|
+
const f = vec.norm(args.forward);
|
|
9498
|
+
const r = vec.norm(args.right);
|
|
9499
|
+
const u = vec.norm(args.up);
|
|
9500
|
+
const yaw = radToDeg(Math.atan2(f[1], f[0]));
|
|
9501
|
+
const pitch = radToDeg(Math.atan2(f[2], Math.sqrt(f[0] * f[0] + f[1] * f[1])));
|
|
9502
|
+
const roll = radToDeg(Math.atan2(r[2], u[2]));
|
|
9503
|
+
return [pitch, yaw, roll];
|
|
9504
|
+
}
|
|
9505
|
+
function unwrapUnrealMcpResult(raw) {
|
|
9506
|
+
if (raw?.result && typeof raw.result === "object") return raw.result;
|
|
9507
|
+
return raw;
|
|
9508
|
+
}
|
|
9509
|
+
mcp.registerTool("unreal_place_asset_relative", {
|
|
9510
|
+
title: "Unreal Place Asset Relative (Context-Aware)",
|
|
9511
|
+
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.",
|
|
9512
|
+
inputSchema: {
|
|
9513
|
+
objectPath: z.string().describe('Unreal asset object path (e.g. "/Game/MyPack/MyMesh.MyMesh").'),
|
|
9514
|
+
name: z.string().optional().describe("Optional actor name (default: derived from asset name)."),
|
|
9515
|
+
anchor: z.enum(["selection", "editor_camera", "player", "player_camera", "hit_result", "actor"]).describe("Reference anchor for placement."),
|
|
9516
|
+
actorName: z.string().optional().describe('When anchor="actor", the actor name/label to anchor to.'),
|
|
9517
|
+
offset: z.array(z.number()).length(3).optional().describe("Offset in anchor local axes [forwardCm, rightCm, upCm] (default [200,0,0])."),
|
|
9518
|
+
alignToSurface: z.boolean().optional().describe("When using hit_result or drop-to-ground hits, align actor up to hit normal (default false)."),
|
|
9519
|
+
dropToGround: z.boolean().optional().describe("After computing target location, raycast down and snap to the hit location (default false)."),
|
|
9520
|
+
raySource: z.enum(["editor", "pie"]).optional().describe('When anchor="hit_result", which camera to raycast from (default editor).'),
|
|
9521
|
+
maxDistance: z.number().positive().optional().describe("Raycast max distance in cm (default 10000)."),
|
|
9522
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 8000).")
|
|
9523
|
+
}
|
|
9524
|
+
}, async (args) => runWithMcpToolCard("unreal_place_asset_relative", args, async () => {
|
|
9525
|
+
const timeoutMs = args.timeoutMs ?? 8e3;
|
|
9526
|
+
const offset = Array.isArray(args.offset) && args.offset.length === 3 ? args.offset : [200, 0, 0];
|
|
9527
|
+
const alignToSurface = args.alignToSurface ?? false;
|
|
9528
|
+
const dropToGround = args.dropToGround ?? false;
|
|
9529
|
+
const maxDistance = typeof args.maxDistance === "number" ? args.maxDistance : 1e4;
|
|
9530
|
+
const raySource = args.raySource ?? "editor";
|
|
9531
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9532
|
+
try {
|
|
9533
|
+
const anchorRaw = String(args.anchor || "").trim();
|
|
9534
|
+
const anchor = anchorRaw === "player" ? "player_camera" : anchorRaw;
|
|
9535
|
+
const objectPath = String(args.objectPath || "").trim();
|
|
9536
|
+
if (!objectPath) throw new Error("Missing objectPath.");
|
|
9537
|
+
if (!vec.isFinite3(offset)) throw new Error("Invalid offset (expected [forward,right,up] numbers).");
|
|
9538
|
+
let origin = null;
|
|
9539
|
+
let rot = null;
|
|
9540
|
+
let surfaceNormal = null;
|
|
9541
|
+
if (anchor === "selection" || anchor === "editor_camera") {
|
|
9542
|
+
const ctxRaw = await sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs });
|
|
9543
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9544
|
+
const ctx = unwrapUnrealMcpResult(ctxRaw);
|
|
9545
|
+
if (anchor === "selection") {
|
|
9546
|
+
const sel = Array.isArray(ctx?.selection) ? ctx.selection : [];
|
|
9547
|
+
if (sel.length === 0) throw new Error("No selected actors in Unreal Editor. Select an actor and retry.");
|
|
9548
|
+
const first = sel[0];
|
|
9549
|
+
origin = Array.isArray(first?.location) ? first.location : null;
|
|
9550
|
+
rot = Array.isArray(first?.rotation) ? first.rotation : null;
|
|
9551
|
+
} else {
|
|
9552
|
+
const cam = ctx?.viewportCamera;
|
|
9553
|
+
if (!cam || cam === null) throw new Error("No active viewport camera. Click the viewport and retry.");
|
|
9554
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9555
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9556
|
+
}
|
|
9557
|
+
} else if (anchor === "player_camera") {
|
|
9558
|
+
const pcRaw = await sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs });
|
|
9559
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9560
|
+
const pc = unwrapUnrealMcpResult(pcRaw);
|
|
9561
|
+
if (!pc?.isPlaying) throw new Error("PIE is not running. Start PIE and retry (or use editor_camera/selection anchors).");
|
|
9562
|
+
const cam = pc?.camera;
|
|
9563
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9564
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9565
|
+
} else if (anchor === "actor") {
|
|
9566
|
+
const actorName = String(args.actorName || "").trim();
|
|
9567
|
+
if (!actorName) throw new Error('anchor="actor" requires actorName.');
|
|
9568
|
+
const tRaw = await sendUnrealMcpTcpCommand({ type: "get_actor_transform", params: { name: actorName, source: "editor" }, timeoutMs });
|
|
9569
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9570
|
+
const t = unwrapUnrealMcpResult(tRaw);
|
|
9571
|
+
origin = Array.isArray(t?.location) ? t.location : null;
|
|
9572
|
+
rot = Array.isArray(t?.rotation) ? t.rotation : null;
|
|
9573
|
+
} else if (anchor === "hit_result") {
|
|
9574
|
+
const hitRaw = await sendUnrealMcpTcpCommand({
|
|
9575
|
+
type: "raycast_from_camera",
|
|
9576
|
+
params: { source: raySource, maxDistance },
|
|
9577
|
+
timeoutMs
|
|
9578
|
+
});
|
|
9579
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9580
|
+
const hit = unwrapUnrealMcpResult(hitRaw);
|
|
9581
|
+
if (!hit?.hit) throw new Error("Raycast did not hit anything.");
|
|
9582
|
+
origin = Array.isArray(hit?.location) ? hit.location : null;
|
|
9583
|
+
surfaceNormal = Array.isArray(hit?.normal) ? hit.normal : null;
|
|
9584
|
+
const ctxRot = raySource === "pie" ? unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs }))?.camera?.rotation : unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs }))?.viewportCamera?.rotation;
|
|
9585
|
+
rot = Array.isArray(ctxRot) ? ctxRot : [0, 0, 0];
|
|
9586
|
+
} else {
|
|
9587
|
+
throw new Error(`Unknown anchor: ${anchor}`);
|
|
9588
|
+
}
|
|
9589
|
+
if (!vec.isFinite3(origin)) throw new Error("Anchor origin is missing or invalid.");
|
|
9590
|
+
if (!rot || !vec.isFinite3(rot)) throw new Error("Anchor rotation is missing or invalid.");
|
|
9591
|
+
const basis = forwardRightUpFromRotator(rot);
|
|
9592
|
+
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]))));
|
|
9593
|
+
let finalNormal = surfaceNormal;
|
|
9594
|
+
if (dropToGround) {
|
|
9595
|
+
const start = vec.add(target, [0, 0, 1e4]);
|
|
9596
|
+
const downRaw = await sendUnrealMcpTcpCommand({
|
|
9597
|
+
type: "raycast_down",
|
|
9598
|
+
params: { source: "editor", startLocation: start, maxDistance: 2e5 },
|
|
9599
|
+
timeoutMs
|
|
9600
|
+
});
|
|
9601
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9602
|
+
const down = unwrapUnrealMcpResult(downRaw);
|
|
9603
|
+
if (down?.hit && Array.isArray(down?.location)) {
|
|
9604
|
+
target = down.location;
|
|
9605
|
+
if (Array.isArray(down?.normal)) finalNormal = down.normal;
|
|
9606
|
+
} else {
|
|
9607
|
+
throw new Error("dropToGround=true but raycast_down did not hit anything.");
|
|
9608
|
+
}
|
|
9609
|
+
}
|
|
9610
|
+
let outRot = rot;
|
|
9611
|
+
if (alignToSurface && vec.isFinite3(finalNormal)) {
|
|
9612
|
+
const up = vec.norm(finalNormal);
|
|
9613
|
+
let fwd = basis.forward;
|
|
9614
|
+
fwd = vec.sub(fwd, vec.mul(up, vec.dot(fwd, up)));
|
|
9615
|
+
if (vec.len(fwd) <= 1e-4) {
|
|
9616
|
+
fwd = vec.cross([0, 1, 0], up);
|
|
9617
|
+
}
|
|
9618
|
+
fwd = vec.norm(fwd);
|
|
9619
|
+
const right = vec.norm(vec.cross(up, fwd));
|
|
9620
|
+
const fixedFwd = vec.norm(vec.cross(right, up));
|
|
9621
|
+
outRot = rotatorFromBasis({ forward: fixedFwd, right, up });
|
|
9622
|
+
}
|
|
9623
|
+
const params = {
|
|
9624
|
+
objectPath,
|
|
9625
|
+
...typeof args.name === "string" && args.name.trim() ? { name: args.name.trim() } : {},
|
|
9626
|
+
location: target,
|
|
9627
|
+
rotation: outRot
|
|
9628
|
+
};
|
|
9629
|
+
const placed = await sendUnrealMcpTcpCommand({ type: "place_asset", params, timeoutMs: Math.max(timeoutMs, 1e4) });
|
|
9630
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9631
|
+
const payload = {
|
|
9632
|
+
anchor,
|
|
9633
|
+
origin,
|
|
9634
|
+
offset,
|
|
9635
|
+
location: target,
|
|
9636
|
+
rotation: outRot,
|
|
9637
|
+
...finalNormal ? { normal: finalNormal } : {},
|
|
9638
|
+
placed
|
|
9639
|
+
};
|
|
9640
|
+
return {
|
|
9641
|
+
content: [
|
|
9642
|
+
{ type: "text", text: "Placed asset relative to context." },
|
|
9643
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
9644
|
+
],
|
|
9645
|
+
isError: false
|
|
9646
|
+
};
|
|
9647
|
+
} catch (error) {
|
|
9648
|
+
return {
|
|
9649
|
+
content: [
|
|
9650
|
+
{ type: "text", text: "Failed to place asset relative to context." },
|
|
9651
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9652
|
+
],
|
|
9653
|
+
isError: true
|
|
9654
|
+
};
|
|
9655
|
+
}
|
|
9656
|
+
}));
|
|
9657
|
+
mcp.registerTool("unreal_spawn_actor_relative", {
|
|
9658
|
+
title: "Unreal Spawn Actor Relative (Context-Aware)",
|
|
9659
|
+
description: "High-level helper built from context/raycast primitives: spawn an actor type relative to selection/camera/player/hit result.",
|
|
9660
|
+
inputSchema: {
|
|
9661
|
+
type: z.string().describe('Actor class/type (e.g. "PointLight", "CameraActor", "StaticMeshActor").'),
|
|
9662
|
+
name: z.string().describe("Actor name."),
|
|
9663
|
+
anchor: z.enum(["selection", "editor_camera", "player", "player_camera", "hit_result", "actor"]).describe("Reference anchor for placement."),
|
|
9664
|
+
actorName: z.string().optional().describe('When anchor="actor", the actor name/label to anchor to.'),
|
|
9665
|
+
offset: z.array(z.number()).length(3).optional().describe("Offset in anchor local axes [forwardCm, rightCm, upCm] (default [200,0,0])."),
|
|
9666
|
+
alignToSurface: z.boolean().optional().describe("When using hit_result or drop-to-ground hits, align actor up to hit normal (default false)."),
|
|
9667
|
+
dropToGround: z.boolean().optional().describe("After computing target location, raycast down and snap to the hit location (default false)."),
|
|
9668
|
+
raySource: z.enum(["editor", "pie"]).optional().describe('When anchor="hit_result", which camera to raycast from (default editor).'),
|
|
9669
|
+
maxDistance: z.number().positive().optional().describe("Raycast max distance in cm (default 10000)."),
|
|
9670
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 8000).")
|
|
9671
|
+
}
|
|
9672
|
+
}, async (args) => runWithMcpToolCard("unreal_spawn_actor_relative", args, async () => {
|
|
9673
|
+
const timeoutMs = args.timeoutMs ?? 8e3;
|
|
9674
|
+
const offset = Array.isArray(args.offset) && args.offset.length === 3 ? args.offset : [200, 0, 0];
|
|
9675
|
+
const alignToSurface = args.alignToSurface ?? false;
|
|
9676
|
+
const dropToGround = args.dropToGround ?? false;
|
|
9677
|
+
const maxDistance = typeof args.maxDistance === "number" ? args.maxDistance : 1e4;
|
|
9678
|
+
const raySource = args.raySource ?? "editor";
|
|
9679
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
9680
|
+
try {
|
|
9681
|
+
const type = String(args.type || "").trim();
|
|
9682
|
+
const name = String(args.name || "").trim();
|
|
9683
|
+
const anchorRaw = String(args.anchor || "").trim();
|
|
9684
|
+
const anchor = anchorRaw === "player" ? "player_camera" : anchorRaw;
|
|
9685
|
+
if (!type) throw new Error("Missing type.");
|
|
9686
|
+
if (!name) throw new Error("Missing name.");
|
|
9687
|
+
if (!vec.isFinite3(offset)) throw new Error("Invalid offset (expected [forward,right,up] numbers).");
|
|
9688
|
+
let origin = null;
|
|
9689
|
+
let rot = null;
|
|
9690
|
+
let surfaceNormal = null;
|
|
9691
|
+
if (anchor === "selection" || anchor === "editor_camera") {
|
|
9692
|
+
const ctx = unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs }));
|
|
9693
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9694
|
+
if (anchor === "selection") {
|
|
9695
|
+
const sel = Array.isArray(ctx?.selection) ? ctx.selection : [];
|
|
9696
|
+
if (sel.length === 0) throw new Error("No selected actors in Unreal Editor. Select an actor and retry.");
|
|
9697
|
+
const first = sel[0];
|
|
9698
|
+
origin = Array.isArray(first?.location) ? first.location : null;
|
|
9699
|
+
rot = Array.isArray(first?.rotation) ? first.rotation : null;
|
|
9700
|
+
} else {
|
|
9701
|
+
const cam = ctx?.viewportCamera;
|
|
9702
|
+
if (!cam || cam === null) throw new Error("No active viewport camera. Click the viewport and retry.");
|
|
9703
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9704
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9705
|
+
}
|
|
9706
|
+
} else if (anchor === "player_camera") {
|
|
9707
|
+
const pc = unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs }));
|
|
9708
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9709
|
+
if (!pc?.isPlaying) throw new Error("PIE is not running. Start PIE and retry (or use editor_camera/selection anchors).");
|
|
9710
|
+
const cam = pc?.camera;
|
|
9711
|
+
origin = Array.isArray(cam?.location) ? cam.location : null;
|
|
9712
|
+
rot = Array.isArray(cam?.rotation) ? cam.rotation : null;
|
|
9713
|
+
} else if (anchor === "actor") {
|
|
9714
|
+
const actorName = String(args.actorName || "").trim();
|
|
9715
|
+
if (!actorName) throw new Error('anchor="actor" requires actorName.');
|
|
9716
|
+
const t = unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "get_actor_transform", params: { name: actorName, source: "editor" }, timeoutMs }));
|
|
9717
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9718
|
+
origin = Array.isArray(t?.location) ? t.location : null;
|
|
9719
|
+
rot = Array.isArray(t?.rotation) ? t.rotation : null;
|
|
9720
|
+
} else if (anchor === "hit_result") {
|
|
9721
|
+
const hit = unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "raycast_from_camera", params: { source: raySource, maxDistance }, timeoutMs }));
|
|
9722
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9723
|
+
if (!hit?.hit) throw new Error("Raycast did not hit anything.");
|
|
9724
|
+
origin = Array.isArray(hit?.location) ? hit.location : null;
|
|
9725
|
+
surfaceNormal = Array.isArray(hit?.normal) ? hit.normal : null;
|
|
9726
|
+
const ctxRot = raySource === "pie" ? unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "get_player_context", params: {}, timeoutMs }))?.camera?.rotation : unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "get_editor_context", params: {}, timeoutMs }))?.viewportCamera?.rotation;
|
|
9727
|
+
rot = Array.isArray(ctxRot) ? ctxRot : [0, 0, 0];
|
|
9728
|
+
} else {
|
|
9729
|
+
throw new Error(`Unknown anchor: ${anchor}`);
|
|
9730
|
+
}
|
|
9731
|
+
if (!vec.isFinite3(origin)) throw new Error("Anchor origin is missing or invalid.");
|
|
9732
|
+
if (!rot || !vec.isFinite3(rot)) throw new Error("Anchor rotation is missing or invalid.");
|
|
9733
|
+
const basis = forwardRightUpFromRotator(rot);
|
|
9734
|
+
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]))));
|
|
9735
|
+
let finalNormal = surfaceNormal;
|
|
9736
|
+
if (dropToGround) {
|
|
9737
|
+
const start = vec.add(target, [0, 0, 1e4]);
|
|
9738
|
+
const down = unwrapUnrealMcpResult(await sendUnrealMcpTcpCommand({ type: "raycast_down", params: { source: "editor", startLocation: start, maxDistance: 2e5 }, timeoutMs }));
|
|
9739
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9740
|
+
if (down?.hit && Array.isArray(down?.location)) {
|
|
9741
|
+
target = down.location;
|
|
9742
|
+
if (Array.isArray(down?.normal)) finalNormal = down.normal;
|
|
9743
|
+
} else {
|
|
9744
|
+
throw new Error("dropToGround=true but raycast_down did not hit anything.");
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
let outRot = rot;
|
|
9748
|
+
if (alignToSurface && vec.isFinite3(finalNormal)) {
|
|
9749
|
+
const up = vec.norm(finalNormal);
|
|
9750
|
+
let fwd = basis.forward;
|
|
9751
|
+
fwd = vec.sub(fwd, vec.mul(up, vec.dot(fwd, up)));
|
|
9752
|
+
if (vec.len(fwd) <= 1e-4) {
|
|
9753
|
+
fwd = vec.cross([0, 1, 0], up);
|
|
9754
|
+
}
|
|
9755
|
+
fwd = vec.norm(fwd);
|
|
9756
|
+
const right = vec.norm(vec.cross(up, fwd));
|
|
9757
|
+
const fixedFwd = vec.norm(vec.cross(right, up));
|
|
9758
|
+
outRot = rotatorFromBasis({ forward: fixedFwd, right, up });
|
|
9759
|
+
}
|
|
9760
|
+
const spawned = await sendUnrealMcpTcpCommand({
|
|
9761
|
+
type: "spawn_actor",
|
|
9762
|
+
params: { type, name, location: target, rotation: outRot },
|
|
9763
|
+
timeoutMs: Math.max(timeoutMs, 1e4)
|
|
9764
|
+
});
|
|
9765
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
9766
|
+
const payload = {
|
|
9767
|
+
anchor,
|
|
9768
|
+
origin,
|
|
9769
|
+
offset,
|
|
9770
|
+
location: target,
|
|
9771
|
+
rotation: outRot,
|
|
9772
|
+
...finalNormal ? { normal: finalNormal } : {},
|
|
9773
|
+
spawned
|
|
9774
|
+
};
|
|
9775
|
+
return {
|
|
9776
|
+
content: [
|
|
9777
|
+
{ type: "text", text: "Spawned actor relative to context." },
|
|
9778
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
9779
|
+
],
|
|
9780
|
+
isError: false
|
|
9781
|
+
};
|
|
9782
|
+
} catch (error) {
|
|
9783
|
+
return {
|
|
9784
|
+
content: [
|
|
9785
|
+
{ type: "text", text: "Failed to spawn actor relative to context." },
|
|
9786
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) }
|
|
9787
|
+
],
|
|
9788
|
+
isError: true
|
|
9789
|
+
};
|
|
9790
|
+
}
|
|
9791
|
+
}));
|
|
9792
|
+
mcp.registerTool("unreal_mcp_get_actors_in_level", {
|
|
9793
|
+
title: "Unreal Actors In Level (UnrealMCP)",
|
|
9794
|
+
description: "List all actors in the current Unreal Editor level via the UnrealMCP engine plugin. Requires Unreal Editor running with the UnrealMCP plugin enabled.",
|
|
9795
|
+
inputSchema: {
|
|
9796
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
9797
|
+
}
|
|
9798
|
+
}, async (args) => runWithMcpToolCard("unreal_mcp_get_actors_in_level", args, async () => {
|
|
7856
9799
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9800
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7857
9801
|
try {
|
|
7858
9802
|
const response = await sendUnrealMcpTcpCommand({ type: "get_actors_in_level", params: {}, timeoutMs });
|
|
9803
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7859
9804
|
const actors = Array.isArray(response?.actors) ? response.actors : Array.isArray(response?.result?.actors) ? response.result.actors : null;
|
|
7860
9805
|
if (!actors) {
|
|
7861
9806
|
return {
|
|
@@ -7892,8 +9837,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
7892
9837
|
}
|
|
7893
9838
|
}, async (args) => runWithMcpToolCard("unreal_mcp_get_play_in_editor_status", args, async () => {
|
|
7894
9839
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9840
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7895
9841
|
try {
|
|
7896
9842
|
const response = await sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", params: {}, timeoutMs });
|
|
9843
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7897
9844
|
return {
|
|
7898
9845
|
content: [
|
|
7899
9846
|
{ type: "text", text: "Fetched Play-In-Editor status." },
|
|
@@ -7920,6 +9867,7 @@ ${String(st.stdout || "").trim()}`
|
|
|
7920
9867
|
}
|
|
7921
9868
|
}, async (args) => runWithMcpToolCard("unreal_mcp_editor_health", args, async () => {
|
|
7922
9869
|
const timeoutMs = args.timeoutMs ?? 1500;
|
|
9870
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7923
9871
|
const classify = (message) => {
|
|
7924
9872
|
const m = message.toLowerCase();
|
|
7925
9873
|
if (m.includes("econnrefused")) return { state: "offline", hint: "UnrealMCP TCP port is not accepting connections (Unreal Editor likely closed/crashed or plugin not running)." };
|
|
@@ -7929,10 +9877,12 @@ ${String(st.stdout || "").trim()}`
|
|
|
7929
9877
|
};
|
|
7930
9878
|
try {
|
|
7931
9879
|
const ping = await sendUnrealMcpTcpCommand({ type: "ping", params: {}, timeoutMs });
|
|
9880
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7932
9881
|
const pluginInfo = await getUnrealMcpPluginInfoBestEffort(timeoutMs);
|
|
7933
9882
|
let pieStatus = null;
|
|
7934
9883
|
try {
|
|
7935
9884
|
pieStatus = await sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", params: {}, timeoutMs });
|
|
9885
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7936
9886
|
} catch {
|
|
7937
9887
|
pieStatus = null;
|
|
7938
9888
|
}
|
|
@@ -7972,8 +9922,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
7972
9922
|
}
|
|
7973
9923
|
}, async (args) => runWithMcpToolCard("unreal_mcp_play_in_editor", args, async () => {
|
|
7974
9924
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9925
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
7975
9926
|
try {
|
|
7976
9927
|
const response = await sendUnrealMcpTcpCommand({ type: "play_in_editor", params: {}, timeoutMs });
|
|
9928
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
7977
9929
|
return {
|
|
7978
9930
|
content: [
|
|
7979
9931
|
{ type: "text", text: "Requested Play In Editor (PIE) in active viewport." },
|
|
@@ -8000,8 +9952,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
8000
9952
|
}
|
|
8001
9953
|
}, async (args) => runWithMcpToolCard("unreal_mcp_play_in_editor_windowed", args, async () => {
|
|
8002
9954
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9955
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8003
9956
|
try {
|
|
8004
9957
|
const response = await sendUnrealMcpTcpCommand({ type: "play_in_editor_windowed", params: {}, timeoutMs });
|
|
9958
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8005
9959
|
return {
|
|
8006
9960
|
content: [
|
|
8007
9961
|
{ type: "text", text: "Requested Play In Editor (PIE) in a new editor window." },
|
|
@@ -8029,8 +9983,10 @@ ${String(st.stdout || "").trim()}`
|
|
|
8029
9983
|
}
|
|
8030
9984
|
}, async (args) => runWithMcpToolCard("unreal_mcp_stop_play_in_editor", args, async () => {
|
|
8031
9985
|
const timeoutMs = args.timeoutMs ?? 5e3;
|
|
9986
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
8032
9987
|
try {
|
|
8033
9988
|
const response = await sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", params: {}, timeoutMs });
|
|
9989
|
+
unrealEditorSupervisor.noteUnrealReachable();
|
|
8034
9990
|
return {
|
|
8035
9991
|
content: [
|
|
8036
9992
|
{ type: "text", text: "Requested Stop Play In Editor (PIE)." },
|
|
@@ -8048,6 +10004,48 @@ ${String(st.stdout || "").trim()}`
|
|
|
8048
10004
|
};
|
|
8049
10005
|
}
|
|
8050
10006
|
}));
|
|
10007
|
+
mcp.registerTool("unreal_smoke_test", {
|
|
10008
|
+
title: "Unreal Smoke Test (PIE + Screenshot Validation)",
|
|
10009
|
+
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.',
|
|
10010
|
+
inputSchema: {
|
|
10011
|
+
uprojectPath: z.string().describe("Absolute path to the .uproject file (used to locate Saved/Screenshots/Flockbay for evidence upload)."),
|
|
10012
|
+
stabilizeMs: z.number().int().positive().optional().describe("Time to wait before taking screenshot (default 1500)."),
|
|
10013
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms for each step (default 30000)."),
|
|
10014
|
+
stopIfPlaying: z.boolean().optional().describe("If PIE is already running, stop it first (default true).")
|
|
10015
|
+
}
|
|
10016
|
+
}, async (args) => runWithMcpToolCard("unreal_smoke_test", args, async () => {
|
|
10017
|
+
const uprojectPath = typeof args?.uprojectPath === "string" ? String(args.uprojectPath).trim() : "";
|
|
10018
|
+
const stabilizeMs = typeof args?.stabilizeMs === "number" ? args.stabilizeMs : void 0;
|
|
10019
|
+
const timeoutMs = typeof args?.timeoutMs === "number" ? args.timeoutMs : void 0;
|
|
10020
|
+
const stopIfPlaying = typeof args?.stopIfPlaying === "boolean" ? args.stopIfPlaying : void 0;
|
|
10021
|
+
if (!uprojectPath || !uprojectPath.toLowerCase().endsWith(".uproject") || !path.isAbsolute(uprojectPath)) {
|
|
10022
|
+
return {
|
|
10023
|
+
content: [{ type: "text", text: `Invalid uprojectPath (must be an absolute path to *.uproject): ${String(uprojectPath)}` }],
|
|
10024
|
+
isError: true
|
|
10025
|
+
};
|
|
10026
|
+
}
|
|
10027
|
+
if (!existsSync(uprojectPath)) {
|
|
10028
|
+
return { content: [{ type: "text", text: `uprojectPath not found: ${uprojectPath}` }], isError: true };
|
|
10029
|
+
}
|
|
10030
|
+
unrealEditorSupervisor.noteUnrealActivity();
|
|
10031
|
+
const result = await runUnrealSmokeTest({ uprojectPath, stabilizeMs, timeoutMs, stopIfPlaying });
|
|
10032
|
+
let viewsPayload = [];
|
|
10033
|
+
if (result.screenshotPath && typeof result.screenshotPath === "string" && existsSync(result.screenshotPath)) {
|
|
10034
|
+
const uploaded = await uploadScreenshotViewsForSession({
|
|
10035
|
+
sessionId: client.sessionId,
|
|
10036
|
+
token: client.getAuthToken(),
|
|
10037
|
+
views: [{ id: "smoke_test", path: result.screenshotPath }]
|
|
10038
|
+
});
|
|
10039
|
+
viewsPayload = uploaded.map((u) => ({ ...u, id: u.id || "smoke_test" }));
|
|
10040
|
+
}
|
|
10041
|
+
const payload = { ...result, ...viewsPayload.length ? { views: viewsPayload } : {} };
|
|
10042
|
+
const content = [
|
|
10043
|
+
{ type: "text", text: result.ok ? "Smoke test succeeded." : "Smoke test failed." },
|
|
10044
|
+
...result.screenshotPath ? [{ type: "text", text: `Screenshot: ${result.screenshotPath}` }] : [],
|
|
10045
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) }
|
|
10046
|
+
];
|
|
10047
|
+
return { content, ...viewsPayload.length ? { views: viewsPayload } : {}, isError: !result.ok };
|
|
10048
|
+
}));
|
|
8051
10049
|
mcp.registerTool("unreal_mechanic_run", {
|
|
8052
10050
|
title: "Unreal Mechanic Builder (Dash MVP)",
|
|
8053
10051
|
description: "Create a project-safe mechanic test map (Parallel mode) and prove it works with Flockbay screenshot evidence. V1 supports dash only.",
|
|
@@ -8209,13 +10207,48 @@ Fix: ${res.hint}` : "";
|
|
|
8209
10207
|
"read_images",
|
|
8210
10208
|
"unreal_headless_screenshot",
|
|
8211
10209
|
"unreal_latest_screenshots",
|
|
10210
|
+
"unreal_editor_launch",
|
|
10211
|
+
"unreal_build_project",
|
|
8212
10212
|
"unreal_mcp_command",
|
|
10213
|
+
"unreal_mcp_list_capabilities",
|
|
10214
|
+
"unreal_mcp_get_command_schema",
|
|
10215
|
+
"unreal_mcp_search_blueprint_functions",
|
|
10216
|
+
"unreal_mcp_resolve_blueprint_function",
|
|
10217
|
+
"unreal_mcp_search_assets",
|
|
10218
|
+
"unreal_mcp_get_asset_info",
|
|
10219
|
+
"unreal_mcp_list_asset_packs",
|
|
10220
|
+
"unreal_mcp_place_asset",
|
|
10221
|
+
"unreal_mcp_get_editor_context",
|
|
10222
|
+
"unreal_mcp_get_player_context",
|
|
10223
|
+
"unreal_mcp_raycast_from_camera",
|
|
10224
|
+
"unreal_mcp_raycast_down",
|
|
10225
|
+
"unreal_mcp_get_actor_transform",
|
|
10226
|
+
"unreal_mcp_get_actor_bounds",
|
|
10227
|
+
"unreal_mcp_create_landscape",
|
|
10228
|
+
"unreal_mcp_map_check",
|
|
10229
|
+
"unreal_mcp_compile_blueprints_all",
|
|
10230
|
+
"unreal_mcp_save_all",
|
|
10231
|
+
"unreal_place_asset_relative",
|
|
10232
|
+
"unreal_spawn_actor_relative",
|
|
8213
10233
|
"unreal_mcp_get_actors_in_level",
|
|
10234
|
+
"unreal_mcp_get_play_in_editor_status",
|
|
10235
|
+
"unreal_mcp_editor_health",
|
|
10236
|
+
"unreal_mcp_play_in_editor",
|
|
10237
|
+
"unreal_mcp_play_in_editor_windowed",
|
|
10238
|
+
"unreal_mcp_stop_play_in_editor",
|
|
10239
|
+
"unreal_smoke_test",
|
|
8214
10240
|
"unreal_fast_preview",
|
|
8215
10241
|
"unreal_mechanic_run"
|
|
8216
10242
|
],
|
|
10243
|
+
unreal: {
|
|
10244
|
+
onIssue: (handler2) => {
|
|
10245
|
+
unrealIssueEmitter.on("issue", handler2);
|
|
10246
|
+
return () => unrealIssueEmitter.off("issue", handler2);
|
|
10247
|
+
}
|
|
10248
|
+
},
|
|
8217
10249
|
stop: () => {
|
|
8218
10250
|
logger.debug("[flockbayMCP] Stopping server");
|
|
10251
|
+
unrealEditorSupervisor.stop();
|
|
8219
10252
|
mcp.close();
|
|
8220
10253
|
server.close();
|
|
8221
10254
|
}
|
|
@@ -8440,8 +10473,20 @@ async function runClaude(credentials, options = {}) {
|
|
|
8440
10473
|
lifecycleStateSince: Date.now(),
|
|
8441
10474
|
flavor: "claude"
|
|
8442
10475
|
};
|
|
8443
|
-
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
8444
|
-
logger.debug(
|
|
10476
|
+
const response = options.sessionId ? await api.getSessionById(options.sessionId) : await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
10477
|
+
logger.debug(`${options.sessionId ? "Session attached" : "Session created"}: ${response.id}`);
|
|
10478
|
+
const session = api.sessionSyncClient(response);
|
|
10479
|
+
if (options.sessionId) {
|
|
10480
|
+
session.updateMetadata((currentMetadata) => ({
|
|
10481
|
+
...currentMetadata,
|
|
10482
|
+
...metadata,
|
|
10483
|
+
// Preserve user-facing fields set by other clients.
|
|
10484
|
+
name: currentMetadata?.name,
|
|
10485
|
+
summary: currentMetadata?.summary
|
|
10486
|
+
}));
|
|
10487
|
+
}
|
|
10488
|
+
await session.connectAndWait(15e3);
|
|
10489
|
+
session.keepAlive(false, options.startingMode === "remote" ? "remote" : "local");
|
|
8445
10490
|
try {
|
|
8446
10491
|
logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
8447
10492
|
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
|
@@ -8456,7 +10501,7 @@ async function runClaude(credentials, options = {}) {
|
|
|
8456
10501
|
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
8457
10502
|
logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
8458
10503
|
try {
|
|
8459
|
-
|
|
10504
|
+
session.updateMetadata((currentMetadata) => ({
|
|
8460
10505
|
...currentMetadata,
|
|
8461
10506
|
tools: sdkMetadata.tools,
|
|
8462
10507
|
slashCommands: sdkMetadata.slashCommands
|
|
@@ -8466,7 +10511,6 @@ async function runClaude(credentials, options = {}) {
|
|
|
8466
10511
|
logger.debug("[start] Failed to update session metadata:", error);
|
|
8467
10512
|
}
|
|
8468
10513
|
});
|
|
8469
|
-
const session = api.sessionSyncClient(response);
|
|
8470
10514
|
const elicitationHub = new ElicitationHub();
|
|
8471
10515
|
const flockbayServer = await startFlockbayServer(session, { elicitationHub });
|
|
8472
10516
|
logger.debug(`[START] Flockbay MCP server started at ${flockbayServer.url}`);
|
|
@@ -8818,20 +10862,45 @@ async function handleAuthCommand(args) {
|
|
|
8818
10862
|
return;
|
|
8819
10863
|
}
|
|
8820
10864
|
switch (subcommand) {
|
|
8821
|
-
case "login":
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
await
|
|
8834
|
-
|
|
10865
|
+
case "login": {
|
|
10866
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
10867
|
+
if (force) {
|
|
10868
|
+
try {
|
|
10869
|
+
await stopDaemon();
|
|
10870
|
+
} catch {
|
|
10871
|
+
}
|
|
10872
|
+
await clearCredentials();
|
|
10873
|
+
if (args.includes("--clear-machine-id")) {
|
|
10874
|
+
await clearMachineId();
|
|
10875
|
+
}
|
|
10876
|
+
}
|
|
10877
|
+
await loginWithClerkAndPairMachine();
|
|
10878
|
+
return;
|
|
10879
|
+
}
|
|
10880
|
+
case "logout": {
|
|
10881
|
+
try {
|
|
10882
|
+
await stopDaemon();
|
|
10883
|
+
} catch {
|
|
10884
|
+
}
|
|
10885
|
+
await clearCredentials();
|
|
10886
|
+
await clearMachineId();
|
|
10887
|
+
console.log(chalk.green("\u2713 Logged out (profile reset)"));
|
|
10888
|
+
return;
|
|
10889
|
+
}
|
|
10890
|
+
case "status": {
|
|
10891
|
+
const auth = await readCredentials();
|
|
10892
|
+
const settings = await readSettings();
|
|
10893
|
+
const daemon = await readDaemonState();
|
|
10894
|
+
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
10895
|
+
console.log(chalk.gray(`Profile: ${configuration.profile}`));
|
|
10896
|
+
console.log(chalk.gray(`Server: ${configuration.serverUrl}`));
|
|
10897
|
+
console.log(chalk.gray(`Web app: ${configuration.webappUrl}`));
|
|
10898
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
10899
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
10900
|
+
console.log(chalk.gray(`Auth: ${auth?.machineToken ? "paired" : "not paired"}`));
|
|
10901
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
10902
|
+
return;
|
|
10903
|
+
}
|
|
8835
10904
|
default:
|
|
8836
10905
|
console.error(chalk.red(`Unknown auth subcommand: ${subcommand}`));
|
|
8837
10906
|
showAuthHelp();
|
|
@@ -8840,186 +10909,14 @@ async function handleAuthCommand(args) {
|
|
|
8840
10909
|
}
|
|
8841
10910
|
function showAuthHelp() {
|
|
8842
10911
|
console.log(`
|
|
8843
|
-
${chalk.bold("flockbay auth")} - Authentication management
|
|
10912
|
+
${chalk.bold("flockbay auth")} - Authentication management (Clerk-based)
|
|
8844
10913
|
|
|
8845
10914
|
${chalk.bold("Usage:")}
|
|
8846
|
-
flockbay auth login [--force] [--
|
|
8847
|
-
flockbay auth logout
|
|
8848
|
-
flockbay auth status
|
|
8849
|
-
flockbay auth show-backup Display backup key for mobile/web clients
|
|
8850
|
-
flockbay auth help Show this help message
|
|
8851
|
-
|
|
8852
|
-
${chalk.bold("Options:")}
|
|
8853
|
-
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
8854
|
-
--mobile Authenticate by scanning a QR code (advanced)
|
|
8855
|
-
--web Authenticate via browser login (default)
|
|
8856
|
-
`);
|
|
8857
|
-
}
|
|
8858
|
-
async function handleAuthLogin(args) {
|
|
8859
|
-
const forceAuth = args.includes("--force") || args.includes("-f");
|
|
8860
|
-
const quiet = args.includes("--quiet") || args.includes("--no-summary");
|
|
8861
|
-
const preferMobile = args.includes("--mobile");
|
|
8862
|
-
const preferWeb = args.includes("--web");
|
|
8863
|
-
if (preferMobile && preferWeb) {
|
|
8864
|
-
console.error(chalk.red("Choose only one authentication method: --mobile or --web"));
|
|
8865
|
-
process.exit(1);
|
|
8866
|
-
}
|
|
8867
|
-
const authMethod = preferMobile ? "mobile" : "web";
|
|
8868
|
-
if (forceAuth) {
|
|
8869
|
-
if (!quiet) {
|
|
8870
|
-
console.log(chalk.yellow("Force authentication requested."));
|
|
8871
|
-
console.log(chalk.gray("This will:"));
|
|
8872
|
-
console.log(chalk.gray(" \u2022 Clear existing credentials"));
|
|
8873
|
-
console.log(chalk.gray(" \u2022 Clear machine ID"));
|
|
8874
|
-
console.log(chalk.gray(" \u2022 Stop daemon if running"));
|
|
8875
|
-
console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
|
|
8876
|
-
}
|
|
8877
|
-
try {
|
|
8878
|
-
logger.debug("Stopping daemon for force auth...");
|
|
8879
|
-
await stopDaemon();
|
|
8880
|
-
if (!quiet) console.log(chalk.gray("\u2713 Stopped daemon"));
|
|
8881
|
-
} catch (error) {
|
|
8882
|
-
logger.debug("Daemon was not running or failed to stop:", error);
|
|
8883
|
-
}
|
|
8884
|
-
await clearCredentials();
|
|
8885
|
-
if (!quiet) console.log(chalk.gray("\u2713 Cleared credentials"));
|
|
8886
|
-
await clearMachineId();
|
|
8887
|
-
if (!quiet) console.log(chalk.gray("\u2713 Cleared machine ID"));
|
|
8888
|
-
if (!quiet) console.log("");
|
|
8889
|
-
}
|
|
8890
|
-
if (!forceAuth) {
|
|
8891
|
-
const existingCreds = await readCredentials();
|
|
8892
|
-
const settings = await readSettings();
|
|
8893
|
-
if (existingCreds && settings?.machineId) {
|
|
8894
|
-
if (!quiet) {
|
|
8895
|
-
console.log(chalk.green("\u2713 Already authenticated"));
|
|
8896
|
-
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
8897
|
-
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
8898
|
-
console.log(chalk.gray(` Use 'flockbay auth login --force' to re-authenticate`));
|
|
8899
|
-
}
|
|
8900
|
-
return;
|
|
8901
|
-
} else if (existingCreds && !settings?.machineId) {
|
|
8902
|
-
if (!quiet) {
|
|
8903
|
-
console.log(chalk.yellow("\u26A0\uFE0F Credentials exist but machine ID is missing"));
|
|
8904
|
-
console.log(chalk.gray(" This can happen if --auth flag was used previously"));
|
|
8905
|
-
console.log(chalk.gray(" Fixing by setting up machine...\n"));
|
|
8906
|
-
}
|
|
8907
|
-
}
|
|
8908
|
-
}
|
|
8909
|
-
try {
|
|
8910
|
-
const result = await authAndSetupMachineIfNeeded({ authMethod });
|
|
8911
|
-
if (!quiet) {
|
|
8912
|
-
console.log(chalk.green("\n\u2713 Authentication successful"));
|
|
8913
|
-
console.log(chalk.gray(` Machine ID: ${result.machineId}`));
|
|
8914
|
-
console.log(chalk.gray(` Server: ${configuration.serverUrl}`));
|
|
8915
|
-
console.log(chalk.gray(` Web app: ${configuration.webappUrl}`));
|
|
8916
|
-
console.log(
|
|
8917
|
-
chalk.gray(
|
|
8918
|
-
`
|
|
8919
|
-
Next: run ${chalk.bold("flockbay start")} to start the daemon (this is what makes your computer appear as connected in the app).`
|
|
8920
|
-
)
|
|
8921
|
-
);
|
|
8922
|
-
}
|
|
8923
|
-
} catch (error) {
|
|
8924
|
-
console.error(chalk.red("Authentication failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
8925
|
-
process.exit(1);
|
|
8926
|
-
}
|
|
8927
|
-
}
|
|
8928
|
-
async function handleAuthLogout() {
|
|
8929
|
-
const dataDir = configuration.flockbayHomeDir;
|
|
8930
|
-
const credentials = await readCredentials();
|
|
8931
|
-
if (!credentials) {
|
|
8932
|
-
console.log(chalk.yellow("Not currently authenticated"));
|
|
8933
|
-
return;
|
|
8934
|
-
}
|
|
8935
|
-
console.log(chalk.blue("This will log you out of Flockbay"));
|
|
8936
|
-
console.log(chalk.yellow("\u26A0\uFE0F You will need to re-authenticate to use Flockbay again"));
|
|
8937
|
-
const rl = createInterface({
|
|
8938
|
-
input: process.stdin,
|
|
8939
|
-
output: process.stdout
|
|
8940
|
-
});
|
|
8941
|
-
const answer = await new Promise((resolve) => {
|
|
8942
|
-
rl.question(chalk.yellow("Are you sure you want to log out? (y/N): "), resolve);
|
|
8943
|
-
});
|
|
8944
|
-
rl.close();
|
|
8945
|
-
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
8946
|
-
try {
|
|
8947
|
-
try {
|
|
8948
|
-
await stopDaemon();
|
|
8949
|
-
console.log(chalk.gray("Stopped daemon"));
|
|
8950
|
-
} catch (err) {
|
|
8951
|
-
console.error("[auth logout] Failed to stop daemon during logout", err);
|
|
8952
|
-
console.log(chalk.yellow("Warning: failed to stop daemon (continuing logout)."));
|
|
8953
|
-
}
|
|
8954
|
-
if (existsSync(dataDir)) {
|
|
8955
|
-
rmSync(dataDir, { recursive: true, force: true });
|
|
8956
|
-
}
|
|
8957
|
-
console.log(chalk.green("\u2713 Successfully logged out"));
|
|
8958
|
-
console.log(chalk.gray(' Run "flockbay auth login" to authenticate again'));
|
|
8959
|
-
} catch (error) {
|
|
8960
|
-
throw new Error(`Failed to logout: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
8961
|
-
}
|
|
8962
|
-
} else {
|
|
8963
|
-
console.log(chalk.blue("Logout cancelled"));
|
|
8964
|
-
}
|
|
8965
|
-
}
|
|
8966
|
-
async function handleAuthShowBackup() {
|
|
8967
|
-
const accessKeyPath = configuration.privateKeyFile;
|
|
8968
|
-
if (!existsSync(accessKeyPath)) {
|
|
8969
|
-
console.log(chalk.red("\u2717 Not authenticated"));
|
|
8970
|
-
console.log(chalk.gray(' Run "flockbay auth login" to authenticate'));
|
|
8971
|
-
process.exit(1);
|
|
8972
|
-
}
|
|
8973
|
-
let parsed;
|
|
8974
|
-
try {
|
|
8975
|
-
parsed = JSON.parse(readFileSync(accessKeyPath, "utf8"));
|
|
8976
|
-
} catch {
|
|
8977
|
-
console.error(chalk.red("Backup key unavailable: failed to parse key file."));
|
|
8978
|
-
process.exit(1);
|
|
8979
|
-
}
|
|
8980
|
-
const machineKeyB64 = parsed?.encryption?.machineKey;
|
|
8981
|
-
if (!machineKeyB64 || typeof machineKeyB64 !== "string") {
|
|
8982
|
-
console.error(chalk.red("Backup key unavailable: encryption.machineKey missing in key file."));
|
|
8983
|
-
process.exit(1);
|
|
8984
|
-
}
|
|
8985
|
-
const buf = Buffer.from(machineKeyB64, "base64");
|
|
8986
|
-
const key = buf.toString("base64url");
|
|
8987
|
-
process.stdout.write(`${key}
|
|
10915
|
+
flockbay auth login [--force] [--clear-machine-id] Sign in and pair this machine to a workspace
|
|
10916
|
+
flockbay auth logout Clear local auth + machine id for this profile
|
|
10917
|
+
flockbay auth status Show current status
|
|
8988
10918
|
`);
|
|
8989
10919
|
}
|
|
8990
|
-
async function handleAuthStatus() {
|
|
8991
|
-
const credentials = await readCredentials();
|
|
8992
|
-
const settings = await readSettings();
|
|
8993
|
-
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
8994
|
-
if (!credentials) {
|
|
8995
|
-
console.log(chalk.red("\u2717 Not authenticated"));
|
|
8996
|
-
console.log(chalk.gray(' Run "flockbay auth login" to authenticate'));
|
|
8997
|
-
return;
|
|
8998
|
-
}
|
|
8999
|
-
console.log(chalk.green("\u2713 Authenticated"));
|
|
9000
|
-
const tokenPreview = credentials.token.substring(0, 30) + "...";
|
|
9001
|
-
console.log(chalk.gray(` Token: ${tokenPreview}`));
|
|
9002
|
-
if (settings?.machineId) {
|
|
9003
|
-
console.log(chalk.green("\u2713 Machine registered"));
|
|
9004
|
-
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
9005
|
-
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
9006
|
-
} else {
|
|
9007
|
-
console.log(chalk.yellow("\u26A0\uFE0F Machine not registered"));
|
|
9008
|
-
console.log(chalk.gray(' Run "flockbay auth login --force" to fix this'));
|
|
9009
|
-
}
|
|
9010
|
-
console.log(chalk.gray(`
|
|
9011
|
-
Data directory: ${configuration.flockbayHomeDir}`));
|
|
9012
|
-
try {
|
|
9013
|
-
const running = await checkIfDaemonRunningAndCleanupStaleState();
|
|
9014
|
-
if (running) {
|
|
9015
|
-
console.log(chalk.green("\u2713 Daemon running"));
|
|
9016
|
-
} else {
|
|
9017
|
-
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
9018
|
-
}
|
|
9019
|
-
} catch {
|
|
9020
|
-
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
9021
|
-
}
|
|
9022
|
-
}
|
|
9023
10920
|
|
|
9024
10921
|
const CLIENT_ID$2 = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
9025
10922
|
const AUTH_BASE_URL = "https://auth.openai.com";
|
|
@@ -9645,6 +11542,15 @@ function looksLikeMachineAuthMismatch(logTail) {
|
|
|
9645
11542
|
const hasMachinesEndpoint = /\/v1\/machines\b/i.test(t);
|
|
9646
11543
|
return has401 && hasMachinesEndpoint;
|
|
9647
11544
|
}
|
|
11545
|
+
function looksLikeServerUnreachable(logTail) {
|
|
11546
|
+
const t = String(logTail || "");
|
|
11547
|
+
if (!t) return null;
|
|
11548
|
+
const codeMatch = t.match(/\b(ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT)\b/) || t.match(/connect\s+(ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT)\b/i);
|
|
11549
|
+
const code = codeMatch?.[1] || codeMatch?.[0] || null;
|
|
11550
|
+
if (!code) return null;
|
|
11551
|
+
const url = t.match(/"url"\s*:\s*"([^"]+)"/)?.[1] ?? null;
|
|
11552
|
+
return { code: String(code), url };
|
|
11553
|
+
}
|
|
9648
11554
|
async function promptYesNo(question, { defaultYes }) {
|
|
9649
11555
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
9650
11556
|
if (!isInteractive) return false;
|
|
@@ -9667,10 +11573,112 @@ async function reauthForCurrentServerKeepingMachineId() {
|
|
|
9667
11573
|
} catch {
|
|
9668
11574
|
}
|
|
9669
11575
|
await clearCredentials();
|
|
9670
|
-
await
|
|
11576
|
+
await loginWithClerkAndPairMachine();
|
|
11577
|
+
}
|
|
11578
|
+
function openUrlBestEffort(url) {
|
|
11579
|
+
const u = String(url || "").trim();
|
|
11580
|
+
if (!u) return;
|
|
11581
|
+
if (process.env.FLOCKBAY_NO_OPEN === "1") return;
|
|
11582
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
11583
|
+
if (!isInteractive) return;
|
|
11584
|
+
try {
|
|
11585
|
+
if (process.platform === "darwin") {
|
|
11586
|
+
const p2 = spawn("open", [u], { detached: true, stdio: "ignore" });
|
|
11587
|
+
p2.unref();
|
|
11588
|
+
return;
|
|
11589
|
+
}
|
|
11590
|
+
if (process.platform === "win32") {
|
|
11591
|
+
const p2 = spawn("cmd", ["/c", "start", "", u], { detached: true, stdio: "ignore" });
|
|
11592
|
+
p2.unref();
|
|
11593
|
+
return;
|
|
11594
|
+
}
|
|
11595
|
+
const p = spawn("xdg-open", [u], { detached: true, stdio: "ignore" });
|
|
11596
|
+
p.unref();
|
|
11597
|
+
} catch {
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
async function fetchDaemonStatus() {
|
|
11601
|
+
const state = await readDaemonState().catch(() => null);
|
|
11602
|
+
if (!state?.httpPort || !state?.pid) return null;
|
|
11603
|
+
try {
|
|
11604
|
+
process.kill(state.pid, 0);
|
|
11605
|
+
} catch {
|
|
11606
|
+
return null;
|
|
11607
|
+
}
|
|
11608
|
+
try {
|
|
11609
|
+
const res = await fetch(`http://127.0.0.1:${state.httpPort}/status`, {
|
|
11610
|
+
method: "POST",
|
|
11611
|
+
headers: { "Content-Type": "application/json" },
|
|
11612
|
+
body: "{}",
|
|
11613
|
+
signal: AbortSignal.timeout(1500)
|
|
11614
|
+
});
|
|
11615
|
+
if (!res.ok) return null;
|
|
11616
|
+
return await res.json().catch(() => null);
|
|
11617
|
+
} catch {
|
|
11618
|
+
return null;
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
async function waitForDaemonConnected(timeoutMs) {
|
|
11622
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
11623
|
+
while (Date.now() < deadline) {
|
|
11624
|
+
const status = await fetchDaemonStatus();
|
|
11625
|
+
const connected = Boolean(status?.connection?.connected);
|
|
11626
|
+
if (connected) return status;
|
|
11627
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
11628
|
+
}
|
|
11629
|
+
return fetchDaemonStatus();
|
|
9671
11630
|
}
|
|
9672
11631
|
async function startDaemonDetachedOrExit(opts) {
|
|
9673
|
-
const
|
|
11632
|
+
const alreadyRunning = await checkIfDaemonRunningAndCleanupStaleState().catch(() => false);
|
|
11633
|
+
if (alreadyRunning) {
|
|
11634
|
+
const versionMatches = await isDaemonRunningCurrentCliVersion().catch(() => true);
|
|
11635
|
+
if (!versionMatches) {
|
|
11636
|
+
await stopDaemon();
|
|
11637
|
+
} else {
|
|
11638
|
+
const status = await waitForDaemonConnected(5e3);
|
|
11639
|
+
const connected = Boolean(status?.connection?.connected);
|
|
11640
|
+
const lastUpsertStatus = Number.isFinite(Number(status?.connection?.lastHttpUpsertStatus)) ? Number(status.connection.lastHttpUpsertStatus) : null;
|
|
11641
|
+
const lastConnectError = typeof status?.connection?.lastConnectError === "string" ? status.connection.lastConnectError : "";
|
|
11642
|
+
if (connected) {
|
|
11643
|
+
const auth = await readCredentials().catch(() => null);
|
|
11644
|
+
const settings = await readSettings().catch(() => null);
|
|
11645
|
+
const daemon = await readDaemonState().catch(() => null);
|
|
11646
|
+
console.log(chalk.bold("\nFlockbay ready\n"));
|
|
11647
|
+
console.log(chalk.gray(`Profile: ${configuration.profile}`));
|
|
11648
|
+
console.log(chalk.gray(`Server: ${configuration.serverUrl}`));
|
|
11649
|
+
console.log(chalk.gray(`Web app: ${configuration.webappUrl}`));
|
|
11650
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
11651
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
11652
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
11653
|
+
console.log("");
|
|
11654
|
+
openUrlBestEffort(configuration.webappUrl);
|
|
11655
|
+
process.exit(0);
|
|
11656
|
+
}
|
|
11657
|
+
const authMismatch = lastUpsertStatus === 401 || /unauthorized/i.test(lastConnectError);
|
|
11658
|
+
if (authMismatch && !opts?.reauthAttempted) {
|
|
11659
|
+
console.error("");
|
|
11660
|
+
console.error(chalk.yellow("Your saved CLI token was rejected by the server (401/unauthorized)."));
|
|
11661
|
+
console.error(chalk.gray("This is common in local dev if the backend store was reset/rebuilt."));
|
|
11662
|
+
console.error("");
|
|
11663
|
+
const shouldReauth = await promptYesNo("Re-authenticate now and retry?", { defaultYes: true });
|
|
11664
|
+
if (shouldReauth) {
|
|
11665
|
+
await reauthForCurrentServerKeepingMachineId();
|
|
11666
|
+
await startDaemonDetachedOrExit({ reauthAttempted: true });
|
|
11667
|
+
return;
|
|
11668
|
+
}
|
|
11669
|
+
process.exit(1);
|
|
11670
|
+
}
|
|
11671
|
+
console.error("");
|
|
11672
|
+
console.error(chalk.red("Daemon is running but not connected to the server."));
|
|
11673
|
+
if (typeof status?.connection?.lastHttpUpsertError === "string" && status.connection.lastHttpUpsertError.trim()) {
|
|
11674
|
+
console.error(chalk.gray(`Last upsert error: ${status.connection.lastHttpUpsertError.trim()}`));
|
|
11675
|
+
}
|
|
11676
|
+
if (lastConnectError) console.error(chalk.gray(`Last connect error: ${lastConnectError}`));
|
|
11677
|
+
console.error(chalk.gray("Tip: if the backend is restarting, wait a moment and re-run `flockbay start`."));
|
|
11678
|
+
process.exit(1);
|
|
11679
|
+
}
|
|
11680
|
+
}
|
|
11681
|
+
const child = spawnFlockbayCLI(["daemon", "start-sync", "--profile", configuration.profile], {
|
|
9674
11682
|
detached: true,
|
|
9675
11683
|
stdio: "ignore",
|
|
9676
11684
|
env: process.env
|
|
@@ -9685,7 +11693,28 @@ async function startDaemonDetachedOrExit(opts) {
|
|
|
9685
11693
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
9686
11694
|
}
|
|
9687
11695
|
if (started) {
|
|
9688
|
-
|
|
11696
|
+
const status = await waitForDaemonConnected(5e3);
|
|
11697
|
+
if (!Boolean(status?.connection?.connected)) {
|
|
11698
|
+
const lastUpsertError = typeof status?.connection?.lastHttpUpsertError === "string" ? status.connection.lastHttpUpsertError.trim() : "";
|
|
11699
|
+
const lastConnectError = typeof status?.connection?.lastConnectError === "string" ? status.connection.lastConnectError.trim() : "";
|
|
11700
|
+
console.error(chalk.red("Daemon started, but failed to connect to the server."));
|
|
11701
|
+
if (lastUpsertError) console.error(chalk.gray(`Last upsert error: ${lastUpsertError}`));
|
|
11702
|
+
if (lastConnectError) console.error(chalk.gray(`Last connect error: ${lastConnectError}`));
|
|
11703
|
+
console.error(chalk.gray("Tip: if the backend is restarting, wait a moment and re-run `flockbay start`."));
|
|
11704
|
+
process.exit(1);
|
|
11705
|
+
}
|
|
11706
|
+
const auth = await readCredentials().catch(() => null);
|
|
11707
|
+
const settings = await readSettings().catch(() => null);
|
|
11708
|
+
const daemon = await readDaemonState().catch(() => null);
|
|
11709
|
+
console.log(chalk.bold("\nFlockbay ready\n"));
|
|
11710
|
+
console.log(chalk.gray(`Profile: ${configuration.profile}`));
|
|
11711
|
+
console.log(chalk.gray(`Server: ${configuration.serverUrl}`));
|
|
11712
|
+
console.log(chalk.gray(`Web app: ${configuration.webappUrl}`));
|
|
11713
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
11714
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
11715
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
11716
|
+
console.log("");
|
|
11717
|
+
openUrlBestEffort(configuration.webappUrl);
|
|
9689
11718
|
process.exit(0);
|
|
9690
11719
|
} else {
|
|
9691
11720
|
const latest = await getLatestDaemonLog();
|
|
@@ -9706,6 +11735,19 @@ async function startDaemonDetachedOrExit(opts) {
|
|
|
9706
11735
|
console.error(chalk.gray("Fix: run `flockbay start` to re-authenticate and restart the daemon."));
|
|
9707
11736
|
process.exit(1);
|
|
9708
11737
|
}
|
|
11738
|
+
const unreachable = looksLikeServerUnreachable(logTail);
|
|
11739
|
+
if (unreachable) {
|
|
11740
|
+
console.error("");
|
|
11741
|
+
console.error(chalk.red(`Cannot reach server while starting daemon (${unreachable.code}).`));
|
|
11742
|
+
console.error(chalk.gray(`Server: ${configuration.serverUrl}`));
|
|
11743
|
+
if (unreachable.url) console.error(chalk.gray(`Failed request: ${unreachable.url}`));
|
|
11744
|
+
if (/^https?:\/\/(127\.0\.0\.1|localhost)\b/i.test(configuration.serverUrl)) {
|
|
11745
|
+
console.error("");
|
|
11746
|
+
console.error(chalk.gray("Local dev fix: start the backend first, then re-run `flockbay start`:"));
|
|
11747
|
+
console.error(chalk.gray(" cd backend && npm run dev"));
|
|
11748
|
+
}
|
|
11749
|
+
process.exit(1);
|
|
11750
|
+
}
|
|
9709
11751
|
console.error("Failed to start daemon");
|
|
9710
11752
|
console.error("Tip: run `flockbay auth status` to confirm you are logged in.");
|
|
9711
11753
|
process.exit(1);
|
|
@@ -9716,10 +11758,10 @@ function showStartHelp() {
|
|
|
9716
11758
|
${chalk.bold("flockbay start")} - One-command setup
|
|
9717
11759
|
|
|
9718
11760
|
${chalk.bold("Usage:")}
|
|
9719
|
-
flockbay start [--engine-root <path>]
|
|
11761
|
+
flockbay start [--engine-root <path>] [--profile <name>] Ensure login + daemon running
|
|
9720
11762
|
|
|
9721
11763
|
${chalk.bold("Options:")}
|
|
9722
|
-
|
|
11764
|
+
--profile <name> CLI profile (isolates server/workspace/machine state)
|
|
9723
11765
|
--engine-root <path> Unreal Engine install folder (UE_5.5\u2013UE_5.7)
|
|
9724
11766
|
--skip-unreal Skip Unreal bridge install
|
|
9725
11767
|
`);
|
|
@@ -9731,6 +11773,10 @@ function readArgValue(args, key) {
|
|
|
9731
11773
|
if (!value || value.startsWith("-")) return null;
|
|
9732
11774
|
return value;
|
|
9733
11775
|
}
|
|
11776
|
+
async function authAndSetupMachineIfNeeded() {
|
|
11777
|
+
const { auth, machineId } = await ensureMachineAuthOrLogin();
|
|
11778
|
+
return { credentials: auth, machineId };
|
|
11779
|
+
}
|
|
9734
11780
|
(async () => {
|
|
9735
11781
|
const invokedCwd = process.env.FLOCKBAY_INVOKED_CWD;
|
|
9736
11782
|
if (invokedCwd?.trim()) {
|
|
@@ -9751,6 +11797,9 @@ function readArgValue(args, key) {
|
|
|
9751
11797
|
args = args.slice(1);
|
|
9752
11798
|
subcommand = args[0];
|
|
9753
11799
|
}
|
|
11800
|
+
if (subcommand === "setup") {
|
|
11801
|
+
subcommand = "start";
|
|
11802
|
+
}
|
|
9754
11803
|
if (!args.includes("--version")) ;
|
|
9755
11804
|
if (subcommand === "doctor") {
|
|
9756
11805
|
if (args[1] === "clean") {
|
|
@@ -9774,6 +11823,43 @@ function readArgValue(args, key) {
|
|
|
9774
11823
|
process.exit(1);
|
|
9775
11824
|
}
|
|
9776
11825
|
return;
|
|
11826
|
+
} else if (subcommand === "login") {
|
|
11827
|
+
try {
|
|
11828
|
+
await loginWithClerkAndPairMachine();
|
|
11829
|
+
} catch (error) {
|
|
11830
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
11831
|
+
if (process.env.DEBUG) console.error(error);
|
|
11832
|
+
process.exit(1);
|
|
11833
|
+
}
|
|
11834
|
+
return;
|
|
11835
|
+
} else if (subcommand === "status") {
|
|
11836
|
+
const auth = await readCredentials();
|
|
11837
|
+
const settings = await readSettings();
|
|
11838
|
+
const daemon = await readDaemonState();
|
|
11839
|
+
console.log(chalk.bold("\nFlockbay Status\n"));
|
|
11840
|
+
console.log(chalk.gray(`Profile: ${configuration.profile}`));
|
|
11841
|
+
console.log(chalk.gray(`Server: ${configuration.serverUrl}`));
|
|
11842
|
+
console.log(chalk.gray(`Web app: ${configuration.webappUrl}`));
|
|
11843
|
+
console.log(chalk.gray(`Machine: ${String(settings?.machineId || "missing")}`));
|
|
11844
|
+
console.log(chalk.gray(`Workspace:${auth?.orgId ? ` ${auth.orgId}` : " missing"}`));
|
|
11845
|
+
console.log(chalk.gray(`Auth: ${auth?.machineToken ? "paired" : "not paired"}`));
|
|
11846
|
+
console.log(chalk.gray(`Daemon: ${daemon?.pid ? `pid=${daemon.pid} port=${daemon.httpPort}` : "not running"}`));
|
|
11847
|
+
process.exit(0);
|
|
11848
|
+
} else if (subcommand === "reset") {
|
|
11849
|
+
const force = args.includes("--force");
|
|
11850
|
+
if (!force) {
|
|
11851
|
+
console.error(chalk.red("Refusing to reset without --force"));
|
|
11852
|
+
console.error(chalk.gray("This clears local auth + machine binding for this profile."));
|
|
11853
|
+
process.exit(1);
|
|
11854
|
+
}
|
|
11855
|
+
try {
|
|
11856
|
+
await stopDaemon();
|
|
11857
|
+
} catch {
|
|
11858
|
+
}
|
|
11859
|
+
await clearCredentials();
|
|
11860
|
+
await clearMachineId();
|
|
11861
|
+
console.log("Reset complete");
|
|
11862
|
+
process.exit(0);
|
|
9777
11863
|
} else if (subcommand === "connect") {
|
|
9778
11864
|
try {
|
|
9779
11865
|
await handleConnectCommand(args.slice(1));
|
|
@@ -9785,22 +11871,14 @@ function readArgValue(args, key) {
|
|
|
9785
11871
|
process.exit(1);
|
|
9786
11872
|
}
|
|
9787
11873
|
return;
|
|
9788
|
-
} else if (subcommand === "start"
|
|
9789
|
-
if (subcommand === "setup") {
|
|
9790
|
-
console.log(chalk.yellow("Note: `flockbay setup` was renamed to `flockbay start`."));
|
|
9791
|
-
console.log(chalk.gray("Running `flockbay start`...\n"));
|
|
9792
|
-
}
|
|
11874
|
+
} else if (subcommand === "start") {
|
|
9793
11875
|
const startArgs = args.slice(1);
|
|
9794
11876
|
if (startArgs.includes("--help") || startArgs.includes("-h") || startArgs.includes("help")) {
|
|
9795
11877
|
showStartHelp();
|
|
9796
11878
|
return;
|
|
9797
11879
|
}
|
|
9798
11880
|
try {
|
|
9799
|
-
|
|
9800
|
-
await checkIfDaemonRunningAndCleanupStaleState();
|
|
9801
|
-
await stopDaemon();
|
|
9802
|
-
} catch {
|
|
9803
|
-
}
|
|
11881
|
+
await ensureMachineAuthOrLogin();
|
|
9804
11882
|
const skipUnreal = startArgs.includes("--skip-unreal");
|
|
9805
11883
|
if (!skipUnreal) {
|
|
9806
11884
|
const engineRoot = readArgValue(startArgs, "--engine-root") || (process.env.UE_ENGINE_ROOT || "").trim() || (process.env.ENGINE_ROOT || "").trim() || null;
|
|
@@ -9822,7 +11900,6 @@ ${engineRoot}`, {
|
|
|
9822
11900
|
}
|
|
9823
11901
|
}
|
|
9824
11902
|
}
|
|
9825
|
-
await handleAuthCommand(["login", "--force", "--quiet"]);
|
|
9826
11903
|
await startDaemonDetachedOrExit();
|
|
9827
11904
|
} catch (error) {
|
|
9828
11905
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -9835,17 +11912,20 @@ ${engineRoot}`, {
|
|
|
9835
11912
|
} else if (subcommand === "codex") {
|
|
9836
11913
|
try {
|
|
9837
11914
|
await chdirToNearestUprojectRootIfPresent();
|
|
9838
|
-
const { runCodex } = await import('./runCodex-
|
|
11915
|
+
const { runCodex } = await import('./runCodex-Di9eHddq.mjs');
|
|
9839
11916
|
let startedBy = void 0;
|
|
11917
|
+
let sessionId = void 0;
|
|
9840
11918
|
for (let i = 1; i < args.length; i++) {
|
|
9841
11919
|
if (args[i] === "--started-by") {
|
|
9842
11920
|
startedBy = args[++i];
|
|
11921
|
+
} else if (args[i] === "--flockbay-session-id") {
|
|
11922
|
+
sessionId = args[++i];
|
|
9843
11923
|
}
|
|
9844
11924
|
}
|
|
9845
11925
|
const {
|
|
9846
11926
|
credentials
|
|
9847
11927
|
} = await authAndSetupMachineIfNeeded();
|
|
9848
|
-
await runCodex({ credentials, startedBy });
|
|
11928
|
+
await runCodex({ credentials, startedBy, sessionId });
|
|
9849
11929
|
} catch (error) {
|
|
9850
11930
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
9851
11931
|
if (process.env.DEBUG) {
|
|
@@ -9927,11 +12007,14 @@ ${engineRoot}`, {
|
|
|
9927
12007
|
}
|
|
9928
12008
|
try {
|
|
9929
12009
|
await chdirToNearestUprojectRootIfPresent();
|
|
9930
|
-
const { runGemini } = await import('./runGemini-
|
|
12010
|
+
const { runGemini } = await import('./runGemini-BS6sBU_V.mjs');
|
|
9931
12011
|
let startedBy = void 0;
|
|
12012
|
+
let sessionId = void 0;
|
|
9932
12013
|
for (let i = 1; i < args.length; i++) {
|
|
9933
12014
|
if (args[i] === "--started-by") {
|
|
9934
12015
|
startedBy = args[++i];
|
|
12016
|
+
} else if (args[i] === "--flockbay-session-id") {
|
|
12017
|
+
sessionId = args[++i];
|
|
9935
12018
|
}
|
|
9936
12019
|
}
|
|
9937
12020
|
const {
|
|
@@ -9948,19 +12031,7 @@ ${engineRoot}`, {
|
|
|
9948
12031
|
daemonProcess.unref();
|
|
9949
12032
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
9950
12033
|
}
|
|
9951
|
-
await runGemini({ credentials, startedBy });
|
|
9952
|
-
} catch (error) {
|
|
9953
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
9954
|
-
if (process.env.DEBUG) {
|
|
9955
|
-
console.error(error);
|
|
9956
|
-
}
|
|
9957
|
-
process.exit(1);
|
|
9958
|
-
}
|
|
9959
|
-
return;
|
|
9960
|
-
} else if (subcommand === "logout") {
|
|
9961
|
-
console.log(chalk.yellow('Note: "logout" is deprecated. Use "flockbay auth logout" instead.\n'));
|
|
9962
|
-
try {
|
|
9963
|
-
await handleAuthCommand(["logout"]);
|
|
12034
|
+
await runGemini({ credentials, startedBy, sessionId });
|
|
9964
12035
|
} catch (error) {
|
|
9965
12036
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
9966
12037
|
if (process.env.DEBUG) {
|
|
@@ -10089,6 +12160,8 @@ ${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("flockbay docto
|
|
|
10089
12160
|
unknownArgs.push("--dangerously-skip-permissions");
|
|
10090
12161
|
} else if (arg === "--started-by") {
|
|
10091
12162
|
options.startedBy = args[++i];
|
|
12163
|
+
} else if (arg === "--flockbay-session-id") {
|
|
12164
|
+
options.sessionId = args[++i];
|
|
10092
12165
|
} else if (arg === "--claude-env") {
|
|
10093
12166
|
const envArg = args[++i];
|
|
10094
12167
|
if (envArg && envArg.includes("=")) {
|
|
@@ -10265,4 +12338,4 @@ ${chalk.bold("Examples:")}
|
|
|
10265
12338
|
}
|
|
10266
12339
|
}
|
|
10267
12340
|
|
|
10268
|
-
export { ElicitationHub as E, MessageQueue2 as M, PLATFORM_SYSTEM_PROMPT as P, setLatestUserImages as a, MessageBuffer as b, consumeToolQuota as c, startFlockbayServer as d, detectUnrealProject as e, formatQuotaDeniedReason as f, buildProjectCapsule as g, hashObject as h, initialMachineMetadata as i, autoFinalizeCoordinationWorkItem as j,
|
|
12341
|
+
export { ElicitationHub as E, MessageQueue2 as M, PLATFORM_SYSTEM_PROMPT as P, setLatestUserImages as a, MessageBuffer as b, consumeToolQuota as c, startFlockbayServer as d, detectUnrealProject as e, formatQuotaDeniedReason as f, buildProjectCapsule as g, hashObject as h, initialMachineMetadata as i, autoFinalizeCoordinationWorkItem as j, detectScreenshotsForGate as k, applyCoordinationSideEffectsFromMcpToolResult as l, stopCaffeinate as m, notifyDaemonSessionStarted as n, extractUserImagesMarker as o, getLatestUserImages as p, registerKillSessionHandler as r, shouldCountToolCall as s, trimIdent as t, withUserImagesMarker as w };
|