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.
@@ -4,8 +4,8 @@ import { randomUUID, createHash } from 'node:crypto';
4
4
  import os from 'node:os';
5
5
  import path, { resolve, join as join$1, basename } from 'node:path';
6
6
  import { mkdir, writeFile, readFile } from 'node:fs/promises';
7
- import { l as logger, b as packageJson, A as ApiClient, r as readSettings, p as projectPath, c as configuration } from './types-C4QeUggl.mjs';
8
- import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, n as notifyDaemonSessionStarted, M as MessageQueue2, g as buildProjectCapsule, a as setLatestUserImages, b as MessageBuffer, w as withUserImagesMarker, P as PLATFORM_SYSTEM_PROMPT, r as registerKillSessionHandler, d as startFlockbayServer, o as extractUserImagesMarker, p as getLatestUserImages, j as autoFinalizeCoordinationWorkItem, E as ElicitationHub, k as detectScreenshotsForGate, m as stopCaffeinate } from './index-CX0Z8pmz.mjs';
7
+ import { l as logger, b as packageJson, A as ApiClient, r as readSettings, p as projectPath, c as configuration } from './types-CMWcip0F.mjs';
8
+ import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, n as notifyDaemonSessionStarted, M as MessageQueue2, g as buildProjectCapsule, a as setLatestUserImages, b as MessageBuffer, w as withUserImagesMarker, r as registerKillSessionHandler, d as startFlockbayServer, o as extractUserImagesMarker, p as getLatestUserImages, P as PLATFORM_SYSTEM_PROMPT, j as autoFinalizeCoordinationWorkItem, E as ElicitationHub, k as detectScreenshotsForGate, m as stopCaffeinate } from './index-BjZUYSzh.mjs';
9
9
  import { spawn, spawnSync } from 'node:child_process';
10
10
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
11
11
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
@@ -43,6 +43,17 @@ const KNOWN_TOOL_PATTERNS = {
43
43
  save_memory: ["save_memory", "save-memory"],
44
44
  think: ["think"]
45
45
  };
46
+ const MCP_FLOCKBAY_TOOL_RE = /\bmcp__flockbay__[a-z0-9_]+\b/gi;
47
+ const UNREAL_MCP_TOOL_RE = /\bunreal_mcp_[a-z0-9_]+\b/gi;
48
+ function extractFirstToolNameFromText(text) {
49
+ if (!text) return null;
50
+ const lower = text.toLowerCase();
51
+ const mcpMatch = lower.match(MCP_FLOCKBAY_TOOL_RE);
52
+ if (mcpMatch && mcpMatch[0]) return mcpMatch[0];
53
+ const unrealMatch = lower.match(UNREAL_MCP_TOOL_RE);
54
+ if (unrealMatch && unrealMatch[0]) return `mcp__flockbay__${unrealMatch[0]}`;
55
+ return null;
56
+ }
46
57
  function isInvestigationTool(toolCallId, toolKind) {
47
58
  return toolCallId.includes("codebase_investigator") || toolCallId.includes("investigator") || typeof toolKind === "string" && toolKind.includes("investigator");
48
59
  }
@@ -55,6 +66,18 @@ function extractToolNameFromId(toolCallId) {
55
66
  }
56
67
  }
57
68
  }
69
+ const uuidLike = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(toolCallId);
70
+ if (!uuidLike) {
71
+ const prefix = toolCallId.split(/[-:]/, 1)[0] || "";
72
+ const trimmed = prefix.trim();
73
+ if (trimmed.length >= 2 && trimmed.length <= 160) {
74
+ const hasNonHexAlpha = /[g-z]/i.test(trimmed) || /[A-Z]/.test(trimmed) || /_/.test(trimmed);
75
+ const saneChars = /^[A-Za-z0-9_]+$/.test(trimmed);
76
+ if (saneChars && hasNonHexAlpha) {
77
+ return trimmed;
78
+ }
79
+ }
80
+ }
58
81
  return null;
59
82
  }
60
83
  function determineToolName(toolName, toolCallId, input, params, context) {
@@ -65,20 +88,30 @@ function determineToolName(toolName, toolCallId, input, params, context) {
65
88
  if (idToolName) {
66
89
  return idToolName;
67
90
  }
91
+ const idTextToolName = extractFirstToolNameFromText(toolCallId);
92
+ if (idTextToolName) {
93
+ return idTextToolName;
94
+ }
68
95
  if (input && typeof input === "object") {
69
- const inputStr = JSON.stringify(input).toLowerCase();
96
+ const inputStr = JSON.stringify(input);
97
+ const extracted = extractFirstToolNameFromText(inputStr);
98
+ if (extracted) return extracted;
99
+ const inputLower = inputStr.toLowerCase();
70
100
  for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
71
101
  for (const pattern of patterns) {
72
- if (inputStr.includes(pattern.toLowerCase())) {
102
+ if (inputLower.includes(pattern.toLowerCase())) {
73
103
  return toolName2;
74
104
  }
75
105
  }
76
106
  }
77
107
  }
78
- const paramsStr = JSON.stringify(params).toLowerCase();
108
+ const paramsStr = JSON.stringify(params);
109
+ const paramsExtracted = extractFirstToolNameFromText(paramsStr);
110
+ if (paramsExtracted) return paramsExtracted;
111
+ const paramsLower = paramsStr.toLowerCase();
79
112
  for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
80
113
  for (const pattern of patterns) {
81
- if (paramsStr.includes(pattern.toLowerCase())) {
114
+ if (paramsLower.includes(pattern.toLowerCase())) {
82
115
  return toolName2;
83
116
  }
84
117
  }
@@ -315,7 +348,7 @@ class AcpSdkBackend {
315
348
  this.emit({
316
349
  type: "status",
317
350
  status: "error",
318
- detail: "Model not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite"
351
+ detail: "Model not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro-preview, gemini-3-flash-preview"
319
352
  });
320
353
  } else if (/authentication required/i.test(text) || /login required/i.test(text) || /invalid.*(api key|key)/i.test(text) || /permission denied/i.test(text)) {
321
354
  const excerpt = this.stderrExcerpt(20);
@@ -1818,7 +1851,7 @@ class GeminiPermissionHandler {
1818
1851
  const decision = result.decision;
1819
1852
  const reason = result.reason;
1820
1853
  const kind = decision === "approved" || decision === "approved_for_session" ? "policy_allow" : decision === "abort" && reason === "permission_prompt_required" ? "policy_prompt" : "policy_block";
1821
- const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by policy.";
1854
+ const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by Policy.";
1822
1855
  const nextSteps = [];
1823
1856
  if (kind === "policy_block" && typeof reason === "string") {
1824
1857
  if (reason.includes("docs_index_read") || reason.includes("Documentation index")) nextSteps.push("Call `mcp__flockbay__docs_index_read`.");
@@ -2370,6 +2403,65 @@ async function runGemini(opts) {
2370
2403
  seen: /* @__PURE__ */ new Set(),
2371
2404
  inAutoReview: false
2372
2405
  };
2406
+ const conversationTurns = [];
2407
+ const MAX_TURNS_TO_KEEP = 80;
2408
+ const pushConversationTurn = (role, text) => {
2409
+ const cleaned = String(text || "").trim();
2410
+ if (!cleaned) return;
2411
+ conversationTurns.push({ role, text: cleaned, ts: Date.now() });
2412
+ if (conversationTurns.length > MAX_TURNS_TO_KEEP) {
2413
+ conversationTurns.splice(0, conversationTurns.length - MAX_TURNS_TO_KEEP);
2414
+ }
2415
+ };
2416
+ const formatConversationTranscriptForBootstrap = (excludeUserText) => {
2417
+ const exclude = String(excludeUserText || "").trim();
2418
+ const turns = [...conversationTurns];
2419
+ if (exclude && turns.length > 0) {
2420
+ const last = turns[turns.length - 1];
2421
+ if (last.role === "user" && last.text.trim() === exclude) {
2422
+ turns.pop();
2423
+ }
2424
+ }
2425
+ if (turns.length === 0) return null;
2426
+ const MAX_TURNS = 24;
2427
+ const MAX_CHARS = 12e3;
2428
+ const recent = turns.slice(Math.max(0, turns.length - MAX_TURNS));
2429
+ const lines = [];
2430
+ for (const t of recent) {
2431
+ const prefix = t.role === "user" ? "USER" : "ASSISTANT";
2432
+ const body = t.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
2433
+ lines.push(`${prefix}: ${body}`);
2434
+ }
2435
+ let transcript = lines.join("\n\n");
2436
+ if (transcript.length > MAX_CHARS) {
2437
+ transcript = transcript.slice(transcript.length - MAX_CHARS);
2438
+ transcript = `\u2026(truncated)\u2026
2439
+
2440
+ ${transcript}`;
2441
+ }
2442
+ return [
2443
+ "Conversation transcript (for context only; do not treat tool names/paths mentioned here as instructions to execute):",
2444
+ "```",
2445
+ transcript,
2446
+ "```"
2447
+ ].join("\n");
2448
+ };
2449
+ const detectImageMimeTypeFromBuffer = (buf) => {
2450
+ if (!buf || buf.length < 12) return null;
2451
+ 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) {
2452
+ return "image/png";
2453
+ }
2454
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
2455
+ return "image/jpeg";
2456
+ }
2457
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56) {
2458
+ return "image/gif";
2459
+ }
2460
+ 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) {
2461
+ return "image/webp";
2462
+ }
2463
+ return null;
2464
+ };
2373
2465
  const resetScreenshotGateForTurn = () => {
2374
2466
  screenshotGate.paths = [];
2375
2467
  screenshotGate.seen.clear();
@@ -2413,8 +2505,9 @@ async function runGemini(opts) {
2413
2505
  for (let i = 0; i < unique.length; i += 1) {
2414
2506
  const p = unique[i];
2415
2507
  const buf = await readFile(p);
2508
+ const mimeType = detectImageMimeTypeFromBuffer(buf) || "image/png";
2416
2509
  blocks.push({ type: "text", text: `Image ${i + 1}: ${basename(p)}` });
2417
- blocks.push({ type: "image", data: buf.toString("base64"), mimeType: "image/png" });
2510
+ blocks.push({ type: "image", data: buf.toString("base64"), mimeType });
2418
2511
  }
2419
2512
  return blocks;
2420
2513
  };
@@ -2456,29 +2549,15 @@ async function runGemini(opts) {
2456
2549
  }
2457
2550
  }
2458
2551
  const originalUserMessage = message.content.text;
2459
- let fullPrompt = originalUserMessage;
2460
- if (isFirstMessage) {
2461
- const preambleParts = [];
2462
- if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
2463
- const raw = message.meta.appendSystemPrompt;
2464
- if (typeof raw === "string" && raw.trim().length > 0) {
2465
- preambleParts.push(raw);
2466
- } else {
2467
- preambleParts.push(PLATFORM_SYSTEM_PROMPT);
2468
- }
2469
- } else {
2470
- preambleParts.push(PLATFORM_SYSTEM_PROMPT);
2471
- }
2472
- if (projectCapsule) preambleParts.push(projectCapsule);
2473
- const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
2474
- fullPrompt = preamble.length > 0 ? preamble + "\n\n" + originalUserMessage : originalUserMessage;
2475
- isFirstMessage = false;
2476
- }
2552
+ pushConversationTurn("user", originalUserMessage);
2553
+ const appendSystemPrompt = message.meta?.hasOwnProperty("appendSystemPrompt") && typeof message.meta.appendSystemPrompt === "string" ? message.meta.appendSystemPrompt : null;
2554
+ const fullPrompt = originalUserMessage;
2477
2555
  const mode = {
2478
2556
  permissionMode: messagePermissionMode || "default",
2479
2557
  model: messageModel,
2480
- originalUserMessage
2558
+ originalUserMessage,
2481
2559
  // Store original message separately
2560
+ appendSystemPrompt
2482
2561
  };
2483
2562
  const promptText = images.length > 0 ? withUserImagesMarker(fullPrompt, images.length) : fullPrompt;
2484
2563
  messageQueue.push(promptText, mode, images.length > 0 ? { isolate: true } : void 0);
@@ -2488,19 +2567,14 @@ async function runGemini(opts) {
2488
2567
  const keepAliveInterval = setInterval(() => {
2489
2568
  session.keepAlive(thinking, "remote");
2490
2569
  }, 2e3);
2491
- let isFirstMessage = true;
2492
2570
  const autoPrompt = String(process.env.FLOCKBAY_AUTO_PROMPT || "").trim();
2493
2571
  const autoExitOnIdle = String(process.env.FLOCKBAY_AUTO_EXIT_ON_IDLE || "").trim() === "1";
2494
2572
  if (autoPrompt) {
2495
- const preambleParts = [PLATFORM_SYSTEM_PROMPT];
2496
- if (projectCapsule) preambleParts.push(projectCapsule);
2497
- const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
2498
- const fullPrompt = preamble.length > 0 ? preamble + "\n\n" + autoPrompt : autoPrompt;
2499
- isFirstMessage = false;
2500
- messageQueue.push(fullPrompt, {
2573
+ messageQueue.push(autoPrompt, {
2501
2574
  permissionMode: "default",
2502
2575
  model: void 0,
2503
- originalUserMessage: autoPrompt
2576
+ originalUserMessage: autoPrompt,
2577
+ appendSystemPrompt: null
2504
2578
  });
2505
2579
  }
2506
2580
  const sendReady = () => {
@@ -2515,6 +2589,30 @@ async function runGemini(opts) {
2515
2589
  logger.debug("[Gemini] Failed to send ready push", pushError);
2516
2590
  }
2517
2591
  };
2592
+ let pendingTurnCompletion = null;
2593
+ const beginTurnCompletionWait = () => {
2594
+ if (pendingTurnCompletion) {
2595
+ logger.debug("[Gemini] beginTurnCompletionWait called while a turn is already pending; replacing pending wait");
2596
+ try {
2597
+ pendingTurnCompletion.resolve("error");
2598
+ } catch {
2599
+ }
2600
+ pendingTurnCompletion = null;
2601
+ }
2602
+ let resolve2;
2603
+ const promise = new Promise((res) => {
2604
+ resolve2 = res;
2605
+ });
2606
+ pendingTurnCompletion = { sawRunning: false, resolve: resolve2, promise };
2607
+ return promise;
2608
+ };
2609
+ const maybeResolveTurnCompletion = (status) => {
2610
+ if (!pendingTurnCompletion) return;
2611
+ if (!pendingTurnCompletion.sawRunning) return;
2612
+ const pending = pendingTurnCompletion;
2613
+ pendingTurnCompletion = null;
2614
+ pending.resolve(status);
2615
+ };
2518
2616
  const emitReadyIfIdle = () => {
2519
2617
  if (shouldExit) {
2520
2618
  return false;
@@ -2542,10 +2640,36 @@ async function runGemini(opts) {
2542
2640
  let wasSessionCreated = false;
2543
2641
  let abortNote = null;
2544
2642
  let abortNoteSentToSession = false;
2643
+ let abortRequested = false;
2644
+ let readySentForAbort = false;
2645
+ function normalizeCancelNote(input) {
2646
+ if (typeof input === "string") return input.trim() ? input.trim() : null;
2647
+ if (!input || typeof input !== "object") return null;
2648
+ const note = typeof input.note === "string" ? String(input.note).trim() : "";
2649
+ if (note) return note;
2650
+ const reason = typeof input.reason === "string" ? String(input.reason).trim() : "";
2651
+ if (reason) return reason;
2652
+ return null;
2653
+ }
2654
+ function makeAbortError(message = "Aborted") {
2655
+ const error = new Error(message);
2656
+ error.name = "AbortError";
2657
+ return error;
2658
+ }
2659
+ function abortable(signal, promise) {
2660
+ if (signal.aborted) return Promise.reject(makeAbortError());
2661
+ return new Promise((resolve2, reject) => {
2662
+ const onAbort = () => reject(makeAbortError());
2663
+ signal.addEventListener("abort", onAbort, { once: true });
2664
+ promise.then(resolve2, reject).finally(() => signal.removeEventListener("abort", onAbort));
2665
+ });
2666
+ }
2545
2667
  async function handleAbort(note, options) {
2546
2668
  logger.debug("[Gemini] Abort requested - stopping current task");
2547
- if (typeof note === "string" && note.trim()) {
2548
- abortNote = note.trim();
2669
+ abortRequested = true;
2670
+ const normalizedNote = normalizeCancelNote(note);
2671
+ if (normalizedNote) {
2672
+ abortNote = normalizedNote;
2549
2673
  abortNoteSentToSession = Boolean(options?.alreadySentToSession);
2550
2674
  }
2551
2675
  session.sendCodexMessage({
@@ -2556,6 +2680,13 @@ async function runGemini(opts) {
2556
2680
  diffProcessor.reset();
2557
2681
  try {
2558
2682
  abortController.abort();
2683
+ if (pendingTurnCompletion) {
2684
+ try {
2685
+ pendingTurnCompletion.resolve("error");
2686
+ } catch {
2687
+ }
2688
+ pendingTurnCompletion = null;
2689
+ }
2559
2690
  messageQueue.reset();
2560
2691
  if (geminiBackend && acpSessionId) {
2561
2692
  await geminiBackend.cancel(acpSessionId);
@@ -2694,6 +2825,7 @@ async function runGemini(opts) {
2694
2825
  };
2695
2826
  let accumulatedResponse = "";
2696
2827
  let isResponseInProgress = false;
2828
+ let currentResponseMessageId = null;
2697
2829
  function setupGeminiMessageHandler(backend) {
2698
2830
  backend.onMessage((msg) => {
2699
2831
  switch (msg.type) {
@@ -2723,6 +2855,9 @@ async function runGemini(opts) {
2723
2855
  if (msg.status === "running") {
2724
2856
  thinking = true;
2725
2857
  session.keepAlive(thinking, "remote");
2858
+ if (pendingTurnCompletion) {
2859
+ pendingTurnCompletion.sawRunning = true;
2860
+ }
2726
2861
  session.sendCodexMessage({
2727
2862
  type: "task_started",
2728
2863
  id: randomUUID()
@@ -2763,14 +2898,18 @@ async function runGemini(opts) {
2763
2898
  logger.debug(`[gemini] Sending complete message to mobile (length: ${finalMessageText.length}): ${finalMessageText.substring(0, 100)}...`);
2764
2899
  logger.debug(`[gemini] Full message payload:`, JSON.stringify(messagePayload, null, 2));
2765
2900
  session.sendCodexMessage(messagePayload);
2901
+ pushConversationTurn("assistant", finalMessageText);
2766
2902
  accumulatedResponse = "";
2767
2903
  isResponseInProgress = false;
2768
2904
  }
2905
+ maybeResolveTurnCompletion(msg.status);
2769
2906
  } else if (msg.status === "error") {
2770
2907
  thinking = false;
2771
2908
  session.keepAlive(thinking, "remote");
2772
2909
  accumulatedResponse = "";
2773
2910
  isResponseInProgress = false;
2911
+ currentResponseMessageId = null;
2912
+ maybeResolveTurnCompletion("error");
2774
2913
  const errorMessage = stringifyErrorish(msg.detail || "Unknown error");
2775
2914
  messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
2776
2915
  session.sendCodexMessage({
@@ -2967,6 +3106,7 @@ async function runGemini(opts) {
2967
3106
  if (!message) {
2968
3107
  break;
2969
3108
  }
3109
+ let startedSessionThisTurn = false;
2970
3110
  if (wasSessionCreated && currentModeHash && message.hash !== currentModeHash) {
2971
3111
  logger.debug("[Gemini] Mode changed \u2013 restarting Gemini session");
2972
3112
  messageBuffer.addMessage("\u2550".repeat(40), "status");
@@ -2993,8 +3133,9 @@ async function runGemini(opts) {
2993
3133
  const actualModel = determineGeminiModel(modelToUse, localConfigForModel);
2994
3134
  logger.debug(`[gemini] Model change - modelToUse=${modelToUse}, actualModel=${actualModel}`);
2995
3135
  logger.debug("[gemini] Starting new ACP session with model:", actualModel);
2996
- const { sessionId } = await geminiBackend.startSession();
3136
+ const { sessionId } = await abortable(abortController.signal, geminiBackend.startSession());
2997
3137
  acpSessionId = sessionId;
3138
+ startedSessionThisTurn = true;
2998
3139
  logger.debug(`[gemini] New ACP session started: ${acpSessionId}`);
2999
3140
  logger.debug(`[gemini] Calling updateDisplayedModel with: ${actualModel}`);
3000
3141
  updateDisplayedModel(actualModel, false);
@@ -3009,6 +3150,15 @@ async function runGemini(opts) {
3009
3150
  let retryThisTurn = false;
3010
3151
  let skipAutoFinalize = false;
3011
3152
  try {
3153
+ const turnSignal = abortController.signal;
3154
+ const sendPromptAndWait = async (prompt) => {
3155
+ if (!geminiBackend || !acpSessionId) {
3156
+ throw new Error("Gemini backend or session not initialized");
3157
+ }
3158
+ const completion = beginTurnCompletionWait();
3159
+ await abortable(turnSignal, geminiBackend.sendPrompt(acpSessionId, prompt));
3160
+ return await abortable(turnSignal, completion);
3161
+ };
3012
3162
  if (first || !wasSessionCreated) {
3013
3163
  if (!geminiBackend) {
3014
3164
  await refreshGeminiCloudToken("before-backend-create");
@@ -3033,8 +3183,9 @@ async function runGemini(opts) {
3033
3183
  if (!acpSessionId) {
3034
3184
  logger.debug("[gemini] Starting ACP session...");
3035
3185
  updatePermissionMode(message.mode.permissionMode);
3036
- const { sessionId } = await geminiBackend.startSession();
3186
+ const { sessionId } = await abortable(turnSignal, geminiBackend.startSession());
3037
3187
  acpSessionId = sessionId;
3188
+ startedSessionThisTurn = true;
3038
3189
  logger.debug(`[gemini] ACP session started: ${acpSessionId}`);
3039
3190
  wasSessionCreated = true;
3040
3191
  currentModeHash = message.hash;
@@ -3052,26 +3203,48 @@ async function runGemini(opts) {
3052
3203
  }
3053
3204
  const promptToSend = message.message;
3054
3205
  const parsedPrompt = extractUserImagesMarker(promptToSend);
3055
- const promptText = parsedPrompt.text;
3056
3206
  const promptImages = parsedPrompt.hasImages ? getLatestUserImages().slice(0, parsedPrompt.count) : [];
3207
+ const originalUserMessage = (message.mode?.originalUserMessage || parsedPrompt.text || "").trim();
3208
+ let promptText = parsedPrompt.text;
3209
+ if (startedSessionThisTurn) {
3210
+ const override = typeof message.mode?.appendSystemPrompt === "string" ? message.mode.appendSystemPrompt.trim() : "";
3211
+ const systemPrompt = override.length > 0 ? override : PLATFORM_SYSTEM_PROMPT;
3212
+ const preambleParts = [systemPrompt];
3213
+ if (projectCapsule) preambleParts.push(projectCapsule);
3214
+ const transcript = formatConversationTranscriptForBootstrap(originalUserMessage);
3215
+ if (transcript) preambleParts.push(transcript);
3216
+ const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
3217
+ promptText = preamble.length > 0 ? `${preamble}
3218
+
3219
+ ${originalUserMessage}` : originalUserMessage;
3220
+ }
3057
3221
  if (promptImages.length > 0) {
3058
3222
  const blocks = [
3059
3223
  { type: "text", text: promptText },
3060
3224
  ...promptImages.map((img) => ({ type: "image", data: img.base64, mimeType: img.mimeType }))
3061
3225
  ];
3062
3226
  logger.debug(`[gemini] Sending multimodal prompt blocks (textLength=${promptText.length}, images=${promptImages.length})`);
3063
- await geminiBackend.sendPrompt(acpSessionId, blocks);
3227
+ const status = await sendPromptAndWait(blocks);
3228
+ if (status !== "idle") {
3229
+ skipAutoFinalize = true;
3230
+ }
3064
3231
  } else {
3065
3232
  logger.debug(`[gemini] Sending prompt to Gemini (length: ${promptText.length}): ${promptText.substring(0, 100)}...`);
3066
3233
  logger.debug(`[gemini] Full prompt: ${promptText}`);
3067
- await geminiBackend.sendPrompt(acpSessionId, promptText);
3234
+ const status = await sendPromptAndWait(promptText);
3235
+ if (status !== "idle") {
3236
+ skipAutoFinalize = true;
3237
+ }
3068
3238
  }
3069
3239
  logger.debug("[gemini] Prompt sent successfully");
3070
- if (!screenshotGate.inAutoReview && screenshotGate.paths.length > 0) {
3240
+ if (!skipAutoFinalize && !screenshotGate.inAutoReview && screenshotGate.paths.length > 0) {
3071
3241
  screenshotGate.inAutoReview = true;
3072
3242
  messageBuffer.addMessage("Auto-reviewing screenshots\u2026", "status");
3073
3243
  const blocks = await buildScreenshotReviewBlocks(screenshotGate.paths);
3074
- await geminiBackend.sendPrompt(acpSessionId, blocks);
3244
+ const status = await sendPromptAndWait(blocks);
3245
+ if (status !== "idle") {
3246
+ skipAutoFinalize = true;
3247
+ }
3075
3248
  screenshotGate.inAutoReview = false;
3076
3249
  }
3077
3250
  if (first) {
@@ -3080,14 +3253,21 @@ async function runGemini(opts) {
3080
3253
  } catch (error) {
3081
3254
  logger.debug("[gemini] Error in gemini session:", error);
3082
3255
  const isAbortError = error instanceof Error && error.name === "AbortError";
3083
- if (isAbortError) {
3256
+ const treatAsAbort = abortRequested || isAbortError;
3257
+ if (treatAsAbort) {
3084
3258
  skipAutoFinalize = true;
3085
- const note = abortNote || "Aborted by user";
3259
+ readySentForAbort = true;
3260
+ const note = abortNote || "Canceled by user";
3086
3261
  abortNote = null;
3087
3262
  const alreadySent = abortNoteSentToSession;
3088
3263
  abortNoteSentToSession = false;
3264
+ abortRequested = false;
3265
+ accumulatedResponse = "";
3266
+ isResponseInProgress = false;
3267
+ currentResponseMessageId = null;
3089
3268
  messageBuffer.addMessage(note, "status");
3090
3269
  if (!alreadySent) session.sendSessionEvent({ type: "message", message: note });
3270
+ session.sendSessionEvent({ type: "ready" });
3091
3271
  } else {
3092
3272
  const rawErrorString = error instanceof Error ? error.message : String(error);
3093
3273
  const looksLikeAuthOrQuotaError = /unauthenticated|unauthorized|invalid (api )?key|api key not valid|permission denied|forbidden|expired|quota|usage limit|rate limit|resource exhausted|resource_exhausted|status 401|status 403|status 429|\b401\b|\b403\b|\b429\b/i.test(rawErrorString);
@@ -3119,7 +3299,7 @@ async function runGemini(opts) {
3119
3299
  const errorString = String(error);
3120
3300
  if (errorCode === 404 || errorDetails.includes("notFound") || errorDetails.includes("404") || errorMessage.includes("not found") || errorMessage.includes("404")) {
3121
3301
  const currentModel2 = displayedModel || "gemini-2.5-pro";
3122
- errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite`;
3302
+ errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro-preview, gemini-3-flash-preview`;
3123
3303
  } else if (errorCode === 429 || errorDetails.includes("429") || errorMessage.includes("429") || errorString.includes("429") || errorDetails.includes("rateLimitExceeded") || errorDetails.includes("RESOURCE_EXHAUSTED") || errorMessage.includes("Rate limit exceeded") || errorMessage.includes("Resource exhausted") || errorString.includes("rateLimitExceeded") || errorString.includes("RESOURCE_EXHAUSTED")) {
3124
3304
  errorMsg = "Gemini API rate limit exceeded. Please wait a moment and try again. The API will retry automatically.";
3125
3305
  } else if (errorDetails.includes("quota") || errorMessage.includes("quota") || errorString.includes("quota")) {
@@ -3143,6 +3323,7 @@ async function runGemini(opts) {
3143
3323
  permissionHandler.reset();
3144
3324
  reasoningProcessor.abort();
3145
3325
  diffProcessor.reset();
3326
+ abortRequested = false;
3146
3327
  thinking = false;
3147
3328
  session.keepAlive(thinking, "remote");
3148
3329
  if (!retryThisTurn) {
@@ -3172,7 +3353,11 @@ async function runGemini(opts) {
3172
3353
  });
3173
3354
  }
3174
3355
  }
3175
- emitReadyIfIdle();
3356
+ if (readySentForAbort) {
3357
+ readySentForAbort = false;
3358
+ } else {
3359
+ emitReadyIfIdle();
3360
+ }
3176
3361
  }
3177
3362
  logger.debug(`[gemini] Main loop: turn completed, continuing to next iteration (queue size: ${messageQueue.size()})`);
3178
3363
  }
@@ -11,7 +11,7 @@ import { readFile, unlink, open, stat, mkdir, writeFile, rename } from 'node:fs/
11
11
  import * as z from 'zod';
12
12
  import { z as z$1 } from 'zod';
13
13
  import { spawn } from 'child_process';
14
- import { realpath, stat as stat$1, readdir, rm, mkdir as mkdir$1, readFile as readFile$1, open as open$1, writeFile as writeFile$1, chmod } from 'fs/promises';
14
+ import { readdir, mkdir as mkdir$1, realpath, stat as stat$1, rm, readFile as readFile$1, open as open$1, writeFile as writeFile$1, chmod } from 'fs/promises';
15
15
  import { randomUUID, createHash } from 'crypto';
16
16
  import { dirname, resolve, join as join$1, relative } from 'path';
17
17
  import { fileURLToPath } from 'url';
@@ -21,7 +21,7 @@ import net from 'node:net';
21
21
  import { spawn as spawn$1 } from 'node:child_process';
22
22
 
23
23
  var name = "flockbay";
24
- var version = "0.10.20";
24
+ var version = "0.10.22";
25
25
  var description = "Flockbay CLI (local agent + daemon)";
26
26
  var author = "Eduardo Orellana";
27
27
  var license = "UNLICENSED";
@@ -1570,6 +1570,44 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordinatio
1570
1570
  };
1571
1571
  }
1572
1572
  });
1573
+ rpcHandlerManager.registerHandler("fs_list_dir", async (data) => {
1574
+ const p = String(data?.path || "").trim();
1575
+ if (!p) return { success: false, error: "missing_path" };
1576
+ try {
1577
+ const out = [];
1578
+ const items = await readdir(p, { withFileTypes: true });
1579
+ for (const d of items) {
1580
+ out.push({
1581
+ name: d.name,
1582
+ isDir: d.isDirectory(),
1583
+ isFile: d.isFile(),
1584
+ isSymlink: d.isSymbolicLink()
1585
+ });
1586
+ }
1587
+ out.sort((a, b) => {
1588
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1589
+ return a.name.localeCompare(b.name);
1590
+ });
1591
+ return { success: true, path: p, entries: out };
1592
+ } catch (error) {
1593
+ const e = error;
1594
+ const msg = e?.message ? String(e.message) : String(error);
1595
+ return { success: false, path: p, error: msg };
1596
+ }
1597
+ });
1598
+ rpcHandlerManager.registerHandler("fs_mkdir", async (data) => {
1599
+ const p = String(data?.path || "").trim();
1600
+ if (!p) return { success: false, error: "missing_path" };
1601
+ const recursive = data?.recursive !== false;
1602
+ try {
1603
+ await mkdir$1(p, { recursive });
1604
+ return { success: true, path: p };
1605
+ } catch (error) {
1606
+ const e = error;
1607
+ const msg = e?.message ? String(e.message) : String(error);
1608
+ return { success: false, path: p, error: msg };
1609
+ }
1610
+ });
1573
1611
  rpcHandlerManager.registerHandler(
1574
1612
  "unreal-mcp-bridge-status",
1575
1613
  async (params) => {
@@ -3774,4 +3812,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
3774
3812
  }).passthrough()
3775
3813
  ]);
3776
3814
 
3777
- export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, readDaemonState as f, clearDaemonState as g, readCredentials as h, unrealMcpPythonDir as i, acquireDaemonLock as j, writeDaemonState as k, logger as l, ApiMachineClient as m, releaseDaemonLock as n, clearCredentials as o, projectPath as p, clearMachineId as q, readSettings as r, sendUnrealMcpTcpCommand as s, installUnrealMcpPluginToEngine as t, updateSettings as u, getLatestDaemonLog as v, writeCredentials as w, normalizeServerUrlForNode as x };
3815
+ export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, readDaemonState as f, clearDaemonState as g, readCredentials as h, unrealMcpPythonDir as i, acquireDaemonLock as j, writeDaemonState as k, logger as l, ApiMachineClient as m, releaseDaemonLock as n, clearCredentials as o, projectPath as p, clearMachineId as q, readSettings as r, sendUnrealMcpTcpCommand as s, installUnrealMcpPluginToEngine as t, updateSettings as u, buildAndInstallUnrealMcpPlugin as v, writeCredentials as w, getLatestDaemonLog as x, normalizeServerUrlForNode as y };
@@ -42,7 +42,7 @@ function _interopNamespaceDefault(e) {
42
42
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
43
43
 
44
44
  var name = "flockbay";
45
- var version = "0.10.20";
45
+ var version = "0.10.22";
46
46
  var description = "Flockbay CLI (local agent + daemon)";
47
47
  var author = "Eduardo Orellana";
48
48
  var license = "UNLICENSED";
@@ -770,7 +770,7 @@ class RpcHandlerManager {
770
770
  }
771
771
  }
772
772
 
773
- const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-BYHCKlu_.cjs', document.baseURI).href))));
773
+ const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-Z2OYpI8c.cjs', document.baseURI).href))));
774
774
  function projectPath() {
775
775
  const path = path$1.resolve(__dirname$1, "..");
776
776
  return path;
@@ -1591,6 +1591,44 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordinatio
1591
1591
  };
1592
1592
  }
1593
1593
  });
1594
+ rpcHandlerManager.registerHandler("fs_list_dir", async (data) => {
1595
+ const p = String(data?.path || "").trim();
1596
+ if (!p) return { success: false, error: "missing_path" };
1597
+ try {
1598
+ const out = [];
1599
+ const items = await fs$3.readdir(p, { withFileTypes: true });
1600
+ for (const d of items) {
1601
+ out.push({
1602
+ name: d.name,
1603
+ isDir: d.isDirectory(),
1604
+ isFile: d.isFile(),
1605
+ isSymlink: d.isSymbolicLink()
1606
+ });
1607
+ }
1608
+ out.sort((a, b) => {
1609
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1610
+ return a.name.localeCompare(b.name);
1611
+ });
1612
+ return { success: true, path: p, entries: out };
1613
+ } catch (error) {
1614
+ const e = error;
1615
+ const msg = e?.message ? String(e.message) : String(error);
1616
+ return { success: false, path: p, error: msg };
1617
+ }
1618
+ });
1619
+ rpcHandlerManager.registerHandler("fs_mkdir", async (data) => {
1620
+ const p = String(data?.path || "").trim();
1621
+ if (!p) return { success: false, error: "missing_path" };
1622
+ const recursive = data?.recursive !== false;
1623
+ try {
1624
+ await fs$3.mkdir(p, { recursive });
1625
+ return { success: true, path: p };
1626
+ } catch (error) {
1627
+ const e = error;
1628
+ const msg = e?.message ? String(e.message) : String(error);
1629
+ return { success: false, path: p, error: msg };
1630
+ }
1631
+ });
1594
1632
  rpcHandlerManager.registerHandler(
1595
1633
  "unreal-mcp-bridge-status",
1596
1634
  async (params) => {
@@ -3801,6 +3839,7 @@ exports.ApiSessionClient = ApiSessionClient;
3801
3839
  exports.RawJSONLinesSchema = RawJSONLinesSchema;
3802
3840
  exports.acquireDaemonLock = acquireDaemonLock;
3803
3841
  exports.backoff = backoff;
3842
+ exports.buildAndInstallUnrealMcpPlugin = buildAndInstallUnrealMcpPlugin;
3804
3843
  exports.clearCredentials = clearCredentials;
3805
3844
  exports.clearDaemonState = clearDaemonState;
3806
3845
  exports.clearMachineId = clearMachineId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flockbay",
3
- "version": "0.10.20",
3
+ "version": "0.10.22",
4
4
  "description": "Flockbay CLI (local agent + daemon)",
5
5
  "author": "Eduardo Orellana",
6
6
  "license": "UNLICENSED",