flockbay 0.10.20 → 0.10.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  var chalk = require('chalk');
4
4
  var os = require('node:os');
5
5
  var node_crypto = require('node:crypto');
6
- var types = require('./types-BYHCKlu_.cjs');
6
+ var types = require('./types-Z2OYpI8c.cjs');
7
7
  var node_child_process = require('node:child_process');
8
8
  var path = require('node:path');
9
9
  var node_readline = require('node:readline');
@@ -408,7 +408,7 @@ const PLATFORM_SYSTEM_PROMPT = trimIdent(`
408
408
 
409
409
  # Policy blocks (not user rejections)
410
410
 
411
- If a tool call is **blocked by policy** (e.g. a \`FlockbayPolicy\` card, or a denial reason like \u201CBlocked by policy \u2026\u201D), this is automatic enforcement by the platform \u2014 it is **not** the user rejecting your tool call. Follow the provided next-step instructions (read docs/ledger, claim files, etc) and then retry.
411
+ If a tool call is **blocked by Policy** (e.g. a \`FlockbayPolicy\` card, or a denial reason like \u201CBlocked by Policy \u2026\u201D), this is automatic enforcement by the platform \u2014 it is **not** the user rejecting your tool call. Follow the provided next-step instructions (read docs/ledger, claim files, etc) and then retry.
412
412
 
413
413
  # Documentation Library (server-stored docs)
414
414
 
@@ -1258,7 +1258,8 @@ function buildDaemonSafeEnv(baseEnv, binPath) {
1258
1258
  if (!p) return;
1259
1259
  if (!prepend.includes(p) && !existingParts.includes(p)) prepend.push(p);
1260
1260
  };
1261
- if (binPath && binPath.includes("/") && !binPath.startsWith("\\\\")) {
1261
+ const isPathLike = typeof binPath === "string" && binPath.length > 0 && (binPath.includes("/") || binPath.includes("\\")) && !binPath.startsWith("\\\\");
1262
+ if (isPathLike) {
1262
1263
  add(path.dirname(binPath));
1263
1264
  }
1264
1265
  add(path.dirname(process$1.execPath));
@@ -1272,12 +1273,12 @@ function buildDaemonSafeEnv(baseEnv, binPath) {
1272
1273
  env[pathKey] = [...prepend, ...existingParts].join(pathSep);
1273
1274
  return env;
1274
1275
  }
1275
- const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D_mglYG0.cjs', document.baseURI).href)));
1276
+ const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DQTqwzYd.cjs', document.baseURI).href)));
1276
1277
  const __dirname$1 = path.join(__filename$1, "..");
1277
1278
  function getGlobalClaudeVersion(claudeExecutable) {
1278
1279
  try {
1279
1280
  const cleanEnv = buildDaemonSafeEnv(getCleanEnv(), claudeExecutable);
1280
- const output = claudeExecutable.includes("/") && !claudeExecutable.startsWith("\\\\") ? node_child_process.execFileSync(claudeExecutable, ["--version"], {
1281
+ const output = (claudeExecutable.includes("/") || claudeExecutable.includes("\\")) && !claudeExecutable.startsWith("\\\\") ? node_child_process.execFileSync(claudeExecutable, ["--version"], {
1281
1282
  encoding: "utf8",
1282
1283
  stdio: ["pipe", "pipe", "pipe"],
1283
1284
  cwd: os.homedir(),
@@ -1295,6 +1296,35 @@ function getGlobalClaudeVersion(claudeExecutable) {
1295
1296
  return null;
1296
1297
  }
1297
1298
  }
1299
+ function tryReadBundledClaudeVersion(nodeModulesCliPath) {
1300
+ try {
1301
+ const pkgPath = path.join(path.dirname(path.dirname(nodeModulesCliPath)), "package.json");
1302
+ if (!fs.existsSync(pkgPath)) return null;
1303
+ const raw = fs.readFileSync(pkgPath, "utf8");
1304
+ const parsed = JSON.parse(raw);
1305
+ const v = typeof parsed?.version === "string" ? parsed.version.trim() : "";
1306
+ return v || null;
1307
+ } catch {
1308
+ return null;
1309
+ }
1310
+ }
1311
+ function parseSemver3(v) {
1312
+ const m = String(v || "").trim().match(/^(\d+)\.(\d+)\.(\d+)/);
1313
+ if (!m) return null;
1314
+ const a = Number(m[1]);
1315
+ const b = Number(m[2]);
1316
+ const c = Number(m[3]);
1317
+ if (!Number.isFinite(a) || !Number.isFinite(b) || !Number.isFinite(c)) return null;
1318
+ return [a, b, c];
1319
+ }
1320
+ function compareSemver(a, b) {
1321
+ const pa = parseSemver3(a);
1322
+ const pb = parseSemver3(b);
1323
+ if (!pa || !pb) return null;
1324
+ if (pa[0] !== pb[0]) return pa[0] - pb[0];
1325
+ if (pa[1] !== pb[1]) return pa[1] - pb[1];
1326
+ return pa[2] - pb[2];
1327
+ }
1298
1328
  function getCleanEnv() {
1299
1329
  const env = { ...process$1.env };
1300
1330
  const cwd = process$1.cwd();
@@ -1414,11 +1444,22 @@ function getDefaultClaudeCodePath() {
1414
1444
  return nodeModulesPath;
1415
1445
  }
1416
1446
  const globalVersion = getGlobalClaudeVersion(globalPath);
1447
+ const bundledVersion = tryReadBundledClaudeVersion(nodeModulesPath);
1417
1448
  types.logger.debug(`[Claude SDK] Global version: ${globalVersion || "unknown"}`);
1418
- if (!globalVersion) {
1449
+ types.logger.debug(`[Claude SDK] Bundled version: ${bundledVersion || "unknown"}`);
1450
+ if (!globalVersion || !bundledVersion) {
1419
1451
  types.logger.debug(`[Claude SDK] Cannot compare versions, using global: ${globalPath}`);
1420
1452
  return globalPath;
1421
1453
  }
1454
+ const cmp = compareSemver(bundledVersion, globalVersion);
1455
+ if (cmp === null) {
1456
+ types.logger.debug(`[Claude SDK] Cannot parse versions, using global: ${globalPath}`);
1457
+ return globalPath;
1458
+ }
1459
+ if (cmp > 0) {
1460
+ types.logger.debug(`[Claude SDK] Bundled Claude is newer (${bundledVersion} > ${globalVersion}), using bundled: ${nodeModulesPath}`);
1461
+ return nodeModulesPath;
1462
+ }
1422
1463
  return globalPath;
1423
1464
  }
1424
1465
  function logDebug(message) {
@@ -1693,8 +1734,9 @@ function query(config) {
1693
1734
  stdio: ["pipe", "pipe", "pipe"],
1694
1735
  signal: config.options?.abort,
1695
1736
  env: spawnEnv,
1696
- // Use shell on Windows for global binaries and command-only mode
1697
- shell: !isJsFile && process$1.platform === "win32"
1737
+ // Only use a shell on Windows when spawning a bare command (e.g. "claude").
1738
+ // Passing large `--allowedTools` lists through cmd.exe can hit the ~8k command line limit.
1739
+ shell: isCommandOnly && process$1.platform === "win32"
1698
1740
  });
1699
1741
  let childStdin = null;
1700
1742
  if (typeof prompt === "string") {
@@ -2470,6 +2512,21 @@ async function consumeToolQuota(args) {
2470
2512
  return { ok: true, allowed: true, unlimited: true };
2471
2513
  }
2472
2514
 
2515
+ function canonicalizeToolNameForMatching(name) {
2516
+ if (name === "ExitPlanMode") return "exit_plan_mode";
2517
+ return name;
2518
+ }
2519
+ function stripUndefinedDeep(value) {
2520
+ if (value === null || value === void 0) return value;
2521
+ if (Array.isArray(value)) return value.map(stripUndefinedDeep);
2522
+ if (typeof value !== "object") return value;
2523
+ const out = {};
2524
+ for (const [key, child] of Object.entries(value)) {
2525
+ if (child === void 0) continue;
2526
+ out[key] = stripUndefinedDeep(child);
2527
+ }
2528
+ return out;
2529
+ }
2473
2530
  class PermissionHandler {
2474
2531
  toolCalls = [];
2475
2532
  responses = /* @__PURE__ */ new Map();
@@ -2488,7 +2545,7 @@ class PermissionHandler {
2488
2545
  const decision = args.decision;
2489
2546
  const reason = args.reason;
2490
2547
  const kind = decision === "approved" || decision === "approved_for_session" ? "policy_allow" : decision === "abort" && reason === "permission_prompt_required" ? "policy_prompt" : "policy_block";
2491
- const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by policy.";
2548
+ const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by Policy.";
2492
2549
  const callId = `policy:${args.toolCallId}:${node_crypto.randomUUID().slice(0, 8)}`;
2493
2550
  const payload = {
2494
2551
  kind,
@@ -2553,13 +2610,13 @@ class PermissionHandler {
2553
2610
  if (args.reason === "docs_index_read_required") {
2554
2611
  return {
2555
2612
  uiReason: "read the game Documentation index before making edits.",
2556
- modelMessage: "Blocked by policy: read the game Documentation index before making edits.\nNext: call `mcp__flockbay__docs_index_read`, then retry the edit."
2613
+ modelMessage: "Blocked by Policy: read the game Documentation index before making edits.\nNext: call `mcp__flockbay__docs_index_read`, then retry the edit."
2557
2614
  };
2558
2615
  }
2559
2616
  if (args.reason === "ledger_read_required") {
2560
2617
  return {
2561
2618
  uiReason: "read the ledger before making file edits.",
2562
- modelMessage: "Blocked by policy: read the ledger before making file edits.\nNext: call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`), then retry the edit."
2619
+ modelMessage: "Blocked by Policy: read the ledger before making file edits.\nNext: call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`), then retry the edit."
2563
2620
  };
2564
2621
  }
2565
2622
  if (args.reason === "file_claim_required") {
@@ -2567,13 +2624,13 @@ class PermissionHandler {
2567
2624
  const next = file ? `Next: claim it via \`mcp__flockbay__ledger_claim\` (files: ["${file}"]) or \`mcp__flockbay__coordination_claim_files\`, then retry the edit.` : "Next: claim the file via `mcp__flockbay__ledger_claim` (or `mcp__flockbay__coordination_claim_files`), then retry the edit.";
2568
2625
  return {
2569
2626
  uiReason: display,
2570
- modelMessage: `Blocked by policy: ${display}
2627
+ modelMessage: `Blocked by Policy: ${display}
2571
2628
  ${next}`
2572
2629
  };
2573
2630
  }
2574
2631
  return {
2575
2632
  uiReason: "this session is in read-only mode.",
2576
- modelMessage: "Blocked by policy: this session is in read-only mode.\nNext: switch permission mode to allow edits, then retry."
2633
+ modelMessage: "Blocked by Policy: this session is in read-only mode.\nNext: switch permission mode to allow edits, then retry."
2577
2634
  };
2578
2635
  }
2579
2636
  enforceCoordinationGate(toolName, input) {
@@ -2753,8 +2810,13 @@ ${next}`
2753
2810
  }
2754
2811
  let toolCallId = this.resolveToolCallId(toolName, input);
2755
2812
  if (!toolCallId) {
2756
- await types.delay(1e3);
2757
- toolCallId = this.resolveToolCallId(toolName, input);
2813
+ const isPlanMode = toolName === "exit_plan_mode" || toolName === "ExitPlanMode";
2814
+ const timeoutMs = isPlanMode ? 3e3 : 1e3;
2815
+ const deadline = Date.now() + timeoutMs;
2816
+ while (!toolCallId && Date.now() < deadline) {
2817
+ await types.delay(100);
2818
+ toolCallId = this.resolveToolCallId(toolName, input);
2819
+ }
2758
2820
  if (!toolCallId) {
2759
2821
  throw new Error(`Could not resolve tool call ID for ${toolName}`);
2760
2822
  }
@@ -2835,9 +2897,12 @@ ${next}`
2835
2897
  * Resolves tool call ID based on tool name and input
2836
2898
  */
2837
2899
  resolveToolCallId(name, args) {
2900
+ const normalizedName = canonicalizeToolNameForMatching(name);
2901
+ const normalizedArgs = stripUndefinedDeep(args);
2838
2902
  for (let i = this.toolCalls.length - 1; i >= 0; i--) {
2839
2903
  const call = this.toolCalls[i];
2840
- if (call.name === name && deepEqual(call.input, args)) {
2904
+ const callName = canonicalizeToolNameForMatching(call.name);
2905
+ if (callName === normalizedName && deepEqual(stripUndefinedDeep(call.input), normalizedArgs)) {
2841
2906
  if (call.used) {
2842
2907
  return null;
2843
2908
  }
@@ -2845,6 +2910,18 @@ ${next}`
2845
2910
  return call.id;
2846
2911
  }
2847
2912
  }
2913
+ const candidates = [];
2914
+ for (let i = this.toolCalls.length - 1; i >= 0; i--) {
2915
+ const call = this.toolCalls[i];
2916
+ if (call.used) continue;
2917
+ const callName = canonicalizeToolNameForMatching(call.name);
2918
+ if (callName === normalizedName) candidates.push(call);
2919
+ if (candidates.length > 1) break;
2920
+ }
2921
+ if (candidates.length === 1) {
2922
+ candidates[0].used = true;
2923
+ return candidates[0].id;
2924
+ }
2848
2925
  return null;
2849
2926
  }
2850
2927
  /**
@@ -7117,7 +7194,11 @@ async function uploadScreenshotViewsForSession(args) {
7117
7194
  for (const v of args.views) {
7118
7195
  const buf = await fs$2.readFile(v.path);
7119
7196
  const filename = path.basename(v.path);
7120
- const contentType = filename.toLowerCase().endsWith(".png") ? "image/png" : "application/octet-stream";
7197
+ const contentType = (() => {
7198
+ const mime = detectImageMimeTypeFromBuffer$1(buf);
7199
+ if (mime) return mime;
7200
+ throw new Error(`Unsupported screenshot format (expected PNG/JPEG): ${v.path}`);
7201
+ })();
7121
7202
  const blob = new Blob([buf], { type: contentType });
7122
7203
  form.append(`file:${v.id}`, blob, filename);
7123
7204
  }
@@ -7158,6 +7239,64 @@ async function uploadScreenshotViewsForSession(args) {
7158
7239
  }
7159
7240
  return out;
7160
7241
  }
7242
+ function detectImageMimeTypeFromBuffer$1(buf) {
7243
+ if (!buf || buf.length < 12) return null;
7244
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10) {
7245
+ return "image/png";
7246
+ }
7247
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
7248
+ return "image/jpeg";
7249
+ }
7250
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56 && (buf[4] === 55 || buf[4] === 57) && buf[5] === 97) {
7251
+ return "image/gif";
7252
+ }
7253
+ if (buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
7254
+ return "image/webp";
7255
+ }
7256
+ return null;
7257
+ }
7258
+ async function readFileHeader(filePath, bytes) {
7259
+ const fh = await fs$2.open(filePath, "r");
7260
+ try {
7261
+ const maxBytes = Math.max(1, Math.floor(bytes));
7262
+ const buf = Buffer.allocUnsafe(maxBytes);
7263
+ const { bytesRead } = await fh.read(buf, 0, maxBytes, 0);
7264
+ return buf.subarray(0, bytesRead);
7265
+ } finally {
7266
+ try {
7267
+ await fh.close();
7268
+ } catch {
7269
+ }
7270
+ }
7271
+ }
7272
+ async function normalizeScreenshotPathExtensionToMatchBytes(filePath) {
7273
+ const abs = String(filePath || "").trim();
7274
+ if (!abs) return { path: abs, changed: false };
7275
+ if (!fs.existsSync(abs)) return { path: abs, changed: false };
7276
+ const lower = abs.toLowerCase();
7277
+ const header = await readFileHeader(abs, 16);
7278
+ const mime = detectImageMimeTypeFromBuffer$1(header);
7279
+ if (!mime) {
7280
+ return { path: abs, changed: false, detail: "unknown_image_format" };
7281
+ }
7282
+ if (mime === "image/jpeg" && lower.endsWith(".png")) {
7283
+ const next = abs.replace(/\.png$/i, ".jpg");
7284
+ if (fs.existsSync(next)) {
7285
+ throw new Error(`Screenshot already exists at normalized path: ${next}`);
7286
+ }
7287
+ await fs$2.rename(abs, next);
7288
+ return { path: next, changed: true, detail: "renamed_png_to_jpg" };
7289
+ }
7290
+ if (mime === "image/png" && (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
7291
+ const next = abs.replace(/\.(jpg|jpeg)$/i, ".png");
7292
+ if (fs.existsSync(next)) {
7293
+ throw new Error(`Screenshot already exists at normalized path: ${next}`);
7294
+ }
7295
+ await fs$2.rename(abs, next);
7296
+ return { path: next, changed: true, detail: "renamed_jpg_to_png" };
7297
+ }
7298
+ return { path: abs, changed: false };
7299
+ }
7161
7300
  async function startFlockbayServer(client, options) {
7162
7301
  const handler = async (title) => {
7163
7302
  types.logger.debug("[flockbayMCP] Changing title to:", title);
@@ -8687,13 +8826,13 @@ ${String(st.stdout || "").trim()}`
8687
8826
  "read_images",
8688
8827
  {
8689
8828
  title: "Read Images",
8690
- description: "Read one or more local PNG images by path and return a `{ views: [...] }` payload so the app can render them as a gallery.",
8829
+ description: "Read one or more local images by path (PNG/JPEG) and return a `{ views: [...] }` payload so the app can render them as a gallery.",
8691
8830
  inputSchema: {
8692
- paths: z.z.array(z.z.string()).describe("Image paths (absolute or relative to the session directory). PNG only."),
8831
+ paths: z.z.array(z.z.string()).describe("Image paths (absolute or relative to the session directory). PNG/JPG only."),
8693
8832
  limit: z.z.number().int().positive().optional().describe("Max number of images to include (default 10)."),
8694
8833
  upload: z.z.boolean().optional().describe("Upload images to the session screenshots store and return HTTPS URLs (default true)."),
8695
8834
  includeBase64: z.z.boolean().optional().describe("Include base64 data in the payload (default false)."),
8696
- includeToolImages: z.z.boolean().optional().describe("Include MCP image blocks so vision models can see the PNGs (default false)."),
8835
+ includeToolImages: z.z.boolean().optional().describe("Include MCP image blocks so vision models can see the images (default false)."),
8697
8836
  maxBytesPerImage: z.z.number().int().positive().optional().describe("Max bytes per image when includeBase64=true (default 2500000).")
8698
8837
  }
8699
8838
  },
@@ -8725,14 +8864,16 @@ ${String(st.stdout || "").trim()}`
8725
8864
  const home = process.env.HOME || process.env.USERPROFILE || "";
8726
8865
  if (home) return path.join(home, p.slice(2));
8727
8866
  }
8728
- return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
8867
+ const baseDir = readSessionWorkingDirectory();
8868
+ return path.isAbsolute(p) ? p : path.resolve(baseDir, p);
8729
8869
  };
8730
8870
  const idsSeen = /* @__PURE__ */ new Set();
8731
8871
  const viewsLocal = [];
8732
8872
  for (const inputPath of paths) {
8733
8873
  const abs = resolvePath(inputPath);
8734
- if (!abs.toLowerCase().endsWith(".png")) {
8735
- throw new Error(`Only PNG images are supported: ${abs}`);
8874
+ const lower = abs.toLowerCase();
8875
+ if (!lower.endsWith(".png") && !lower.endsWith(".jpg") && !lower.endsWith(".jpeg")) {
8876
+ throw new Error(`Only PNG/JPG images are supported: ${abs}`);
8736
8877
  }
8737
8878
  if (!fs.existsSync(abs)) {
8738
8879
  throw new Error(`Image not found: ${abs}`);
@@ -8752,7 +8893,11 @@ ${String(st.stdout || "").trim()}`
8752
8893
  throw new Error(`Image too large (${st.size} bytes) to embed: ${abs}`);
8753
8894
  }
8754
8895
  const buf = await fs$2.readFile(abs);
8755
- viewsLocal.push({ id: unique, path: abs, base64: buf.toString("base64") });
8896
+ const mime = detectImageMimeTypeFromBuffer$1(buf);
8897
+ if (!mime) {
8898
+ throw new Error(`Unsupported image format (expected PNG/JPEG): ${abs}`);
8899
+ }
8900
+ viewsLocal.push({ id: unique, path: abs, base64: buf.toString("base64"), mimeType: mime });
8756
8901
  } else {
8757
8902
  viewsLocal.push({ id: unique, path: abs });
8758
8903
  }
@@ -8777,7 +8922,7 @@ ${String(st.stdout || "").trim()}`
8777
8922
  viewsLocal.forEach((v, idx) => {
8778
8923
  if (!v.base64) return;
8779
8924
  content.push({ type: "text", text: `Image ${idx + 1}: ${v.id}` });
8780
- content.push({ type: "image", data: v.base64, mimeType: "image/png" });
8925
+ content.push({ type: "image", data: v.base64, mimeType: v.mimeType || "image/png" });
8781
8926
  });
8782
8927
  }
8783
8928
  return {
@@ -8789,7 +8934,7 @@ ${String(st.stdout || "").trim()}`
8789
8934
  return {
8790
8935
  content: [
8791
8936
  { type: "text", text: `Failed to read images: ${error instanceof Error ? error.message : String(error)}` },
8792
- { type: "text", text: `Tip: pass absolute PNG paths, or relative paths from the session folder.` }
8937
+ { type: "text", text: `Tip: pass absolute PNG/JPG paths, or relative paths from the session folder.` }
8793
8938
  ],
8794
8939
  isError: true
8795
8940
  };
@@ -8798,7 +8943,7 @@ ${String(st.stdout || "").trim()}`
8798
8943
  );
8799
8944
  mcp.registerTool("unreal_latest_screenshots", {
8800
8945
  title: "Latest Unreal Screenshots (Validation)",
8801
- description: "Fetch the latest PNG screenshots from `Saved/Screenshots/Flockbay/` (for validation) and return a `{ views: [...] }` payload so the app can display them.",
8946
+ description: "Fetch the latest screenshots (PNG/JPG) from `Saved/Screenshots/Flockbay/` (for validation) and return a `{ views: [...] }` payload so the app can display them.",
8802
8947
  inputSchema: {
8803
8948
  uprojectPath: z.z.string().describe("Absolute path to the .uproject file."),
8804
8949
  limit: z.z.number().int().positive().optional().describe("Max number of screenshots to return (default 12)."),
@@ -8835,7 +8980,7 @@ ${String(st.stdout || "").trim()}`
8835
8980
  };
8836
8981
  }
8837
8982
  const files = await fs$2.readdir(outDir);
8838
- const candidates = files.filter((f) => f.toLowerCase().endsWith(".png"));
8983
+ const candidates = files.filter((f) => /\.(png|jpg|jpeg)$/i.test(f));
8839
8984
  if (candidates.length === 0) {
8840
8985
  return {
8841
8986
  content: [
@@ -9090,10 +9235,26 @@ ${String(st.stdout || "").trim()}`
9090
9235
  const pluginInfoWasCached = Boolean(unrealMcpPluginInfoCache);
9091
9236
  const pluginInfo = type !== "get_plugin_info" ? await getUnrealMcpPluginInfoBestEffort(timeoutMs) : null;
9092
9237
  const response = await types.sendUnrealMcpTcpCommand({ type, params, timeoutMs });
9238
+ let screenshotNormalizationNote = null;
9239
+ if (type === "take_screenshot") {
9240
+ const responseObj = response && typeof response === "object" ? response : null;
9241
+ const candidate = responseObj && typeof responseObj.filepath === "string" ? responseObj : responseObj && responseObj.result && typeof responseObj.result === "object" && typeof responseObj.result.filepath === "string" ? responseObj.result : null;
9242
+ if (candidate && typeof candidate.filepath === "string") {
9243
+ const before = String(candidate.filepath || "").trim();
9244
+ if (before) {
9245
+ const normalized = await normalizeScreenshotPathExtensionToMatchBytes(before);
9246
+ if (normalized.changed) {
9247
+ candidate.filepath = normalized.path;
9248
+ screenshotNormalizationNote = `Normalized screenshot path (${normalized.detail || "extension_fixed"}): ${before} \u2192 ${normalized.path}`;
9249
+ }
9250
+ }
9251
+ }
9252
+ }
9093
9253
  unrealEditorSupervisor.noteUnrealReachable();
9094
9254
  return {
9095
9255
  content: [
9096
9256
  { type: "text", text: `UnrealMCP command ok: ${type}` },
9257
+ ...screenshotNormalizationNote ? [{ type: "text", text: screenshotNormalizationNote }] : [],
9097
9258
  { type: "text", text: JSON.stringify(response, null, 2) },
9098
9259
  ...pluginInfo && !pluginInfoWasCached ? [{ type: "text", text: formatUnrealMcpCapabilities(pluginInfo) }] : []
9099
9260
  ],
@@ -11753,6 +11914,269 @@ async function handleConnectVendor(vendor, displayName, flags) {
11753
11914
  }
11754
11915
  }
11755
11916
 
11917
+ function readArgValue$1(args, key) {
11918
+ const idx = args.indexOf(key);
11919
+ if (idx === -1) return null;
11920
+ const value = args[idx + 1];
11921
+ if (!value || value.startsWith("-")) return null;
11922
+ return value;
11923
+ }
11924
+ function splitList(value) {
11925
+ if (!value) return [];
11926
+ return value.split(/[;,]/g).map((v) => v.trim()).filter(Boolean);
11927
+ }
11928
+ function ensureDir(dir) {
11929
+ fs.mkdirSync(dir, { recursive: true });
11930
+ }
11931
+ function detectImageMimeTypeFromBuffer(buf) {
11932
+ if (!buf || buf.length < 12) return null;
11933
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10) {
11934
+ return "image/png";
11935
+ }
11936
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
11937
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56) return "image/gif";
11938
+ if (buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
11939
+ return "image/webp";
11940
+ }
11941
+ return null;
11942
+ }
11943
+ async function waitForUnreal(options) {
11944
+ const startedAt = Date.now();
11945
+ let lastErr = null;
11946
+ while (Date.now() - startedAt < options.timeoutMs) {
11947
+ try {
11948
+ const res = await types.sendUnrealMcpTcpCommand({ type: "ping", host: options.host, port: options.port, timeoutMs: 2e3 });
11949
+ const msg = typeof res?.message === "string" ? res.message : null;
11950
+ if (msg === "pong") return { ok: true };
11951
+ } catch (err) {
11952
+ lastErr = err instanceof Error ? err.message : String(err);
11953
+ }
11954
+ await new Promise((r) => setTimeout(r, 750));
11955
+ }
11956
+ return {
11957
+ ok: false,
11958
+ error: `Timed out waiting for UnrealMCP ping after ${options.timeoutMs}ms.${lastErr ? ` Last error: ${lastErr}` : ""}`
11959
+ };
11960
+ }
11961
+ function resolveUnrealEditorExe(engineRoot) {
11962
+ const exe = process.platform === "win32" ? path.join(engineRoot, "Engine", "Binaries", "Win64", "UnrealEditor.exe") : path.join(engineRoot, "Engine", "Binaries", process.platform === "darwin" ? "Mac" : "Linux", "UnrealEditor");
11963
+ return exe;
11964
+ }
11965
+ async function runRuntimeSmoke(options) {
11966
+ const editorExe = resolveUnrealEditorExe(options.engineRoot);
11967
+ if (!fs.existsSync(editorExe)) {
11968
+ return { ok: false, error: `Missing UnrealEditor executable: ${editorExe}` };
11969
+ }
11970
+ if (!fs.existsSync(options.projectPath)) {
11971
+ return { ok: false, error: `Missing .uproject: ${options.projectPath}` };
11972
+ }
11973
+ let child = null;
11974
+ const ping = await waitForUnreal({ host: options.host, port: options.port, timeoutMs: 2e3 }).catch((e) => ({ ok: false, error: String(e) }));
11975
+ if (!ping.ok && options.launchIfNeeded) {
11976
+ const args = [
11977
+ options.projectPath,
11978
+ "-NoSplash",
11979
+ "-NoSound",
11980
+ "-nop4"
11981
+ ];
11982
+ child = node_child_process.spawn(editorExe, args, {
11983
+ stdio: "ignore",
11984
+ detached: false
11985
+ });
11986
+ }
11987
+ const ready = await waitForUnreal({ host: options.host, port: options.port, timeoutMs: options.connectTimeoutMs });
11988
+ if (!ready.ok) {
11989
+ try {
11990
+ child?.kill();
11991
+ } catch {
11992
+ }
11993
+ return ready;
11994
+ }
11995
+ try {
11996
+ const pluginInfo = await types.sendUnrealMcpTcpCommand({ type: "get_plugin_info", host: options.host, port: options.port, timeoutMs: options.timeoutMs });
11997
+ const createdBy = String(pluginInfo?.createdBy || "").trim();
11998
+ const friendlyName = String(pluginInfo?.friendlyName || "").trim();
11999
+ const baseDir = String(pluginInfo?.baseDir || "").trim();
12000
+ const schemaVersion = Number(pluginInfo?.schemaVersion);
12001
+ const commands = Array.isArray(pluginInfo?.commands) ? pluginInfo.commands.filter((c) => typeof c === "string") : [];
12002
+ if (friendlyName !== "Flockbay MCP" || createdBy !== "Respaced Inc.") {
12003
+ return {
12004
+ ok: false,
12005
+ error: `Unexpected plugin identity loaded by Unreal.
12006
+ friendlyName=${friendlyName || "(missing)"} createdBy=${createdBy || "(missing)"}
12007
+ baseDir=${baseDir || "(missing)"}
12008
+ Expected FriendlyName="Flockbay MCP" CreatedBy="Respaced Inc."`
12009
+ };
12010
+ }
12011
+ if (!Number.isFinite(schemaVersion) || schemaVersion <= 0) {
12012
+ return { ok: false, error: `Invalid schemaVersion from get_plugin_info: ${String(pluginInfo?.schemaVersion)}` };
12013
+ }
12014
+ const requireCommands = [
12015
+ "ping",
12016
+ "get_plugin_info",
12017
+ "list_capabilities",
12018
+ "get_command_schema",
12019
+ "get_play_in_editor_status",
12020
+ "play_in_editor_windowed",
12021
+ "stop_play_in_editor",
12022
+ "take_screenshot",
12023
+ "create_blueprint",
12024
+ "compile_blueprint",
12025
+ "map_check"
12026
+ ];
12027
+ const missing = requireCommands.filter((c) => !commands.includes(c));
12028
+ if (missing.length > 0) {
12029
+ return { ok: false, error: `Missing required commands in this UnrealMCP build: ${missing.join(", ")}` };
12030
+ }
12031
+ const playStatus0 = await types.sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", host: options.host, port: options.port, timeoutMs: options.timeoutMs });
12032
+ const isPlaying = Boolean(playStatus0?.isPlaySessionInProgress);
12033
+ if (isPlaying) {
12034
+ await types.sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", host: options.host, port: options.port, timeoutMs: options.timeoutMs });
12035
+ }
12036
+ await types.sendUnrealMcpTcpCommand({ type: "play_in_editor_windowed", host: options.host, port: options.port, timeoutMs: Math.max(options.timeoutMs, 2e4) });
12037
+ await new Promise((r) => setTimeout(r, 1e3));
12038
+ await types.sendUnrealMcpTcpCommand({ type: "stop_play_in_editor", host: options.host, port: options.port, timeoutMs: Math.max(options.timeoutMs, 2e4) });
12039
+ const projectDir = path.dirname(options.projectPath);
12040
+ const shotsDir = path.join(projectDir, "Saved", "Screenshots", "Flockbay");
12041
+ ensureDir(shotsDir);
12042
+ const shotPath = path.join(shotsDir, `smoke_${Date.now()}.png`);
12043
+ await types.sendUnrealMcpTcpCommand({
12044
+ type: "take_screenshot",
12045
+ params: { filepath: shotPath },
12046
+ host: options.host,
12047
+ port: options.port,
12048
+ timeoutMs: options.timeoutMs
12049
+ });
12050
+ if (!fs.existsSync(shotPath)) return { ok: false, error: `Screenshot did not exist on disk after take_screenshot: ${shotPath}` };
12051
+ const bytes = fs.readFileSync(shotPath);
12052
+ const mime = detectImageMimeTypeFromBuffer(bytes);
12053
+ if (mime !== "image/png") {
12054
+ return { ok: false, error: `Screenshot bytes do not match .png extension (detected ${mime || "unknown"}): ${shotPath}` };
12055
+ }
12056
+ const bpName = `BP_Smoke_${Date.now()}`;
12057
+ await types.sendUnrealMcpTcpCommand({
12058
+ type: "create_blueprint",
12059
+ params: { name: bpName, path: "/Game/FlockbaySmoke/", parent_class: "Actor" },
12060
+ host: options.host,
12061
+ port: options.port,
12062
+ timeoutMs: Math.max(options.timeoutMs, 2e4)
12063
+ });
12064
+ await types.sendUnrealMcpTcpCommand({
12065
+ type: "compile_blueprint",
12066
+ params: { blueprint_name: bpName },
12067
+ host: options.host,
12068
+ port: options.port,
12069
+ timeoutMs: Math.max(options.timeoutMs, 6e4)
12070
+ });
12071
+ await types.sendUnrealMcpTcpCommand({
12072
+ type: "map_check",
12073
+ host: options.host,
12074
+ port: options.port,
12075
+ timeoutMs: Math.max(options.timeoutMs, 3e4)
12076
+ });
12077
+ return { ok: true };
12078
+ } finally {
12079
+ if (options.killAfter && child) {
12080
+ try {
12081
+ if (process.platform === "win32") {
12082
+ node_child_process.spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
12083
+ } else {
12084
+ child.kill();
12085
+ }
12086
+ } catch {
12087
+ }
12088
+ }
12089
+ }
12090
+ }
12091
+ async function runUnrealMcpMatrixSmoke(args) {
12092
+ const engineRoots = splitList(readArgValue$1(args, "--engine-roots")) || [];
12093
+ const engineRootSingle = readArgValue$1(args, "--engine-root");
12094
+ if (engineRootSingle) engineRoots.push(engineRootSingle.trim());
12095
+ const project = readArgValue$1(args, "--project");
12096
+ const host = (readArgValue$1(args, "--host") || "127.0.0.1").trim() || "127.0.0.1";
12097
+ const port = Number(readArgValue$1(args, "--port") || "55557");
12098
+ const connectTimeoutMs = Number(readArgValue$1(args, "--connect-timeout-ms") || "180000");
12099
+ const timeoutMs = Number(readArgValue$1(args, "--timeout-ms") || "30000");
12100
+ const doBuild = !args.includes("--runtime-only");
12101
+ const doRuntime = !args.includes("--build-only");
12102
+ const launch = args.includes("--launch-editor");
12103
+ const killAfter = args.includes("--kill-editor");
12104
+ if (engineRoots.length === 0) {
12105
+ console.error(chalk.red("Missing --engine-root or --engine-roots."));
12106
+ console.error(chalk.gray('Example: flockbay doctor unreal-mcp-smoke --engine-roots "C:\\\\Epic\\\\UE_5.5;C:\\\\Epic\\\\UE_5.6" --project "C:\\\\Projects\\\\MyProj\\\\MyProj.uproject" --launch-editor --kill-editor'));
12107
+ process.exit(1);
12108
+ }
12109
+ if (doRuntime && !project) {
12110
+ console.error(chalk.red("Missing --project (required for runtime smoke)."));
12111
+ process.exit(1);
12112
+ }
12113
+ console.log(chalk.bold("\nUnrealMCP Matrix Smoke\n"));
12114
+ console.log(chalk.gray(`Platform: ${process.platform}`));
12115
+ console.log(chalk.gray(`Host: ${host}:${port}`));
12116
+ console.log(chalk.gray(`Engines: ${engineRoots.join(", ")}`));
12117
+ if (project) console.log(chalk.gray(`Project: ${project}`));
12118
+ console.log(chalk.gray(`Build: ${doBuild ? "yes" : "no"} Runtime: ${doRuntime ? "yes" : "no"} Launch: ${launch ? "yes" : "no"} Kill: ${killAfter ? "yes" : "no"}`));
12119
+ console.log("");
12120
+ const failures = [];
12121
+ for (const engineRootRaw of engineRoots) {
12122
+ const engineRoot = engineRootRaw.trim();
12123
+ if (!engineRoot) continue;
12124
+ console.log(chalk.bold(`== Engine: ${engineRoot} ==`));
12125
+ if (doBuild) {
12126
+ console.log(chalk.cyan("Build: installing UnrealMCP plugin sources..."));
12127
+ const installed = types.installUnrealMcpPluginToEngine(engineRoot);
12128
+ if (!installed.ok) {
12129
+ failures.push({ engineRoot, phase: "build", error: installed.errorMessage });
12130
+ console.log(chalk.red(`Build: failed (install)
12131
+ ${installed.errorMessage}
12132
+ `));
12133
+ if (!doRuntime) continue;
12134
+ } else {
12135
+ console.log(chalk.green(`Build: sources installed to ${installed.destDir}`));
12136
+ console.log(chalk.cyan("Build: compiling plugin via RunUAT BuildPlugin..."));
12137
+ const built = await types.buildAndInstallUnrealMcpPlugin({ engineRoot, flockbayHomeDir: types.configuration.flockbayHomeDir });
12138
+ if (!built.ok) {
12139
+ failures.push({ engineRoot, phase: "build", error: built.errorMessage });
12140
+ console.log(chalk.red(`Build: failed
12141
+ ${built.errorMessage}
12142
+ `));
12143
+ } else {
12144
+ console.log(chalk.green(`Build: ok (log: ${built.buildLogPath})`));
12145
+ }
12146
+ }
12147
+ }
12148
+ if (doRuntime) {
12149
+ console.log(chalk.cyan("Runtime: running command smoke..."));
12150
+ const res = await runRuntimeSmoke({
12151
+ engineRoot,
12152
+ projectPath: project,
12153
+ host,
12154
+ port,
12155
+ connectTimeoutMs,
12156
+ timeoutMs,
12157
+ launchIfNeeded: launch,
12158
+ killAfter
12159
+ });
12160
+ if (!res.ok) {
12161
+ failures.push({ engineRoot, phase: "runtime", error: res.error });
12162
+ console.log(chalk.red(`Runtime: failed
12163
+ ${res.error}
12164
+ `));
12165
+ } else {
12166
+ console.log(chalk.green("Runtime: ok\n"));
12167
+ }
12168
+ }
12169
+ }
12170
+ if (failures.length > 0) {
12171
+ console.error(chalk.red("\nMatrix smoke failed.\n"));
12172
+ for (const f of failures) {
12173
+ console.error(chalk.red(`- ${f.engineRoot} (${f.phase}): ${f.error}`));
12174
+ }
12175
+ process.exit(1);
12176
+ }
12177
+ console.log(chalk.green("\nMatrix smoke passed.\n"));
12178
+ }
12179
+
11756
12180
  function readTailUtf8(filePath, maxBytes) {
11757
12181
  try {
11758
12182
  const stat = fs__namespace.statSync(filePath);
@@ -12113,6 +12537,16 @@ async function authAndSetupMachineIfNeeded() {
12113
12537
  }
12114
12538
  if (!args.includes("--version")) ;
12115
12539
  if (subcommand === "doctor") {
12540
+ if (args[1] === "unreal-mcp-smoke") {
12541
+ try {
12542
+ await runUnrealMcpMatrixSmoke(args.slice(2));
12543
+ } catch (error) {
12544
+ console.error(chalk.red("UnrealMCP smoke failed:"), error instanceof Error ? error.message : String(error));
12545
+ if (process.env.DEBUG) console.error(error);
12546
+ process.exit(1);
12547
+ }
12548
+ return;
12549
+ }
12116
12550
  if (args[1] === "clean") {
12117
12551
  const result = await killRunawayFlockbayProcesses();
12118
12552
  console.log(`Cleaned up ${result.killed} runaway processes`);
@@ -12224,7 +12658,7 @@ ${engineRoot}`, {
12224
12658
  } else if (subcommand === "codex") {
12225
12659
  try {
12226
12660
  await chdirToNearestUprojectRootIfPresent();
12227
- const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-CXJW0tzo.cjs'); });
12661
+ const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-CaWagdzG.cjs'); });
12228
12662
  let startedBy = void 0;
12229
12663
  let sessionId = void 0;
12230
12664
  for (let i = 1; i < args.length; i++) {
@@ -12250,7 +12684,13 @@ ${engineRoot}`, {
12250
12684
  const geminiSubcommand = args[1];
12251
12685
  if (geminiSubcommand === "model" && args[2] === "set" && args[3]) {
12252
12686
  const modelName = args[3];
12253
- const validModels = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"];
12687
+ const validModels = [
12688
+ "gemini-2.5-pro",
12689
+ "gemini-2.5-flash",
12690
+ "gemini-2.5-flash-lite",
12691
+ "gemini-3-pro-preview",
12692
+ "gemini-3-flash-preview"
12693
+ ];
12254
12694
  if (!validModels.includes(modelName)) {
12255
12695
  console.error(`Invalid model: ${modelName}`);
12256
12696
  console.error(`Available models: ${validModels.join(", ")}`);
@@ -12319,7 +12759,7 @@ ${engineRoot}`, {
12319
12759
  }
12320
12760
  try {
12321
12761
  await chdirToNearestUprojectRootIfPresent();
12322
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-FOBXtEU6.cjs'); });
12762
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-8w5P093W.cjs'); });
12323
12763
  let startedBy = void 0;
12324
12764
  let sessionId = void 0;
12325
12765
  for (let i = 1; i < args.length; i++) {