flockbay 0.10.27 → 0.10.30

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.
@@ -1,14 +1,14 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
2
  import os, { homedir } from 'node:os';
3
3
  import { randomUUID, createCipheriv, randomBytes } from 'node:crypto';
4
- import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as readDaemonState, g as clearDaemonState, b as packageJson, r as readSettings, h as readCredentials, u as updateSettings, w as writeCredentials, i as unrealMcpPythonDir, j as acquireDaemonLock, k as writeDaemonState, m as ApiMachineClient, n as releaseDaemonLock, s as sendUnrealMcpTcpCommand, A as ApiClient, o as clearCredentials, q as clearMachineId, t as installUnrealMcpPluginToEngine, v as buildAndInstallUnrealMcpPlugin, x as getLatestDaemonLog, y as normalizeServerUrlForNode } from './types-DYcgk0yQ.mjs';
4
+ import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as readDaemonState, g as clearDaemonState, b as packageJson, r as readSettings, h as readCredentials, u as updateSettings, w as writeCredentials, i as unrealMcpPythonDir, j as acquireDaemonLock, k as writeDaemonState, m as ApiMachineClient, n as releaseDaemonLock, s as sendUnrealMcpTcpCommand, A as ApiClient, o as clearCredentials, q as clearMachineId, t as installUnrealMcpPluginToEngine, v as buildAndInstallUnrealMcpPlugin, x as getLatestDaemonLog, y as normalizeServerUrlForNode } from './types-DdJKBH6T.mjs';
5
5
  import { spawn, execFileSync, execSync } from 'node:child_process';
6
6
  import path, { resolve, join, dirname } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
8
  import * as fs from 'node:fs';
9
9
  import fs__default, { existsSync, readFileSync, mkdirSync, readdirSync, accessSync, constants, statSync, createReadStream, writeFileSync, unlinkSync } from 'node:fs';
10
10
  import process$1 from 'node:process';
11
- import fs$1, { readFile, access as access$1, mkdir, readdir, stat, rename, open as open$1 } from 'node:fs/promises';
11
+ import fs$1, { readFile, access as access$1, mkdir, readdir, stat, writeFile, rename, open as open$1 } from 'node:fs/promises';
12
12
  import fs$2, { watch, access } from 'fs/promises';
13
13
  import { useStdout, useInput, Box, Text, render } from 'ink';
14
14
  import React, { useState, useRef, useEffect, useCallback } from 'react';
@@ -35,6 +35,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
35
35
  import 'tweetnacl';
36
36
  import { createServer as createServer$1 } from 'http';
37
37
  import { promisify } from 'util';
38
+ import { deflateSync } from 'node:zlib';
38
39
 
39
40
  class Session {
40
41
  path;
@@ -4651,6 +4652,10 @@ async function stopDaemonSession(sessionId) {
4651
4652
  const result = await daemonPost("/stop-session", { sessionId });
4652
4653
  return result.success || false;
4653
4654
  }
4655
+ async function spawnDaemonSession(directory, sessionId, agent) {
4656
+ const result = await daemonPost("/spawn-session", { directory, sessionId, agent });
4657
+ return result;
4658
+ }
4654
4659
  async function stopDaemonHttp() {
4655
4660
  await daemonPost("/stop");
4656
4661
  }
@@ -5331,7 +5336,8 @@ function startDaemonControlServer({
5331
5336
  schema: {
5332
5337
  body: z.object({
5333
5338
  directory: z.string(),
5334
- sessionId: z.string().optional()
5339
+ sessionId: z.string().optional(),
5340
+ agent: z.enum(["codex", "claude", "gemini"]).optional()
5335
5341
  }),
5336
5342
  response: {
5337
5343
  200: z.object({
@@ -5352,9 +5358,9 @@ function startDaemonControlServer({
5352
5358
  }
5353
5359
  }
5354
5360
  }, async (request, reply) => {
5355
- const { directory, sessionId } = request.body;
5356
- logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
5357
- const result = await spawnSession({ directory, sessionId });
5361
+ const { directory, sessionId, agent } = request.body;
5362
+ logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}, agent=${agent || "default"}`);
5363
+ const result = await spawnSession({ directory, sessionId, agent });
5358
5364
  switch (result.type) {
5359
5365
  case "success":
5360
5366
  if (!result.sessionId) {
@@ -6307,14 +6313,42 @@ Log: ${logPath || `not found (check ${configuration.logsDir})`}` + formatLogExce
6307
6313
  httpPort: controlPort,
6308
6314
  startedAt: Date.now()
6309
6315
  };
6316
+ let hydratedMetadata = { ...initialMachineMetadata };
6317
+ let hydratedSeq = 0;
6318
+ try {
6319
+ const endpoint = configuration.serverUrl.replace(/\/+$/, "");
6320
+ const res = await fetch(`${endpoint}/v1/machines/${encodeURIComponent(machineId)}`, {
6321
+ method: "GET",
6322
+ headers: {
6323
+ Authorization: `Machine ${credentials.machineToken}`
6324
+ },
6325
+ signal: AbortSignal.timeout(1e4)
6326
+ });
6327
+ if (res.ok) {
6328
+ const data = await res.json().catch(() => null);
6329
+ const existingMachine = data?.machine && typeof data.machine === "object" ? data.machine : null;
6330
+ const existingMetadata = existingMachine?.metadata && typeof existingMachine.metadata === "object" ? existingMachine.metadata : null;
6331
+ if (existingMetadata) {
6332
+ hydratedMetadata = { ...existingMetadata, ...initialMachineMetadata };
6333
+ hydratedSeq = Number(existingMachine?.seq || 0);
6334
+ }
6335
+ } else if (res.status !== 404) {
6336
+ const detail = await res.text().catch(() => "");
6337
+ logger.debug(
6338
+ `[DAEMON RUN] Failed to hydrate machine metadata (status=${res.status}): ${detail.slice(0, 800)}`
6339
+ );
6340
+ }
6341
+ } catch (err) {
6342
+ logger.debug("[DAEMON RUN] Failed to hydrate machine metadata (best-effort):", err);
6343
+ }
6310
6344
  const machine = {
6311
6345
  id: machineId,
6312
- seq: 0,
6346
+ seq: hydratedSeq,
6313
6347
  active: false,
6314
6348
  activeAt: null,
6315
6349
  createdAt: null,
6316
6350
  updatedAt: null,
6317
- metadata: { ...initialMachineMetadata },
6351
+ metadata: hydratedMetadata,
6318
6352
  daemonState: initialDaemonState
6319
6353
  };
6320
6354
  machineRef = machine;
@@ -7185,10 +7219,37 @@ async function runCmdAndCapture(args) {
7185
7219
  function serverBaseUrl() {
7186
7220
  return (configuration.serverUrl || "").replace(/\/+$/, "");
7187
7221
  }
7222
+ function readHttpTimeoutMs(envKey, fallbackMs) {
7223
+ const raw = String(process.env[envKey] ?? "").trim();
7224
+ const parsed = Number(raw);
7225
+ if (Number.isFinite(parsed) && parsed > 0) return Math.max(250, parsed);
7226
+ return fallbackMs;
7227
+ }
7228
+ function isAbortError(err) {
7229
+ if (!err) return false;
7230
+ if (typeof err === "object" && err.name === "AbortError") return true;
7231
+ const msg = err instanceof Error ? err.message : String(err);
7232
+ return /aborted|aborterror/i.test(msg);
7233
+ }
7234
+ async function fetchWithTimeout(url, init, timeoutMs, label) {
7235
+ const controller = new AbortController();
7236
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
7237
+ try {
7238
+ return await fetch(url, { ...init, signal: controller.signal });
7239
+ } catch (err) {
7240
+ if (isAbortError(err)) {
7241
+ throw new Error(`Request timed out after ${timeoutMs}ms: ${label}`);
7242
+ }
7243
+ throw err;
7244
+ } finally {
7245
+ clearTimeout(timeout);
7246
+ }
7247
+ }
7188
7248
  async function uploadScreenshotViewsForSession(args) {
7189
7249
  const baseUrl = serverBaseUrl();
7190
7250
  if (!baseUrl) throw new Error("Missing configuration.serverUrl");
7191
7251
  const endpoint = `${baseUrl}/v1/sessions/${encodeURIComponent(args.sessionId)}/screenshots`;
7252
+ const timeoutMs = readHttpTimeoutMs("FLOCKBAY_MCP_SCREENSHOT_UPLOAD_TIMEOUT_MS", 18e4);
7192
7253
  const form = new FormData();
7193
7254
  for (const v of args.views) {
7194
7255
  const buf = await readFile(v.path);
@@ -7201,15 +7262,20 @@ async function uploadScreenshotViewsForSession(args) {
7201
7262
  const blob = new Blob([buf], { type: contentType });
7202
7263
  form.append(`file:${v.id}`, blob, filename);
7203
7264
  }
7204
- const res = await fetch(endpoint, {
7205
- method: "POST",
7206
- headers: {
7207
- // This tool runs inside the CLI/daemon context, so we authenticate as the machine.
7208
- // The backend accepts `Machine <token>` for machine-scoped auth.
7209
- Authorization: `Machine ${args.token}`
7265
+ const res = await fetchWithTimeout(
7266
+ endpoint,
7267
+ {
7268
+ method: "POST",
7269
+ headers: {
7270
+ // This tool runs inside the CLI/daemon context, so we authenticate as the machine.
7271
+ // The backend accepts `Machine <token>` for machine-scoped auth.
7272
+ Authorization: `Machine ${args.token}`
7273
+ },
7274
+ body: form
7210
7275
  },
7211
- body: form
7212
- });
7276
+ timeoutMs,
7277
+ `POST ${endpoint}`
7278
+ );
7213
7279
  if (!res.ok) {
7214
7280
  const text = await res.text().catch(() => "");
7215
7281
  throw new Error(`Screenshot upload failed (${res.status}): ${text || res.statusText}`);
@@ -7333,14 +7399,21 @@ async function startFlockbayServer(client, options) {
7333
7399
  logger.debug("[flockbayMCP] Failed to register elicitation RPC handler", err);
7334
7400
  }
7335
7401
  const postJson = async (pathname, body) => {
7336
- const res = await fetch(`${configuration.serverUrl.replace(/\/+$/, "")}${pathname}`, {
7337
- method: "POST",
7338
- headers: {
7339
- Authorization: `Machine ${client.getAuthToken()}`,
7340
- "Content-Type": "application/json"
7402
+ const timeoutMs = readHttpTimeoutMs("FLOCKBAY_MCP_HTTP_TIMEOUT_MS", 6e4);
7403
+ const url = `${configuration.serverUrl.replace(/\/+$/, "")}${pathname}`;
7404
+ const res = await fetchWithTimeout(
7405
+ url,
7406
+ {
7407
+ method: "POST",
7408
+ headers: {
7409
+ Authorization: `Machine ${client.getAuthToken()}`,
7410
+ "Content-Type": "application/json"
7411
+ },
7412
+ body: JSON.stringify(body ?? {})
7341
7413
  },
7342
- body: JSON.stringify(body ?? {})
7343
- });
7414
+ timeoutMs,
7415
+ `POST ${url}`
7416
+ );
7344
7417
  const data = await res.json().catch(() => null);
7345
7418
  if (!res.ok) {
7346
7419
  const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
@@ -7774,20 +7847,200 @@ ${String(st.stdout || "").trim()}`
7774
7847
  isError
7775
7848
  };
7776
7849
  };
7777
- const runWithMcpToolCard = async (toolName, input, fn) => {
7778
- const callId = emitFlockbayMcpToolCall(toolName, input);
7779
- try {
7780
- const result = await fn();
7781
- emitFlockbayMcpToolResult(callId, result, Boolean(result?.isError));
7782
- return result;
7783
- } catch (err) {
7784
- const fallback = textToolResult(
7785
- `Tool "${toolName}" failed: ${err instanceof Error ? err.message : String(err)}`,
7786
- true
7787
- );
7788
- emitFlockbayMcpToolResult(callId, fallback, true);
7789
- return fallback;
7850
+ let mcpToolRunChain = Promise.resolve();
7851
+ const runMcpToolSerialized = async (fn) => {
7852
+ const res = mcpToolRunChain.then(fn, fn);
7853
+ mcpToolRunChain = res.then(() => void 0, () => void 0);
7854
+ return await res;
7855
+ };
7856
+ const toolResultsArtifactsRoot = () => {
7857
+ const sessionDir = readSessionWorkingDirectory();
7858
+ return path.join(sessionDir, ".flockbay", "artifacts", "tool-results");
7859
+ };
7860
+ const safePathSegment = (raw) => String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+/, "").replace(/_+$/, "") || "unknown";
7861
+ const coerceTextBlocks = (content) => {
7862
+ if (!Array.isArray(content)) return [];
7863
+ const out = [];
7864
+ for (const block of content) {
7865
+ if (!block || typeof block !== "object") continue;
7866
+ if (block.type !== "text") continue;
7867
+ const t = block.text;
7868
+ if (typeof t === "string" && t.trim()) out.push(t.trim());
7869
+ }
7870
+ return out;
7871
+ };
7872
+ const writeToolResultArtifact = async (args) => {
7873
+ const root = toolResultsArtifactsRoot();
7874
+ const toolDir = path.join(root, safePathSegment(args.toolName));
7875
+ await mkdir(toolDir, { recursive: true });
7876
+ const filePath = path.join(toolDir, `${safePathSegment(args.toolResultId)}.json`);
7877
+ const payload = {
7878
+ kind: "mcp_tool_result",
7879
+ toolName: args.toolName,
7880
+ toolResultId: args.toolResultId,
7881
+ isError: args.isError,
7882
+ createdAtMs: Date.now(),
7883
+ input: args.input ?? null,
7884
+ output: args.output ?? null
7885
+ };
7886
+ await writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
7887
+ return { id: args.toolResultId, path: filePath };
7888
+ };
7889
+ const buildModelMinimalToolResult = (args) => {
7890
+ const headLines = [];
7891
+ if (args.isError) headLines.push(`Tool failed: ${args.toolName}`);
7892
+ else headLines.push(`Tool completed: ${args.toolName}`);
7893
+ const texts = coerceTextBlocks(args.fullResult?.content);
7894
+ const first = texts[0];
7895
+ if (first && !/^\s*[{[]/.test(first)) headLines.push(first);
7896
+ headLines.push(`Evidence: toolResultId=${args.artifact.id}`);
7897
+ headLines.push(`Tip: use tool_result_query/tool_result_read for details (avoid re-reading full tool JSON unless necessary).`);
7898
+ const modelContent = [{ type: "text", text: headLines.join("\n") }];
7899
+ if (args.passthroughContent && Array.isArray(args.fullResult?.content)) {
7900
+ for (const block of args.fullResult.content) {
7901
+ if (!block || typeof block !== "object") continue;
7902
+ if (block.type === "image") modelContent.push(block);
7903
+ }
7904
+ }
7905
+ const views = Array.isArray(args.fullResult?.views) ? args.fullResult.views : null;
7906
+ const safeViews = views && views.map((v) => {
7907
+ if (!v || typeof v !== "object") return v;
7908
+ const { base64: _base64, data: _data, ...rest } = v;
7909
+ return rest;
7910
+ });
7911
+ return {
7912
+ content: modelContent,
7913
+ ...safeViews ? { views: safeViews } : {},
7914
+ isError: args.isError,
7915
+ toolResultId: args.artifact.id
7916
+ };
7917
+ };
7918
+ const resolveToolResultArtifactPath = async (args) => {
7919
+ const root = toolResultsArtifactsRoot();
7920
+ const resolvedRoot = path.resolve(root);
7921
+ const toolResultId = String(args.toolResultId || "").trim();
7922
+ if (!toolResultId) return { ok: false, error: "Missing toolResultId." };
7923
+ const rawPath = String(args.artifactPath || "").trim();
7924
+ const toolName = String(args.toolName || "").trim();
7925
+ const safeId = safePathSegment(toolResultId);
7926
+ let filePath = "";
7927
+ if (rawPath) {
7928
+ filePath = rawPath;
7929
+ } else if (toolName) {
7930
+ filePath = path.join(root, safePathSegment(toolName), `${safeId}.json`);
7931
+ } else {
7932
+ if (!existsSync(root)) return { ok: false, error: `Tool results root not found: ${root}` };
7933
+ const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
7934
+ const matches = [];
7935
+ for (const entry of entries) {
7936
+ if (!entry.isDirectory()) continue;
7937
+ const candidate = path.join(root, entry.name, `${safeId}.json`);
7938
+ if (existsSync(candidate)) matches.push(candidate);
7939
+ }
7940
+ if (matches.length === 0) return { ok: false, error: `tool result not found: ${toolResultId}` };
7941
+ if (matches.length > 1) {
7942
+ return {
7943
+ ok: false,
7944
+ error: `tool result id is ambiguous (found in multiple tool folders). Provide toolName to disambiguate. toolResultId=${toolResultId}`
7945
+ };
7946
+ }
7947
+ filePath = matches[0];
7948
+ }
7949
+ const resolvedFile = path.resolve(filePath);
7950
+ if (!resolvedFile.startsWith(resolvedRoot + path.sep) && resolvedFile !== resolvedRoot) {
7951
+ return { ok: false, error: `Refusing to read outside tool-results root.` };
7952
+ }
7953
+ if (!existsSync(resolvedFile)) return { ok: false, error: `tool result artifact not found` };
7954
+ return { ok: true, path: resolvedFile };
7955
+ };
7956
+ const normalizeQueryPath = (raw) => {
7957
+ const s = String(raw || "").trim();
7958
+ if (!s) return "";
7959
+ if (s.startsWith("/")) return s;
7960
+ const parts = s.split(".").map((p) => p.trim()).filter(Boolean).map((p) => p.replace(/~/g, "~0").replace(/\//g, "~1"));
7961
+ return `/${parts.join("/")}`;
7962
+ };
7963
+ const getValueAtJsonPointer = (root, pointerRaw) => {
7964
+ const pointer = normalizeQueryPath(pointerRaw);
7965
+ if (!pointer) return { ok: false, error: "Empty path." };
7966
+ if (pointer === "/" || pointer === "") return { ok: true, value: root };
7967
+ if (!pointer.startsWith("/")) return { ok: false, error: 'Invalid JSON pointer (must start with "/").' };
7968
+ const decode = (seg) => seg.replace(/~1/g, "/").replace(/~0/g, "~");
7969
+ const segments = pointer.split("/").slice(1).map((seg) => decode(seg));
7970
+ let cur = root;
7971
+ for (const seg of segments) {
7972
+ if (cur == null) return { ok: false, error: `Missing path segment: ${seg}` };
7973
+ if (Array.isArray(cur)) {
7974
+ if (!/^\d+$/.test(seg)) return { ok: false, error: `Expected array index but got "${seg}"` };
7975
+ const idx = Number(seg);
7976
+ if (!Number.isFinite(idx) || idx < 0 || idx >= cur.length) return { ok: false, error: `Array index out of range: ${seg}` };
7977
+ cur = cur[idx];
7978
+ continue;
7979
+ }
7980
+ if (typeof cur !== "object") return { ok: false, error: `Cannot descend into non-object at "${seg}"` };
7981
+ if (!(seg in cur)) return { ok: false, error: `Missing key: ${seg}` };
7982
+ cur = cur[seg];
7790
7983
  }
7984
+ return { ok: true, value: cur };
7985
+ };
7986
+ const runWithMcpToolCard = async (toolName, input, fn, options2) => {
7987
+ return await runMcpToolSerialized(async () => {
7988
+ const uiCallId = emitFlockbayMcpToolCall(toolName, input);
7989
+ const toolResultId = uiCallId || randomUUID();
7990
+ try {
7991
+ const fullResult = await fn();
7992
+ const isError = Boolean(fullResult?.isError);
7993
+ const artifact = await writeToolResultArtifact({
7994
+ toolName,
7995
+ toolResultId,
7996
+ input,
7997
+ output: fullResult,
7998
+ isError
7999
+ });
8000
+ const uiPayload = fullResult && typeof fullResult === "object" && !Array.isArray(fullResult) ? { ...fullResult, toolResultId: artifact.id, artifactPath: artifact.path } : fullResult;
8001
+ emitFlockbayMcpToolResult(uiCallId, uiPayload, isError);
8002
+ if (options2?.modelReturn === "full") {
8003
+ if (fullResult && typeof fullResult === "object" && !Array.isArray(fullResult)) {
8004
+ return { ...fullResult, toolResultId: artifact.id, isError };
8005
+ }
8006
+ return fullResult;
8007
+ }
8008
+ return buildModelMinimalToolResult({
8009
+ toolName,
8010
+ artifact,
8011
+ fullResult,
8012
+ isError,
8013
+ passthroughContent: Boolean(options2?.passthroughImagesToModel)
8014
+ });
8015
+ } catch (err) {
8016
+ const fullResult = textToolResult(
8017
+ `Tool "${toolName}" failed: ${err instanceof Error ? err.message : String(err)}`,
8018
+ true
8019
+ );
8020
+ const artifact = await writeToolResultArtifact({
8021
+ toolName,
8022
+ toolResultId,
8023
+ input,
8024
+ output: fullResult,
8025
+ isError: true
8026
+ });
8027
+ const uiPayload = fullResult && typeof fullResult === "object" && !Array.isArray(fullResult) ? { ...fullResult, toolResultId: artifact.id, artifactPath: artifact.path } : fullResult;
8028
+ emitFlockbayMcpToolResult(uiCallId, uiPayload, true);
8029
+ if (options2?.modelReturn === "full") {
8030
+ if (fullResult && typeof fullResult === "object" && !Array.isArray(fullResult)) {
8031
+ return { ...fullResult, toolResultId: artifact.id, isError: true };
8032
+ }
8033
+ return fullResult;
8034
+ }
8035
+ return buildModelMinimalToolResult({
8036
+ toolName,
8037
+ artifact,
8038
+ fullResult,
8039
+ isError: true,
8040
+ passthroughContent: Boolean(options2?.passthroughImagesToModel)
8041
+ });
8042
+ }
8043
+ });
7791
8044
  };
7792
8045
  mcp.registerTool(
7793
8046
  "ask_user_question",
@@ -7811,27 +8064,15 @@ ${String(st.stdout || "").trim()}`
7811
8064
  )
7812
8065
  }
7813
8066
  },
7814
- async (args) => {
7815
- const callId = emitFlockbayMcpToolCall("ask_user_question", args);
7816
- if (!callId) {
7817
- return textToolResult("AskUserQuestion is unavailable (no UI client attached).", true);
7818
- }
7819
- try {
7820
- const result = textToolResult(
7821
- [
7822
- "Questions were shown to the user.",
7823
- "Stop here and wait for the user to send a new message with the answers when ready."
7824
- ].join("\n"),
7825
- false
7826
- );
7827
- emitFlockbayMcpToolResult(callId, result, false);
7828
- return result;
7829
- } catch (err) {
7830
- const result = textToolResult(`AskUserQuestion failed: ${err instanceof Error ? err.message : String(err)}`, true);
7831
- emitFlockbayMcpToolResult(callId, result, true);
7832
- return result;
7833
- }
7834
- }
8067
+ async (args) => runWithMcpToolCard("ask_user_question", args, async () => {
8068
+ return textToolResult(
8069
+ [
8070
+ "Questions were shown to the user.",
8071
+ "Stop here and wait for the user to send a new message with the answers when ready."
8072
+ ].join("\n"),
8073
+ false
8074
+ );
8075
+ })
7835
8076
  );
7836
8077
  mcp.registerTool("change_title", {
7837
8078
  description: "Change the title of the current chat session",
@@ -7870,28 +8111,19 @@ ${String(st.stdout || "").trim()}`
7870
8111
  description: "Fetch the current coordination ledger snapshot (work items + planned/active/done intent) for this project.",
7871
8112
  inputSchema: {}
7872
8113
  },
7873
- async () => {
7874
- const callId = emitFlockbayMcpToolCall("coordination_ledger_snapshot", {});
7875
- try {
7876
- const { projectId } = readCoordinationIds();
7877
- const data = await postJson("/v1/coordination/work-items/list", { projectId });
7878
- if (typeof client?.markCoordinationLedgerRead === "function") {
7879
- try {
7880
- client.markCoordinationLedgerRead();
7881
- } catch (error) {
7882
- console.error("[flockbayMCP] markCoordinationLedgerRead failed:", error);
7883
- logger.debug("[flockbayMCP] markCoordinationLedgerRead failed:", error);
7884
- }
8114
+ async () => runWithMcpToolCard("coordination_ledger_snapshot", {}, async () => {
8115
+ const { projectId } = readCoordinationIds();
8116
+ const data = await postJson("/v1/coordination/work-items/list", { projectId });
8117
+ if (typeof client?.markCoordinationLedgerRead === "function") {
8118
+ try {
8119
+ client.markCoordinationLedgerRead();
8120
+ } catch (error) {
8121
+ console.error("[flockbayMCP] markCoordinationLedgerRead failed:", error);
8122
+ logger.debug("[flockbayMCP] markCoordinationLedgerRead failed:", error);
7885
8123
  }
7886
- const result = textToolResult(JSON.stringify({ projectId, workItems: data?.workItems ?? [] }, null, 2), false);
7887
- emitFlockbayMcpToolResult(callId, result, false);
7888
- return result;
7889
- } catch (err) {
7890
- const result = textToolResult(`Failed to fetch ledger snapshot: ${err instanceof Error ? err.message : String(err)}`, true);
7891
- emitFlockbayMcpToolResult(callId, result, true);
7892
- return result;
7893
8124
  }
7894
- }
8125
+ return textToolResult(JSON.stringify({ projectId, workItems: data?.workItems ?? [] }, null, 2), false);
8126
+ })
7895
8127
  );
7896
8128
  mcp.registerTool(
7897
8129
  "ledger_read",
@@ -7900,28 +8132,19 @@ ${String(st.stdout || "").trim()}`
7900
8132
  description: "Alias for coordination_ledger_snapshot: fetch the current shared coordination ledger snapshot for this project.",
7901
8133
  inputSchema: {}
7902
8134
  },
7903
- async () => {
7904
- const callId = emitFlockbayMcpToolCall("ledger_read", {});
7905
- try {
7906
- const { projectId } = readCoordinationIds();
7907
- const data = await postJson("/v1/coordination/work-items/list", { projectId });
7908
- if (typeof client?.markCoordinationLedgerRead === "function") {
7909
- try {
7910
- client.markCoordinationLedgerRead();
7911
- } catch (error) {
7912
- console.error("[flockbayMCP] markCoordinationLedgerRead failed:", error);
7913
- logger.debug("[flockbayMCP] markCoordinationLedgerRead failed:", error);
7914
- }
8135
+ async () => runWithMcpToolCard("ledger_read", {}, async () => {
8136
+ const { projectId } = readCoordinationIds();
8137
+ const data = await postJson("/v1/coordination/work-items/list", { projectId });
8138
+ if (typeof client?.markCoordinationLedgerRead === "function") {
8139
+ try {
8140
+ client.markCoordinationLedgerRead();
8141
+ } catch (error) {
8142
+ console.error("[flockbayMCP] markCoordinationLedgerRead failed:", error);
8143
+ logger.debug("[flockbayMCP] markCoordinationLedgerRead failed:", error);
7915
8144
  }
7916
- const result = textToolResult(JSON.stringify({ projectId, workItems: data?.workItems ?? [] }, null, 2), false);
7917
- emitFlockbayMcpToolResult(callId, result, false);
7918
- return result;
7919
- } catch (err) {
7920
- const result = textToolResult(`Failed to read ledger: ${err instanceof Error ? err.message : String(err)}`, true);
7921
- emitFlockbayMcpToolResult(callId, result, true);
7922
- return result;
7923
8145
  }
7924
- }
8146
+ return textToolResult(JSON.stringify({ projectId, workItems: data?.workItems ?? [] }, null, 2), false);
8147
+ })
7925
8148
  );
7926
8149
  mcp.registerTool(
7927
8150
  "docs_index_read",
@@ -7930,58 +8153,49 @@ ${String(st.stdout || "").trim()}`
7930
8153
  description: "Read the required game Documentation index (index.md) and a compact tree summary. Marks this session as having read the docs index.",
7931
8154
  inputSchema: {}
7932
8155
  },
7933
- async () => {
7934
- const callId = emitFlockbayMcpToolCall("docs_index_read", {});
7935
- try {
7936
- const { projectId } = readCoordinationIds();
7937
- const sessionId = String(client?.sessionId || "").trim();
7938
- const meta = client?.metadata;
7939
- const machineId = String(meta?.machineId || "").trim() || null;
7940
- const index = await postJson("/v1/workspace/projects/docs/index", {
7941
- workspaceProjectId: projectId
7942
- });
7943
- const tree = await postJson("/v1/workspace/projects/docs/tree", {
7944
- workspaceProjectId: projectId
7945
- });
7946
- if (typeof client?.markDocsIndexRead === "function") {
7947
- try {
7948
- client.markDocsIndexRead();
7949
- } catch (error) {
7950
- console.error("[flockbayMCP] markDocsIndexRead failed:", error);
7951
- logger.debug("[flockbayMCP] markDocsIndexRead failed:", error);
7952
- }
7953
- }
7954
- const nodes = Array.isArray(tree?.nodes) ? tree.nodes : [];
7955
- const compact = nodes.filter((n) => n && (n.kind === "folder" || n.kind === "document")).map((n) => ({
7956
- id: n.id,
7957
- kind: n.kind,
7958
- parentId: n.parentId ?? null,
7959
- name: n.name,
7960
- title: n.metadata && n.metadata.title || null,
7961
- isIndex: Boolean(n.isIndex)
7962
- }));
7963
- const result = textToolResult(
7964
- JSON.stringify(
7965
- {
7966
- workspaceProjectId: projectId,
7967
- sessionId,
7968
- machineId,
7969
- index: index?.doc ?? null,
7970
- tree: compact
7971
- },
7972
- null,
7973
- 2
7974
- ),
7975
- false
7976
- );
7977
- emitFlockbayMcpToolResult(callId, result, false);
7978
- return result;
7979
- } catch (err) {
7980
- const result = textToolResult(`Failed to read docs index: ${err instanceof Error ? err.message : String(err)}`, true);
7981
- emitFlockbayMcpToolResult(callId, result, true);
7982
- return result;
7983
- }
7984
- }
8156
+ async () => runWithMcpToolCard("docs_index_read", {}, async () => {
8157
+ const { projectId } = readCoordinationIds();
8158
+ const sessionId = String(client?.sessionId || "").trim();
8159
+ const meta = client?.metadata;
8160
+ const machineId = String(meta?.machineId || "").trim() || null;
8161
+ const index = await postJson("/v1/workspace/projects/docs/index", {
8162
+ workspaceProjectId: projectId
8163
+ });
8164
+ const tree = await postJson("/v1/workspace/projects/docs/tree", {
8165
+ workspaceProjectId: projectId
8166
+ });
8167
+ if (typeof client?.markDocsIndexRead === "function") {
8168
+ try {
8169
+ client.markDocsIndexRead();
8170
+ } catch (error) {
8171
+ console.error("[flockbayMCP] markDocsIndexRead failed:", error);
8172
+ logger.debug("[flockbayMCP] markDocsIndexRead failed:", error);
8173
+ }
8174
+ }
8175
+ const nodes = Array.isArray(tree?.nodes) ? tree.nodes : [];
8176
+ const compact = nodes.filter((n) => n && (n.kind === "folder" || n.kind === "document")).map((n) => ({
8177
+ id: n.id,
8178
+ kind: n.kind,
8179
+ parentId: n.parentId ?? null,
8180
+ name: n.name,
8181
+ title: n.metadata && n.metadata.title || null,
8182
+ isIndex: Boolean(n.isIndex)
8183
+ }));
8184
+ return textToolResult(
8185
+ JSON.stringify(
8186
+ {
8187
+ workspaceProjectId: projectId,
8188
+ sessionId,
8189
+ machineId,
8190
+ index: index?.doc ?? null,
8191
+ tree: compact
8192
+ },
8193
+ null,
8194
+ 2
8195
+ ),
8196
+ false
8197
+ );
8198
+ })
7985
8199
  );
7986
8200
  mcp.registerTool(
7987
8201
  "docs_tree",
@@ -7990,22 +8204,16 @@ ${String(st.stdout || "").trim()}`
7990
8204
  description: "Fetch the Documentation Library tree (folders + docs metadata; no bodies).",
7991
8205
  inputSchema: {}
7992
8206
  },
7993
- async () => {
7994
- const callId = emitFlockbayMcpToolCall("docs_tree", {});
7995
- try {
7996
- const { projectId } = readCoordinationIds();
7997
- const tree = await postJson("/v1/workspace/projects/docs/tree", {
7998
- workspaceProjectId: projectId
7999
- });
8000
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, library: tree?.library ?? null, nodes: tree?.nodes ?? [] }, null, 2), false);
8001
- emitFlockbayMcpToolResult(callId, result, false);
8002
- return result;
8003
- } catch (err) {
8004
- const result = textToolResult(`Failed to fetch docs tree: ${err instanceof Error ? err.message : String(err)}`, true);
8005
- emitFlockbayMcpToolResult(callId, result, true);
8006
- return result;
8007
- }
8008
- }
8207
+ async () => runWithMcpToolCard("docs_tree", {}, async () => {
8208
+ const { projectId } = readCoordinationIds();
8209
+ const tree = await postJson("/v1/workspace/projects/docs/tree", {
8210
+ workspaceProjectId: projectId
8211
+ });
8212
+ return textToolResult(
8213
+ JSON.stringify({ workspaceProjectId: projectId, library: tree?.library ?? null, nodes: tree?.nodes ?? [] }, null, 2),
8214
+ false
8215
+ );
8216
+ })
8009
8217
  );
8010
8218
  mcp.registerTool(
8011
8219
  "docs_get",
@@ -8016,22 +8224,13 @@ ${String(st.stdout || "").trim()}`
8016
8224
  nodeId: z.string().describe("Document node id.")
8017
8225
  }
8018
8226
  },
8019
- async (args) => {
8020
- const callId = emitFlockbayMcpToolCall("docs_get", args);
8021
- try {
8022
- const { projectId } = readCoordinationIds();
8023
- const nodeId = String(args?.nodeId || "").trim();
8024
- if (!nodeId) return textToolResult("Missing nodeId", true);
8025
- const node = await postJson("/v1/workspace/projects/docs/get", { workspaceProjectId: projectId, nodeId });
8026
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, node: node?.node ?? null }, null, 2), false);
8027
- emitFlockbayMcpToolResult(callId, result, false);
8028
- return result;
8029
- } catch (err) {
8030
- const result = textToolResult(`Failed to fetch doc: ${err instanceof Error ? err.message : String(err)}`, true);
8031
- emitFlockbayMcpToolResult(callId, result, true);
8032
- return result;
8033
- }
8034
- }
8227
+ async (args) => runWithMcpToolCard("docs_get", args, async () => {
8228
+ const { projectId } = readCoordinationIds();
8229
+ const nodeId = String(args?.nodeId || "").trim();
8230
+ if (!nodeId) return textToolResult("Missing nodeId", true);
8231
+ const node = await postJson("/v1/workspace/projects/docs/get", { workspaceProjectId: projectId, nodeId });
8232
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, node: node?.node ?? null }, null, 2), false);
8233
+ })
8035
8234
  );
8036
8235
  mcp.registerTool(
8037
8236
  "docs_search",
@@ -8043,26 +8242,17 @@ ${String(st.stdout || "").trim()}`
8043
8242
  limit: z.number().int().positive().optional().describe("Max results (default 50).")
8044
8243
  }
8045
8244
  },
8046
- async (args) => {
8047
- const callId = emitFlockbayMcpToolCall("docs_search", args);
8048
- try {
8049
- const { projectId } = readCoordinationIds();
8050
- const q = String(args?.q || "").trim();
8051
- const limit = args?.limit;
8052
- const res = await postJson("/v1/workspace/projects/docs/search", {
8053
- workspaceProjectId: projectId,
8054
- q,
8055
- limit
8056
- });
8057
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, results: res?.results ?? [] }, null, 2), false);
8058
- emitFlockbayMcpToolResult(callId, result, false);
8059
- return result;
8060
- } catch (err) {
8061
- const result = textToolResult(`Failed to search docs: ${err instanceof Error ? err.message : String(err)}`, true);
8062
- emitFlockbayMcpToolResult(callId, result, true);
8063
- return result;
8064
- }
8065
- }
8245
+ async (args) => runWithMcpToolCard("docs_search", args, async () => {
8246
+ const { projectId } = readCoordinationIds();
8247
+ const q = String(args?.q || "").trim();
8248
+ const limit = args?.limit;
8249
+ const res = await postJson("/v1/workspace/projects/docs/search", {
8250
+ workspaceProjectId: projectId,
8251
+ q,
8252
+ limit
8253
+ });
8254
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, results: res?.results ?? [] }, null, 2), false);
8255
+ })
8066
8256
  );
8067
8257
  mcp.registerTool(
8068
8258
  "docs_create_folder",
@@ -8074,26 +8264,17 @@ ${String(st.stdout || "").trim()}`
8074
8264
  parentId: z.string().optional().describe("Parent folder node id (defaults to root).")
8075
8265
  }
8076
8266
  },
8077
- async (args) => {
8078
- const callId = emitFlockbayMcpToolCall("docs_create_folder", args);
8079
- try {
8080
- const { projectId } = readCoordinationIds();
8081
- const name = String(args?.name || "").trim();
8082
- const parentId = args?.parentId ? String(args.parentId).trim() : void 0;
8083
- const res = await postJson("/v1/workspace/projects/docs/create-folder", {
8084
- workspaceProjectId: projectId,
8085
- name,
8086
- parentId
8087
- });
8088
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, node: res?.node ?? null }, null, 2), false);
8089
- emitFlockbayMcpToolResult(callId, result, false);
8090
- return result;
8091
- } catch (err) {
8092
- const result = textToolResult(`Failed to create folder: ${err instanceof Error ? err.message : String(err)}`, true);
8093
- emitFlockbayMcpToolResult(callId, result, true);
8094
- return result;
8095
- }
8096
- }
8267
+ async (args) => runWithMcpToolCard("docs_create_folder", args, async () => {
8268
+ const { projectId } = readCoordinationIds();
8269
+ const name = String(args?.name || "").trim();
8270
+ const parentId = args?.parentId ? String(args.parentId).trim() : void 0;
8271
+ const res = await postJson("/v1/workspace/projects/docs/create-folder", {
8272
+ workspaceProjectId: projectId,
8273
+ name,
8274
+ parentId
8275
+ });
8276
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, node: res?.node ?? null }, null, 2), false);
8277
+ })
8097
8278
  );
8098
8279
  mcp.registerTool(
8099
8280
  "docs_create_doc",
@@ -8107,30 +8288,21 @@ ${String(st.stdout || "").trim()}`
8107
8288
  metadata: z.record(z.any()).optional().describe("Metadata object (JSON).")
8108
8289
  }
8109
8290
  },
8110
- async (args) => {
8111
- const callId = emitFlockbayMcpToolCall("docs_create_doc", args);
8112
- try {
8113
- const { projectId } = readCoordinationIds();
8114
- const name = String(args?.name || "").trim();
8115
- const parentId = args?.parentId ? String(args.parentId).trim() : void 0;
8116
- const bodyMarkdown = args?.bodyMarkdown ?? "";
8117
- const metadata = args?.metadata ?? {};
8118
- const res = await postJson("/v1/workspace/projects/docs/create-doc", {
8119
- workspaceProjectId: projectId,
8120
- name,
8121
- parentId,
8122
- bodyMarkdown,
8123
- metadata
8124
- });
8125
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, node: res?.node ?? null }, null, 2), false);
8126
- emitFlockbayMcpToolResult(callId, result, false);
8127
- return result;
8128
- } catch (err) {
8129
- const result = textToolResult(`Failed to create doc: ${err instanceof Error ? err.message : String(err)}`, true);
8130
- emitFlockbayMcpToolResult(callId, result, true);
8131
- return result;
8132
- }
8133
- }
8291
+ async (args) => runWithMcpToolCard("docs_create_doc", args, async () => {
8292
+ const { projectId } = readCoordinationIds();
8293
+ const name = String(args?.name || "").trim();
8294
+ const parentId = args?.parentId ? String(args.parentId).trim() : void 0;
8295
+ const bodyMarkdown = args?.bodyMarkdown ?? "";
8296
+ const metadata = args?.metadata ?? {};
8297
+ const res = await postJson("/v1/workspace/projects/docs/create-doc", {
8298
+ workspaceProjectId: projectId,
8299
+ name,
8300
+ parentId,
8301
+ bodyMarkdown,
8302
+ metadata
8303
+ });
8304
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, node: res?.node ?? null }, null, 2), false);
8305
+ })
8134
8306
  );
8135
8307
  mcp.registerTool(
8136
8308
  "docs_claim",
@@ -8142,32 +8314,23 @@ ${String(st.stdout || "").trim()}`
8142
8314
  leaseMs: z.number().int().positive().optional().describe("Lease TTL (ms).")
8143
8315
  }
8144
8316
  },
8145
- async (args) => {
8146
- const callId = emitFlockbayMcpToolCall("docs_claim", args);
8147
- try {
8148
- const { projectId, workItemId } = readCoordinationIds();
8149
- const sessionId = String(client?.sessionId || "").trim();
8150
- const meta = client?.metadata;
8151
- const machineId = String(meta?.machineId || "").trim() || null;
8152
- const nodeId = String(args?.nodeId || "").trim();
8153
- const leaseMs = args?.leaseMs;
8154
- const res = await postJson("/v1/workspace/projects/docs/leases/claim", {
8155
- workspaceProjectId: projectId,
8156
- nodeId,
8157
- workItemId,
8158
- sessionId,
8159
- machineId,
8160
- leaseMs
8161
- });
8162
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8163
- emitFlockbayMcpToolResult(callId, result, false);
8164
- return result;
8165
- } catch (err) {
8166
- const result = textToolResult(`Failed to claim doc: ${err instanceof Error ? err.message : String(err)}`, true);
8167
- emitFlockbayMcpToolResult(callId, result, true);
8168
- return result;
8169
- }
8170
- }
8317
+ async (args) => runWithMcpToolCard("docs_claim", args, async () => {
8318
+ const { projectId, workItemId } = readCoordinationIds();
8319
+ const sessionId = String(client?.sessionId || "").trim();
8320
+ const meta = client?.metadata;
8321
+ const machineId = String(meta?.machineId || "").trim() || null;
8322
+ const nodeId = String(args?.nodeId || "").trim();
8323
+ const leaseMs = args?.leaseMs;
8324
+ const res = await postJson("/v1/workspace/projects/docs/leases/claim", {
8325
+ workspaceProjectId: projectId,
8326
+ nodeId,
8327
+ workItemId,
8328
+ sessionId,
8329
+ machineId,
8330
+ leaseMs
8331
+ });
8332
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8333
+ })
8171
8334
  );
8172
8335
  mcp.registerTool(
8173
8336
  "docs_release",
@@ -8178,27 +8341,18 @@ ${String(st.stdout || "").trim()}`
8178
8341
  nodeId: z.string().describe("Document node id.")
8179
8342
  }
8180
8343
  },
8181
- async (args) => {
8182
- const callId = emitFlockbayMcpToolCall("docs_release", args);
8183
- try {
8184
- const { projectId, workItemId } = readCoordinationIds();
8185
- const sessionId = String(client?.sessionId || "").trim();
8186
- const nodeId = String(args?.nodeId || "").trim();
8187
- const res = await postJson("/v1/workspace/projects/docs/leases/release", {
8188
- workspaceProjectId: projectId,
8189
- nodeId,
8190
- workItemId,
8191
- sessionId
8192
- });
8193
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8194
- emitFlockbayMcpToolResult(callId, result, false);
8195
- return result;
8196
- } catch (err) {
8197
- const result = textToolResult(`Failed to release doc: ${err instanceof Error ? err.message : String(err)}`, true);
8198
- emitFlockbayMcpToolResult(callId, result, true);
8199
- return result;
8200
- }
8201
- }
8344
+ async (args) => runWithMcpToolCard("docs_release", args, async () => {
8345
+ const { projectId, workItemId } = readCoordinationIds();
8346
+ const sessionId = String(client?.sessionId || "").trim();
8347
+ const nodeId = String(args?.nodeId || "").trim();
8348
+ const res = await postJson("/v1/workspace/projects/docs/leases/release", {
8349
+ workspaceProjectId: projectId,
8350
+ nodeId,
8351
+ workItemId,
8352
+ sessionId
8353
+ });
8354
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8355
+ })
8202
8356
  );
8203
8357
  mcp.registerTool(
8204
8358
  "docs_update",
@@ -8213,56 +8367,47 @@ ${String(st.stdout || "").trim()}`
8213
8367
  expectedBodyVersion: z.number().int().nonnegative().optional().describe("Optional optimistic concurrency check.")
8214
8368
  }
8215
8369
  },
8216
- async (args) => {
8217
- const callId = emitFlockbayMcpToolCall("docs_update", args);
8370
+ async (args) => runWithMcpToolCard("docs_update", args, async () => {
8371
+ const { projectId, workItemId } = readCoordinationIds();
8372
+ const sessionId = String(client?.sessionId || "").trim();
8373
+ const nodeId = String(args?.nodeId || "").trim();
8374
+ const name = args?.name;
8375
+ const bodyMarkdown = args?.bodyMarkdown;
8376
+ const metadata = args?.metadata;
8377
+ const expectedBodyVersion = args?.expectedBodyVersion;
8378
+ const attemptUpdate = async () => {
8379
+ return await postJson("/v1/workspace/projects/docs/update-doc", {
8380
+ workspaceProjectId: projectId,
8381
+ nodeId,
8382
+ workItemId,
8383
+ sessionId,
8384
+ name,
8385
+ bodyMarkdown,
8386
+ metadata,
8387
+ expectedBodyVersion
8388
+ });
8389
+ };
8390
+ let res;
8218
8391
  try {
8219
- const { projectId, workItemId } = readCoordinationIds();
8220
- const sessionId = String(client?.sessionId || "").trim();
8221
- const nodeId = String(args?.nodeId || "").trim();
8222
- const name = args?.name;
8223
- const bodyMarkdown = args?.bodyMarkdown;
8224
- const metadata = args?.metadata;
8225
- const expectedBodyVersion = args?.expectedBodyVersion;
8226
- const attemptUpdate = async () => {
8227
- return await postJson("/v1/workspace/projects/docs/update-doc", {
8392
+ res = await attemptUpdate();
8393
+ } catch (e) {
8394
+ const msg = e instanceof Error ? e.message : String(e);
8395
+ if (msg === "lease_required") {
8396
+ await postJson("/v1/workspace/projects/docs/leases/claim", {
8228
8397
  workspaceProjectId: projectId,
8229
8398
  nodeId,
8230
8399
  workItemId,
8231
8400
  sessionId,
8232
- name,
8233
- bodyMarkdown,
8234
- metadata,
8235
- expectedBodyVersion
8401
+ machineId: String(client?.metadata?.machineId || "").trim() || null,
8402
+ leaseMs: 2 * 6e4
8236
8403
  });
8237
- };
8238
- let res;
8239
- try {
8240
8404
  res = await attemptUpdate();
8241
- } catch (e) {
8242
- const msg = e instanceof Error ? e.message : String(e);
8243
- if (msg === "lease_required") {
8244
- await postJson("/v1/workspace/projects/docs/leases/claim", {
8245
- workspaceProjectId: projectId,
8246
- nodeId,
8247
- workItemId,
8248
- sessionId,
8249
- machineId: String(client?.metadata?.machineId || "").trim() || null,
8250
- leaseMs: 2 * 6e4
8251
- });
8252
- res = await attemptUpdate();
8253
- } else {
8254
- throw e;
8255
- }
8405
+ } else {
8406
+ throw e;
8256
8407
  }
8257
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8258
- emitFlockbayMcpToolResult(callId, result, false);
8259
- return result;
8260
- } catch (err) {
8261
- const result = textToolResult(`Failed to update doc: ${err instanceof Error ? err.message : String(err)}`, true);
8262
- emitFlockbayMcpToolResult(callId, result, true);
8263
- return result;
8264
8408
  }
8265
- }
8409
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8410
+ })
8266
8411
  );
8267
8412
  mcp.registerTool(
8268
8413
  "docs_delete",
@@ -8273,27 +8418,18 @@ ${String(st.stdout || "").trim()}`
8273
8418
  nodeId: z.string().describe("Node id (folder or document).")
8274
8419
  }
8275
8420
  },
8276
- async (args) => {
8277
- const callId = emitFlockbayMcpToolCall("docs_delete", args);
8278
- try {
8279
- const { projectId, workItemId } = readCoordinationIds();
8280
- const sessionId = String(client?.sessionId || "").trim();
8281
- const nodeId = String(args?.nodeId || "").trim();
8282
- const res = await postJson("/v1/workspace/projects/docs/delete-node", {
8283
- workspaceProjectId: projectId,
8284
- nodeId,
8285
- workItemId,
8286
- sessionId
8287
- });
8288
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8289
- emitFlockbayMcpToolResult(callId, result, false);
8290
- return result;
8291
- } catch (err) {
8292
- const result = textToolResult(`Failed to delete node: ${err instanceof Error ? err.message : String(err)}`, true);
8293
- emitFlockbayMcpToolResult(callId, result, true);
8294
- return result;
8295
- }
8296
- }
8421
+ async (args) => runWithMcpToolCard("docs_delete", args, async () => {
8422
+ const { projectId, workItemId } = readCoordinationIds();
8423
+ const sessionId = String(client?.sessionId || "").trim();
8424
+ const nodeId = String(args?.nodeId || "").trim();
8425
+ const res = await postJson("/v1/workspace/projects/docs/delete-node", {
8426
+ workspaceProjectId: projectId,
8427
+ nodeId,
8428
+ workItemId,
8429
+ sessionId
8430
+ });
8431
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8432
+ })
8297
8433
  );
8298
8434
  mcp.registerTool(
8299
8435
  "docs_import",
@@ -8309,21 +8445,12 @@ ${String(st.stdout || "").trim()}`
8309
8445
  )
8310
8446
  }
8311
8447
  },
8312
- async (args) => {
8313
- const callId = emitFlockbayMcpToolCall("docs_import", args);
8314
- try {
8315
- const { projectId } = readCoordinationIds();
8316
- const entries = Array.isArray(args?.entries) ? args.entries : [];
8317
- const res = await postJson("/v1/workspace/projects/docs/import", { workspaceProjectId: projectId, entries });
8318
- const result = textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8319
- emitFlockbayMcpToolResult(callId, result, false);
8320
- return result;
8321
- } catch (err) {
8322
- const result = textToolResult(`Failed to import docs: ${err instanceof Error ? err.message : String(err)}`, true);
8323
- emitFlockbayMcpToolResult(callId, result, true);
8324
- return result;
8325
- }
8326
- }
8448
+ async (args) => runWithMcpToolCard("docs_import", args, async () => {
8449
+ const { projectId } = readCoordinationIds();
8450
+ const entries = Array.isArray(args?.entries) ? args.entries : [];
8451
+ const res = await postJson("/v1/workspace/projects/docs/import", { workspaceProjectId: projectId, entries });
8452
+ return textToolResult(JSON.stringify({ workspaceProjectId: projectId, ...res }, null, 2), false);
8453
+ })
8327
8454
  );
8328
8455
  mcp.registerTool(
8329
8456
  "evidence_list",
@@ -8337,10 +8464,11 @@ ${String(st.stdout || "").trim()}`
8337
8464
  async (args) => runWithMcpToolCard("evidence_list", args, async () => {
8338
8465
  const limit = Math.max(1, Math.min(200, Number(args?.limit || 50) || 50));
8339
8466
  const url = `${configuration.serverUrl.replace(/\/+$/, "")}/v1/sessions/${encodeURIComponent(client.sessionId)}/evidence-artifacts?limit=${limit}`;
8340
- const res = await fetch(url, {
8467
+ const timeoutMs = readHttpTimeoutMs("FLOCKBAY_MCP_HTTP_TIMEOUT_MS", 6e4);
8468
+ const res = await fetchWithTimeout(url, {
8341
8469
  method: "GET",
8342
8470
  headers: { Authorization: `Machine ${client.getAuthToken()}`, accept: "application/json" }
8343
- });
8471
+ }, timeoutMs, `GET ${url}`);
8344
8472
  const data = await res.json().catch(() => null);
8345
8473
  if (!res.ok) {
8346
8474
  const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
@@ -8362,10 +8490,11 @@ ${String(st.stdout || "").trim()}`
8362
8490
  const id = String(args?.evidenceArtifactId || "").trim();
8363
8491
  if (!id) return textToolResult("missing-evidenceArtifactId", true);
8364
8492
  const url = `${configuration.serverUrl.replace(/\/+$/, "")}/v1/sessions/${encodeURIComponent(client.sessionId)}/evidence-artifacts/${encodeURIComponent(id)}`;
8365
- const res = await fetch(url, {
8493
+ const timeoutMs = readHttpTimeoutMs("FLOCKBAY_MCP_HTTP_TIMEOUT_MS", 6e4);
8494
+ const res = await fetchWithTimeout(url, {
8366
8495
  method: "GET",
8367
8496
  headers: { Authorization: `Machine ${client.getAuthToken()}`, accept: "application/json" }
8368
- });
8497
+ }, timeoutMs, `GET ${url}`);
8369
8498
  const data = await res.json().catch(() => null);
8370
8499
  if (!res.ok) {
8371
8500
  const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
@@ -8374,6 +8503,72 @@ ${String(st.stdout || "").trim()}`
8374
8503
  return textToolResult(JSON.stringify(data, null, 2), false);
8375
8504
  })
8376
8505
  );
8506
+ mcp.registerTool(
8507
+ "tool_result_read",
8508
+ {
8509
+ title: "Tool Result Read",
8510
+ description: "Read a previously-stored MCP tool result artifact produced by Flockbay MCP (full tool input/output JSON). Use this when you explicitly need details beyond the default minimal tool observation.",
8511
+ inputSchema: {
8512
+ toolName: z.string().optional().describe("Tool name (folder under .flockbay/artifacts/tool-results)."),
8513
+ toolResultId: z.string().describe("toolResultId from a prior tool observation."),
8514
+ artifactPath: z.string().optional().describe("Optional absolute artifact path (advanced).")
8515
+ }
8516
+ },
8517
+ async (args) => runWithMcpToolCard("tool_result_read", args, async () => {
8518
+ const toolName = typeof args?.toolName === "string" ? String(args.toolName).trim() : null;
8519
+ const toolResultId = typeof args?.toolResultId === "string" ? String(args.toolResultId).trim() : "";
8520
+ const artifactPath = typeof args?.artifactPath === "string" ? String(args.artifactPath).trim() : null;
8521
+ const resolved = await resolveToolResultArtifactPath({ toolName, toolResultId, artifactPath });
8522
+ if (!resolved.ok) return textToolResult(resolved.error, true);
8523
+ const raw = await readFile(resolved.path, "utf8");
8524
+ return {
8525
+ content: [
8526
+ { type: "text", text: raw }
8527
+ ],
8528
+ isError: false
8529
+ };
8530
+ }, { modelReturn: "full" })
8531
+ );
8532
+ mcp.registerTool(
8533
+ "tool_result_query",
8534
+ {
8535
+ title: "Tool Result Query",
8536
+ description: "Query one or more JSON paths from a stored tool result artifact (small slices). Prefer this over reading the full artifact to avoid re-injecting huge payloads into the model context.",
8537
+ inputSchema: {
8538
+ toolName: z.string().optional().describe("Tool name (folder under .flockbay/artifacts/tool-results)."),
8539
+ toolResultId: z.string().describe("toolResultId from a prior tool observation."),
8540
+ artifactPath: z.string().optional().describe("Optional absolute artifact path (advanced)."),
8541
+ paths: z.array(z.string()).min(1).describe('JSON pointer paths ("/output/result/location") or dot-paths ("output.result.location").')
8542
+ }
8543
+ },
8544
+ async (args) => runWithMcpToolCard("tool_result_query", args, async () => {
8545
+ const toolName = typeof args?.toolName === "string" ? String(args.toolName).trim() : null;
8546
+ const toolResultId = typeof args?.toolResultId === "string" ? String(args.toolResultId).trim() : "";
8547
+ const artifactPath = typeof args?.artifactPath === "string" ? String(args.artifactPath).trim() : null;
8548
+ const paths = Array.isArray(args?.paths) ? args.paths.map((p) => String(p || "").trim()).filter(Boolean) : [];
8549
+ if (!toolResultId) return textToolResult("Missing toolResultId.", true);
8550
+ if (paths.length === 0) return textToolResult("Missing paths[] (provide one or more JSON paths).", true);
8551
+ const resolved = await resolveToolResultArtifactPath({ toolName, toolResultId, artifactPath });
8552
+ if (!resolved.ok) return textToolResult(resolved.error, true);
8553
+ let json;
8554
+ try {
8555
+ json = JSON.parse(await readFile(resolved.path, "utf8"));
8556
+ } catch (err) {
8557
+ return textToolResult(`Failed to parse tool result artifact JSON: ${err instanceof Error ? err.message : String(err)}`, true);
8558
+ }
8559
+ const out = {};
8560
+ for (const p of paths) {
8561
+ const res = getValueAtJsonPointer(json, p);
8562
+ if (!res.ok) return textToolResult(`Invalid path "${p}": ${res.error}`, true);
8563
+ out[p] = res.value;
8564
+ }
8565
+ return {
8566
+ content: [{ type: "text", text: JSON.stringify({ toolResultId, values: out }, null, 2) }],
8567
+ values: out,
8568
+ isError: false
8569
+ };
8570
+ }, { modelReturn: "full" })
8571
+ );
8377
8572
  mcp.registerTool(
8378
8573
  "coordination_update_intent",
8379
8574
  {
@@ -8389,31 +8584,22 @@ ${String(st.stdout || "").trim()}`
8389
8584
  leaseMs: z.number().int().positive().optional().describe("Soft lease TTL for this ledger entry (ms). UI-only staleness hint; default ~2h.")
8390
8585
  }
8391
8586
  },
8392
- async (args) => {
8393
- const callId = emitFlockbayMcpToolCall("coordination_update_intent", args);
8394
- try {
8395
- const { projectId, workItemId } = readCoordinationIds();
8396
- const body = {
8397
- projectId,
8398
- workItemId,
8399
- summary: args?.summary ?? void 0,
8400
- status: args?.status ?? void 0,
8401
- plannedFiles: args?.plannedFiles ?? void 0,
8402
- activeFiles: args?.activeFiles ?? void 0,
8403
- doneFiles: args?.doneFiles ?? void 0,
8404
- waitingOnFiles: args?.waitingOnFiles ?? void 0,
8405
- leaseMs: args?.leaseMs ?? void 0
8406
- };
8407
- const data = await postJson("/v1/coordination/work-items/update", body);
8408
- const result = textToolResult(JSON.stringify({ success: true, workItem: data?.workItem ?? null }, null, 2), false);
8409
- emitFlockbayMcpToolResult(callId, result, false);
8410
- return result;
8411
- } catch (err) {
8412
- const result = textToolResult(`Failed to update intent: ${err instanceof Error ? err.message : String(err)}`, true);
8413
- emitFlockbayMcpToolResult(callId, result, true);
8414
- return result;
8415
- }
8416
- }
8587
+ async (args) => runWithMcpToolCard("coordination_update_intent", args, async () => {
8588
+ const { projectId, workItemId } = readCoordinationIds();
8589
+ const body = {
8590
+ projectId,
8591
+ workItemId,
8592
+ summary: args?.summary ?? void 0,
8593
+ status: args?.status ?? void 0,
8594
+ plannedFiles: args?.plannedFiles ?? void 0,
8595
+ activeFiles: args?.activeFiles ?? void 0,
8596
+ doneFiles: args?.doneFiles ?? void 0,
8597
+ waitingOnFiles: args?.waitingOnFiles ?? void 0,
8598
+ leaseMs: args?.leaseMs ?? void 0
8599
+ };
8600
+ const data = await postJson("/v1/coordination/work-items/update", body);
8601
+ return textToolResult(JSON.stringify({ success: true, workItem: data?.workItem ?? null }, null, 2), false);
8602
+ })
8417
8603
  );
8418
8604
  mcp.registerTool(
8419
8605
  "ledger_claim",
@@ -8430,8 +8616,7 @@ ${String(st.stdout || "").trim()}`
8430
8616
  }
8431
8617
  },
8432
8618
  async (args) => {
8433
- const callId = emitFlockbayMcpToolCall("ledger_claim", args);
8434
- try {
8619
+ return await runWithMcpToolCard("ledger_claim", args, async () => {
8435
8620
  const { projectId, workItemId } = readCoordinationIds();
8436
8621
  if (args?.summary !== void 0 || args?.status !== void 0 || args?.plannedFiles !== void 0) {
8437
8622
  await postJson("/v1/coordination/work-items/update", {
@@ -8467,9 +8652,7 @@ ${String(st.stdout || "").trim()}`
8467
8652
  const data = await postJson("/v1/coordination/work-items/claim-files", { projectId, workItemId, ...payloadArgs });
8468
8653
  const ok = Boolean(data?.success);
8469
8654
  if (!ok) {
8470
- const result2 = textToolResult(JSON.stringify(data ?? { success: false, error: "claim_failed" }, null, 2), true);
8471
- emitFlockbayMcpToolResult(callId, result2, true);
8472
- return result2;
8655
+ return textToolResult(JSON.stringify(data ?? { success: false, error: "claim_failed" }, null, 2), true);
8473
8656
  }
8474
8657
  const claimedFiles = Array.isArray(data?.claimedFiles) ? data.claimedFiles : files;
8475
8658
  const claimedSet = new Set(claimedFiles.map((v) => String(v || "").trim()).filter(Boolean));
@@ -8501,7 +8684,6 @@ ${String(st.stdout || "").trim()}`
8501
8684
  ),
8502
8685
  true
8503
8686
  );
8504
- emitFlockbayMcpToolResult(callId, result2, true);
8505
8687
  return result2;
8506
8688
  }
8507
8689
  try {
@@ -8525,13 +8707,8 @@ ${String(st.stdout || "").trim()}`
8525
8707
  ),
8526
8708
  false
8527
8709
  );
8528
- emitFlockbayMcpToolResult(callId, result, false);
8529
8710
  return result;
8530
- } catch (err) {
8531
- const result = textToolResult(`Failed to claim via ledger: ${err instanceof Error ? err.message : String(err)}`, true);
8532
- emitFlockbayMcpToolResult(callId, result, true);
8533
- return result;
8534
- }
8711
+ });
8535
8712
  }
8536
8713
  );
8537
8714
  mcp.registerTool(
@@ -8548,8 +8725,7 @@ ${String(st.stdout || "").trim()}`
8548
8725
  }
8549
8726
  },
8550
8727
  async (args) => {
8551
- const callId = emitFlockbayMcpToolCall("ledger_release", args);
8552
- try {
8728
+ return await runWithMcpToolCard("ledger_release", args, async () => {
8553
8729
  const { projectId, workItemId } = readCoordinationIds();
8554
8730
  const data = await postJson("/v1/coordination/work-items/list", { projectId });
8555
8731
  const items = Array.isArray(data?.workItems) ? data.workItems : [];
@@ -8598,14 +8774,8 @@ ${String(st.stdout || "").trim()}`
8598
8774
  } catch (err) {
8599
8775
  logger.debug("[flockbayMCP] Failed to revoke local write tokens:", err);
8600
8776
  }
8601
- const result = textToolResult(JSON.stringify({ success: true, workItem: updated?.workItem ?? null }, null, 2), false);
8602
- emitFlockbayMcpToolResult(callId, result, false);
8603
- return result;
8604
- } catch (err) {
8605
- const result = textToolResult(`Failed to release ledger entry: ${err instanceof Error ? err.message : String(err)}`, true);
8606
- emitFlockbayMcpToolResult(callId, result, true);
8607
- return result;
8608
- }
8777
+ return textToolResult(JSON.stringify({ success: true, workItem: updated?.workItem ?? null }, null, 2), false);
8778
+ });
8609
8779
  }
8610
8780
  );
8611
8781
  mcp.registerTool(
@@ -8619,8 +8789,7 @@ ${String(st.stdout || "").trim()}`
8619
8789
  }
8620
8790
  },
8621
8791
  async (args) => {
8622
- const callId = emitFlockbayMcpToolCall("coordination_check_files", args);
8623
- try {
8792
+ return await runWithMcpToolCard("coordination_check_files", args, async () => {
8624
8793
  const { projectId, workItemId } = readCoordinationIds();
8625
8794
  const includeSelf = args?.includeSelf !== false;
8626
8795
  const rawFiles = Array.isArray(args?.files) ? args.files : [];
@@ -8675,14 +8844,8 @@ ${String(st.stdout || "").trim()}`
8675
8844
  const otherOwners = owners.filter((o) => !o.isSelf);
8676
8845
  return { filePath, available: otherOwners.length === 0, owners };
8677
8846
  });
8678
- const result = textToolResult(JSON.stringify({ projectId, files: out }, null, 2), false);
8679
- emitFlockbayMcpToolResult(callId, result, false);
8680
- return result;
8681
- } catch (err) {
8682
- const result = textToolResult(`Failed to check files: ${err instanceof Error ? err.message : String(err)}`, true);
8683
- emitFlockbayMcpToolResult(callId, result, true);
8684
- return result;
8685
- }
8847
+ return textToolResult(JSON.stringify({ projectId, files: out }, null, 2), false);
8848
+ });
8686
8849
  }
8687
8850
  );
8688
8851
  mcp.registerTool(
@@ -8697,8 +8860,7 @@ ${String(st.stdout || "").trim()}`
8697
8860
  }
8698
8861
  },
8699
8862
  async (args) => {
8700
- const callId = emitFlockbayMcpToolCall("coordination_claim_files", args);
8701
- try {
8863
+ return await runWithMcpToolCard("coordination_claim_files", args, async () => {
8702
8864
  const { projectId, workItemId } = readCoordinationIds();
8703
8865
  const rawFiles = Array.isArray(args?.files) ? args.files : [];
8704
8866
  const files = rawFiles.map((v) => String(v || "").trim().replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/^\/+/, "")).filter((v) => Boolean(v));
@@ -8726,9 +8888,7 @@ ${String(st.stdout || "").trim()}`
8726
8888
  };
8727
8889
  const data = await postJson("/v1/coordination/work-items/claim-files", body);
8728
8890
  if (!data?.success) {
8729
- const result2 = textToolResult(JSON.stringify(data ?? { success: false, error: "claim_failed" }, null, 2), true);
8730
- emitFlockbayMcpToolResult(callId, result2, true);
8731
- return result2;
8891
+ return textToolResult(JSON.stringify(data ?? { success: false, error: "claim_failed" }, null, 2), true);
8732
8892
  }
8733
8893
  const claimedFiles = Array.isArray(data?.claimedFiles) ? data.claimedFiles : files;
8734
8894
  const claimedSet = new Set(claimedFiles.map((v) => String(v || "").trim()).filter(Boolean));
@@ -8760,7 +8920,6 @@ ${String(st.stdout || "").trim()}`
8760
8920
  ),
8761
8921
  true
8762
8922
  );
8763
- emitFlockbayMcpToolResult(callId, result2, true);
8764
8923
  return result2;
8765
8924
  }
8766
8925
  try {
@@ -8784,13 +8943,8 @@ ${String(st.stdout || "").trim()}`
8784
8943
  ),
8785
8944
  false
8786
8945
  );
8787
- emitFlockbayMcpToolResult(callId, result, false);
8788
8946
  return result;
8789
- } catch (err) {
8790
- const result = textToolResult(`Failed to claim files: ${err instanceof Error ? err.message : String(err)}`, true);
8791
- emitFlockbayMcpToolResult(callId, result, true);
8792
- return result;
8793
- }
8947
+ });
8794
8948
  }
8795
8949
  );
8796
8950
  mcp.registerTool(
@@ -8819,7 +8973,7 @@ ${String(st.stdout || "").trim()}`
8819
8973
  content.push({ type: "image", data: img.base64, mimeType: img.mimeType });
8820
8974
  });
8821
8975
  return { content, isError: false };
8822
- })
8976
+ }, { passthroughImagesToModel: true })
8823
8977
  );
8824
8978
  mcp.registerTool(
8825
8979
  "read_images",
@@ -8938,7 +9092,7 @@ ${String(st.stdout || "").trim()}`
8938
9092
  isError: true
8939
9093
  };
8940
9094
  }
8941
- })
9095
+ }, { passthroughImagesToModel: true })
8942
9096
  );
8943
9097
  mcp.registerTool("unreal_latest_screenshots", {
8944
9098
  title: "Latest Unreal Screenshots (Validation)",
@@ -9780,10 +9934,10 @@ ${String(st.stdout || "").trim()}`
9780
9934
  paths: z.array(z.string()).optional().describe('Root content paths to search (default ["/Game"]).'),
9781
9935
  includeWarnings: z.boolean().optional().describe("Include warnings in the per-blueprint messages list (default true)."),
9782
9936
  limit: z.number().int().positive().optional().describe("Max number of blueprints to compile (default 500, max 10000)."),
9783
- timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 60000).")
9937
+ timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 30000, max 30000).")
9784
9938
  }
9785
9939
  }, async (args) => runWithMcpToolCard("unreal_mcp_compile_blueprints_all", args, async () => {
9786
- const timeoutMs = args.timeoutMs ?? 6e4;
9940
+ const timeoutMs = Math.min(3e4, Math.max(250, Number(args.timeoutMs ?? 3e4) || 3e4));
9787
9941
  unrealEditorSupervisor.noteUnrealActivity();
9788
9942
  try {
9789
9943
  const params = {};
@@ -9814,10 +9968,10 @@ ${String(st.stdout || "").trim()}`
9814
9968
  title: "Unreal Save All (UnrealMCP)",
9815
9969
  description: "Save all dirty packages (maps + content) in the running Unreal Editor without prompting. Intended to prevent leaving editor changes unsaved. Fails if PIE is running.",
9816
9970
  inputSchema: {
9817
- timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 60000).")
9971
+ timeoutMs: z.number().int().positive().optional().describe("Socket timeout in ms (default 30000, max 30000).")
9818
9972
  }
9819
9973
  }, async (args) => runWithMcpToolCard("unreal_mcp_save_all", args, async () => {
9820
- const timeoutMs = args?.timeoutMs ?? 6e4;
9974
+ const timeoutMs = Math.min(3e4, Math.max(250, Number(args?.timeoutMs ?? 3e4) || 3e4));
9821
9975
  unrealEditorSupervisor.noteUnrealActivity();
9822
9976
  try {
9823
9977
  const response = await sendUnrealMcpTcpCommand({ type: "save_all", params: {}, timeoutMs });
@@ -10592,6 +10746,8 @@ Fix: ${res.hint}` : "";
10592
10746
  "docs_import",
10593
10747
  "evidence_list",
10594
10748
  "evidence_get",
10749
+ "tool_result_read",
10750
+ "tool_result_query",
10595
10751
  "coordination_update_intent",
10596
10752
  "coordination_check_files",
10597
10753
  "coordination_claim_files",
@@ -11913,6 +12069,410 @@ async function handleConnectVendor(vendor, displayName, flags) {
11913
12069
  }
11914
12070
  }
11915
12071
 
12072
+ function readFlag(args, flag) {
12073
+ return args.includes(flag);
12074
+ }
12075
+ function readArgValue$2(args, flag) {
12076
+ const idx = args.indexOf(flag);
12077
+ if (idx < 0) return null;
12078
+ const v = args[idx + 1];
12079
+ return v ? String(v) : null;
12080
+ }
12081
+ function parseAgentList(args) {
12082
+ const rawAgent = readArgValue$2(args, "--agent") || readArgValue$2(args, "-a");
12083
+ const rawAgents = readArgValue$2(args, "--agents");
12084
+ const wantsAll = readFlag(args, "--all");
12085
+ const split = (raw) => raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
12086
+ const desired = wantsAll ? ["codex", "claude", "gemini"] : rawAgents ? split(rawAgents) : rawAgent ? [rawAgent.trim().toLowerCase()] : ["codex", "claude", "gemini"];
12087
+ const allow = /* @__PURE__ */ new Set(["codex", "claude", "gemini"]);
12088
+ const agents = desired.filter((a) => allow.has(a));
12089
+ return agents.length > 0 ? agents : ["codex", "claude", "gemini"];
12090
+ }
12091
+ function permissionModeForAgent(agent) {
12092
+ if (agent === "claude") return "bypassPermissions";
12093
+ return "yolo";
12094
+ }
12095
+ function nowIsoCompact() {
12096
+ const d = /* @__PURE__ */ new Date();
12097
+ const pad = (n) => String(n).padStart(2, "0");
12098
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
12099
+ }
12100
+ async function ensureDaemonRunning({ skipUnreal }) {
12101
+ const ok = await checkIfDaemonRunningAndCleanupStaleState();
12102
+ if (ok) {
12103
+ const same = await isDaemonRunningCurrentCliVersion();
12104
+ if (same) return;
12105
+ logger.debug("[smoke-test] daemon running old CLI version; restarting");
12106
+ await stopDaemon();
12107
+ }
12108
+ logger.debug("[smoke-test] daemon not running; starting via `flockbay start`");
12109
+ const args = ["start"];
12110
+ if (skipUnreal) args.push("--skip-unreal");
12111
+ const child = spawnFlockbayCLI(args, {
12112
+ stdio: "ignore",
12113
+ env: {
12114
+ ...process.env,
12115
+ // Do not pop a browser while smoke-testing.
12116
+ FLOCKBAY_NO_OPEN: "1"
12117
+ },
12118
+ windowsHide: true
12119
+ });
12120
+ child.unref();
12121
+ const deadline = Date.now() + 2e4;
12122
+ while (Date.now() < deadline) {
12123
+ const running = await checkIfDaemonRunningAndCleanupStaleState();
12124
+ if (running) return;
12125
+ await delay(250);
12126
+ }
12127
+ throw new Error("daemon_start_timeout");
12128
+ }
12129
+ async function createSmokeWorkspaceDir(baseDir) {
12130
+ const root = baseDir?.trim() ? path.resolve(baseDir) : path.join(os.tmpdir(), "flockbay-smoke", nowIsoCompact() + "_" + randomUUID().slice(0, 8));
12131
+ await fs$1.mkdir(root, { recursive: true });
12132
+ const secret = `SMOKE_SECRET_${randomUUID().slice(0, 8)}`;
12133
+ await fs$1.writeFile(path.join(root, "smoke.txt"), `flockbay smoke test
12134
+ SMOKE_SECRET=${secret}
12135
+ `, "utf8");
12136
+ return { dir: root, secret };
12137
+ }
12138
+ function isAgentRecord(record) {
12139
+ return record && typeof record === "object" && record.role === "agent";
12140
+ }
12141
+ function extractTextFromRecord(record) {
12142
+ try {
12143
+ if (!record || typeof record !== "object") return "";
12144
+ const content = record.content;
12145
+ if (!content || typeof content !== "object") return "";
12146
+ if (content.type === "text" && typeof content.text === "string") return content.text;
12147
+ if (content.type === "tool" && Array.isArray(content.tools)) return JSON.stringify(content.tools);
12148
+ if (content.type === "output") {
12149
+ const msg = content?.data?.message;
12150
+ const items = Array.isArray(msg?.content) ? msg.content : [];
12151
+ const texts = [];
12152
+ for (const item of items) {
12153
+ if (item?.type === "text" && typeof item.text === "string") texts.push(item.text);
12154
+ }
12155
+ return texts.join("\n");
12156
+ }
12157
+ return "";
12158
+ } catch {
12159
+ return "";
12160
+ }
12161
+ }
12162
+ function recordIncludesNeedle(record, needle) {
12163
+ const n = String(needle || "").trim();
12164
+ if (!n) return true;
12165
+ const text = extractTextFromRecord(record);
12166
+ if (text && text.includes(n)) return true;
12167
+ try {
12168
+ return JSON.stringify(record).includes(n);
12169
+ } catch {
12170
+ return false;
12171
+ }
12172
+ }
12173
+ async function waitForMessage(opts) {
12174
+ const { sessionClient, predicate, timeoutMs } = opts;
12175
+ const deadline = Date.now() + timeoutMs;
12176
+ const existing = await sessionClient.listMessages().catch(() => []);
12177
+ for (const msg of existing) {
12178
+ const record = msg?.content;
12179
+ if (predicate(record)) return msg;
12180
+ }
12181
+ return await new Promise((resolve, reject) => {
12182
+ const onUpdate = (u) => {
12183
+ if (u?.body?.t !== "new-message") return;
12184
+ const record = u?.body?.message?.content;
12185
+ if (!record) return;
12186
+ if (!predicate(record)) return;
12187
+ cleanup();
12188
+ resolve(u?.body?.message);
12189
+ };
12190
+ const tick = () => {
12191
+ if (Date.now() < deadline) return;
12192
+ cleanup();
12193
+ reject(new Error("timeout_waiting_for_message"));
12194
+ };
12195
+ const interval = setInterval(tick, 200);
12196
+ const cleanup = () => {
12197
+ clearInterval(interval);
12198
+ sessionClient.off("update", onUpdate);
12199
+ };
12200
+ sessionClient.on("update", onUpdate);
12201
+ });
12202
+ }
12203
+ async function runScenario(name, fn) {
12204
+ const startedAtMs = Date.now();
12205
+ try {
12206
+ await fn();
12207
+ return { name, ok: true, startedAtMs, finishedAtMs: Date.now() };
12208
+ } catch (err) {
12209
+ const msg = err instanceof Error ? err.message : String(err);
12210
+ return { name, ok: false, error: msg, startedAtMs, finishedAtMs: Date.now() };
12211
+ }
12212
+ }
12213
+ function tinyPngBase64() {
12214
+ const width = 64;
12215
+ const height = 64;
12216
+ const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
12217
+ const crcTable = (() => {
12218
+ const table = new Uint32Array(256);
12219
+ for (let i = 0; i < 256; i += 1) {
12220
+ let c = i;
12221
+ for (let k = 0; k < 8; k += 1) {
12222
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
12223
+ }
12224
+ table[i] = c >>> 0;
12225
+ }
12226
+ return table;
12227
+ })();
12228
+ const crc32 = (buf) => {
12229
+ let c = 4294967295;
12230
+ for (let i = 0; i < buf.length; i += 1) {
12231
+ c = crcTable[(c ^ buf[i]) & 255] ^ c >>> 8;
12232
+ }
12233
+ return (c ^ 4294967295) >>> 0;
12234
+ };
12235
+ const chunk = (type, data) => {
12236
+ const typeBuf = Buffer.from(type, "ascii");
12237
+ const lenBuf = Buffer.alloc(4);
12238
+ lenBuf.writeUInt32BE(data.length, 0);
12239
+ const crcBuf = Buffer.alloc(4);
12240
+ const crc = crc32(Buffer.concat([typeBuf, data]));
12241
+ crcBuf.writeUInt32BE(crc, 0);
12242
+ return Buffer.concat([lenBuf, typeBuf, data, crcBuf]);
12243
+ };
12244
+ const ihdr = Buffer.alloc(13);
12245
+ ihdr.writeUInt32BE(width, 0);
12246
+ ihdr.writeUInt32BE(height, 4);
12247
+ ihdr[8] = 8;
12248
+ ihdr[9] = 6;
12249
+ ihdr[10] = 0;
12250
+ ihdr[11] = 0;
12251
+ ihdr[12] = 0;
12252
+ const bytesPerPixel = 4;
12253
+ const rowBytes = 1 + width * bytesPerPixel;
12254
+ const raw = Buffer.alloc(rowBytes * height);
12255
+ for (let y = 0; y < height; y += 1) {
12256
+ const rowStart = y * rowBytes;
12257
+ raw[rowStart] = 0;
12258
+ for (let x = 0; x < width; x += 1) {
12259
+ const p = rowStart + 1 + x * bytesPerPixel;
12260
+ raw[p + 0] = 45;
12261
+ raw[p + 1] = 125;
12262
+ raw[p + 2] = 255;
12263
+ raw[p + 3] = 255;
12264
+ }
12265
+ }
12266
+ const compressed = deflateSync(raw, { level: 6 });
12267
+ const png = Buffer.concat([
12268
+ pngSignature,
12269
+ chunk("IHDR", ihdr),
12270
+ chunk("IDAT", compressed),
12271
+ chunk("IEND", Buffer.alloc(0))
12272
+ ]);
12273
+ return png.toString("base64");
12274
+ }
12275
+ async function safeDeleteDir(dir) {
12276
+ try {
12277
+ await fs$1.rm(dir, { recursive: true, force: true });
12278
+ } catch {
12279
+ }
12280
+ }
12281
+ async function deleteSession(api, sessionId) {
12282
+ try {
12283
+ await api.deleteSessionById(sessionId);
12284
+ } catch {
12285
+ }
12286
+ }
12287
+ async function ensureSessionActive(api, sessionId, timeoutMs) {
12288
+ const deadline = Date.now() + timeoutMs;
12289
+ while (Date.now() < deadline) {
12290
+ const s = await api.getSessionById(sessionId).catch(() => null);
12291
+ if (s?.active) return;
12292
+ await delay(350);
12293
+ }
12294
+ throw new Error("session_not_active");
12295
+ }
12296
+ async function runSmokeForAgent(agent, args) {
12297
+ const startedAtMs = Date.now();
12298
+ const scenarios = [];
12299
+ const keep = readFlag(args, "--keep");
12300
+ const skipUnreal = !readFlag(args, "--with-unreal");
12301
+ const baseDir = readArgValue$2(args, "--directory") || readArgValue$2(args, "--dir");
12302
+ const timeoutMsRaw = readArgValue$2(args, "--timeout-ms");
12303
+ const timeoutMs = timeoutMsRaw && Number.isFinite(Number(timeoutMsRaw)) ? Number(timeoutMsRaw) : 9e4;
12304
+ let sessionId;
12305
+ let directory;
12306
+ let sessionClient = null;
12307
+ let api = null;
12308
+ try {
12309
+ await ensureDaemonRunning({ skipUnreal });
12310
+ const { auth, machineId } = await ensureMachineAuthOrLogin();
12311
+ api = await ApiClient.create(auth);
12312
+ const { dir, secret } = await createSmokeWorkspaceDir(baseDir);
12313
+ directory = dir;
12314
+ const sessionMeta = {
12315
+ path: dir,
12316
+ machineId,
12317
+ flavor: agent,
12318
+ createdBy: "smoke-test",
12319
+ tag: `smoke_${agent}_${nowIsoCompact()}`
12320
+ };
12321
+ const session = await api.createSession({ metadata: sessionMeta, state: null });
12322
+ sessionId = session.id;
12323
+ if (!sessionId) throw new Error("create_session_failed");
12324
+ const spawnResult = await spawnDaemonSession(dir, sessionId, agent);
12325
+ if (!spawnResult?.success) {
12326
+ const msg = String(spawnResult?.error || "spawn_failed");
12327
+ throw new Error(msg);
12328
+ }
12329
+ sessionClient = api.sessionSyncClient(session);
12330
+ await sessionClient.connectAndWait(15e3);
12331
+ await ensureSessionActive(api, sessionId, 25e3);
12332
+ const permissionMode = permissionModeForAgent(agent);
12333
+ scenarios.push(await runScenario("basic-message", async () => {
12334
+ const token = `SMOKE_OK_${agent}_${randomUUID().slice(0, 6)}`;
12335
+ sessionClient.sendUserText(`Reply with exactly: ${token}`, { permissionMode });
12336
+ await waitForMessage({
12337
+ sessionClient,
12338
+ predicate: (r) => isAgentRecord(r) && recordIncludesNeedle(r, token),
12339
+ timeoutMs
12340
+ });
12341
+ }));
12342
+ scenarios.push(await runScenario("tool-search", async () => {
12343
+ const token = `TOOL_SEARCH_OK_${randomUUID().slice(0, 6)}`;
12344
+ sessionClient.sendUserText(
12345
+ [
12346
+ "Read the file `smoke.txt` in the project directory using MCP tools (e.g. `read_file_chunk` or `search`).",
12347
+ "Find the value after `SMOKE_SECRET=`.",
12348
+ "",
12349
+ `Reply with two lines exactly:`,
12350
+ `1) SMOKE_SECRET=<value>`,
12351
+ `2) ${token}`
12352
+ ].join("\n"),
12353
+ { permissionMode }
12354
+ );
12355
+ await waitForMessage({
12356
+ sessionClient,
12357
+ predicate: (r) => isAgentRecord(r) && recordIncludesNeedle(r, `SMOKE_SECRET=${secret}`) && recordIncludesNeedle(r, token),
12358
+ timeoutMs
12359
+ });
12360
+ }));
12361
+ scenarios.push(await runScenario("image-attachment", async () => {
12362
+ const token = `IMAGE_OK_${randomUUID().slice(0, 6)}`;
12363
+ const base64 = tinyPngBase64();
12364
+ sessionClient.sendUserText(
12365
+ `Describe the attached image in one short sentence, then reply with exactly: ${token}`,
12366
+ {
12367
+ permissionMode,
12368
+ attachments: {
12369
+ images: [{ mimeType: "image/png", base64, name: "smoke.png" }]
12370
+ }
12371
+ }
12372
+ );
12373
+ const msg = await waitForMessage({
12374
+ sessionClient,
12375
+ predicate: (r) => isAgentRecord(r) && (recordIncludesNeedle(r, token) || agent === "codex" && recordIncludesNeedle(r, "image-attachment-missing") || recordIncludesNeedle(r, "Could not process image") || recordIncludesNeedle(r, "messages.") || recordIncludesNeedle(r, "invalid_request_error")),
12376
+ timeoutMs
12377
+ });
12378
+ const record = msg?.content;
12379
+ if (agent === "codex" && recordIncludesNeedle(record, "image-attachment-missing")) {
12380
+ throw new Error("image-attachment-missing (Codex did not receive images; check latest_user_images tool + vision support)");
12381
+ }
12382
+ if (recordIncludesNeedle(record, "Could not process image") || recordIncludesNeedle(record, "invalid_request_error")) {
12383
+ throw new Error("provider_image_invalid (image rejected by provider; check base64/mimeType pipeline)");
12384
+ }
12385
+ }));
12386
+ const ok = scenarios.every((s) => s.ok);
12387
+ return {
12388
+ agent,
12389
+ ok,
12390
+ sessionId,
12391
+ directory,
12392
+ scenarios,
12393
+ startedAtMs,
12394
+ finishedAtMs: Date.now()
12395
+ };
12396
+ } finally {
12397
+ if (sessionClient) {
12398
+ try {
12399
+ sessionClient.removeAllListeners();
12400
+ } catch {
12401
+ }
12402
+ try {
12403
+ sessionClient.close();
12404
+ } catch {
12405
+ }
12406
+ }
12407
+ if (sessionId) {
12408
+ if (!keep) {
12409
+ try {
12410
+ await stopDaemonSession(sessionId);
12411
+ } catch {
12412
+ }
12413
+ }
12414
+ }
12415
+ if (directory && !keep) {
12416
+ await safeDeleteDir(directory);
12417
+ }
12418
+ if (sessionId && api && !keep) {
12419
+ await deleteSession(api, sessionId);
12420
+ }
12421
+ }
12422
+ }
12423
+ function showHelp() {
12424
+ console.log(`
12425
+ ${chalk.bold("flockbay smoke-test")} - end-to-end smoke tests for daemon + session runtime + MCP tools
12426
+
12427
+ Usage:
12428
+ flockbay smoke-test [--all] [--agent codex|claude|gemini] [--agents codex,claude] [--directory <path>]
12429
+
12430
+ Options:
12431
+ --all Run all agents (default)
12432
+ --agent, -a Run a single agent
12433
+ --agents Comma-separated list of agents
12434
+ --directory, --dir Directory to run the session in (defaults to a temp folder)
12435
+ --timeout-ms Per-scenario timeout (default 90000)
12436
+ --keep Do not stop session or delete temp dir
12437
+ --json Write results JSON to a file
12438
+ --with-unreal Allow Unreal bridge prompts (off by default)
12439
+ `);
12440
+ }
12441
+ async function handleSmokeTestCommand(args) {
12442
+ if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
12443
+ showHelp();
12444
+ return;
12445
+ }
12446
+ const agents = parseAgentList(args);
12447
+ const outPath = readArgValue$2(args, "--json");
12448
+ console.log(chalk.bold("\nFlockbay smoke test\n"));
12449
+ console.log(chalk.gray(`Server: ${configuration.serverUrl}`));
12450
+ console.log(chalk.gray(`Profile: ${configuration.profile}`));
12451
+ console.log(chalk.gray(`Agents: ${agents.join(", ")}`));
12452
+ console.log("");
12453
+ const results = [];
12454
+ for (const agent of agents) {
12455
+ console.log(chalk.bold(`== ${agent} ==`));
12456
+ const r = await runSmokeForAgent(agent, args);
12457
+ results.push(r);
12458
+ for (const s of r.scenarios) {
12459
+ if (s.ok) console.log(chalk.green(` \u2713 ${s.name}`));
12460
+ else console.log(chalk.red(` \u2717 ${s.name}: ${s.error || "failed"}`));
12461
+ }
12462
+ console.log(r.ok ? chalk.green(" PASS\n") : chalk.red(" FAIL\n"));
12463
+ }
12464
+ const allOk = results.every((r) => r.ok);
12465
+ if (outPath) {
12466
+ const abs = path.resolve(outPath);
12467
+ await fs$1.mkdir(path.dirname(abs), { recursive: true });
12468
+ await fs$1.writeFile(abs, JSON.stringify({ ok: allOk, results }, null, 2) + "\n", "utf8");
12469
+ console.log(chalk.gray(`Wrote: ${abs}`));
12470
+ }
12471
+ if (!allOk) {
12472
+ process.exitCode = 1;
12473
+ }
12474
+ }
12475
+
11916
12476
  function readArgValue$1(args, key) {
11917
12477
  const idx = args.indexOf(key);
11918
12478
  if (idx === -1) return null;
@@ -12636,6 +13196,16 @@ async function authAndSetupMachineIfNeeded() {
12636
13196
  process.exit(1);
12637
13197
  }
12638
13198
  return;
13199
+ } else if (subcommand === "smoke-test") {
13200
+ try {
13201
+ await ensureProdServerWhenLocalDevUnreachable();
13202
+ await handleSmokeTestCommand(args.slice(1));
13203
+ } catch (error) {
13204
+ console.error(chalk.red("Smoke test failed:"), error instanceof Error ? error.message : "Unknown error");
13205
+ if (process.env.DEBUG) console.error(error);
13206
+ process.exit(1);
13207
+ }
13208
+ return;
12639
13209
  } else if (subcommand === "start") {
12640
13210
  const startArgs = args.slice(1);
12641
13211
  if (startArgs.includes("--help") || startArgs.includes("-h") || startArgs.includes("help")) {
@@ -12678,7 +13248,7 @@ ${engineRoot}`, {
12678
13248
  } else if (subcommand === "codex") {
12679
13249
  try {
12680
13250
  await chdirToNearestUprojectRootIfPresent();
12681
- const { runCodex } = await import('./runCodex-Ujz1z7Tp.mjs');
13251
+ const { runCodex } = await import('./runCodex-B0JRo8YP.mjs');
12682
13252
  let startedBy = void 0;
12683
13253
  let sessionId = void 0;
12684
13254
  for (let i = 1; i < args.length; i++) {
@@ -12780,7 +13350,7 @@ ${engineRoot}`, {
12780
13350
  }
12781
13351
  try {
12782
13352
  await chdirToNearestUprojectRootIfPresent();
12783
- const { runGemini } = await import('./runGemini-BbdMLMnn.mjs');
13353
+ const { runGemini } = await import('./runGemini-DO9xzjyY.mjs');
12784
13354
  let startedBy = void 0;
12785
13355
  let sessionId = void 0;
12786
13356
  for (let i = 1; i < args.length; i++) {