flockbay 0.10.19 → 0.10.21

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.
@@ -614,6 +614,14 @@ async function main() {
614
614
  "Unreal Editor: Launch Project",
615
615
  "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."
616
616
  );
617
+ forwardTool(
618
+ "unreal_editor_relaunch_last",
619
+ {
620
+ extraArgs: z.z.array(z.z.string()).optional().describe("Optional replacement UnrealEditor command-line args (advanced).")
621
+ },
622
+ "Unreal Editor: Relaunch Last Project",
623
+ "Relaunch the last Unreal project previously launched via unreal_editor_launch in this session (no auto-restart). Use this after a crash once you\u2019ve fixed files."
624
+ );
617
625
  forwardTool(
618
626
  "unreal_headless_screenshot",
619
627
  {
@@ -612,6 +612,14 @@ async function main() {
612
612
  "Unreal Editor: Launch Project",
613
613
  "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."
614
614
  );
615
+ forwardTool(
616
+ "unreal_editor_relaunch_last",
617
+ {
618
+ extraArgs: z.array(z.string()).optional().describe("Optional replacement UnrealEditor command-line args (advanced).")
619
+ },
620
+ "Unreal Editor: Relaunch Last Project",
621
+ "Relaunch the last Unreal project previously launched via unreal_editor_launch in this session (no auto-restart). Use this after a crash once you\u2019ve fixed files."
622
+ );
615
623
  forwardTool(
616
624
  "unreal_headless_screenshot",
617
625
  {
@@ -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-CL_3YyS9.cjs');
6
+ var types = require('./types-DeH24uWs.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');
@@ -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-BiUf5vLX.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-Bhkn02hu.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();
@@ -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(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,22 @@ async function uploadScreenshotViewsForSession(args) {
7158
7239
  }
7159
7240
  return out;
7160
7241
  }
7242
+ function detectImageMimeTypeFromBuffer(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
+ }
7161
7258
  async function startFlockbayServer(client, options) {
7162
7259
  const handler = async (title) => {
7163
7260
  types.logger.debug("[flockbayMCP] Changing title to:", title);
@@ -7230,7 +7327,8 @@ async function startFlockbayServer(client, options) {
7230
7327
  lastReachableAtMs: 0,
7231
7328
  lastIssueAtMs: 0,
7232
7329
  lastIssueKey: "",
7233
- launched: null
7330
+ launched: null,
7331
+ lastLaunch: null
7234
7332
  };
7235
7333
  const emitIssue = (event) => {
7236
7334
  const key = `${event.kind}:${event.severity}:${event.message}`;
@@ -7255,6 +7353,93 @@ async function startFlockbayServer(client, options) {
7255
7353
  }
7256
7354
  }
7257
7355
  };
7356
+ const tailText = (text, maxChars) => {
7357
+ const t = String(text || "").trim();
7358
+ if (!t) return "";
7359
+ if (t.length <= maxChars) return t;
7360
+ return t.slice(t.length - maxChars);
7361
+ };
7362
+ const findLatestFile = async (dir, filter) => {
7363
+ try {
7364
+ const entries = await fs$2.readdir(dir);
7365
+ let best = null;
7366
+ for (const name of entries) {
7367
+ if (!filter(name)) continue;
7368
+ const full = path.join(dir, name);
7369
+ let st;
7370
+ try {
7371
+ st = await fs$2.stat(full);
7372
+ } catch {
7373
+ continue;
7374
+ }
7375
+ if (!st?.isFile?.()) continue;
7376
+ const mtimeMs = Number(st.mtimeMs || 0);
7377
+ if (!best || mtimeMs > best.mtimeMs) best = { path: full, mtimeMs };
7378
+ }
7379
+ return best?.path ?? null;
7380
+ } catch {
7381
+ return null;
7382
+ }
7383
+ };
7384
+ const findLatestCrashDir = async (projectRoot) => {
7385
+ const crashesDir = path.join(projectRoot, "Saved", "Crashes");
7386
+ try {
7387
+ const entries = await fs$2.readdir(crashesDir);
7388
+ let best = null;
7389
+ for (const name of entries) {
7390
+ const full = path.join(crashesDir, name);
7391
+ let st;
7392
+ try {
7393
+ st = await fs$2.stat(full);
7394
+ } catch {
7395
+ continue;
7396
+ }
7397
+ if (!st?.isDirectory?.()) continue;
7398
+ const mtimeMs = Number(st.mtimeMs || 0);
7399
+ if (!best || mtimeMs > best.mtimeMs) best = { path: full, mtimeMs };
7400
+ }
7401
+ return best?.path ?? null;
7402
+ } catch {
7403
+ return null;
7404
+ }
7405
+ };
7406
+ const readFileTail = async (filePath, maxBytes) => {
7407
+ try {
7408
+ const buf = await fs$2.readFile(filePath);
7409
+ const slice = buf.length > maxBytes ? buf.subarray(buf.length - maxBytes) : buf;
7410
+ return slice.toString("utf8");
7411
+ } catch {
7412
+ return null;
7413
+ }
7414
+ };
7415
+ const gatherCrashDiagnosticsBestEffort = async (uprojectPath) => {
7416
+ const projectRoot = path.dirname(uprojectPath);
7417
+ const summary = [];
7418
+ const detail = { uprojectPath, projectRoot };
7419
+ const latestLog = await findLatestFile(path.join(projectRoot, "Saved", "Logs"), (n) => n.toLowerCase().endsWith(".log"));
7420
+ if (latestLog) {
7421
+ detail.projectLogPath = latestLog;
7422
+ const tail = await readFileTail(latestLog, 24e3);
7423
+ if (tail) {
7424
+ detail.projectLogTail = tailText(tail, 12e3);
7425
+ summary.push(`Latest log: ${latestLog}`);
7426
+ }
7427
+ }
7428
+ const latestCrashDir = await findLatestCrashDir(projectRoot);
7429
+ if (latestCrashDir) {
7430
+ detail.latestCrashDir = latestCrashDir;
7431
+ const crashContext = await findLatestFile(latestCrashDir, (n) => n.toLowerCase().includes("crashcontext") && n.toLowerCase().endsWith(".xml"));
7432
+ if (crashContext) {
7433
+ detail.crashContextPath = crashContext;
7434
+ const tail = await readFileTail(crashContext, 24e3);
7435
+ if (tail) {
7436
+ detail.crashContextTail = tailText(tail, 12e3);
7437
+ summary.push(`CrashContext: ${crashContext}`);
7438
+ }
7439
+ }
7440
+ }
7441
+ return { detail, summary };
7442
+ };
7258
7443
  const getUnrealEditorExe = (engineRoot) => {
7259
7444
  const root = engineRoot.trim().replace(/[\\/]+$/, "");
7260
7445
  if (process.platform === "darwin") {
@@ -7310,7 +7495,7 @@ ${res.stderr}`;
7310
7495
  kind: "unreachable",
7311
7496
  severity: "warning",
7312
7497
  detectedAtMs: now,
7313
- message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.",
7498
+ message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.\nNext: fix the issue, then relaunch via unreal_editor_relaunch_last (if you launched from this session) or unreal_editor_launch.",
7314
7499
  detail: {
7315
7500
  lastReachableAtMs: state.lastReachableAtMs
7316
7501
  }
@@ -7342,20 +7527,35 @@ ${res.stderr}`;
7342
7527
  engineRoot,
7343
7528
  startedAtMs: Date.now()
7344
7529
  };
7530
+ state.lastLaunch = {
7531
+ uprojectPath,
7532
+ engineRoot,
7533
+ extraArgs,
7534
+ startedAtMs: Date.now()
7535
+ };
7345
7536
  child.on("exit", (code, signal) => {
7346
- const now = Date.now();
7347
- const exitCode = typeof code === "number" ? code : null;
7348
- const sig = typeof signal === "string" ? signal : null;
7349
- const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
7350
- state.launched = null;
7351
- if (!isCrash) return;
7352
- emitIssue({
7353
- kind: "process_exit",
7354
- severity: "crash",
7355
- detectedAtMs: now,
7356
- message: `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
7357
- detail: { exitCode, signal: sig, pid, uprojectPath }
7358
- });
7537
+ void (async () => {
7538
+ const now = Date.now();
7539
+ const exitCode = typeof code === "number" ? code : null;
7540
+ const sig = typeof signal === "string" ? signal : null;
7541
+ const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
7542
+ state.launched = null;
7543
+ if (!isCrash) return;
7544
+ const diag = await gatherCrashDiagnosticsBestEffort(uprojectPath).catch(() => ({ detail: {}, summary: [] }));
7545
+ const msgParts = [
7546
+ `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
7547
+ `Project: ${uprojectPath}`,
7548
+ ...diag.summary,
7549
+ `Next: fix the issue, then relaunch via unreal_editor_relaunch_last (or unreal_editor_launch).`
7550
+ ];
7551
+ emitIssue({
7552
+ kind: "process_exit",
7553
+ severity: "crash",
7554
+ detectedAtMs: now,
7555
+ message: msgParts.filter(Boolean).join("\n"),
7556
+ detail: { exitCode, signal: sig, pid, uprojectPath, ...diag.detail }
7557
+ });
7558
+ })();
7359
7559
  });
7360
7560
  child.on("error", (err) => {
7361
7561
  const now = Date.now();
@@ -7364,7 +7564,9 @@ ${res.stderr}`;
7364
7564
  kind: "process_exit",
7365
7565
  severity: "crash",
7366
7566
  detectedAtMs: now,
7367
- message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}`,
7567
+ message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}
7568
+ Project: ${uprojectPath}
7569
+ Next: fix the issue, then relaunch via unreal_editor_relaunch_last (or unreal_editor_launch).`,
7368
7570
  detail: { pid, uprojectPath }
7369
7571
  });
7370
7572
  });
@@ -7374,6 +7576,12 @@ ${res.stderr}`;
7374
7576
  noteUnrealActivity,
7375
7577
  noteUnrealReachable,
7376
7578
  launchEditor,
7579
+ relaunchLast: async (extraArgs) => {
7580
+ const last = state.lastLaunch;
7581
+ if (!last) throw new Error("No known prior Unreal launch in this session. Use unreal_editor_launch with an explicit uprojectPath.");
7582
+ const mergedArgs = Array.isArray(extraArgs) && extraArgs.length > 0 ? extraArgs.filter((a) => typeof a === "string" && a.trim()) : last.extraArgs;
7583
+ return launchEditor({ uprojectPath: last.uprojectPath, engineRoot: last.engineRoot, extraArgs: mergedArgs });
7584
+ },
7377
7585
  stop: () => {
7378
7586
  try {
7379
7587
  interval.unref();
@@ -8576,13 +8784,13 @@ ${String(st.stdout || "").trim()}`
8576
8784
  "read_images",
8577
8785
  {
8578
8786
  title: "Read Images",
8579
- description: "Read one or more local PNG images by path and return a `{ views: [...] }` payload so the app can render them as a gallery.",
8787
+ 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.",
8580
8788
  inputSchema: {
8581
- paths: z.z.array(z.z.string()).describe("Image paths (absolute or relative to the session directory). PNG only."),
8789
+ paths: z.z.array(z.z.string()).describe("Image paths (absolute or relative to the session directory). PNG/JPG only."),
8582
8790
  limit: z.z.number().int().positive().optional().describe("Max number of images to include (default 10)."),
8583
8791
  upload: z.z.boolean().optional().describe("Upload images to the session screenshots store and return HTTPS URLs (default true)."),
8584
8792
  includeBase64: z.z.boolean().optional().describe("Include base64 data in the payload (default false)."),
8585
- includeToolImages: z.z.boolean().optional().describe("Include MCP image blocks so vision models can see the PNGs (default false)."),
8793
+ includeToolImages: z.z.boolean().optional().describe("Include MCP image blocks so vision models can see the images (default false)."),
8586
8794
  maxBytesPerImage: z.z.number().int().positive().optional().describe("Max bytes per image when includeBase64=true (default 2500000).")
8587
8795
  }
8588
8796
  },
@@ -8614,14 +8822,16 @@ ${String(st.stdout || "").trim()}`
8614
8822
  const home = process.env.HOME || process.env.USERPROFILE || "";
8615
8823
  if (home) return path.join(home, p.slice(2));
8616
8824
  }
8617
- return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
8825
+ const baseDir = readSessionWorkingDirectory();
8826
+ return path.isAbsolute(p) ? p : path.resolve(baseDir, p);
8618
8827
  };
8619
8828
  const idsSeen = /* @__PURE__ */ new Set();
8620
8829
  const viewsLocal = [];
8621
8830
  for (const inputPath of paths) {
8622
8831
  const abs = resolvePath(inputPath);
8623
- if (!abs.toLowerCase().endsWith(".png")) {
8624
- throw new Error(`Only PNG images are supported: ${abs}`);
8832
+ const lower = abs.toLowerCase();
8833
+ if (!lower.endsWith(".png") && !lower.endsWith(".jpg") && !lower.endsWith(".jpeg")) {
8834
+ throw new Error(`Only PNG/JPG images are supported: ${abs}`);
8625
8835
  }
8626
8836
  if (!fs.existsSync(abs)) {
8627
8837
  throw new Error(`Image not found: ${abs}`);
@@ -8641,7 +8851,11 @@ ${String(st.stdout || "").trim()}`
8641
8851
  throw new Error(`Image too large (${st.size} bytes) to embed: ${abs}`);
8642
8852
  }
8643
8853
  const buf = await fs$2.readFile(abs);
8644
- viewsLocal.push({ id: unique, path: abs, base64: buf.toString("base64") });
8854
+ const mime = detectImageMimeTypeFromBuffer(buf);
8855
+ if (!mime) {
8856
+ throw new Error(`Unsupported image format (expected PNG/JPEG): ${abs}`);
8857
+ }
8858
+ viewsLocal.push({ id: unique, path: abs, base64: buf.toString("base64"), mimeType: mime });
8645
8859
  } else {
8646
8860
  viewsLocal.push({ id: unique, path: abs });
8647
8861
  }
@@ -8666,7 +8880,7 @@ ${String(st.stdout || "").trim()}`
8666
8880
  viewsLocal.forEach((v, idx) => {
8667
8881
  if (!v.base64) return;
8668
8882
  content.push({ type: "text", text: `Image ${idx + 1}: ${v.id}` });
8669
- content.push({ type: "image", data: v.base64, mimeType: "image/png" });
8883
+ content.push({ type: "image", data: v.base64, mimeType: v.mimeType || "image/png" });
8670
8884
  });
8671
8885
  }
8672
8886
  return {
@@ -8678,7 +8892,7 @@ ${String(st.stdout || "").trim()}`
8678
8892
  return {
8679
8893
  content: [
8680
8894
  { type: "text", text: `Failed to read images: ${error instanceof Error ? error.message : String(error)}` },
8681
- { type: "text", text: `Tip: pass absolute PNG paths, or relative paths from the session folder.` }
8895
+ { type: "text", text: `Tip: pass absolute PNG/JPG paths, or relative paths from the session folder.` }
8682
8896
  ],
8683
8897
  isError: true
8684
8898
  };
@@ -8880,6 +9094,32 @@ ${String(st.stdout || "").trim()}`
8880
9094
  isError: false
8881
9095
  };
8882
9096
  }));
9097
+ mcp.registerTool("unreal_editor_relaunch_last", {
9098
+ title: "Unreal Editor: Relaunch Last Project",
9099
+ description: "Relaunch the last Unreal project previously launched via unreal_editor_launch in this session (no auto-restart). Use this after a crash once you\u2019ve fixed files. If it crashes again, Flockbay will abort and report again.",
9100
+ inputSchema: {
9101
+ extraArgs: z.z.array(z.z.string()).optional().describe("Optional replacement UnrealEditor command-line args (advanced).")
9102
+ }
9103
+ }, async (args) => runWithMcpToolCard("unreal_editor_relaunch_last", args, async () => {
9104
+ const extraArgs = Array.isArray(args?.extraArgs) ? args.extraArgs : void 0;
9105
+ unrealEditorSupervisor.noteUnrealActivity();
9106
+ try {
9107
+ const launched = await unrealEditorSupervisor.relaunchLast(extraArgs);
9108
+ return {
9109
+ content: [
9110
+ { type: "text", text: `Relaunched Unreal Editor (last project).` },
9111
+ { type: "text", text: JSON.stringify({ pid: launched.pid, exePath: launched.exePath }, null, 2) },
9112
+ { type: "text", text: "Next: wait for the editor to finish loading, then re-run UnrealMCP tools." }
9113
+ ],
9114
+ isError: false
9115
+ };
9116
+ } catch (err) {
9117
+ return {
9118
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
9119
+ isError: true
9120
+ };
9121
+ }
9122
+ }));
8883
9123
  mcp.registerTool("unreal_build_project", {
8884
9124
  title: "Unreal Build Project (UBT)",
8885
9125
  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.",
@@ -10305,6 +10545,7 @@ Fix: ${res.hint}` : "";
10305
10545
  "unreal_headless_screenshot",
10306
10546
  "unreal_latest_screenshots",
10307
10547
  "unreal_editor_launch",
10548
+ "unreal_editor_relaunch_last",
10308
10549
  "unreal_build_project",
10309
10550
  "unreal_mcp_command",
10310
10551
  "unreal_mcp_list_capabilities",
@@ -12086,7 +12327,7 @@ ${engineRoot}`, {
12086
12327
  } else if (subcommand === "codex") {
12087
12328
  try {
12088
12329
  await chdirToNearestUprojectRootIfPresent();
12089
- const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-Bh3-ebwT.cjs'); });
12330
+ const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-CH4lz1QX.cjs'); });
12090
12331
  let startedBy = void 0;
12091
12332
  let sessionId = void 0;
12092
12333
  for (let i = 1; i < args.length; i++) {
@@ -12181,7 +12422,7 @@ ${engineRoot}`, {
12181
12422
  }
12182
12423
  try {
12183
12424
  await chdirToNearestUprojectRootIfPresent();
12184
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-hXryGqFd.cjs'); });
12425
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-DNSymY04.cjs'); });
12185
12426
  let startedBy = void 0;
12186
12427
  let sessionId = void 0;
12187
12428
  for (let i = 1; i < args.length; i++) {