flockbay 0.10.15
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/README.md +56 -0
- package/bin/flockbay-mcp.mjs +56 -0
- package/bin/flockbay.mjs +78 -0
- package/dist/codex/flockbayMcpStdioBridge.cjs +383 -0
- package/dist/codex/flockbayMcpStdioBridge.d.cts +2 -0
- package/dist/codex/flockbayMcpStdioBridge.d.mts +2 -0
- package/dist/codex/flockbayMcpStdioBridge.mjs +381 -0
- package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +136 -0
- package/dist/flockbayScreenshotGate-DkxU24cR.cjs +138 -0
- package/dist/index--o4BPz5o.cjs +10311 -0
- package/dist/index-CUp3juDS.mjs +10268 -0
- package/dist/index.cjs +43 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +40 -0
- package/dist/lib.cjs +33 -0
- package/dist/lib.d.cts +957 -0
- package/dist/lib.d.mts +957 -0
- package/dist/lib.mjs +23 -0
- package/dist/runCodex-D3eT-TvB.cjs +3449 -0
- package/dist/runCodex-o6PCbHQ7.mjs +3446 -0
- package/dist/runGemini-Bt0oEj_g.mjs +3183 -0
- package/dist/runGemini-CBxZp6I7.cjs +3185 -0
- package/dist/types-C-jnUdn_.cjs +4498 -0
- package/dist/types-DGd6ea2Z.mjs +4450 -0
- package/kits/kit.open_world/kit.json +59 -0
- package/package.json +130 -0
- package/scripts/claude_local_launcher.cjs +73 -0
- package/scripts/claude_remote_launcher.cjs +16 -0
- package/scripts/claude_version_utils.cjs +391 -0
- package/scripts/ripgrep_launcher.cjs +33 -0
- package/scripts/session_hook_forwarder.cjs +49 -0
- package/scripts/test-codex-abort-history.mjs +77 -0
- package/scripts/unpack-tools.cjs +222 -0
- package/tools/licenses/difftastic-LICENSE +21 -0
- package/tools/licenses/ripgrep-LICENSE +3 -0
- package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -0
- package/tools/unreal-mcp/upstream/Docs/README.md +8 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/README.md +7 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +184 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Config/FilterPlugin.ini +8 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1160 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +924 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +709 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +896 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPProjectCommands.cpp +72 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPUMGCommands.cpp +544 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +321 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +419 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPModule.cpp +21 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +34 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +27 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +40 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/MCPServerRunnable.h +34 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPBridge.h +64 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPModule.h +22 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +78 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/UnrealMCP.uplugin +36 -0
- package/tools/unreal-mcp/upstream/Python/README.md +40 -0
- package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -0
- package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +203 -0
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +497 -0
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +194 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +267 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +618 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +366 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +390 -0
- package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +420 -0
- package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -0
- package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +430 -0
- package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +64 -0
- package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +333 -0
- package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -0
- package/tools/unreal-mcp/upstream/Python/uv.lock +521 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
function getToolCallTimeoutMs(name, args) {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
let url = null;
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (a === "--url" && i + 1 < argv.length) {
|
|
15
|
+
url = argv[i + 1];
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { url };
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
const { url: urlFromArgs } = parseArgs(process.argv.slice(2));
|
|
23
|
+
const baseUrl = urlFromArgs || process.env.FLOCKBAY_HTTP_MCP_URL || "";
|
|
24
|
+
if (!baseUrl) {
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
"[flockbay-mcp] Missing target URL. Set FLOCKBAY_HTTP_MCP_URL or pass --url <http://127.0.0.1:PORT>\n"
|
|
27
|
+
);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
let httpClient = null;
|
|
31
|
+
async function ensureHttpClient() {
|
|
32
|
+
if (httpClient) return httpClient;
|
|
33
|
+
const client = new Client(
|
|
34
|
+
{ name: "flockbay-stdio-bridge", version: "1.0.0" },
|
|
35
|
+
{ capabilities: {} }
|
|
36
|
+
);
|
|
37
|
+
const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
|
|
38
|
+
await client.connect(transport);
|
|
39
|
+
httpClient = client;
|
|
40
|
+
return client;
|
|
41
|
+
}
|
|
42
|
+
const server = new McpServer({
|
|
43
|
+
name: "Flockbay MCP Bridge",
|
|
44
|
+
version: "1.0.0"
|
|
45
|
+
});
|
|
46
|
+
function forwardTool(name, inputSchema, title, description) {
|
|
47
|
+
server.registerTool(
|
|
48
|
+
name,
|
|
49
|
+
{ title, description, inputSchema },
|
|
50
|
+
async (args) => {
|
|
51
|
+
try {
|
|
52
|
+
const client = await ensureHttpClient();
|
|
53
|
+
const timeout = getToolCallTimeoutMs(name, args);
|
|
54
|
+
const response = await client.callTool(
|
|
55
|
+
{ name, arguments: args },
|
|
56
|
+
void 0,
|
|
57
|
+
timeout ? { timeout } : void 0
|
|
58
|
+
);
|
|
59
|
+
return response;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{ type: "text", text: `Failed to call tool "${name}": ${error instanceof Error ? error.message : String(error)}` }
|
|
64
|
+
],
|
|
65
|
+
isError: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
forwardTool(
|
|
72
|
+
"change_title",
|
|
73
|
+
{ title: z.string().describe("The new title for the chat session") },
|
|
74
|
+
"Change Chat Title",
|
|
75
|
+
"Change the title of the current chat session"
|
|
76
|
+
);
|
|
77
|
+
forwardTool(
|
|
78
|
+
"latest_user_images",
|
|
79
|
+
{
|
|
80
|
+
limit: z.number().int().positive().optional().describe("Max number of images to return (default 4).")
|
|
81
|
+
},
|
|
82
|
+
"Latest User Images",
|
|
83
|
+
"Return the latest image attachments sent by the user (as image content blocks)."
|
|
84
|
+
);
|
|
85
|
+
forwardTool(
|
|
86
|
+
"read_images",
|
|
87
|
+
{
|
|
88
|
+
paths: z.array(z.string()).describe("Image paths (absolute or relative to the session directory). PNG only."),
|
|
89
|
+
limit: z.number().int().positive().optional().describe("Max number of images to include (default 10)."),
|
|
90
|
+
upload: z.boolean().optional().describe("Upload images to the session screenshots store and return HTTPS URLs (default true)."),
|
|
91
|
+
includeBase64: z.boolean().optional().describe("Include base64 data in the payload (default false)."),
|
|
92
|
+
maxBytesPerImage: z.number().int().positive().optional().describe("Max bytes per image when includeBase64=true (default 2500000).")
|
|
93
|
+
},
|
|
94
|
+
"Read Images",
|
|
95
|
+
"Read one or more local PNG images by path and return a `{ views: [...] }` payload for the UI."
|
|
96
|
+
);
|
|
97
|
+
forwardTool(
|
|
98
|
+
"ask_user_question",
|
|
99
|
+
{
|
|
100
|
+
questions: z.array(
|
|
101
|
+
z.object({
|
|
102
|
+
key: z.string().optional().describe("Stable key for this question (defaults to header)."),
|
|
103
|
+
header: z.string().describe("Short label for the question."),
|
|
104
|
+
question: z.string().describe("The question to show to the user."),
|
|
105
|
+
options: z.array(z.object({
|
|
106
|
+
label: z.string().describe("Option label"),
|
|
107
|
+
description: z.string().optional().describe("Optional short description")
|
|
108
|
+
})).min(1).describe("Available choices"),
|
|
109
|
+
multiSelect: z.boolean().optional().describe("Allow multiple selections")
|
|
110
|
+
})
|
|
111
|
+
).describe("Questions to ask the user")
|
|
112
|
+
},
|
|
113
|
+
"Ask User Question",
|
|
114
|
+
"Ask the user one or more multiple-choice questions and return structured answers."
|
|
115
|
+
);
|
|
116
|
+
forwardTool(
|
|
117
|
+
"coordination_ledger_snapshot",
|
|
118
|
+
{},
|
|
119
|
+
"Coordination Ledger Snapshot",
|
|
120
|
+
"Fetch the current coordination ledger snapshot (work items + intent) for this project."
|
|
121
|
+
);
|
|
122
|
+
forwardTool(
|
|
123
|
+
"ledger_read",
|
|
124
|
+
{},
|
|
125
|
+
"Ledger Read",
|
|
126
|
+
"Alias for coordination_ledger_snapshot: fetch the current shared coordination ledger snapshot for this project."
|
|
127
|
+
);
|
|
128
|
+
forwardTool(
|
|
129
|
+
"coordination_update_intent",
|
|
130
|
+
{
|
|
131
|
+
summary: z.string().optional().describe("Optional short summary/title for this work item."),
|
|
132
|
+
status: z.string().optional().describe("Optional work item status (e.g. wip, blocked)."),
|
|
133
|
+
plannedFiles: z.array(z.string()).optional().describe("Repo-relative file paths you plan to touch."),
|
|
134
|
+
activeFiles: z.array(z.string()).optional().describe("Repo-relative file paths you are actively editing right now."),
|
|
135
|
+
doneFiles: z.array(z.string()).optional().describe("Repo-relative file paths you are done with (safe for others to start)."),
|
|
136
|
+
waitingOnFiles: z.array(z.string()).optional().describe("Repo-relative file paths you are waiting on (owned/planned by others)."),
|
|
137
|
+
leaseMs: z.number().int().positive().optional().describe("Soft lease TTL for this ledger entry (ms). UI-only staleness hint; default ~2h.")
|
|
138
|
+
},
|
|
139
|
+
"Coordination Update Intent",
|
|
140
|
+
"Update this work item\u2019s coordination intent (planned/active/done/waiting). Communication-only (trust-first), not an enforced lock."
|
|
141
|
+
);
|
|
142
|
+
forwardTool(
|
|
143
|
+
"coordination_check_files",
|
|
144
|
+
{
|
|
145
|
+
files: z.array(z.string()).describe('Repo-relative file paths to check (e.g. "Config/DefaultGame.ini").'),
|
|
146
|
+
includeSelf: z.boolean().optional().describe("Include this work item in results (default true).")
|
|
147
|
+
},
|
|
148
|
+
"Coordination Check Files",
|
|
149
|
+
"Check which work items currently claim the given files (planned/active), ignoring stale/done entries."
|
|
150
|
+
);
|
|
151
|
+
forwardTool(
|
|
152
|
+
"coordination_claim_files",
|
|
153
|
+
{
|
|
154
|
+
files: z.array(z.string()).describe("Repo-relative file paths to claim."),
|
|
155
|
+
mode: z.enum(["planned", "active", "planned_and_active"]).optional().describe("How to claim: planned or active (default active)."),
|
|
156
|
+
leaseMs: z.number().int().positive().optional().describe("Soft lease TTL for this ledger entry (ms). Used to avoid stale blocks; default ~2h.")
|
|
157
|
+
},
|
|
158
|
+
"Coordination Claim Files",
|
|
159
|
+
"Atomically claim files for this work item if they are not currently owned by another active work item."
|
|
160
|
+
);
|
|
161
|
+
forwardTool(
|
|
162
|
+
"ledger_claim",
|
|
163
|
+
{
|
|
164
|
+
summary: z.string().optional().describe("Optional short summary/title for this work item."),
|
|
165
|
+
status: z.string().optional().describe("Optional work item status (e.g. wip, blocked)."),
|
|
166
|
+
plannedFiles: z.array(z.string()).optional().describe("Repo-relative file paths you plan to touch."),
|
|
167
|
+
files: z.array(z.string()).describe("Repo-relative file paths to claim."),
|
|
168
|
+
mode: z.enum(["planned", "active", "planned_and_active"]).optional().describe("How to claim: planned or active (default active)."),
|
|
169
|
+
leaseMs: z.number().int().positive().optional().describe("Soft lease TTL for this ledger entry (ms). Used to avoid stale blocks; default ~2h.")
|
|
170
|
+
},
|
|
171
|
+
"Ledger Claim",
|
|
172
|
+
"Alias for intent+claim: updates intent (optional) then atomically claims files for this work item."
|
|
173
|
+
);
|
|
174
|
+
forwardTool(
|
|
175
|
+
"ledger_release",
|
|
176
|
+
{
|
|
177
|
+
status: z.string().optional().describe('Status to set (default "done").'),
|
|
178
|
+
doneFiles: z.array(z.string()).optional().describe("Repo-relative file paths you finished (will be unioned with existing doneFiles)."),
|
|
179
|
+
clearActive: z.boolean().optional().describe("If true, clears activeFiles (default true)."),
|
|
180
|
+
clearPlanned: z.boolean().optional().describe("If true, clears plannedFiles too (default false)."),
|
|
181
|
+
commitHash: z.string().optional().describe("Optional commit hash to record on the work item.")
|
|
182
|
+
},
|
|
183
|
+
"Ledger Release",
|
|
184
|
+
"Mark work item complete/released: union doneFiles, clear active (and optionally planned), and set status (default done)."
|
|
185
|
+
);
|
|
186
|
+
forwardTool(
|
|
187
|
+
"docs_index_read",
|
|
188
|
+
{},
|
|
189
|
+
"Docs Index Read",
|
|
190
|
+
"Read the required game Documentation index (index.md) and a compact tree summary."
|
|
191
|
+
);
|
|
192
|
+
forwardTool(
|
|
193
|
+
"docs_tree",
|
|
194
|
+
{},
|
|
195
|
+
"Docs Tree",
|
|
196
|
+
"Fetch the Documentation Library tree (folders + docs metadata; no bodies)."
|
|
197
|
+
);
|
|
198
|
+
forwardTool(
|
|
199
|
+
"docs_get",
|
|
200
|
+
{
|
|
201
|
+
nodeId: z.string().describe("Document node id.")
|
|
202
|
+
},
|
|
203
|
+
"Docs Get",
|
|
204
|
+
"Fetch a single Documentation document node (includes markdown body)."
|
|
205
|
+
);
|
|
206
|
+
forwardTool(
|
|
207
|
+
"docs_search",
|
|
208
|
+
{
|
|
209
|
+
q: z.string().describe("Search query."),
|
|
210
|
+
limit: z.number().int().positive().optional().describe("Max results (default 50).")
|
|
211
|
+
},
|
|
212
|
+
"Docs Search",
|
|
213
|
+
"Search documentation by title/body (server-side)."
|
|
214
|
+
);
|
|
215
|
+
forwardTool(
|
|
216
|
+
"docs_create_folder",
|
|
217
|
+
{
|
|
218
|
+
name: z.string().describe("Folder name."),
|
|
219
|
+
parentId: z.string().optional().describe("Parent folder node id (defaults to root).")
|
|
220
|
+
},
|
|
221
|
+
"Docs Create Folder",
|
|
222
|
+
"Create a folder node in the Documentation Library."
|
|
223
|
+
);
|
|
224
|
+
forwardTool(
|
|
225
|
+
"docs_create_doc",
|
|
226
|
+
{
|
|
227
|
+
name: z.string().describe("Doc name (e.g. Design.md)."),
|
|
228
|
+
parentId: z.string().optional().describe("Parent folder node id (defaults to root)."),
|
|
229
|
+
bodyMarkdown: z.string().optional().describe("Markdown body."),
|
|
230
|
+
metadata: z.record(z.any()).optional().describe("Metadata object (JSON).")
|
|
231
|
+
},
|
|
232
|
+
"Docs Create Doc",
|
|
233
|
+
"Create a document node in the Documentation Library."
|
|
234
|
+
);
|
|
235
|
+
forwardTool(
|
|
236
|
+
"docs_claim",
|
|
237
|
+
{
|
|
238
|
+
nodeId: z.string().describe("Document node id."),
|
|
239
|
+
leaseMs: z.number().int().positive().optional().describe("Lease TTL (ms).")
|
|
240
|
+
},
|
|
241
|
+
"Docs Claim",
|
|
242
|
+
"Claim a documentation document for editing (exclusive lease)."
|
|
243
|
+
);
|
|
244
|
+
forwardTool(
|
|
245
|
+
"docs_release",
|
|
246
|
+
{
|
|
247
|
+
nodeId: z.string().describe("Document node id.")
|
|
248
|
+
},
|
|
249
|
+
"Docs Release",
|
|
250
|
+
"Release a documentation document lease."
|
|
251
|
+
);
|
|
252
|
+
forwardTool(
|
|
253
|
+
"docs_update",
|
|
254
|
+
{
|
|
255
|
+
nodeId: z.string().describe("Document node id."),
|
|
256
|
+
name: z.string().optional().describe("Optional new name (rename; index.md cannot be renamed)."),
|
|
257
|
+
bodyMarkdown: z.string().optional().describe("Markdown body."),
|
|
258
|
+
metadata: z.record(z.any()).optional().describe("Metadata object (JSON)."),
|
|
259
|
+
expectedBodyVersion: z.number().int().nonnegative().optional().describe("Optional optimistic concurrency check.")
|
|
260
|
+
},
|
|
261
|
+
"Docs Update",
|
|
262
|
+
"Update a documentation document. Claims the lease automatically if required."
|
|
263
|
+
);
|
|
264
|
+
forwardTool(
|
|
265
|
+
"docs_delete",
|
|
266
|
+
{
|
|
267
|
+
nodeId: z.string().describe("Node id (folder or document).")
|
|
268
|
+
},
|
|
269
|
+
"Docs Delete",
|
|
270
|
+
"Delete a documentation node (folders must be empty; deleting docs requires active lease; index.md cannot be deleted)."
|
|
271
|
+
);
|
|
272
|
+
forwardTool(
|
|
273
|
+
"docs_import",
|
|
274
|
+
{
|
|
275
|
+
entries: z.array(
|
|
276
|
+
z.object({
|
|
277
|
+
path: z.string().describe("Relative path (within the imported folder)."),
|
|
278
|
+
bodyMarkdown: z.string().describe("Markdown content.")
|
|
279
|
+
})
|
|
280
|
+
).describe("Markdown entries to import.")
|
|
281
|
+
},
|
|
282
|
+
"Docs Import",
|
|
283
|
+
"Import markdown files into the Documentation Library (copy into backend DB)."
|
|
284
|
+
);
|
|
285
|
+
forwardTool(
|
|
286
|
+
"unreal_latest_screenshots",
|
|
287
|
+
{
|
|
288
|
+
uprojectPath: z.string().describe("Absolute path to the .uproject file."),
|
|
289
|
+
limit: z.number().int().positive().optional().describe("Max number of screenshots to return (default 12)."),
|
|
290
|
+
includeBase64: z.boolean().optional().describe("Include base64 image data in the payload (default false)."),
|
|
291
|
+
maxBytesPerImage: z.number().int().positive().optional().describe("Max bytes per image when includeBase64=true (default 2500000).")
|
|
292
|
+
},
|
|
293
|
+
"Latest Unreal Screenshots",
|
|
294
|
+
"Fetch the latest PNG screenshots from Saved/Screenshots/Flockbay and return views for the UI to display."
|
|
295
|
+
);
|
|
296
|
+
forwardTool(
|
|
297
|
+
"unreal_mcp_command",
|
|
298
|
+
{
|
|
299
|
+
type: z.string().describe('UnrealMCP command type (e.g. "get_actors_in_level", "spawn_actor").'),
|
|
300
|
+
params: z.record(z.any()).optional().describe("Optional params object for the command."),
|
|
301
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
302
|
+
},
|
|
303
|
+
"Unreal Editor Command (UnrealMCP)",
|
|
304
|
+
"Send a single UnrealMCP command to the running Unreal Editor (engine plugin) and return the JSON response."
|
|
305
|
+
);
|
|
306
|
+
forwardTool(
|
|
307
|
+
"unreal_mcp_get_actors_in_level",
|
|
308
|
+
{
|
|
309
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
310
|
+
},
|
|
311
|
+
"Unreal Actors In Level (UnrealMCP)",
|
|
312
|
+
"List all actors in the current Unreal Editor level via the UnrealMCP engine plugin."
|
|
313
|
+
);
|
|
314
|
+
forwardTool(
|
|
315
|
+
"unreal_mcp_get_play_in_editor_status",
|
|
316
|
+
{
|
|
317
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
318
|
+
},
|
|
319
|
+
"Unreal Play Status (PIE)",
|
|
320
|
+
"Get Play-In-Editor status from the UnrealMCP engine plugin."
|
|
321
|
+
);
|
|
322
|
+
forwardTool(
|
|
323
|
+
"unreal_mcp_editor_health",
|
|
324
|
+
{
|
|
325
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 1500).")
|
|
326
|
+
},
|
|
327
|
+
"Unreal Editor Health (UnrealMCP)",
|
|
328
|
+
"Best-effort health check for Unreal Editor + UnrealMCP (detects if the editor is reachable)."
|
|
329
|
+
);
|
|
330
|
+
forwardTool(
|
|
331
|
+
"unreal_mcp_play_in_editor",
|
|
332
|
+
{
|
|
333
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
334
|
+
},
|
|
335
|
+
"Unreal Play In Editor (Viewport)",
|
|
336
|
+
"Start Play-In-Editor (PIE) in the active editor viewport via the UnrealMCP engine plugin."
|
|
337
|
+
);
|
|
338
|
+
forwardTool(
|
|
339
|
+
"unreal_mcp_play_in_editor_windowed",
|
|
340
|
+
{
|
|
341
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
342
|
+
},
|
|
343
|
+
"Unreal Play In Editor (New Window)",
|
|
344
|
+
"Start Play-In-Editor (PIE) in a new editor window via the UnrealMCP engine plugin."
|
|
345
|
+
);
|
|
346
|
+
forwardTool(
|
|
347
|
+
"unreal_mcp_stop_play_in_editor",
|
|
348
|
+
{
|
|
349
|
+
timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 5000).")
|
|
350
|
+
},
|
|
351
|
+
"Unreal Stop Play In Editor",
|
|
352
|
+
"Stop a running Play-In-Editor session via the UnrealMCP engine plugin."
|
|
353
|
+
);
|
|
354
|
+
forwardTool(
|
|
355
|
+
"unreal_mechanic_run",
|
|
356
|
+
{
|
|
357
|
+
uprojectPath: z.string().describe("Absolute path to the .uproject file."),
|
|
358
|
+
engineRoot: z.string().optional().describe(
|
|
359
|
+
"Unreal Engine install root (e.g. /Users/Shared/Epic Games/UE_5.7). Defaults to UE_ENGINE_ROOT / ENGINE_ROOT env vars."
|
|
360
|
+
),
|
|
361
|
+
slug: z.string().optional().describe("Optional slug used for naming the test map (default: dash)."),
|
|
362
|
+
prompt: z.string().optional().describe("Optional prompt string to record in the receipt."),
|
|
363
|
+
cooldownSeconds: z.number().optional().describe("Dash cooldown seconds (default 1.0)."),
|
|
364
|
+
staminaCost: z.number().optional().describe("Dash stamina cost (default 20.0)."),
|
|
365
|
+
dashStrength: z.number().optional().describe("Dash horizontal strength (default 1200.0)."),
|
|
366
|
+
dashUpwardStrength: z.number().optional().describe("Dash upward strength (default 0.0).")
|
|
367
|
+
},
|
|
368
|
+
"Unreal Mechanic Builder (Dash MVP)",
|
|
369
|
+
"Create a project-safe mechanic test map (Parallel mode) and prove it works with Flockbay screenshot evidence. V1 supports dash only."
|
|
370
|
+
);
|
|
371
|
+
const stdio = new StdioServerTransport();
|
|
372
|
+
await server.connect(stdio);
|
|
373
|
+
}
|
|
374
|
+
main().catch((err) => {
|
|
375
|
+
try {
|
|
376
|
+
process.stderr.write(`[flockbay-mcp] Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
377
|
+
`);
|
|
378
|
+
} finally {
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
function uniqPush(into, seen, value) {
|
|
4
|
+
const v = value.trim();
|
|
5
|
+
if (!v) return;
|
|
6
|
+
if (seen.has(v)) return;
|
|
7
|
+
seen.add(v);
|
|
8
|
+
into.push(v);
|
|
9
|
+
}
|
|
10
|
+
function normalizeFilePathToken(token) {
|
|
11
|
+
return token.trim().replace(/^['"`]+/, "").replace(/['"`]+$/, "").replace(/[),.;:'"`]+$/, "").trim();
|
|
12
|
+
}
|
|
13
|
+
function resolveCandidatePath(candidate, cwd) {
|
|
14
|
+
const raw = normalizeFilePathToken(candidate);
|
|
15
|
+
if (!raw) return raw;
|
|
16
|
+
if (raw.startsWith("~/")) {
|
|
17
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
18
|
+
if (home) return path.join(home, raw.slice(2));
|
|
19
|
+
}
|
|
20
|
+
return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
21
|
+
}
|
|
22
|
+
function isImageBlock(block) {
|
|
23
|
+
if (!block || typeof block !== "object") return false;
|
|
24
|
+
if (block.type !== "image") return false;
|
|
25
|
+
if (typeof block.data === "string" && block.data.length > 0) return true;
|
|
26
|
+
if (block.source && typeof block.source === "object" && typeof block.source.data === "string" && block.source.data.length > 0) return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function tryParseJsonObjectWithViews(text) {
|
|
30
|
+
const trimmed = text.trim();
|
|
31
|
+
if (!trimmed) return null;
|
|
32
|
+
try {
|
|
33
|
+
const direct = JSON.parse(trimmed);
|
|
34
|
+
if (direct && typeof direct === "object" && Array.isArray(direct.views)) return direct;
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
for (let i = trimmed.length - 1; i >= 0; i -= 1) {
|
|
38
|
+
if (trimmed[i] !== "{") continue;
|
|
39
|
+
const candidate = trimmed.slice(i);
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(candidate);
|
|
42
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.views)) return parsed;
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function extractScreenshotViewsFromText(text, cwd) {
|
|
49
|
+
const out = [];
|
|
50
|
+
const seen = /* @__PURE__ */ new Set();
|
|
51
|
+
const re = /(?:^|[\s"'(])(?:-\s*)?([^\s"'()]*Saved[\\/]+Screenshots[\\/]+Flockbay[\\/]+[^\s"'()]+\.(?:png|jpg|jpeg))/gi;
|
|
52
|
+
for (const match of text.matchAll(re)) {
|
|
53
|
+
const token = String(match[1] || "");
|
|
54
|
+
const resolved = resolveCandidatePath(token, cwd);
|
|
55
|
+
if (!resolved) continue;
|
|
56
|
+
if (!resolved.includes(`${path.sep}Saved${path.sep}Screenshots${path.sep}Flockbay${path.sep}`) && !resolved.includes("Saved/Screenshots/Flockbay/") && !resolved.includes("Saved\\Screenshots\\Flockbay\\")) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
uniqPush(out, seen, resolved);
|
|
60
|
+
}
|
|
61
|
+
if (out.length === 0 && (text.includes("Saved/Screenshots/Flockbay") || text.includes("Saved\\Screenshots\\Flockbay"))) {
|
|
62
|
+
const filenameRe = /(?:^|[\s"'(\\/])(?:-\s*)?(Flockbay_[A-Za-z0-9_.-]+\.(?:png|jpg|jpeg))/gi;
|
|
63
|
+
for (const match of text.matchAll(filenameRe)) {
|
|
64
|
+
const filename = normalizeFilePathToken(String(match[1] || ""));
|
|
65
|
+
if (!filename) continue;
|
|
66
|
+
const rel = path.join("Saved", "Screenshots", "Flockbay", filename);
|
|
67
|
+
const resolved = resolveCandidatePath(rel, cwd);
|
|
68
|
+
uniqPush(out, seen, resolved);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function extractViewsFromParsedJson(parsed, cwd) {
|
|
74
|
+
const out = [];
|
|
75
|
+
const seen = /* @__PURE__ */ new Set();
|
|
76
|
+
const views = Array.isArray(parsed?.views) ? parsed.views : [];
|
|
77
|
+
for (const v of views) {
|
|
78
|
+
const p = typeof v?.path === "string" ? v.path : "";
|
|
79
|
+
if (!p.trim()) continue;
|
|
80
|
+
uniqPush(out, seen, resolveCandidatePath(p, cwd));
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function extractFromContentBlocks(content, cwd) {
|
|
85
|
+
const texts = [];
|
|
86
|
+
let hasImages = false;
|
|
87
|
+
for (const block of content) {
|
|
88
|
+
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
89
|
+
texts.push(block.text);
|
|
90
|
+
}
|
|
91
|
+
if (!hasImages && isImageBlock(block)) hasImages = true;
|
|
92
|
+
}
|
|
93
|
+
const text = texts.join("\n");
|
|
94
|
+
const parsed = tryParseJsonObjectWithViews(text);
|
|
95
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
96
|
+
const pathMatches = extractScreenshotViewsFromText(text, cwd);
|
|
97
|
+
const combined = [];
|
|
98
|
+
const seen = /* @__PURE__ */ new Set();
|
|
99
|
+
for (const p of jsonPaths) uniqPush(combined, seen, p);
|
|
100
|
+
for (const p of pathMatches) uniqPush(combined, seen, p);
|
|
101
|
+
return { paths: combined, hasImages };
|
|
102
|
+
}
|
|
103
|
+
function detectScreenshotsForGate(args) {
|
|
104
|
+
const cwd = args.cwd && args.cwd.trim().length > 0 ? args.cwd : process.cwd();
|
|
105
|
+
const output = args.output;
|
|
106
|
+
if (Array.isArray(output)) {
|
|
107
|
+
const extracted = extractFromContentBlocks(output, cwd);
|
|
108
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
109
|
+
}
|
|
110
|
+
if (output && typeof output === "object" && Array.isArray(output.content)) {
|
|
111
|
+
const extracted = extractFromContentBlocks(output.content, cwd);
|
|
112
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
113
|
+
}
|
|
114
|
+
if (output && typeof output === "object" && Array.isArray(output.views)) {
|
|
115
|
+
return { paths: extractViewsFromParsedJson(output, cwd), hasImageBlocks: false };
|
|
116
|
+
}
|
|
117
|
+
const candidates = [
|
|
118
|
+
typeof output === "string" ? output : null,
|
|
119
|
+
typeof output?.stdout === "string" ? output.stdout : null,
|
|
120
|
+
typeof output?.stderr === "string" ? output.stderr : null,
|
|
121
|
+
typeof output?.output === "string" ? output.output : null,
|
|
122
|
+
typeof output?.message === "string" ? output.message : null
|
|
123
|
+
];
|
|
124
|
+
const combinedText = candidates.filter((v) => typeof v === "string" && v.trim().length > 0).join("\n");
|
|
125
|
+
if (!combinedText) return { paths: [], hasImageBlocks: false };
|
|
126
|
+
const parsed = tryParseJsonObjectWithViews(combinedText);
|
|
127
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
128
|
+
const pathMatches = extractScreenshotViewsFromText(combinedText, cwd);
|
|
129
|
+
const out = [];
|
|
130
|
+
const seen = /* @__PURE__ */ new Set();
|
|
131
|
+
for (const p of jsonPaths) uniqPush(out, seen, p);
|
|
132
|
+
for (const p of pathMatches) uniqPush(out, seen, p);
|
|
133
|
+
return { paths: out, hasImageBlocks: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { detectScreenshotsForGate as d };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('node:path');
|
|
4
|
+
|
|
5
|
+
function uniqPush(into, seen, value) {
|
|
6
|
+
const v = value.trim();
|
|
7
|
+
if (!v) return;
|
|
8
|
+
if (seen.has(v)) return;
|
|
9
|
+
seen.add(v);
|
|
10
|
+
into.push(v);
|
|
11
|
+
}
|
|
12
|
+
function normalizeFilePathToken(token) {
|
|
13
|
+
return token.trim().replace(/^['"`]+/, "").replace(/['"`]+$/, "").replace(/[),.;:'"`]+$/, "").trim();
|
|
14
|
+
}
|
|
15
|
+
function resolveCandidatePath(candidate, cwd) {
|
|
16
|
+
const raw = normalizeFilePathToken(candidate);
|
|
17
|
+
if (!raw) return raw;
|
|
18
|
+
if (raw.startsWith("~/")) {
|
|
19
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
20
|
+
if (home) return path.join(home, raw.slice(2));
|
|
21
|
+
}
|
|
22
|
+
return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
23
|
+
}
|
|
24
|
+
function isImageBlock(block) {
|
|
25
|
+
if (!block || typeof block !== "object") return false;
|
|
26
|
+
if (block.type !== "image") return false;
|
|
27
|
+
if (typeof block.data === "string" && block.data.length > 0) return true;
|
|
28
|
+
if (block.source && typeof block.source === "object" && typeof block.source.data === "string" && block.source.data.length > 0) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function tryParseJsonObjectWithViews(text) {
|
|
32
|
+
const trimmed = text.trim();
|
|
33
|
+
if (!trimmed) return null;
|
|
34
|
+
try {
|
|
35
|
+
const direct = JSON.parse(trimmed);
|
|
36
|
+
if (direct && typeof direct === "object" && Array.isArray(direct.views)) return direct;
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
for (let i = trimmed.length - 1; i >= 0; i -= 1) {
|
|
40
|
+
if (trimmed[i] !== "{") continue;
|
|
41
|
+
const candidate = trimmed.slice(i);
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(candidate);
|
|
44
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.views)) return parsed;
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function extractScreenshotViewsFromText(text, cwd) {
|
|
51
|
+
const out = [];
|
|
52
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53
|
+
const re = /(?:^|[\s"'(])(?:-\s*)?([^\s"'()]*Saved[\\/]+Screenshots[\\/]+Flockbay[\\/]+[^\s"'()]+\.(?:png|jpg|jpeg))/gi;
|
|
54
|
+
for (const match of text.matchAll(re)) {
|
|
55
|
+
const token = String(match[1] || "");
|
|
56
|
+
const resolved = resolveCandidatePath(token, cwd);
|
|
57
|
+
if (!resolved) continue;
|
|
58
|
+
if (!resolved.includes(`${path.sep}Saved${path.sep}Screenshots${path.sep}Flockbay${path.sep}`) && !resolved.includes("Saved/Screenshots/Flockbay/") && !resolved.includes("Saved\\Screenshots\\Flockbay\\")) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
uniqPush(out, seen, resolved);
|
|
62
|
+
}
|
|
63
|
+
if (out.length === 0 && (text.includes("Saved/Screenshots/Flockbay") || text.includes("Saved\\Screenshots\\Flockbay"))) {
|
|
64
|
+
const filenameRe = /(?:^|[\s"'(\\/])(?:-\s*)?(Flockbay_[A-Za-z0-9_.-]+\.(?:png|jpg|jpeg))/gi;
|
|
65
|
+
for (const match of text.matchAll(filenameRe)) {
|
|
66
|
+
const filename = normalizeFilePathToken(String(match[1] || ""));
|
|
67
|
+
if (!filename) continue;
|
|
68
|
+
const rel = path.join("Saved", "Screenshots", "Flockbay", filename);
|
|
69
|
+
const resolved = resolveCandidatePath(rel, cwd);
|
|
70
|
+
uniqPush(out, seen, resolved);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function extractViewsFromParsedJson(parsed, cwd) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const seen = /* @__PURE__ */ new Set();
|
|
78
|
+
const views = Array.isArray(parsed?.views) ? parsed.views : [];
|
|
79
|
+
for (const v of views) {
|
|
80
|
+
const p = typeof v?.path === "string" ? v.path : "";
|
|
81
|
+
if (!p.trim()) continue;
|
|
82
|
+
uniqPush(out, seen, resolveCandidatePath(p, cwd));
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function extractFromContentBlocks(content, cwd) {
|
|
87
|
+
const texts = [];
|
|
88
|
+
let hasImages = false;
|
|
89
|
+
for (const block of content) {
|
|
90
|
+
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
91
|
+
texts.push(block.text);
|
|
92
|
+
}
|
|
93
|
+
if (!hasImages && isImageBlock(block)) hasImages = true;
|
|
94
|
+
}
|
|
95
|
+
const text = texts.join("\n");
|
|
96
|
+
const parsed = tryParseJsonObjectWithViews(text);
|
|
97
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
98
|
+
const pathMatches = extractScreenshotViewsFromText(text, cwd);
|
|
99
|
+
const combined = [];
|
|
100
|
+
const seen = /* @__PURE__ */ new Set();
|
|
101
|
+
for (const p of jsonPaths) uniqPush(combined, seen, p);
|
|
102
|
+
for (const p of pathMatches) uniqPush(combined, seen, p);
|
|
103
|
+
return { paths: combined, hasImages };
|
|
104
|
+
}
|
|
105
|
+
function detectScreenshotsForGate(args) {
|
|
106
|
+
const cwd = args.cwd && args.cwd.trim().length > 0 ? args.cwd : process.cwd();
|
|
107
|
+
const output = args.output;
|
|
108
|
+
if (Array.isArray(output)) {
|
|
109
|
+
const extracted = extractFromContentBlocks(output, cwd);
|
|
110
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
111
|
+
}
|
|
112
|
+
if (output && typeof output === "object" && Array.isArray(output.content)) {
|
|
113
|
+
const extracted = extractFromContentBlocks(output.content, cwd);
|
|
114
|
+
return { paths: extracted.paths, hasImageBlocks: extracted.hasImages };
|
|
115
|
+
}
|
|
116
|
+
if (output && typeof output === "object" && Array.isArray(output.views)) {
|
|
117
|
+
return { paths: extractViewsFromParsedJson(output, cwd), hasImageBlocks: false };
|
|
118
|
+
}
|
|
119
|
+
const candidates = [
|
|
120
|
+
typeof output === "string" ? output : null,
|
|
121
|
+
typeof output?.stdout === "string" ? output.stdout : null,
|
|
122
|
+
typeof output?.stderr === "string" ? output.stderr : null,
|
|
123
|
+
typeof output?.output === "string" ? output.output : null,
|
|
124
|
+
typeof output?.message === "string" ? output.message : null
|
|
125
|
+
];
|
|
126
|
+
const combinedText = candidates.filter((v) => typeof v === "string" && v.trim().length > 0).join("\n");
|
|
127
|
+
if (!combinedText) return { paths: [], hasImageBlocks: false };
|
|
128
|
+
const parsed = tryParseJsonObjectWithViews(combinedText);
|
|
129
|
+
const jsonPaths = parsed ? extractViewsFromParsedJson(parsed, cwd) : [];
|
|
130
|
+
const pathMatches = extractScreenshotViewsFromText(combinedText, cwd);
|
|
131
|
+
const out = [];
|
|
132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
133
|
+
for (const p of jsonPaths) uniqPush(out, seen, p);
|
|
134
|
+
for (const p of pathMatches) uniqPush(out, seen, p);
|
|
135
|
+
return { paths: out, hasImageBlocks: false };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exports.detectScreenshotsForGate = detectScreenshotsForGate;
|