flockbay 0.10.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +56 -0
  2. package/bin/flockbay-mcp.mjs +56 -0
  3. package/bin/flockbay.mjs +78 -0
  4. package/dist/codex/flockbayMcpStdioBridge.cjs +383 -0
  5. package/dist/codex/flockbayMcpStdioBridge.d.cts +2 -0
  6. package/dist/codex/flockbayMcpStdioBridge.d.mts +2 -0
  7. package/dist/codex/flockbayMcpStdioBridge.mjs +381 -0
  8. package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +136 -0
  9. package/dist/flockbayScreenshotGate-DkxU24cR.cjs +138 -0
  10. package/dist/index--o4BPz5o.cjs +10311 -0
  11. package/dist/index-CUp3juDS.mjs +10268 -0
  12. package/dist/index.cjs +43 -0
  13. package/dist/index.d.cts +1 -0
  14. package/dist/index.d.mts +1 -0
  15. package/dist/index.mjs +40 -0
  16. package/dist/lib.cjs +33 -0
  17. package/dist/lib.d.cts +957 -0
  18. package/dist/lib.d.mts +957 -0
  19. package/dist/lib.mjs +23 -0
  20. package/dist/runCodex-D3eT-TvB.cjs +3449 -0
  21. package/dist/runCodex-o6PCbHQ7.mjs +3446 -0
  22. package/dist/runGemini-Bt0oEj_g.mjs +3183 -0
  23. package/dist/runGemini-CBxZp6I7.cjs +3185 -0
  24. package/dist/types-C-jnUdn_.cjs +4498 -0
  25. package/dist/types-DGd6ea2Z.mjs +4450 -0
  26. package/kits/kit.open_world/kit.json +59 -0
  27. package/package.json +130 -0
  28. package/scripts/claude_local_launcher.cjs +73 -0
  29. package/scripts/claude_remote_launcher.cjs +16 -0
  30. package/scripts/claude_version_utils.cjs +391 -0
  31. package/scripts/ripgrep_launcher.cjs +33 -0
  32. package/scripts/session_hook_forwarder.cjs +49 -0
  33. package/scripts/test-codex-abort-history.mjs +77 -0
  34. package/scripts/unpack-tools.cjs +222 -0
  35. package/tools/licenses/difftastic-LICENSE +21 -0
  36. package/tools/licenses/ripgrep-LICENSE +3 -0
  37. package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -0
  38. package/tools/unreal-mcp/upstream/Docs/README.md +8 -0
  39. package/tools/unreal-mcp/upstream/Docs/Tools/README.md +7 -0
  40. package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +184 -0
  41. package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -0
  42. package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -0
  43. package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -0
  44. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Config/FilterPlugin.ini +8 -0
  45. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1160 -0
  46. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +924 -0
  47. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +709 -0
  48. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +896 -0
  49. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPProjectCommands.cpp +72 -0
  50. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPUMGCommands.cpp +544 -0
  51. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +321 -0
  52. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +419 -0
  53. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPModule.cpp +21 -0
  54. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +34 -0
  55. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +27 -0
  56. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -0
  57. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +40 -0
  58. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -0
  59. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -0
  60. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/MCPServerRunnable.h +34 -0
  61. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPBridge.h +64 -0
  62. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPModule.h +22 -0
  63. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +78 -0
  64. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/UnrealMCP.uplugin +36 -0
  65. package/tools/unreal-mcp/upstream/Python/README.md +40 -0
  66. package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -0
  67. package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +203 -0
  68. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +497 -0
  69. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +194 -0
  70. package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +267 -0
  71. package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +618 -0
  72. package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +366 -0
  73. package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +390 -0
  74. package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +420 -0
  75. package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -0
  76. package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +430 -0
  77. package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +64 -0
  78. package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +333 -0
  79. package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -0
  80. package/tools/unreal-mcp/upstream/Python/uv.lock +521 -0
@@ -0,0 +1,4450 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import { appendFileSync } from 'fs';
4
+ import fs__default, { existsSync, readFileSync, mkdirSync, constants, unlinkSync, writeFileSync, readdirSync, statSync, createWriteStream } from 'node:fs';
5
+ import os, { homedir } from 'node:os';
6
+ import path, { join, basename } from 'node:path';
7
+ import { readFile, open, stat, unlink, mkdir, writeFile, rename } from 'node:fs/promises';
8
+ import * as z from 'zod';
9
+ import { z as z$1 } from 'zod';
10
+ import { randomBytes, createCipheriv, createDecipheriv, randomUUID as randomUUID$1 } from 'node:crypto';
11
+ import tweetnacl from 'tweetnacl';
12
+ import { EventEmitter } from 'node:events';
13
+ import { io } from 'socket.io-client';
14
+ import { spawn as spawn$1 } from 'node:child_process';
15
+ import { spawn } from 'child_process';
16
+ 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';
17
+ import { randomUUID, createHash } from 'crypto';
18
+ import { dirname, resolve, join as join$1, relative } from 'path';
19
+ import { fileURLToPath } from 'url';
20
+ import process$1 from 'node:process';
21
+ import { platform } from 'os';
22
+ import net from 'node:net';
23
+ import { Expo } from 'expo-server-sdk';
24
+
25
+ var name = "flockbay";
26
+ var version = "0.10.15";
27
+ var description = "Flockbay CLI (local agent + daemon)";
28
+ var author = "Eduardo Orellana";
29
+ var license = "UNLICENSED";
30
+ var type = "module";
31
+ var homepage = "https://flockbay.com";
32
+ var bugs = "https://flockbay.com";
33
+ var repository = {
34
+ type: "git",
35
+ url: "https://flockbay.com"
36
+ };
37
+ var bin = {
38
+ flockbay: "bin/flockbay.mjs",
39
+ "flockbay-mcp": "bin/flockbay-mcp.mjs"
40
+ };
41
+ var main = "./dist/index.cjs";
42
+ var module$1 = "./dist/index.mjs";
43
+ var types = "./dist/index.d.cts";
44
+ var exports$1 = {
45
+ ".": {
46
+ require: {
47
+ types: "./dist/index.d.cts",
48
+ "default": "./dist/index.cjs"
49
+ },
50
+ "import": {
51
+ types: "./dist/index.d.mts",
52
+ "default": "./dist/index.mjs"
53
+ }
54
+ },
55
+ "./lib": {
56
+ require: {
57
+ types: "./dist/lib.d.cts",
58
+ "default": "./dist/lib.cjs"
59
+ },
60
+ "import": {
61
+ types: "./dist/lib.d.mts",
62
+ "default": "./dist/lib.mjs"
63
+ }
64
+ },
65
+ "./codex/flockbayMcpStdioBridge": {
66
+ require: {
67
+ types: "./dist/codex/flockbayMcpStdioBridge.d.cts",
68
+ "default": "./dist/codex/flockbayMcpStdioBridge.cjs"
69
+ },
70
+ "import": {
71
+ types: "./dist/codex/flockbayMcpStdioBridge.d.mts",
72
+ "default": "./dist/codex/flockbayMcpStdioBridge.mjs"
73
+ }
74
+ }
75
+ };
76
+ var files = [
77
+ "dist",
78
+ "bin",
79
+ "kits",
80
+ "scripts",
81
+ "tools/licenses",
82
+ "tools/unreal-mcp",
83
+ "package.json"
84
+ ];
85
+ var scripts = {
86
+ "why do we need to build before running tests / dev?": "We need the binary to be built so daemon commands run the built CLI (avoids drift between dev entrypoints and the packaged runtime).",
87
+ typecheck: "tsc --noEmit",
88
+ build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
89
+ test: "yarn build && vitest run",
90
+ start: "yarn build && node ./bin/flockbay.mjs",
91
+ dev: "tsx src/index.ts",
92
+ "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
93
+ "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
94
+ prepublishOnly: "yarn build && yarn test",
95
+ release: "yarn install && release-it",
96
+ postinstall: "node scripts/unpack-tools.cjs"
97
+ };
98
+ var dependencies = {
99
+ "@agentclientprotocol/sdk": "^0.8.0",
100
+ "@modelcontextprotocol/sdk": "^1.22.0",
101
+ "@stablelib/base64": "^2.0.1",
102
+ "@stablelib/hex": "^2.0.1",
103
+ "@types/cross-spawn": "^6.0.6",
104
+ "@types/http-proxy": "^1.17.17",
105
+ "@types/ps-list": "^6.2.1",
106
+ "@types/qrcode-terminal": "^0.12.2",
107
+ "@types/react": "^19.2.7",
108
+ "@types/tmp": "^0.2.6",
109
+ ai: "^5.0.107",
110
+ axios: "^1.13.2",
111
+ chalk: "^5.6.2",
112
+ "cross-spawn": "^7.0.6",
113
+ "expo-server-sdk": "^3.15.0",
114
+ fastify: "^5.6.2",
115
+ "fastify-type-provider-zod": "4.0.2",
116
+ "http-proxy": "^1.18.1",
117
+ "http-proxy-middleware": "^3.0.5",
118
+ ink: "^6.5.1",
119
+ open: "^10.2.0",
120
+ "ps-list": "^8.1.1",
121
+ "qrcode-terminal": "^0.12.0",
122
+ react: "^19.2.0",
123
+ "socket.io-client": "^4.8.1",
124
+ tar: "^7.5.2",
125
+ tmp: "^0.2.5",
126
+ tweetnacl: "^1.0.3",
127
+ zod: "^3.23.8"
128
+ };
129
+ var devDependencies = {
130
+ "@eslint/compat": "^1",
131
+ "@types/node": ">=20",
132
+ "cross-env": "^10.1.0",
133
+ dotenv: "^16.6.1",
134
+ eslint: "^9",
135
+ "eslint-config-prettier": "^10",
136
+ pkgroll: "^2.14.2",
137
+ "release-it": "^19.0.6",
138
+ shx: "^0.3.3",
139
+ "ts-node": "^10",
140
+ tsx: "^4.20.6",
141
+ typescript: "^5",
142
+ vitest: "^3.2.4"
143
+ };
144
+ var resolutions = {
145
+ "whatwg-url": "14.2.0",
146
+ "parse-path": "7.0.3",
147
+ "@types/parse-path": "7.0.3"
148
+ };
149
+ var publishConfig = {
150
+ registry: "https://registry.npmjs.org"
151
+ };
152
+ var packageManager = "yarn@1.22.22";
153
+ var packageJson = {
154
+ name: name,
155
+ version: version,
156
+ description: description,
157
+ author: author,
158
+ license: license,
159
+ type: type,
160
+ homepage: homepage,
161
+ bugs: bugs,
162
+ repository: repository,
163
+ bin: bin,
164
+ main: main,
165
+ module: module$1,
166
+ types: types,
167
+ exports: exports$1,
168
+ files: files,
169
+ scripts: scripts,
170
+ dependencies: dependencies,
171
+ devDependencies: devDependencies,
172
+ resolutions: resolutions,
173
+ publishConfig: publishConfig,
174
+ packageManager: packageManager
175
+ };
176
+
177
+ function normalizeServerUrlForNode(url) {
178
+ try {
179
+ const u = new URL(url);
180
+ if (u.hostname === "localhost") u.hostname = "127.0.0.1";
181
+ return u.origin;
182
+ } catch {
183
+ return url;
184
+ }
185
+ }
186
+ class Configuration {
187
+ serverUrl;
188
+ webappUrl;
189
+ isDaemonProcess;
190
+ // Directories and paths (from persistence)
191
+ flockbayHomeDir;
192
+ logsDir;
193
+ settingsFile;
194
+ privateKeyFile;
195
+ daemonStateFile;
196
+ daemonLockFile;
197
+ currentCliVersion;
198
+ isExperimentalEnabled;
199
+ disableCaffeinate;
200
+ constructor() {
201
+ const args = process.argv.slice(2);
202
+ this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
203
+ const homeOverride = process.env.FLOCKBAY_HOME_DIR;
204
+ if (homeOverride) {
205
+ const expandedPath = homeOverride.replace(/^~/, homedir());
206
+ this.flockbayHomeDir = expandedPath;
207
+ } else {
208
+ this.flockbayHomeDir = join(homedir(), ".flockbay");
209
+ }
210
+ this.logsDir = join(this.flockbayHomeDir, "logs");
211
+ this.settingsFile = join(this.flockbayHomeDir, "settings.json");
212
+ this.privateKeyFile = join(this.flockbayHomeDir, "access.key");
213
+ this.daemonStateFile = join(this.flockbayHomeDir, "daemon.state.json");
214
+ this.daemonLockFile = join(this.flockbayHomeDir, "daemon.state.json.lock");
215
+ let persistedSettings = null;
216
+ try {
217
+ if (existsSync(this.settingsFile)) {
218
+ persistedSettings = JSON.parse(readFileSync(this.settingsFile, "utf8"));
219
+ }
220
+ } catch {
221
+ }
222
+ const rawServerUrl = process.env.FLOCKBAY_SERVER_URL || persistedSettings?.serverUrl || "https://api-internal.flockbay.com";
223
+ this.serverUrl = normalizeServerUrlForNode(rawServerUrl);
224
+ this.webappUrl = process.env.FLOCKBAY_WEBAPP_URL || persistedSettings?.webappUrl || "https://flockbay.com";
225
+ this.isExperimentalEnabled = ["true", "1", "yes"].includes(
226
+ (process.env.FLOCKBAY_EXPERIMENTAL || "").toLowerCase()
227
+ );
228
+ this.disableCaffeinate = ["true", "1", "yes"].includes(
229
+ (process.env.FLOCKBAY_DISABLE_CAFFEINATE || "").toLowerCase()
230
+ );
231
+ this.currentCliVersion = packageJson.version;
232
+ if (!existsSync(this.flockbayHomeDir)) {
233
+ mkdirSync(this.flockbayHomeDir, { recursive: true });
234
+ }
235
+ if (!existsSync(this.logsDir)) {
236
+ mkdirSync(this.logsDir, { recursive: true });
237
+ }
238
+ }
239
+ }
240
+ const configuration = new Configuration();
241
+
242
+ function encodeBase64(buffer, variant = "base64") {
243
+ if (variant === "base64url") {
244
+ return encodeBase64Url(buffer);
245
+ }
246
+ return Buffer.from(buffer).toString("base64");
247
+ }
248
+ function encodeBase64Url(buffer) {
249
+ return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
250
+ }
251
+ function decodeBase64(base64, variant = "base64") {
252
+ if (variant === "base64url") {
253
+ const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
254
+ return new Uint8Array(Buffer.from(base64Standard, "base64"));
255
+ }
256
+ return new Uint8Array(Buffer.from(base64, "base64"));
257
+ }
258
+ function getRandomBytes(size) {
259
+ return new Uint8Array(randomBytes(size));
260
+ }
261
+ function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
262
+ const ephemeralKeyPair = tweetnacl.box.keyPair();
263
+ const nonce = getRandomBytes(tweetnacl.box.nonceLength);
264
+ const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
265
+ const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
266
+ result.set(ephemeralKeyPair.publicKey, 0);
267
+ result.set(nonce, ephemeralKeyPair.publicKey.length);
268
+ result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
269
+ return result;
270
+ }
271
+ function encryptWithDataKey(data, dataKey) {
272
+ const nonce = getRandomBytes(12);
273
+ const cipher = createCipheriv("aes-256-gcm", dataKey, nonce);
274
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
275
+ const encrypted = Buffer.concat([
276
+ cipher.update(plaintext),
277
+ cipher.final()
278
+ ]);
279
+ const authTag = cipher.getAuthTag();
280
+ const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
281
+ bundle.set([0], 0);
282
+ bundle.set(nonce, 1);
283
+ bundle.set(new Uint8Array(encrypted), 13);
284
+ bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
285
+ return bundle;
286
+ }
287
+ function decryptWithDataKey(bundle, dataKey) {
288
+ if (bundle.length < 1) {
289
+ return null;
290
+ }
291
+ if (bundle[0] !== 0) {
292
+ return null;
293
+ }
294
+ if (bundle.length < 12 + 16 + 1) {
295
+ return null;
296
+ }
297
+ const nonce = bundle.slice(1, 13);
298
+ const authTag = bundle.slice(bundle.length - 16);
299
+ const ciphertext = bundle.slice(13, bundle.length - 16);
300
+ try {
301
+ const decipher = createDecipheriv("aes-256-gcm", dataKey, nonce);
302
+ decipher.setAuthTag(authTag);
303
+ const decrypted = Buffer.concat([
304
+ decipher.update(ciphertext),
305
+ decipher.final()
306
+ ]);
307
+ return JSON.parse(new TextDecoder().decode(decrypted));
308
+ } catch (error) {
309
+ return null;
310
+ }
311
+ }
312
+ function encrypt(key, data) {
313
+ return encryptWithDataKey(data, key);
314
+ }
315
+ function decrypt(key, data) {
316
+ return decryptWithDataKey(data, key);
317
+ }
318
+
319
+ const defaultSettings = {
320
+ onboardingCompleted: false
321
+ };
322
+ async function readSettings() {
323
+ if (!existsSync(configuration.settingsFile)) {
324
+ return { ...defaultSettings };
325
+ }
326
+ try {
327
+ const content = await readFile(configuration.settingsFile, "utf8");
328
+ return JSON.parse(content);
329
+ } catch {
330
+ return { ...defaultSettings };
331
+ }
332
+ }
333
+ async function updateSettings(updater) {
334
+ const LOCK_RETRY_INTERVAL_MS = 100;
335
+ const MAX_LOCK_ATTEMPTS = 50;
336
+ const STALE_LOCK_TIMEOUT_MS = 1e4;
337
+ const lockFile = configuration.settingsFile + ".lock";
338
+ const tmpFile = configuration.settingsFile + ".tmp";
339
+ let fileHandle;
340
+ let attempts = 0;
341
+ while (attempts < MAX_LOCK_ATTEMPTS) {
342
+ try {
343
+ fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
344
+ break;
345
+ } catch (err) {
346
+ if (err.code === "EEXIST") {
347
+ attempts++;
348
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
349
+ try {
350
+ const stats = await stat(lockFile);
351
+ if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
352
+ await unlink(lockFile).catch((unlinkErr) => {
353
+ if (unlinkErr?.code !== "ENOENT") {
354
+ console.error("[persistence] Failed to remove stale settings lock file", { lockFile, unlinkErr });
355
+ }
356
+ });
357
+ }
358
+ } catch (staleErr) {
359
+ console.error("[persistence] Failed while checking/removing stale settings lock file", { lockFile, staleErr });
360
+ }
361
+ } else {
362
+ throw err;
363
+ }
364
+ }
365
+ }
366
+ if (!fileHandle) {
367
+ throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
368
+ }
369
+ try {
370
+ const current = await readSettings() || { ...defaultSettings };
371
+ const updated = await updater(current);
372
+ if (!existsSync(configuration.flockbayHomeDir)) {
373
+ await mkdir(configuration.flockbayHomeDir, { recursive: true });
374
+ }
375
+ await writeFile(tmpFile, JSON.stringify(updated, null, 2));
376
+ await rename(tmpFile, configuration.settingsFile);
377
+ return updated;
378
+ } finally {
379
+ await fileHandle.close();
380
+ await unlink(lockFile).catch(() => {
381
+ });
382
+ }
383
+ }
384
+ const credentialsSchema = z.object({
385
+ token: z.string(),
386
+ encryption: z.object({
387
+ publicKey: z.string().base64(),
388
+ machineKey: z.string().base64()
389
+ })
390
+ });
391
+ async function readCredentials() {
392
+ if (!existsSync(configuration.privateKeyFile)) {
393
+ return null;
394
+ }
395
+ try {
396
+ const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
397
+ const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
398
+ return {
399
+ token: credentials.token,
400
+ encryption: {
401
+ type: "dataKey",
402
+ publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
403
+ machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
404
+ }
405
+ };
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ async function writeCredentialsDataKey(credentials) {
411
+ if (!existsSync(configuration.flockbayHomeDir)) {
412
+ await mkdir(configuration.flockbayHomeDir, { recursive: true });
413
+ }
414
+ await writeFile(configuration.privateKeyFile, JSON.stringify({
415
+ encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
416
+ token: credentials.token
417
+ }, null, 2));
418
+ }
419
+ async function clearCredentials() {
420
+ if (existsSync(configuration.privateKeyFile)) {
421
+ await unlink(configuration.privateKeyFile);
422
+ }
423
+ }
424
+ async function clearMachineId() {
425
+ await updateSettings((settings) => ({
426
+ ...settings,
427
+ machineId: void 0
428
+ }));
429
+ }
430
+ async function readDaemonState() {
431
+ try {
432
+ if (!existsSync(configuration.daemonStateFile)) {
433
+ return null;
434
+ }
435
+ const content = await readFile(configuration.daemonStateFile, "utf-8");
436
+ return JSON.parse(content);
437
+ } catch (error) {
438
+ console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
439
+ return null;
440
+ }
441
+ }
442
+ function writeDaemonState(state) {
443
+ writeFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2), "utf-8");
444
+ }
445
+ async function clearDaemonState() {
446
+ if (existsSync(configuration.daemonStateFile)) {
447
+ await unlink(configuration.daemonStateFile);
448
+ }
449
+ if (existsSync(configuration.daemonLockFile)) {
450
+ try {
451
+ await unlink(configuration.daemonLockFile);
452
+ } catch {
453
+ }
454
+ }
455
+ }
456
+ async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
457
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
458
+ try {
459
+ const fileHandle = await open(
460
+ configuration.daemonLockFile,
461
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY
462
+ );
463
+ await fileHandle.writeFile(String(process.pid));
464
+ return fileHandle;
465
+ } catch (error) {
466
+ if (error.code === "EEXIST") {
467
+ try {
468
+ const lockPid = readFileSync(configuration.daemonLockFile, "utf-8").trim();
469
+ if (lockPid && !isNaN(Number(lockPid))) {
470
+ try {
471
+ process.kill(Number(lockPid), 0);
472
+ } catch {
473
+ unlinkSync(configuration.daemonLockFile);
474
+ continue;
475
+ }
476
+ }
477
+ } catch {
478
+ }
479
+ }
480
+ if (attempt === maxAttempts) {
481
+ return null;
482
+ }
483
+ const delayMs = attempt * delayIncrementMs;
484
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
485
+ }
486
+ }
487
+ return null;
488
+ }
489
+ async function releaseDaemonLock(lockHandle) {
490
+ try {
491
+ await lockHandle.close();
492
+ } catch (err) {
493
+ console.error("[persistence] Failed to close daemon lock handle", err);
494
+ }
495
+ try {
496
+ if (existsSync(configuration.daemonLockFile)) {
497
+ unlinkSync(configuration.daemonLockFile);
498
+ }
499
+ } catch (err) {
500
+ console.error("[persistence] Failed to remove daemon lock file", { lockFile: configuration.daemonLockFile, err });
501
+ }
502
+ }
503
+
504
+ function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
505
+ return date.toLocaleString("sv-SE", {
506
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
507
+ year: "numeric",
508
+ month: "2-digit",
509
+ day: "2-digit",
510
+ hour: "2-digit",
511
+ minute: "2-digit",
512
+ second: "2-digit"
513
+ }).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
514
+ }
515
+ function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
516
+ return date.toLocaleTimeString("en-US", {
517
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
518
+ hour12: false,
519
+ hour: "2-digit",
520
+ minute: "2-digit",
521
+ second: "2-digit",
522
+ fractionalSecondDigits: 3
523
+ });
524
+ }
525
+ function getSessionLogPath() {
526
+ const timestamp = createTimestampForFilename();
527
+ const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
528
+ return join(configuration.logsDir, filename);
529
+ }
530
+ class Logger {
531
+ constructor(logFilePath = getSessionLogPath()) {
532
+ this.logFilePath = logFilePath;
533
+ if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.FLOCKBAY_SERVER_URL) {
534
+ this.dangerouslyUnencryptedServerLoggingUrl = process.env.FLOCKBAY_SERVER_URL;
535
+ console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
536
+ }
537
+ }
538
+ dangerouslyUnencryptedServerLoggingUrl;
539
+ remoteLogErrorCount = 0;
540
+ lastRemoteLogErrorAt = 0;
541
+ logFileAppendErrorCount = 0;
542
+ lastLogFileAppendErrorAt = 0;
543
+ reportRemoteLoggingError(error) {
544
+ this.remoteLogErrorCount += 1;
545
+ const now = Date.now();
546
+ if (this.remoteLogErrorCount <= 3 || now - this.lastRemoteLogErrorAt > 6e4) {
547
+ this.lastRemoteLogErrorAt = now;
548
+ console.error("[REMOTE LOGGING] Failed to send logs to server:", error);
549
+ }
550
+ }
551
+ reportLogFileAppendError(error) {
552
+ this.logFileAppendErrorCount += 1;
553
+ const now = Date.now();
554
+ if (this.logFileAppendErrorCount <= 3 || now - this.lastLogFileAppendErrorAt > 6e4) {
555
+ this.lastLogFileAppendErrorAt = now;
556
+ console.error("[LOGGER] Failed to append to log file:", this.logFilePath, error);
557
+ }
558
+ }
559
+ // Use local timezone for simplicity of locating the logs,
560
+ // in practice you will not need absolute timestamps
561
+ localTimezoneTimestamp() {
562
+ return createTimestampForLogEntry();
563
+ }
564
+ debug(message, ...args) {
565
+ this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
566
+ }
567
+ debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
568
+ if (!process.env.DEBUG) {
569
+ this.debug(`In production, skipping message inspection`);
570
+ }
571
+ const truncateStrings = (obj) => {
572
+ if (typeof obj === "string") {
573
+ return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
574
+ }
575
+ if (Array.isArray(obj)) {
576
+ const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
577
+ if (obj.length > maxArrayLength) {
578
+ truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
579
+ }
580
+ return truncatedArray;
581
+ }
582
+ if (obj && typeof obj === "object") {
583
+ const result = {};
584
+ for (const [key, value] of Object.entries(obj)) {
585
+ if (key === "usage") {
586
+ continue;
587
+ }
588
+ result[key] = truncateStrings(value);
589
+ }
590
+ return result;
591
+ }
592
+ return obj;
593
+ };
594
+ const truncatedObject = truncateStrings(object);
595
+ const json = JSON.stringify(truncatedObject, null, 2);
596
+ this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
597
+ }
598
+ info(message, ...args) {
599
+ this.logToConsole("info", "", message, ...args);
600
+ this.debug(message, args);
601
+ }
602
+ infoDeveloper(message, ...args) {
603
+ this.debug(message, ...args);
604
+ if (process.env.DEBUG) {
605
+ this.logToConsole("info", "[DEV]", message, ...args);
606
+ }
607
+ }
608
+ warn(message, ...args) {
609
+ this.logToConsole("warn", "", message, ...args);
610
+ this.debug(`[WARN] ${message}`, ...args);
611
+ }
612
+ getLogPath() {
613
+ return this.logFilePath;
614
+ }
615
+ logToConsole(level, prefix, message, ...args) {
616
+ switch (level) {
617
+ case "debug": {
618
+ console.log(chalk.gray(prefix), message, ...args);
619
+ break;
620
+ }
621
+ case "error": {
622
+ console.error(chalk.red(prefix), message, ...args);
623
+ break;
624
+ }
625
+ case "info": {
626
+ console.log(chalk.blue(prefix), message, ...args);
627
+ break;
628
+ }
629
+ case "warn": {
630
+ console.log(chalk.yellow(prefix), message, ...args);
631
+ break;
632
+ }
633
+ default: {
634
+ this.debug("Unknown log level:", level);
635
+ console.log(chalk.blue(prefix), message, ...args);
636
+ break;
637
+ }
638
+ }
639
+ }
640
+ async sendToRemoteServer(level, message, ...args) {
641
+ if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
642
+ try {
643
+ await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
644
+ method: "POST",
645
+ headers: { "Content-Type": "application/json" },
646
+ body: JSON.stringify({
647
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
648
+ level,
649
+ message: `${message} ${args.map(
650
+ (a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)
651
+ ).join(" ")}`,
652
+ source: "cli",
653
+ platform: process.platform
654
+ })
655
+ });
656
+ } catch (error) {
657
+ this.reportRemoteLoggingError(error);
658
+ }
659
+ }
660
+ logToFile(prefix, message, ...args) {
661
+ const logLine = `${prefix} ${message} ${args.map(
662
+ (arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
663
+ ).join(" ")}
664
+ `;
665
+ if (this.dangerouslyUnencryptedServerLoggingUrl) {
666
+ let level = "info";
667
+ if (prefix.includes(this.localTimezoneTimestamp())) {
668
+ level = "debug";
669
+ }
670
+ this.sendToRemoteServer(level, message, ...args).catch((error) => this.reportRemoteLoggingError(error));
671
+ }
672
+ try {
673
+ appendFileSync(this.logFilePath, logLine);
674
+ } catch (appendError) {
675
+ this.reportLogFileAppendError(appendError);
676
+ }
677
+ }
678
+ }
679
+ let logger = new Logger();
680
+ async function listDaemonLogFiles(limit = 50) {
681
+ try {
682
+ const logsDir = configuration.logsDir;
683
+ if (!existsSync(logsDir)) {
684
+ return [];
685
+ }
686
+ const logs = readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
687
+ const fullPath = join(logsDir, file);
688
+ const stats = statSync(fullPath);
689
+ return { file, path: fullPath, modified: stats.mtime };
690
+ }).sort((a, b) => b.modified.getTime() - a.modified.getTime());
691
+ try {
692
+ const state = await readDaemonState();
693
+ if (!state) {
694
+ return logs;
695
+ }
696
+ if (state.daemonLogPath && existsSync(state.daemonLogPath)) {
697
+ const stats = statSync(state.daemonLogPath);
698
+ const persisted = {
699
+ file: basename(state.daemonLogPath),
700
+ path: state.daemonLogPath,
701
+ modified: stats.mtime
702
+ };
703
+ const idx = logs.findIndex((l) => l.path === persisted.path);
704
+ if (idx >= 0) {
705
+ const [found] = logs.splice(idx, 1);
706
+ logs.unshift(found);
707
+ } else {
708
+ logs.unshift(persisted);
709
+ }
710
+ }
711
+ } catch {
712
+ }
713
+ return logs.slice(0, Math.max(0, limit));
714
+ } catch {
715
+ return [];
716
+ }
717
+ }
718
+ async function getLatestDaemonLog() {
719
+ const [latest] = await listDaemonLogFiles(1);
720
+ return latest || null;
721
+ }
722
+
723
+ const SessionMessageContentSchema = z$1.object({
724
+ c: z$1.string(),
725
+ // Base64 encoded encrypted content
726
+ t: z$1.literal("encrypted")
727
+ });
728
+ const UpdateBodySchema = z$1.object({
729
+ message: z$1.object({
730
+ id: z$1.string(),
731
+ seq: z$1.number(),
732
+ content: SessionMessageContentSchema
733
+ }),
734
+ sid: z$1.string(),
735
+ // Session ID
736
+ t: z$1.literal("new-message")
737
+ });
738
+ const UpdateSessionBodySchema = z$1.object({
739
+ t: z$1.literal("update-session"),
740
+ sid: z$1.string(),
741
+ metadata: z$1.object({
742
+ version: z$1.number(),
743
+ value: z$1.string()
744
+ }).nullish(),
745
+ agentState: z$1.object({
746
+ version: z$1.number(),
747
+ value: z$1.string()
748
+ }).nullish()
749
+ });
750
+ const UpdateMachineBodySchema = z$1.object({
751
+ t: z$1.literal("update-machine"),
752
+ machineId: z$1.string(),
753
+ metadata: z$1.object({
754
+ version: z$1.number(),
755
+ value: z$1.string()
756
+ }).nullish(),
757
+ daemonState: z$1.object({
758
+ version: z$1.number(),
759
+ value: z$1.string()
760
+ }).nullish()
761
+ });
762
+ z$1.object({
763
+ id: z$1.string(),
764
+ seq: z$1.number(),
765
+ body: z$1.union([
766
+ UpdateBodySchema,
767
+ UpdateSessionBodySchema,
768
+ UpdateMachineBodySchema
769
+ ]),
770
+ createdAt: z$1.number()
771
+ });
772
+ z$1.object({
773
+ host: z$1.string(),
774
+ platform: z$1.string(),
775
+ flockbayCliVersion: z$1.string(),
776
+ homeDir: z$1.string(),
777
+ flockbayHomeDir: z$1.string(),
778
+ flockbayLibDir: z$1.string(),
779
+ // Dev-only: bypass Unreal project + SDK gating (so sessions can run in arbitrary folders).
780
+ flockbayDevBypassUeGates: z$1.boolean().optional()
781
+ });
782
+ z$1.object({
783
+ status: z$1.union([
784
+ z$1.enum(["running", "shutting-down"]),
785
+ z$1.string()
786
+ // Forward compatibility
787
+ ]),
788
+ pid: z$1.number().optional(),
789
+ httpPort: z$1.number().optional(),
790
+ startedAt: z$1.number().optional(),
791
+ shutdownRequestedAt: z$1.number().optional(),
792
+ shutdownSource: z$1.union([
793
+ z$1.enum(["mobile-app", "cli", "os-signal", "unknown"]),
794
+ z$1.string()
795
+ // Forward compatibility
796
+ ]).optional()
797
+ });
798
+ z$1.object({
799
+ content: SessionMessageContentSchema,
800
+ createdAt: z$1.number(),
801
+ id: z$1.string(),
802
+ seq: z$1.number(),
803
+ updatedAt: z$1.number()
804
+ });
805
+ const MessageMetaSchema = z$1.object({
806
+ sentFrom: z$1.string().optional(),
807
+ // Source identifier
808
+ permissionMode: z$1.string().optional(),
809
+ // Permission mode for this message
810
+ model: z$1.string().nullable().optional(),
811
+ // Model name for this message (null = reset)
812
+ customSystemPrompt: z$1.string().nullable().optional(),
813
+ // Custom system prompt for this message (null = reset)
814
+ appendSystemPrompt: z$1.string().nullable().optional(),
815
+ // Append to system prompt for this message (null = reset)
816
+ allowedTools: z$1.array(z$1.string()).nullable().optional(),
817
+ // Allowed tools for this message (null = reset)
818
+ disallowedTools: z$1.array(z$1.string()).nullable().optional(),
819
+ // Disallowed tools for this message (null = reset)
820
+ // Optional text to show in UI instead of the raw prompt (mobile/web feature).
821
+ displayText: z$1.string().optional(),
822
+ // Optional multimodal attachments (base64-embedded; used for vision-capable agents).
823
+ attachments: z$1.object({
824
+ images: z$1.array(
825
+ z$1.object({
826
+ mimeType: z$1.string(),
827
+ base64: z$1.string(),
828
+ name: z$1.string().optional(),
829
+ width: z$1.number().optional(),
830
+ height: z$1.number().optional()
831
+ })
832
+ ).optional()
833
+ }).optional()
834
+ });
835
+ z$1.object({
836
+ session: z$1.object({
837
+ id: z$1.string(),
838
+ tag: z$1.string(),
839
+ seq: z$1.number(),
840
+ createdAt: z$1.number(),
841
+ updatedAt: z$1.number(),
842
+ metadata: z$1.string(),
843
+ metadataVersion: z$1.number(),
844
+ agentState: z$1.string().nullable(),
845
+ agentStateVersion: z$1.number()
846
+ })
847
+ });
848
+ const UserMessageSchema = z$1.object({
849
+ role: z$1.literal("user"),
850
+ content: z$1.object({
851
+ type: z$1.literal("text"),
852
+ text: z$1.string()
853
+ }),
854
+ localKey: z$1.string().optional(),
855
+ // Mobile messages include this
856
+ meta: MessageMetaSchema.optional()
857
+ });
858
+ const AgentMessageSchema = z$1.object({
859
+ role: z$1.literal("agent"),
860
+ content: z$1.object({
861
+ type: z$1.literal("output"),
862
+ data: z$1.any()
863
+ }),
864
+ meta: MessageMetaSchema.optional()
865
+ });
866
+ z$1.union([UserMessageSchema, AgentMessageSchema]);
867
+
868
+ async function delay(ms) {
869
+ return new Promise((resolve) => setTimeout(resolve, ms));
870
+ }
871
+ function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
872
+ let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
873
+ return Math.round(Math.random() * maxDelayRet);
874
+ }
875
+ function createBackoff(opts) {
876
+ return async (callback) => {
877
+ let currentFailureCount = 0;
878
+ const minDelay = 250;
879
+ const maxDelay = 1e3;
880
+ const maxFailureCount = 50;
881
+ while (true) {
882
+ try {
883
+ return await callback();
884
+ } catch (e) {
885
+ if (currentFailureCount < maxFailureCount) {
886
+ currentFailureCount++;
887
+ }
888
+ let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
889
+ await delay(waitForRequest);
890
+ }
891
+ }
892
+ };
893
+ }
894
+ let backoff = createBackoff();
895
+
896
+ class AsyncLock {
897
+ permits = 1;
898
+ promiseResolverQueue = [];
899
+ async inLock(func) {
900
+ try {
901
+ await this.lock();
902
+ return await func();
903
+ } finally {
904
+ this.unlock();
905
+ }
906
+ }
907
+ async lock() {
908
+ if (this.permits > 0) {
909
+ this.permits = this.permits - 1;
910
+ return;
911
+ }
912
+ await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
913
+ }
914
+ unlock() {
915
+ this.permits += 1;
916
+ if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
917
+ throw new Error("this.permits should never be > 0 when there is someone waiting.");
918
+ } else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
919
+ this.permits -= 1;
920
+ const nextResolver = this.promiseResolverQueue.shift();
921
+ if (nextResolver) {
922
+ setTimeout(() => {
923
+ nextResolver(true);
924
+ }, 0);
925
+ }
926
+ }
927
+ }
928
+ }
929
+
930
+ class RpcHandlerManager {
931
+ handlers = /* @__PURE__ */ new Map();
932
+ scopePrefix;
933
+ encryptionKey;
934
+ logger;
935
+ socket = null;
936
+ constructor(config) {
937
+ this.scopePrefix = config.scopePrefix;
938
+ this.encryptionKey = config.encryptionKey;
939
+ this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
940
+ }
941
+ /**
942
+ * Register an RPC handler for a specific method
943
+ * @param method - The method name (without prefix)
944
+ * @param handler - The handler function
945
+ */
946
+ registerHandler(method, handler) {
947
+ const prefixedMethod = this.getPrefixedMethod(method);
948
+ this.handlers.set(prefixedMethod, handler);
949
+ if (this.socket) {
950
+ this.socket.emit("rpc-register", { method: prefixedMethod });
951
+ }
952
+ }
953
+ /**
954
+ * Handle an incoming RPC request
955
+ * @param request - The RPC request data
956
+ * @param callback - The response callback
957
+ */
958
+ async handleRequest(request) {
959
+ try {
960
+ const handler = this.handlers.get(request.method);
961
+ if (!handler) {
962
+ this.logger("[RPC] [ERROR] Method not found", { method: request.method });
963
+ const errorResponse = { error: "Method not found" };
964
+ const encryptedError = encodeBase64(encrypt(this.encryptionKey, errorResponse));
965
+ return encryptedError;
966
+ }
967
+ const decryptedParams = decrypt(this.encryptionKey, decodeBase64(request.params));
968
+ const result = await handler(decryptedParams);
969
+ const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, result));
970
+ return encryptedResponse;
971
+ } catch (error) {
972
+ this.logger("[RPC] [ERROR] Error handling request", { error });
973
+ const errorResponse = {
974
+ error: error instanceof Error ? error.message : "Unknown error"
975
+ };
976
+ return encodeBase64(encrypt(this.encryptionKey, errorResponse));
977
+ }
978
+ }
979
+ onSocketConnect(socket) {
980
+ this.socket = socket;
981
+ for (const [prefixedMethod] of this.handlers) {
982
+ socket.emit("rpc-register", { method: prefixedMethod });
983
+ }
984
+ }
985
+ onSocketDisconnect() {
986
+ this.socket = null;
987
+ }
988
+ /**
989
+ * Get the number of registered handlers
990
+ */
991
+ getHandlerCount() {
992
+ return this.handlers.size;
993
+ }
994
+ /**
995
+ * Check if a handler is registered
996
+ * @param method - The method name (without prefix)
997
+ */
998
+ hasHandler(method) {
999
+ const prefixedMethod = this.getPrefixedMethod(method);
1000
+ return this.handlers.has(prefixedMethod);
1001
+ }
1002
+ /**
1003
+ * Clear all handlers
1004
+ */
1005
+ clearHandlers() {
1006
+ this.handlers.clear();
1007
+ this.logger("Cleared all RPC handlers");
1008
+ }
1009
+ /**
1010
+ * Get the prefixed method name
1011
+ * @param method - The method name
1012
+ */
1013
+ getPrefixedMethod(method) {
1014
+ return `${this.scopePrefix}:${method}`;
1015
+ }
1016
+ }
1017
+
1018
+ const __dirname$1 = dirname(fileURLToPath(import.meta.url));
1019
+ function projectPath() {
1020
+ const path = resolve(__dirname$1, "..");
1021
+ return path;
1022
+ }
1023
+
1024
+ function run$1(args, options) {
1025
+ const RUNNER_PATH = resolve(join$1(projectPath(), "scripts", "ripgrep_launcher.cjs"));
1026
+ return new Promise((resolve2, reject) => {
1027
+ const child = spawn(process$1.execPath, [RUNNER_PATH, JSON.stringify(args)], {
1028
+ stdio: ["pipe", "pipe", "pipe"],
1029
+ cwd: options?.cwd
1030
+ });
1031
+ let stdout = "";
1032
+ let stderr = "";
1033
+ child.stdout.on("data", (data) => {
1034
+ stdout += data.toString();
1035
+ });
1036
+ child.stderr.on("data", (data) => {
1037
+ stderr += data.toString();
1038
+ });
1039
+ child.on("close", (code) => {
1040
+ resolve2({
1041
+ exitCode: code || 0,
1042
+ stdout,
1043
+ stderr
1044
+ });
1045
+ });
1046
+ child.on("error", (err) => {
1047
+ reject(err);
1048
+ });
1049
+ });
1050
+ }
1051
+
1052
+ function getBinaryPath() {
1053
+ const platformName = platform();
1054
+ const binaryName = platformName === "win32" ? "difft.exe" : "difft";
1055
+ return resolve(join$1(projectPath(), "tools", "unpacked", binaryName));
1056
+ }
1057
+ function run(args, options) {
1058
+ const binaryPath = getBinaryPath();
1059
+ return new Promise((resolve2, reject) => {
1060
+ const child = spawn(binaryPath, args, {
1061
+ stdio: ["pipe", "pipe", "pipe"],
1062
+ cwd: options?.cwd,
1063
+ env: {
1064
+ ...process.env,
1065
+ // Force color output when needed
1066
+ FORCE_COLOR: "1"
1067
+ }
1068
+ });
1069
+ let stdout = "";
1070
+ let stderr = "";
1071
+ child.stdout.on("data", (data) => {
1072
+ stdout += data.toString();
1073
+ });
1074
+ child.stderr.on("data", (data) => {
1075
+ stderr += data.toString();
1076
+ });
1077
+ child.on("close", (code) => {
1078
+ resolve2({
1079
+ exitCode: code || 0,
1080
+ stdout,
1081
+ stderr
1082
+ });
1083
+ });
1084
+ child.on("error", (err) => {
1085
+ reject(err);
1086
+ });
1087
+ });
1088
+ }
1089
+
1090
+ function unrealMcpBundleRoot() {
1091
+ return path.join(projectPath(), "tools", "unreal-mcp", "upstream");
1092
+ }
1093
+ function unrealMcpPythonDir() {
1094
+ return path.join(unrealMcpBundleRoot(), "Python");
1095
+ }
1096
+ function unrealMcpPluginSourceDir() {
1097
+ return path.join(unrealMcpBundleRoot(), "MCPGameProject", "Plugins", "UnrealMCP");
1098
+ }
1099
+
1100
+ function waitForJsonFromSocket(socket, timeoutMs) {
1101
+ return new Promise((resolve, reject) => {
1102
+ let buffer = "";
1103
+ let settled = false;
1104
+ const cleanup = () => {
1105
+ socket.off("data", onData);
1106
+ socket.off("error", onError);
1107
+ socket.off("close", onClose);
1108
+ };
1109
+ const finishOk = (obj) => {
1110
+ if (settled) return;
1111
+ settled = true;
1112
+ cleanup();
1113
+ resolve(obj);
1114
+ };
1115
+ const finishErr = (err) => {
1116
+ if (settled) return;
1117
+ settled = true;
1118
+ cleanup();
1119
+ reject(err);
1120
+ };
1121
+ const onError = (err) => finishErr(err);
1122
+ const onClose = () => {
1123
+ if (!settled) finishErr(new Error("Connection closed before receiving a complete JSON response."));
1124
+ };
1125
+ const onData = (chunk) => {
1126
+ buffer += chunk.toString("utf8");
1127
+ try {
1128
+ const parsed = JSON.parse(buffer);
1129
+ finishOk(parsed);
1130
+ } catch {
1131
+ }
1132
+ };
1133
+ socket.on("data", onData);
1134
+ socket.on("error", onError);
1135
+ socket.on("close", onClose);
1136
+ socket.setTimeout(timeoutMs, () => finishErr(new Error(`Timed out waiting for Unreal response after ${timeoutMs}ms.`)));
1137
+ });
1138
+ }
1139
+ async function sendUnrealMcpTcpCommand(options) {
1140
+ const host = (options.host || "127.0.0.1").trim() || "127.0.0.1";
1141
+ const port = Number.isFinite(options.port) ? Number(options.port) : 55557;
1142
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(250, Number(options.timeoutMs)) : 5e3;
1143
+ const type = String(options.type || "").trim();
1144
+ if (!type) throw new Error("Missing Unreal MCP command type.");
1145
+ const payload = JSON.stringify({ type, params: options.params || {} });
1146
+ const socket = new net.Socket();
1147
+ socket.setNoDelay(true);
1148
+ await new Promise((resolve, reject) => {
1149
+ const onError = (err) => {
1150
+ socket.off("connect", onConnect);
1151
+ reject(err);
1152
+ };
1153
+ const onConnect = () => {
1154
+ socket.off("error", onError);
1155
+ resolve();
1156
+ };
1157
+ socket.once("error", onError);
1158
+ socket.once("connect", onConnect);
1159
+ socket.connect(port, host);
1160
+ });
1161
+ socket.write(payload, "utf8");
1162
+ const response = await waitForJsonFromSocket(socket, timeoutMs);
1163
+ if (response && typeof response === "object") {
1164
+ const status = response.status;
1165
+ if (status === "error") {
1166
+ const errorMessage = typeof response.error === "string" ? response.error : `UnrealMCP error (unrecognized response): ${JSON.stringify(response)}`;
1167
+ const hints = [];
1168
+ const isUnknownCommand = errorMessage.includes("Unknown command:") || errorMessage.includes("Unknown editor command:") || errorMessage.includes("Unknown blueprint command:") || errorMessage.includes("Unknown blueprint node command:") || errorMessage.includes("Unknown project command:") || errorMessage.includes("Unknown UMG command:");
1169
+ if (isUnknownCommand) {
1170
+ hints.push(
1171
+ "Tip: your UnrealMCP plugin build likely does not expose that command. Run `get_plugin_info` to see the exact supported command list for the running editor, then retry with a supported command."
1172
+ );
1173
+ hints.push(
1174
+ "Fix (if commands are missing): your UnrealMCP engine plugin is likely out of date, shadowed by another UnrealMCP install, or running stale binaries.\n- Ensure there is only ONE UnrealMCP plugin under your engine folder (common conflict: Engine/Plugins/Marketplace/UnrealMCP).\n- Reinstall the engine plugin (delete Engine/Plugins/UnrealMCP so Unreal rebuilds), then restart the editor."
1175
+ );
1176
+ }
1177
+ const missingParamMatch = errorMessage.match(/Missing '([^']+)' parameter/);
1178
+ if (missingParamMatch) {
1179
+ const paramName = missingParamMatch[1];
1180
+ hints.push(`Tip: include the required parameter \`${paramName}\` in the request params (UnrealMCP does not infer defaults).`);
1181
+ }
1182
+ const hint = hints.length > 0 ? `
1183
+
1184
+ ${hints.join("\n\n")}` : "";
1185
+ throw new Error(`${errorMessage}${hint}`);
1186
+ }
1187
+ }
1188
+ try {
1189
+ socket.end();
1190
+ } catch {
1191
+ }
1192
+ return response;
1193
+ }
1194
+
1195
+ function validatePath(targetPath, workingDirectory) {
1196
+ const resolvedTarget = resolve(workingDirectory, targetPath);
1197
+ const resolvedWorkingDir = resolve(workingDirectory);
1198
+ if (!resolvedTarget.startsWith(resolvedWorkingDir + "/") && resolvedTarget !== resolvedWorkingDir) {
1199
+ return {
1200
+ valid: false,
1201
+ error: `Access denied: Path '${targetPath}' is outside the working directory`
1202
+ };
1203
+ }
1204
+ return { valid: true, resolvedPath: resolvedTarget };
1205
+ }
1206
+
1207
+ function logNonSilent(context, error, ignoredCodes = []) {
1208
+ const e = error;
1209
+ const code = typeof e?.code === "string" ? e.code : null;
1210
+ if (code && ignoredCodes.includes(code)) return;
1211
+ console.error(context, error);
1212
+ logger.debug(context, error);
1213
+ }
1214
+ async function maybeAutoShipWorkItemAfterCommit(params) {
1215
+ if (String(process.env.FLOCKBAY_COORDINATION_AUTO_SHIP_ON_COMMIT || "").trim() !== "1") {
1216
+ return { ok: true };
1217
+ }
1218
+ const coordination = params.coordination;
1219
+ if (!coordination) return { ok: true };
1220
+ const projectId = String(coordination.workspaceProjectId || "").trim();
1221
+ const workItemId = String(coordination.workItemId || "").trim();
1222
+ if (!projectId || !workItemId) return { ok: true };
1223
+ const head = await runProcess({
1224
+ command: "git",
1225
+ args: ["rev-parse", "HEAD"],
1226
+ cwd: params.cwd,
1227
+ timeoutMs: 15e3
1228
+ });
1229
+ if (!head.success) {
1230
+ return { ok: false, error: head.stderr || head.error || "Failed to read commit hash" };
1231
+ }
1232
+ const commitHash = String(head.stdout || "").trim();
1233
+ if (!commitHash) return { ok: false, error: "Missing commit hash" };
1234
+ {
1235
+ const url = `${coordination.serverUrl.replace(/\/+$/, "")}/v1/coordination/work-items/update`;
1236
+ const res = await fetch(url, {
1237
+ method: "POST",
1238
+ headers: {
1239
+ "Authorization": `Bearer ${coordination.token}`,
1240
+ "Content-Type": "application/json"
1241
+ },
1242
+ body: JSON.stringify({
1243
+ projectId,
1244
+ workItemId,
1245
+ commitHash
1246
+ })
1247
+ });
1248
+ const data = await res.json().catch(() => null);
1249
+ if (!res.ok || !data?.success) {
1250
+ const msg = typeof data?.error === "string" ? data.error : `work item update failed (${res.status})`;
1251
+ return { ok: false, error: msg };
1252
+ }
1253
+ }
1254
+ return { ok: true };
1255
+ }
1256
+ class TailBuffer {
1257
+ constructor(maxBytes) {
1258
+ this.maxBytes = maxBytes;
1259
+ }
1260
+ chunks = [];
1261
+ totalBytes = 0;
1262
+ truncated = false;
1263
+ push(chunk) {
1264
+ if (!chunk?.length) return;
1265
+ this.chunks.push(chunk);
1266
+ this.totalBytes += chunk.length;
1267
+ while (this.totalBytes > this.maxBytes && this.chunks.length > 0) {
1268
+ const extra = this.totalBytes - this.maxBytes;
1269
+ const first = this.chunks[0];
1270
+ this.truncated = true;
1271
+ if (first.length <= extra) {
1272
+ this.chunks.shift();
1273
+ this.totalBytes -= first.length;
1274
+ continue;
1275
+ }
1276
+ this.chunks[0] = first.subarray(extra);
1277
+ this.totalBytes -= extra;
1278
+ break;
1279
+ }
1280
+ }
1281
+ toString() {
1282
+ if (this.chunks.length === 0) return "";
1283
+ return Buffer.concat(this.chunks, this.totalBytes).toString("utf8");
1284
+ }
1285
+ isTruncated() {
1286
+ return this.truncated;
1287
+ }
1288
+ }
1289
+ function asText(value) {
1290
+ return typeof value === "string" ? value : String(value ?? "");
1291
+ }
1292
+ function ensureConfirmed(action, confirm) {
1293
+ const writeActions = [
1294
+ "clone",
1295
+ "init_repo",
1296
+ "stage_paths",
1297
+ "unstage_paths",
1298
+ "unstage_all",
1299
+ "restore_file",
1300
+ "discard_unstaged",
1301
+ "discard_all",
1302
+ "commit_all",
1303
+ "commit_staged",
1304
+ "revert_commit",
1305
+ "reapply_commit",
1306
+ "push_head",
1307
+ "pr_create",
1308
+ "github_snapshot_push",
1309
+ "github_snapshot_pr_create"
1310
+ ];
1311
+ if (!writeActions.includes(action)) return;
1312
+ if (confirm === true) return;
1313
+ throw new Error(`confirmation_required:${action}`);
1314
+ }
1315
+ function sanitizeDestFolder(destFolder) {
1316
+ const dest = destFolder.trim().replace(/^\/+|\/+$/g, "");
1317
+ if (!dest) throw new Error("missing_dest_folder");
1318
+ if (dest.startsWith(".") || dest.includes("..") || dest.includes("\\")) throw new Error("invalid_dest_folder");
1319
+ if (dest.includes("\0")) throw new Error("invalid_dest_folder");
1320
+ return dest;
1321
+ }
1322
+ function sanitizeRelativeGitPaths(value) {
1323
+ const paths = Array.isArray(value) ? value : [];
1324
+ if (paths.length === 0) throw new Error("missing_paths");
1325
+ if (paths.length > 500) throw new Error("too_many_paths");
1326
+ return paths.map((p) => {
1327
+ const s = asText(p).trim().replace(/\\/g, "/");
1328
+ if (!s) throw new Error("invalid_path");
1329
+ if (s.includes("\0")) throw new Error("invalid_path");
1330
+ if (s.startsWith("/")) throw new Error("invalid_path");
1331
+ if (s === "." || s.startsWith("./")) throw new Error("invalid_path");
1332
+ if (s === ".." || s.startsWith("../") || s.includes("/../")) throw new Error("invalid_path");
1333
+ return s;
1334
+ });
1335
+ }
1336
+ function sanitizeGitCommitHash(input) {
1337
+ const s = asText(input).trim();
1338
+ if (!s) throw new Error("missing_commit_hash");
1339
+ if (!/^[0-9a-f]{7,40}$/i.test(s)) throw new Error("invalid_commit_hash");
1340
+ return s;
1341
+ }
1342
+ function normalizePathSlashes(input) {
1343
+ return String(input || "").replace(/\\/g, "/");
1344
+ }
1345
+ function inferEngineRootFromPluginBaseDir(baseDir) {
1346
+ const raw = normalizePathSlashes(baseDir).trim();
1347
+ if (!raw) return null;
1348
+ const lower = raw.toLowerCase();
1349
+ const idx = lower.indexOf("/engine/plugins/");
1350
+ if (idx < 0) return null;
1351
+ const root = raw.slice(0, idx).replace(/\/+$/, "");
1352
+ return root ? root : null;
1353
+ }
1354
+ function isMarketplacePluginDir(baseDir) {
1355
+ const lower = normalizePathSlashes(baseDir).toLowerCase();
1356
+ return lower.includes("/engine/plugins/marketplace/unrealmcp") || lower.includes("/plugins/marketplace/unrealmcp");
1357
+ }
1358
+ function isUnrealMcpUnknownCommandError(err) {
1359
+ const msg = err instanceof Error ? err.message : String(err ?? "");
1360
+ return /\bUnknown (command|editor command|blueprint command|project command|UMG command):\b/i.test(msg);
1361
+ }
1362
+ async function readExpectedUnrealMcpUplugin() {
1363
+ const upluginPath = join$1(unrealMcpPluginSourceDir(), "UnrealMCP.uplugin");
1364
+ const raw = await readFile$1(upluginPath, "utf8");
1365
+ const parsed = JSON.parse(raw);
1366
+ const versionName = typeof parsed?.VersionName === "string" ? parsed.VersionName : null;
1367
+ const version = typeof parsed?.Version === "number" ? parsed.Version : null;
1368
+ return { upluginPath, versionName, version };
1369
+ }
1370
+ async function runProcess({
1371
+ command,
1372
+ args,
1373
+ cwd,
1374
+ timeoutMs,
1375
+ env
1376
+ }) {
1377
+ const MAX_CAPTURE_BYTES = 512 * 1024;
1378
+ const stdoutTail = new TailBuffer(MAX_CAPTURE_BYTES);
1379
+ const stderrTail = new TailBuffer(MAX_CAPTURE_BYTES);
1380
+ try {
1381
+ const { exitCode, signal, timedOut } = await new Promise((resolvePromise, rejectPromise) => {
1382
+ const child = spawn(command, args, {
1383
+ cwd,
1384
+ shell: false,
1385
+ env: env ? { ...process.env, ...env } : process.env,
1386
+ stdio: ["ignore", "pipe", "pipe"]
1387
+ });
1388
+ child.stdout?.on("data", (chunk) => stdoutTail.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1389
+ child.stderr?.on("data", (chunk) => stderrTail.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1390
+ let didTimeout = false;
1391
+ const timeoutHandle = setTimeout(() => {
1392
+ didTimeout = true;
1393
+ try {
1394
+ child.kill("SIGTERM");
1395
+ } catch (error) {
1396
+ logNonSilent(`[registerCommonHandlers] Failed to SIGTERM child process (${command})`, error, ["ESRCH"]);
1397
+ }
1398
+ setTimeout(() => {
1399
+ try {
1400
+ child.kill("SIGKILL");
1401
+ } catch (error) {
1402
+ logNonSilent(`[registerCommonHandlers] Failed to SIGKILL child process (${command})`, error, ["ESRCH"]);
1403
+ }
1404
+ }, 5e3).unref();
1405
+ }, timeoutMs);
1406
+ timeoutHandle.unref();
1407
+ child.on("error", (err) => {
1408
+ clearTimeout(timeoutHandle);
1409
+ rejectPromise(err);
1410
+ });
1411
+ child.on("close", (code, sig) => {
1412
+ clearTimeout(timeoutHandle);
1413
+ resolvePromise({
1414
+ exitCode: typeof code === "number" ? code : null,
1415
+ signal: sig ?? null,
1416
+ timedOut: didTimeout
1417
+ });
1418
+ });
1419
+ });
1420
+ const stdout = stdoutTail.toString();
1421
+ const stderr = stderrTail.toString();
1422
+ if (timedOut) {
1423
+ return {
1424
+ success: false,
1425
+ stdout,
1426
+ stderr,
1427
+ exitCode: typeof exitCode === "number" ? exitCode : -1,
1428
+ error: "Command timed out",
1429
+ stdoutTruncated: stdoutTail.isTruncated(),
1430
+ stderrTruncated: stderrTail.isTruncated()
1431
+ };
1432
+ }
1433
+ const ok = exitCode === 0;
1434
+ return {
1435
+ success: ok,
1436
+ stdout,
1437
+ stderr,
1438
+ exitCode: typeof exitCode === "number" ? exitCode : ok ? 0 : 1,
1439
+ ...ok ? {} : { error: signal ? `Command failed (signal ${signal})` : "Command failed" },
1440
+ stdoutTruncated: stdoutTail.isTruncated(),
1441
+ stderrTruncated: stderrTail.isTruncated()
1442
+ };
1443
+ } catch (error) {
1444
+ const e = error;
1445
+ const msg = e?.code === "ENOENT" ? `Command not found: ${command}` : e?.message || "Command failed";
1446
+ return {
1447
+ success: false,
1448
+ stdout: "",
1449
+ stderr: msg,
1450
+ exitCode: 1,
1451
+ error: msg
1452
+ };
1453
+ }
1454
+ }
1455
+ async function apiJsonGet(params) {
1456
+ const url = `${params.serverUrl.replace(/\/+$/, "")}${params.path}`;
1457
+ const res = await fetch(url, {
1458
+ method: "GET",
1459
+ headers: {
1460
+ Authorization: `Bearer ${params.token}`,
1461
+ Accept: "application/json"
1462
+ }
1463
+ });
1464
+ const data = await res.json().catch(() => null);
1465
+ if (!res.ok) {
1466
+ const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
1467
+ throw new Error(msg);
1468
+ }
1469
+ return data;
1470
+ }
1471
+ async function apiJsonPost(params) {
1472
+ const url = `${params.serverUrl.replace(/\/+$/, "")}${params.path}`;
1473
+ const res = await fetch(url, {
1474
+ method: "POST",
1475
+ headers: {
1476
+ Authorization: `Bearer ${params.token}`,
1477
+ "Content-Type": "application/json",
1478
+ Accept: "application/json"
1479
+ },
1480
+ body: JSON.stringify(params.body ?? {})
1481
+ });
1482
+ const data = await res.json().catch(() => null);
1483
+ if (!res.ok) {
1484
+ const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
1485
+ throw new Error(msg);
1486
+ }
1487
+ return data;
1488
+ }
1489
+ function isGithubRemoteUrl(url) {
1490
+ const raw = String(url || "").trim();
1491
+ if (!raw) return false;
1492
+ if (/^git@github\.com:/i.test(raw)) return true;
1493
+ if (/^ssh:\/\/git@github\.com\//i.test(raw)) return true;
1494
+ try {
1495
+ const u = new URL(raw);
1496
+ return u.hostname.toLowerCase() === "github.com";
1497
+ } catch {
1498
+ return false;
1499
+ }
1500
+ }
1501
+ function parseGithubRepoFullName(repoUrl) {
1502
+ const raw = String(repoUrl || "").trim();
1503
+ if (!raw) return null;
1504
+ if (/^git@github\.com:/i.test(raw)) {
1505
+ const rhs = raw.replace(/^git@github\.com:/i, "");
1506
+ const cleaned = rhs.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
1507
+ const parts = cleaned.split("/").filter(Boolean);
1508
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
1509
+ }
1510
+ if (/^ssh:\/\/git@github\.com\//i.test(raw)) {
1511
+ const rhs = raw.replace(/^ssh:\/\/git@github\.com\//i, "");
1512
+ const cleaned = rhs.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
1513
+ const parts = cleaned.split("/").filter(Boolean);
1514
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
1515
+ }
1516
+ try {
1517
+ const u = new URL(raw);
1518
+ if (u.hostname.toLowerCase() !== "github.com") return null;
1519
+ const cleaned = u.pathname.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
1520
+ const parts = cleaned.split("/").filter(Boolean);
1521
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
1522
+ } catch {
1523
+ return null;
1524
+ }
1525
+ }
1526
+ async function createGithubAskpassScript() {
1527
+ const isWin = os.platform() === "win32";
1528
+ const suffix = isWin ? ".cmd" : ".sh";
1529
+ const askpassPath = join$1(os.tmpdir(), `flockbay-git-askpass-${randomUUID()}${suffix}`);
1530
+ const content = isWin ? [
1531
+ "@echo off",
1532
+ "set prompt=%*",
1533
+ 'echo %prompt% | findstr /i "Username" >nul && (echo x-access-token & exit /b 0)',
1534
+ 'echo %prompt% | findstr /i "Password" >nul && (echo %FLOCKBAY_GITHUB_TOKEN% & exit /b 0)',
1535
+ "echo."
1536
+ ].join("\r\n") : [
1537
+ "#!/bin/sh",
1538
+ 'case "$1" in',
1539
+ ' *Username*) echo "x-access-token" ;;',
1540
+ ' *Password*) echo "$FLOCKBAY_GITHUB_TOKEN" ;;',
1541
+ ' *) echo "" ;;',
1542
+ "esac"
1543
+ ].join("\n");
1544
+ await writeFile$1(askpassPath, content, { encoding: "utf8" });
1545
+ if (!isWin) await chmod(askpassPath, 448);
1546
+ return {
1547
+ askpassPath,
1548
+ cleanup: async () => {
1549
+ try {
1550
+ await rm(askpassPath, { force: true });
1551
+ } catch (error) {
1552
+ logNonSilent(`[registerCommonHandlers] Failed to cleanup git-askpass (${askpassPath})`, error, ["ENOENT"]);
1553
+ }
1554
+ }
1555
+ };
1556
+ }
1557
+ async function resolveBaseBranch(params) {
1558
+ const preferredBaseBranch = String(params.preferredBaseBranch || "").trim() || null;
1559
+ const cwd = params.cwd;
1560
+ const refExists = async (ref) => {
1561
+ const res = await runProcess({ command: "git", args: ["show-ref", "--verify", "--quiet", `refs/heads/${ref}`], cwd, timeoutMs: 1e4 });
1562
+ return res.success;
1563
+ };
1564
+ const current = await runProcess({ command: "git", args: ["branch", "--show-current"], cwd, timeoutMs: 1e4 });
1565
+ const currentBranch = String(current.stdout || "").trim();
1566
+ const candidates = Array.from(new Set([preferredBaseBranch, currentBranch, "main", "master"].map((b) => String(b || "").trim()).filter(Boolean)));
1567
+ for (const candidate of candidates) {
1568
+ if (await refExists(candidate)) return candidate;
1569
+ }
1570
+ const remoteHead = await runProcess({
1571
+ command: "git",
1572
+ args: ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
1573
+ cwd,
1574
+ timeoutMs: 1e4,
1575
+ env: { GIT_TERMINAL_PROMPT: "0" }
1576
+ });
1577
+ const remoteRef = String(remoteHead.stdout || "").trim();
1578
+ const remoteBranch = remoteRef.startsWith("origin/") ? remoteRef.slice("origin/".length) : "";
1579
+ if (remoteBranch && !await refExists(remoteBranch)) {
1580
+ await runProcess({
1581
+ command: "git",
1582
+ args: ["branch", "--track", remoteBranch, `origin/${remoteBranch}`],
1583
+ cwd,
1584
+ timeoutMs: 2e4,
1585
+ env: { GIT_TERMINAL_PROMPT: "0" }
1586
+ });
1587
+ }
1588
+ if (remoteBranch && await refExists(remoteBranch)) return remoteBranch;
1589
+ const heads = await runProcess({ command: "git", args: ["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd, timeoutMs: 1e4 });
1590
+ const available = String(heads.stdout || "").split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 20).join(", ");
1591
+ throw new Error(
1592
+ `Invalid base branch '${preferredBaseBranch || "main"}' (no local branch found). Create the branch locally (or rename your default branch). Available local branches: ${available || "(none)"}`
1593
+ );
1594
+ }
1595
+ async function appendCoordinationActivity(params) {
1596
+ const { coordination, projectId, event } = params;
1597
+ await apiJsonPost({
1598
+ serverUrl: coordination.serverUrl,
1599
+ token: coordination.token,
1600
+ path: "/v1/coordination/activity/append",
1601
+ body: { projectId, events: [event] }
1602
+ });
1603
+ }
1604
+ async function runProcessToFile({
1605
+ command,
1606
+ args,
1607
+ cwd,
1608
+ timeoutMs,
1609
+ outputPath
1610
+ }) {
1611
+ const MAX_CAPTURE_BYTES = 512 * 1024;
1612
+ const stderrTail = new TailBuffer(MAX_CAPTURE_BYTES);
1613
+ try {
1614
+ const { exitCode, signal, timedOut } = await new Promise((resolvePromise, rejectPromise) => {
1615
+ const out = createWriteStream(outputPath, { flags: "w" });
1616
+ const child = spawn(command, args, {
1617
+ cwd,
1618
+ shell: false,
1619
+ stdio: ["ignore", "pipe", "pipe"]
1620
+ });
1621
+ child.stdout?.pipe(out);
1622
+ child.stderr?.on("data", (chunk) => stderrTail.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1623
+ let didTimeout = false;
1624
+ const timeoutHandle = setTimeout(() => {
1625
+ didTimeout = true;
1626
+ try {
1627
+ child.kill("SIGTERM");
1628
+ } catch (error) {
1629
+ logNonSilent(`[registerCommonHandlers] Failed to SIGTERM child process (${command})`, error, ["ESRCH"]);
1630
+ }
1631
+ setTimeout(() => {
1632
+ try {
1633
+ child.kill("SIGKILL");
1634
+ } catch (error) {
1635
+ logNonSilent(`[registerCommonHandlers] Failed to SIGKILL child process (${command})`, error, ["ESRCH"]);
1636
+ }
1637
+ }, 5e3).unref();
1638
+ }, timeoutMs);
1639
+ timeoutHandle.unref();
1640
+ const finish = (code, sig) => {
1641
+ clearTimeout(timeoutHandle);
1642
+ try {
1643
+ out.end();
1644
+ } catch (error) {
1645
+ logNonSilent(`[registerCommonHandlers] Failed to close output stream (${outputPath})`, error);
1646
+ }
1647
+ resolvePromise({ exitCode: typeof code === "number" ? code : null, signal: sig, timedOut: didTimeout });
1648
+ };
1649
+ child.on("error", (err) => {
1650
+ clearTimeout(timeoutHandle);
1651
+ try {
1652
+ out.end();
1653
+ } catch (error) {
1654
+ logNonSilent(`[registerCommonHandlers] Failed to close output stream after spawn error (${outputPath})`, error);
1655
+ }
1656
+ rejectPromise(err);
1657
+ });
1658
+ child.on("close", finish);
1659
+ });
1660
+ const stderr = stderrTail.toString();
1661
+ if (timedOut) {
1662
+ return {
1663
+ success: false,
1664
+ stdout: "",
1665
+ stderr,
1666
+ exitCode: typeof exitCode === "number" ? exitCode : -1,
1667
+ error: "Command timed out",
1668
+ stderrTruncated: stderrTail.isTruncated()
1669
+ };
1670
+ }
1671
+ const ok = exitCode === 0;
1672
+ return {
1673
+ success: ok,
1674
+ stdout: "",
1675
+ stderr,
1676
+ exitCode: typeof exitCode === "number" ? exitCode : ok ? 0 : 1,
1677
+ ...ok ? {} : { error: signal ? `Command failed (signal ${signal})` : "Command failed" },
1678
+ stderrTruncated: stderrTail.isTruncated()
1679
+ };
1680
+ } catch (error) {
1681
+ const e = error;
1682
+ const msg = e?.code === "ENOENT" ? `Command not found: ${command}` : e?.message || "Command failed";
1683
+ return {
1684
+ success: false,
1685
+ stdout: "",
1686
+ stderr: msg,
1687
+ exitCode: 1,
1688
+ error: msg
1689
+ };
1690
+ }
1691
+ }
1692
+ async function ensureGitRepo(cwd) {
1693
+ const res = await runProcess({ command: "git", args: ["rev-parse", "--is-inside-work-tree"], cwd, timeoutMs: 1e4 });
1694
+ if (!res.success) throw new Error(res.stderr || res.error || "not_a_git_repo");
1695
+ const ok = (res.stdout || "").trim().toLowerCase() === "true";
1696
+ if (!ok) throw new Error("not_a_git_repo");
1697
+ }
1698
+ function normalizeCoordinationFilePath(input) {
1699
+ const raw = String(input || "").trim();
1700
+ if (!raw) return null;
1701
+ const candidate = raw.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/^\/+/, "");
1702
+ if (!candidate) return null;
1703
+ const parts = candidate.split("/").filter(Boolean);
1704
+ const out = [];
1705
+ for (const part of parts) {
1706
+ if (part === "." || part === "") continue;
1707
+ if (part === "..") return null;
1708
+ out.push(part);
1709
+ }
1710
+ return out.join("/") || null;
1711
+ }
1712
+ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordination, options) {
1713
+ const bashEnabled = options?.bashEnabled !== false;
1714
+ const spawnEnv = (() => {
1715
+ const basePath = process.env.PATH ?? "";
1716
+ const existingEntries = basePath.split(":").filter(Boolean);
1717
+ const preferredEntries = [];
1718
+ if (process.platform === "darwin") {
1719
+ preferredEntries.push("/opt/homebrew/bin", "/opt/homebrew/sbin", "/usr/local/bin", "/usr/local/sbin");
1720
+ } else if (process.platform !== "win32") {
1721
+ preferredEntries.push("/usr/local/bin", "/usr/local/sbin");
1722
+ }
1723
+ const preferredSet = new Set(preferredEntries);
1724
+ const merged = [...preferredEntries, ...existingEntries.filter((p) => !preferredSet.has(p))];
1725
+ return { ...process.env, PATH: merged.join(":") };
1726
+ })();
1727
+ rpcHandlerManager.registerHandler("bash", async (data) => {
1728
+ if (!bashEnabled) {
1729
+ return {
1730
+ success: false,
1731
+ stdout: "",
1732
+ stderr: "bash_disabled",
1733
+ exitCode: 1,
1734
+ error: "bash_disabled"
1735
+ };
1736
+ }
1737
+ logger.debug("Shell command request:", data.command);
1738
+ const cwd = (() => {
1739
+ const requested = data.cwd || workingDirectory;
1740
+ const validation = validatePath(requested, workingDirectory);
1741
+ if (!validation.valid) return null;
1742
+ return validation.resolvedPath || null;
1743
+ })();
1744
+ if (!cwd) {
1745
+ const validation = validatePath(data.cwd || workingDirectory, workingDirectory);
1746
+ return { success: false, error: validation.error || "Invalid cwd" };
1747
+ }
1748
+ try {
1749
+ const timeoutMs = data.timeout || 3e4;
1750
+ const MAX_CAPTURE_BYTES = 512 * 1024;
1751
+ const stdoutTail = new TailBuffer(MAX_CAPTURE_BYTES);
1752
+ const stderrTail = new TailBuffer(MAX_CAPTURE_BYTES);
1753
+ const { exitCode, signal, timedOut } = await new Promise((resolvePromise, rejectPromise) => {
1754
+ const child = spawn(data.command, {
1755
+ cwd,
1756
+ shell: true,
1757
+ env: spawnEnv,
1758
+ stdio: ["ignore", "pipe", "pipe"]
1759
+ });
1760
+ child.stdout?.on("data", (chunk) => stdoutTail.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1761
+ child.stderr?.on("data", (chunk) => stderrTail.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1762
+ let didTimeout = false;
1763
+ const timeoutHandle = setTimeout(() => {
1764
+ didTimeout = true;
1765
+ try {
1766
+ child.kill("SIGTERM");
1767
+ } catch (error) {
1768
+ logNonSilent(`[registerCommonHandlers] Failed to SIGTERM child process (${data.command})`, error, ["ESRCH"]);
1769
+ }
1770
+ setTimeout(() => {
1771
+ try {
1772
+ child.kill("SIGKILL");
1773
+ } catch (error) {
1774
+ logNonSilent(`[registerCommonHandlers] Failed to SIGKILL child process (${data.command})`, error, ["ESRCH"]);
1775
+ }
1776
+ }, 5e3).unref();
1777
+ }, timeoutMs);
1778
+ timeoutHandle.unref();
1779
+ child.on("error", (err) => {
1780
+ clearTimeout(timeoutHandle);
1781
+ rejectPromise(err);
1782
+ });
1783
+ child.on("close", (code, sig) => {
1784
+ clearTimeout(timeoutHandle);
1785
+ resolvePromise({
1786
+ exitCode: typeof code === "number" ? code : null,
1787
+ signal: sig ?? null,
1788
+ timedOut: didTimeout
1789
+ });
1790
+ });
1791
+ });
1792
+ const stdout = stdoutTail.toString();
1793
+ const stderr = stderrTail.toString();
1794
+ if (timedOut) {
1795
+ return {
1796
+ success: false,
1797
+ stdout,
1798
+ stderr,
1799
+ exitCode: typeof exitCode === "number" ? exitCode : -1,
1800
+ error: "Command timed out",
1801
+ stdoutTruncated: stdoutTail.isTruncated(),
1802
+ stderrTruncated: stderrTail.isTruncated()
1803
+ };
1804
+ }
1805
+ const ok = exitCode === 0;
1806
+ return {
1807
+ success: ok,
1808
+ stdout,
1809
+ stderr,
1810
+ exitCode: typeof exitCode === "number" ? exitCode : ok ? 0 : 1,
1811
+ ...ok ? {} : { error: signal ? `Command failed (signal ${signal})` : "Command failed" },
1812
+ stdoutTruncated: stdoutTail.isTruncated(),
1813
+ stderrTruncated: stderrTail.isTruncated()
1814
+ };
1815
+ } catch (error) {
1816
+ const execError = error;
1817
+ return {
1818
+ success: false,
1819
+ stdout: execError.stdout ? execError.stdout.toString() : "",
1820
+ stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
1821
+ exitCode: typeof execError.code === "number" ? execError.code : 1,
1822
+ error: execError.message || "Command failed"
1823
+ };
1824
+ }
1825
+ });
1826
+ rpcHandlerManager.registerHandler(
1827
+ "unreal-mcp-bridge-status",
1828
+ async (params) => {
1829
+ const checkedAtMs = Date.now();
1830
+ const host = typeof params?.host === "string" && params.host.trim() ? params.host.trim() : "127.0.0.1";
1831
+ const port = Number.isFinite(params?.port) ? Number(params?.port) : 55557;
1832
+ const timeoutMs = Number.isFinite(params?.timeoutMs) ? Math.max(250, Number(params?.timeoutMs)) : 1250;
1833
+ const requiredCommands = [
1834
+ "ping",
1835
+ "get_plugin_info",
1836
+ "get_play_in_editor_status",
1837
+ "play_in_editor",
1838
+ "play_in_editor_windowed",
1839
+ "stop_play_in_editor",
1840
+ "take_screenshot",
1841
+ "spawn_actor",
1842
+ "set_actor_transform"
1843
+ ];
1844
+ const expected = await readExpectedUnrealMcpUplugin().catch((err) => {
1845
+ const message = err instanceof Error ? err.message : String(err);
1846
+ return { upluginPath: join$1(unrealMcpPluginSourceDir(), "UnrealMCP.uplugin"), versionName: null, version: null, error: message };
1847
+ });
1848
+ const baseResponse = {
1849
+ success: true,
1850
+ checkedAtMs,
1851
+ status: "error",
1852
+ reachable: false,
1853
+ expected: {
1854
+ upluginPath: expected.upluginPath,
1855
+ versionName: expected.versionName ?? null,
1856
+ version: expected.version ?? null,
1857
+ requiredCommands
1858
+ }
1859
+ };
1860
+ try {
1861
+ await sendUnrealMcpTcpCommand({ type: "ping", host, port, timeoutMs });
1862
+ } catch (err) {
1863
+ const message = err instanceof Error ? err.message : String(err);
1864
+ return {
1865
+ ...baseResponse,
1866
+ status: "not_running",
1867
+ reachable: false,
1868
+ error: message
1869
+ };
1870
+ }
1871
+ baseResponse.reachable = true;
1872
+ let pluginInfo = null;
1873
+ try {
1874
+ pluginInfo = await sendUnrealMcpTcpCommand({ type: "get_plugin_info", host, port, timeoutMs: Math.max(timeoutMs, 2e3) });
1875
+ } catch (err) {
1876
+ const message = err instanceof Error ? err.message : String(err);
1877
+ if (isUnrealMcpUnknownCommandError(err)) {
1878
+ return {
1879
+ ...baseResponse,
1880
+ status: "update_available",
1881
+ error: message,
1882
+ missingCommands: requiredCommands
1883
+ };
1884
+ }
1885
+ return {
1886
+ ...baseResponse,
1887
+ status: "error",
1888
+ error: message
1889
+ };
1890
+ }
1891
+ const running = pluginInfo?.result && typeof pluginInfo.result === "object" ? pluginInfo.result : pluginInfo;
1892
+ const baseDir = typeof running?.baseDir === "string" ? running.baseDir : null;
1893
+ const versionName = typeof running?.versionName === "string" ? running.versionName : null;
1894
+ const version = typeof running?.version === "number" ? running.version : null;
1895
+ const commands = Array.isArray(running?.commands) ? running.commands.filter((c) => typeof c === "string").map((c) => c.trim()).filter(Boolean) : [];
1896
+ const missingCommands = requiredCommands.filter((c) => !commands.includes(c));
1897
+ const inferredEngineRoot = baseDir ? inferEngineRootFromPluginBaseDir(baseDir) : null;
1898
+ let play = null;
1899
+ if (commands.includes("get_play_in_editor_status")) {
1900
+ try {
1901
+ const playInfo = await sendUnrealMcpTcpCommand({ type: "get_play_in_editor_status", host, port, timeoutMs: Math.max(timeoutMs, 2e3) });
1902
+ const playResult = playInfo?.result && typeof playInfo.result === "object" ? playInfo.result : playInfo;
1903
+ play = {
1904
+ isPlaying: typeof playResult?.isPlaying === "boolean" ? playResult.isPlaying : null,
1905
+ mode: typeof playResult?.mode === "string" ? playResult.mode : null
1906
+ };
1907
+ } catch {
1908
+ }
1909
+ }
1910
+ const conflict = baseDir ? isMarketplacePluginDir(baseDir) : false;
1911
+ const status = missingCommands.length > 0 ? conflict ? "conflicting_install" : "update_available" : "ok";
1912
+ return {
1913
+ ...baseResponse,
1914
+ status,
1915
+ inferredEngineRoot,
1916
+ running: {
1917
+ baseDir,
1918
+ versionName,
1919
+ version,
1920
+ commands,
1921
+ ...play ? { play } : {}
1922
+ },
1923
+ missingCommands
1924
+ };
1925
+ }
1926
+ );
1927
+ rpcHandlerManager.registerHandler("delete-project-folder", async (data) => {
1928
+ const inputPath = typeof data?.path === "string" ? data.path.trim() : "";
1929
+ if (!inputPath) return { success: false, deleted: false, path: "", error: "Missing path" };
1930
+ const requireUproject = data?.requireUproject !== false;
1931
+ const projectsRootInput = typeof data?.projectsRoot === "string" ? data.projectsRoot.trim() : "";
1932
+ if (!projectsRootInput) {
1933
+ return {
1934
+ success: false,
1935
+ deleted: false,
1936
+ path: inputPath,
1937
+ error: "projectsRoot is required (refusing to delete without an explicit machine projects root)"
1938
+ };
1939
+ }
1940
+ let resolvedPath;
1941
+ try {
1942
+ resolvedPath = await realpath(inputPath);
1943
+ } catch (e) {
1944
+ return { success: false, deleted: false, path: inputPath, error: e?.message || "Path not found" };
1945
+ }
1946
+ const st = await stat$1(resolvedPath).catch(() => null);
1947
+ if (!st || !st.isDirectory()) {
1948
+ return { success: false, deleted: false, path: inputPath, resolvedPath, error: "Path is not a directory" };
1949
+ }
1950
+ if (resolvedPath === "/" || resolvedPath.length < 4) {
1951
+ return { success: false, deleted: false, path: inputPath, resolvedPath, error: "Refusing to delete unsafe path" };
1952
+ }
1953
+ let projectsRootResolved = null;
1954
+ try {
1955
+ projectsRootResolved = await realpath(projectsRootInput);
1956
+ } catch (e) {
1957
+ return {
1958
+ success: false,
1959
+ deleted: false,
1960
+ path: inputPath,
1961
+ resolvedPath,
1962
+ projectsRootResolved: null,
1963
+ error: e?.message || "projectsRoot not found"
1964
+ };
1965
+ }
1966
+ if (!resolvedPath.startsWith(projectsRootResolved + "/")) {
1967
+ return {
1968
+ success: false,
1969
+ deleted: false,
1970
+ path: inputPath,
1971
+ resolvedPath,
1972
+ projectsRootResolved,
1973
+ error: `Refusing to delete outside projects root: ${projectsRootResolved}`
1974
+ };
1975
+ }
1976
+ if (requireUproject) {
1977
+ const entries = await readdir(resolvedPath).catch(() => []);
1978
+ const hasUproject = entries.some((n) => String(n || "").toLowerCase().endsWith(".uproject"));
1979
+ if (!hasUproject) {
1980
+ return {
1981
+ success: false,
1982
+ deleted: false,
1983
+ path: inputPath,
1984
+ resolvedPath,
1985
+ projectsRootResolved,
1986
+ error: "Refusing to delete: no *.uproject found in folder"
1987
+ };
1988
+ }
1989
+ }
1990
+ if (data?.dryRun) return { success: true, deleted: false, path: inputPath, resolvedPath, projectsRootResolved };
1991
+ try {
1992
+ await rm(resolvedPath, { recursive: true, force: false, maxRetries: 0 });
1993
+ return { success: true, deleted: true, path: inputPath, resolvedPath, projectsRootResolved };
1994
+ } catch (e) {
1995
+ return { success: false, deleted: false, path: inputPath, resolvedPath, projectsRootResolved, error: e?.message || "Delete failed" };
1996
+ }
1997
+ });
1998
+ rpcHandlerManager.registerHandler("git", async (data) => {
1999
+ const action = asText(data?.action);
2000
+ if (!action) return { success: false, error: "missing_action" };
2001
+ const cwd = (() => {
2002
+ const requested = asText(data.cwd || workingDirectory);
2003
+ const validation = validatePath(requested, workingDirectory);
2004
+ if (!validation.valid) return null;
2005
+ return validation.resolvedPath || null;
2006
+ })();
2007
+ if (!cwd) {
2008
+ const validation = validatePath(asText(data.cwd || workingDirectory), workingDirectory);
2009
+ return { success: false, error: validation.error || "Invalid cwd" };
2010
+ }
2011
+ try {
2012
+ ensureConfirmed(action, data.confirm);
2013
+ } catch (err) {
2014
+ return { success: false, error: err instanceof Error ? err.message : "confirmation_required" };
2015
+ }
2016
+ const timeoutMs = Math.max(1e3, Math.min(10 * 6e4, Number(data.timeout || 3e4) || 3e4));
2017
+ try {
2018
+ if (action === "init_repo") {
2019
+ return await runProcess({
2020
+ command: "git",
2021
+ args: ["init"],
2022
+ cwd,
2023
+ timeoutMs: Math.max(timeoutMs, 3e4)
2024
+ });
2025
+ }
2026
+ if (action === "status_porcelain_v2") {
2027
+ await ensureGitRepo(cwd);
2028
+ return await runProcess({
2029
+ command: "git",
2030
+ args: ["status", "--porcelain=v2", "--branch", "--untracked-files=all"],
2031
+ cwd,
2032
+ timeoutMs
2033
+ });
2034
+ }
2035
+ if (action === "diff_numstat") {
2036
+ await ensureGitRepo(cwd);
2037
+ const unstaged = await runProcess({ command: "git", args: ["diff", "--numstat", "HEAD"], cwd, timeoutMs });
2038
+ if (!unstaged.success) return unstaged;
2039
+ const staged = await runProcess({ command: "git", args: ["diff", "--cached", "--numstat"], cwd, timeoutMs });
2040
+ if (!staged.success) return staged;
2041
+ return {
2042
+ success: true,
2043
+ stdout: `${(unstaged.stdout || "").trim()}
2044
+ ---STAGED---
2045
+ ${(staged.stdout || "").trim()}`.trim(),
2046
+ stderr: [unstaged.stderr, staged.stderr].filter(Boolean).join("\n").trim(),
2047
+ exitCode: 0,
2048
+ stdoutTruncated: !!(unstaged.stdoutTruncated || staged.stdoutTruncated),
2049
+ stderrTruncated: !!(unstaged.stderrTruncated || staged.stderrTruncated)
2050
+ };
2051
+ }
2052
+ if (action === "list_remotes") {
2053
+ await ensureGitRepo(cwd);
2054
+ return await runProcess({
2055
+ command: "git",
2056
+ args: ["remote"],
2057
+ cwd,
2058
+ timeoutMs
2059
+ });
2060
+ }
2061
+ if (action === "list_branches") {
2062
+ await ensureGitRepo(cwd);
2063
+ return await runProcess({
2064
+ command: "git",
2065
+ args: ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
2066
+ cwd,
2067
+ timeoutMs
2068
+ });
2069
+ }
2070
+ if (action === "log") {
2071
+ await ensureGitRepo(cwd);
2072
+ const format = "%H%x1f%an%x1f%ad%x1f%s%x1e";
2073
+ return await runProcess({
2074
+ command: "git",
2075
+ args: ["log", "--date=relative", `--pretty=format:${format}`, "-n", "200"],
2076
+ cwd,
2077
+ timeoutMs: Math.max(timeoutMs, 1e4)
2078
+ });
2079
+ }
2080
+ if (action === "diff_stat") {
2081
+ await ensureGitRepo(cwd);
2082
+ const unstaged = await runProcess({ command: "git", args: ["diff", "--stat"], cwd, timeoutMs });
2083
+ if (!unstaged.success) return unstaged;
2084
+ const staged = await runProcess({ command: "git", args: ["diff", "--cached", "--stat"], cwd, timeoutMs });
2085
+ if (!staged.success) return staged;
2086
+ return {
2087
+ success: true,
2088
+ stdout: `${(unstaged.stdout || "").trim()}
2089
+ ---
2090
+ ${(staged.stdout || "").trim()}`.trim(),
2091
+ stderr: [unstaged.stderr, staged.stderr].filter(Boolean).join("\n").trim(),
2092
+ exitCode: 0,
2093
+ stdoutTruncated: !!(unstaged.stdoutTruncated || staged.stdoutTruncated),
2094
+ stderrTruncated: !!(unstaged.stderrTruncated || staged.stderrTruncated)
2095
+ };
2096
+ }
2097
+ if (action === "diff_file") {
2098
+ await ensureGitRepo(cwd);
2099
+ const safePaths = sanitizeRelativeGitPaths(data.paths);
2100
+ if (safePaths.length !== 1) return { success: false, error: "diff_file_requires_one_path" };
2101
+ const p = safePaths[0];
2102
+ const check = validatePath(join$1(cwd, p), workingDirectory);
2103
+ if (!check.valid) return { success: false, error: check.error || "invalid_path" };
2104
+ const args = ["diff", "--no-ext-diff"];
2105
+ if (data.staged) args.push("--cached");
2106
+ args.push("--", p);
2107
+ return await runProcess({ command: "git", args, cwd, timeoutMs: Math.max(timeoutMs, 1e4) });
2108
+ }
2109
+ if (action === "diff_to_file") {
2110
+ await ensureGitRepo(cwd);
2111
+ const safePaths = data.paths ? sanitizeRelativeGitPaths(data.paths) : [];
2112
+ if (data.paths && safePaths.length === 0) return { success: false, error: "missing_paths" };
2113
+ const tmpDir = join$1(workingDirectory, ".flockbay", "tmp");
2114
+ const validation = validatePath(tmpDir, workingDirectory);
2115
+ if (!validation.valid) return { success: false, error: validation.error || "invalid_tmp_dir" };
2116
+ const resolvedTmp = validation.resolvedPath || tmpDir;
2117
+ await mkdir$1(resolvedTmp, { recursive: true });
2118
+ const outPath = join$1(resolvedTmp, `git-diff-${Date.now()}-${Math.random().toString(16).slice(2)}.patch`);
2119
+ const outValidation = validatePath(outPath, workingDirectory);
2120
+ if (!outValidation.valid) return { success: false, error: outValidation.error || "invalid_output_path" };
2121
+ const resolvedOut = outValidation.resolvedPath || outPath;
2122
+ const args = ["diff"];
2123
+ if (data.staged) args.push("--cached");
2124
+ if (safePaths.length > 0) args.push("--", ...safePaths);
2125
+ const res = await runProcessToFile({
2126
+ command: "git",
2127
+ args,
2128
+ cwd,
2129
+ timeoutMs: Math.max(timeoutMs, 6e4),
2130
+ outputPath: resolvedOut
2131
+ });
2132
+ if (!res.success) return res;
2133
+ const info = await stat$1(resolvedOut);
2134
+ return { success: true, exitCode: 0, stderr: res.stderr, stderrTruncated: res.stderrTruncated, artifact: { path: resolvedOut, bytes: info.size } };
2135
+ }
2136
+ if (action === "repo_info") {
2137
+ await ensureGitRepo(cwd);
2138
+ const rootRes = await runProcess({ command: "git", args: ["rev-parse", "--show-toplevel"], cwd, timeoutMs: 1e4 });
2139
+ if (!rootRes.success) return rootRes;
2140
+ const branchRes = await runProcess({ command: "git", args: ["branch", "--show-current"], cwd, timeoutMs: 1e4 });
2141
+ if (!branchRes.success) return branchRes;
2142
+ const originRes = await runProcess({ command: "git", args: ["remote", "get-url", "origin"], cwd, timeoutMs: 1e4 });
2143
+ const root = (rootRes.stdout || "").trim();
2144
+ const branchRaw = (branchRes.stdout || "").trim();
2145
+ const originUrlRaw = originRes.success ? (originRes.stdout || "").trim() : "";
2146
+ return {
2147
+ success: true,
2148
+ stdout: "",
2149
+ stderr: "",
2150
+ exitCode: 0,
2151
+ repo: {
2152
+ root,
2153
+ branch: branchRaw ? branchRaw : null,
2154
+ originUrl: originUrlRaw ? originUrlRaw : null
2155
+ }
2156
+ };
2157
+ }
2158
+ if (action === "clone") {
2159
+ const url = asText(data.repoUrl).trim();
2160
+ if (!url) return { success: false, error: "missing_repo_url" };
2161
+ const destFolder = sanitizeDestFolder(asText(data.destFolder));
2162
+ const validation = validatePath(join$1(cwd, destFolder), workingDirectory);
2163
+ if (!validation.valid) return { success: false, error: validation.error || "invalid_dest_folder" };
2164
+ return await runProcess({
2165
+ command: "git",
2166
+ args: ["clone", url, destFolder],
2167
+ cwd,
2168
+ timeoutMs: Math.max(timeoutMs, 5 * 6e4)
2169
+ });
2170
+ }
2171
+ if (action === "stage_paths") {
2172
+ await ensureGitRepo(cwd);
2173
+ const safePaths = sanitizeRelativeGitPaths(data.paths);
2174
+ for (const p of safePaths) {
2175
+ const check = validatePath(join$1(cwd, p), workingDirectory);
2176
+ if (!check.valid) return { success: false, error: check.error || "invalid_path" };
2177
+ }
2178
+ return await runProcess({
2179
+ command: "git",
2180
+ args: ["add", "--", ...safePaths],
2181
+ cwd,
2182
+ timeoutMs: Math.max(timeoutMs, 6e4)
2183
+ });
2184
+ }
2185
+ if (action === "unstage_paths") {
2186
+ await ensureGitRepo(cwd);
2187
+ const safePaths = sanitizeRelativeGitPaths(data.paths);
2188
+ for (const p of safePaths) {
2189
+ const check = validatePath(join$1(cwd, p), workingDirectory);
2190
+ if (!check.valid) return { success: false, error: check.error || "invalid_path" };
2191
+ }
2192
+ return await runProcess({
2193
+ command: "git",
2194
+ args: ["restore", "--staged", "--", ...safePaths],
2195
+ cwd,
2196
+ timeoutMs: Math.max(timeoutMs, 6e4)
2197
+ });
2198
+ }
2199
+ if (action === "unstage_all") {
2200
+ await ensureGitRepo(cwd);
2201
+ return await runProcess({
2202
+ command: "git",
2203
+ args: ["restore", "--staged", "--", "."],
2204
+ cwd,
2205
+ timeoutMs: Math.max(timeoutMs, 6e4)
2206
+ });
2207
+ }
2208
+ if (action === "restore_file") {
2209
+ await ensureGitRepo(cwd);
2210
+ const safePaths = sanitizeRelativeGitPaths(data.paths);
2211
+ if (safePaths.length !== 1) return { success: false, error: "restore_file_requires_one_path" };
2212
+ const p = safePaths[0];
2213
+ const check = validatePath(join$1(cwd, p), workingDirectory);
2214
+ if (!check.valid) return { success: false, error: check.error || "invalid_path" };
2215
+ return await runProcess({
2216
+ command: "git",
2217
+ args: ["restore", "--source=HEAD", "--", p],
2218
+ cwd,
2219
+ timeoutMs: Math.max(timeoutMs, 6e4)
2220
+ });
2221
+ }
2222
+ if (action === "discard_unstaged") {
2223
+ await ensureGitRepo(cwd);
2224
+ return await runProcess({
2225
+ command: "git",
2226
+ args: ["restore", "--worktree", "--source=HEAD", "--", "."],
2227
+ cwd,
2228
+ timeoutMs: Math.max(timeoutMs, 2 * 6e4)
2229
+ });
2230
+ }
2231
+ if (action === "discard_all") {
2232
+ await ensureGitRepo(cwd);
2233
+ const reset = await runProcess({
2234
+ command: "git",
2235
+ args: ["reset", "--hard", "HEAD"],
2236
+ cwd,
2237
+ timeoutMs: Math.max(timeoutMs, 2 * 6e4)
2238
+ });
2239
+ if (!reset.success) return reset;
2240
+ return await runProcess({
2241
+ command: "git",
2242
+ args: ["clean", "-fd"],
2243
+ cwd,
2244
+ timeoutMs: Math.max(timeoutMs, 2 * 6e4)
2245
+ });
2246
+ }
2247
+ if (action === "commit_all") {
2248
+ await ensureGitRepo(cwd);
2249
+ const msg = asText(data.message).trim();
2250
+ if (!msg) return { success: false, error: "missing_commit_message" };
2251
+ const add = await runProcess({ command: "git", args: ["add", "-A"], cwd, timeoutMs: Math.max(timeoutMs, 6e4) });
2252
+ if (!add.success) return add;
2253
+ const committed = await runProcess({
2254
+ command: "git",
2255
+ args: ["commit", "-m", msg],
2256
+ cwd,
2257
+ timeoutMs: Math.max(timeoutMs, 6e4)
2258
+ });
2259
+ if (!committed.success) return committed;
2260
+ const shipped = await maybeAutoShipWorkItemAfterCommit({ coordination, cwd });
2261
+ if (!shipped.ok) {
2262
+ return {
2263
+ ...committed,
2264
+ success: false,
2265
+ error: `auto_ship_failed: ${shipped.error}`,
2266
+ stderr: `${committed.stderr || ""}
2267
+ AUTO_SHIP_FAILED: ${shipped.error}`.trim()
2268
+ };
2269
+ }
2270
+ return committed;
2271
+ }
2272
+ if (action === "commit_staged") {
2273
+ await ensureGitRepo(cwd);
2274
+ const msg = asText(data.message).trim();
2275
+ if (!msg) return { success: false, error: "missing_commit_message" };
2276
+ const committed = await runProcess({
2277
+ command: "git",
2278
+ args: ["commit", "-m", msg],
2279
+ cwd,
2280
+ timeoutMs: Math.max(timeoutMs, 6e4)
2281
+ });
2282
+ if (!committed.success) return committed;
2283
+ const shipped = await maybeAutoShipWorkItemAfterCommit({ coordination, cwd });
2284
+ if (!shipped.ok) {
2285
+ return {
2286
+ ...committed,
2287
+ success: false,
2288
+ error: `auto_ship_failed: ${shipped.error}`,
2289
+ stderr: `${committed.stderr || ""}
2290
+ AUTO_SHIP_FAILED: ${shipped.error}`.trim()
2291
+ };
2292
+ }
2293
+ return committed;
2294
+ }
2295
+ if (action === "revert_commit") {
2296
+ await ensureGitRepo(cwd);
2297
+ const commit = sanitizeGitCommitHash(data.commitHash);
2298
+ const reverted = await runProcess({
2299
+ command: "git",
2300
+ args: ["revert", "--no-edit", commit],
2301
+ cwd,
2302
+ timeoutMs: Math.max(timeoutMs, 5 * 6e4)
2303
+ });
2304
+ if (!reverted.success) return reverted;
2305
+ return reverted;
2306
+ }
2307
+ if (action === "reapply_commit") {
2308
+ await ensureGitRepo(cwd);
2309
+ const commit = sanitizeGitCommitHash(data.commitHash);
2310
+ const applied = await runProcess({
2311
+ command: "git",
2312
+ args: ["cherry-pick", "-x", commit],
2313
+ cwd,
2314
+ timeoutMs: Math.max(timeoutMs, 5 * 6e4)
2315
+ });
2316
+ if (!applied.success) return applied;
2317
+ return applied;
2318
+ }
2319
+ if (action === "push_head") {
2320
+ await ensureGitRepo(cwd);
2321
+ const remote = asText(data.remote || "origin").trim();
2322
+ if (!remote) return { success: false, error: "missing_remote" };
2323
+ return await runProcess({
2324
+ command: "git",
2325
+ args: ["push", "-u", remote, "HEAD"],
2326
+ cwd,
2327
+ timeoutMs: Math.max(timeoutMs, 5 * 6e4)
2328
+ });
2329
+ }
2330
+ if (action === "pr_create") {
2331
+ await ensureGitRepo(cwd);
2332
+ const title = asText(data.title).trim();
2333
+ const body = asText(data.body).trim();
2334
+ const base = asText(data.base).trim();
2335
+ if (!title) return { success: false, error: "missing_pr_title" };
2336
+ const ghExists = await runProcess({ command: "gh", args: ["--version"], cwd, timeoutMs: 1e4 });
2337
+ if (!ghExists.success) return { success: false, error: "gh_not_installed", stderr: ghExists.stderr };
2338
+ const ghAuth = await runProcess({ command: "gh", args: ["auth", "status"], cwd, timeoutMs: 2e4 });
2339
+ if (!ghAuth.success) return { success: false, error: "gh_not_authenticated", stderr: ghAuth.stderr || ghAuth.stdout };
2340
+ const args = ["pr", "create", "--title", title, "--body", body];
2341
+ if (data.draft) args.push("--draft");
2342
+ if (base) args.push("--base", base);
2343
+ return await runProcess({ command: "gh", args, cwd, timeoutMs: Math.max(timeoutMs, 6e4) });
2344
+ }
2345
+ if (action === "github_snapshot_push" || action === "github_snapshot_pr_create") {
2346
+ await ensureGitRepo(cwd);
2347
+ if (!coordination) return { success: false, error: "coordination_required" };
2348
+ const projectId = String(asText(data.workspaceProjectId || coordination.workspaceProjectId)).trim();
2349
+ if (!projectId) return { success: false, error: "missing_project_id" };
2350
+ const repoUrl = await (async () => {
2351
+ const explicit = asText(data.repoUrl).trim();
2352
+ if (explicit) return explicit;
2353
+ try {
2354
+ const listed = await apiJsonPost({
2355
+ serverUrl: coordination.serverUrl,
2356
+ token: coordination.token,
2357
+ path: "/v1/workspace/projects/list",
2358
+ body: {}
2359
+ });
2360
+ const projects = Array.isArray(listed?.projects) ? listed.projects : [];
2361
+ const match = projects.find((p) => String(p?.id || "").trim() === projectId);
2362
+ const url = match?.repoUrl ? String(match.repoUrl || "").trim() : "";
2363
+ return url || null;
2364
+ } catch {
2365
+ return null;
2366
+ }
2367
+ })();
2368
+ if (!repoUrl) return { success: false, error: "missing_repo_url" };
2369
+ if (!isGithubRemoteUrl(repoUrl)) return { success: false, error: "repo_url_not_github" };
2370
+ const baseBranch = await resolveBaseBranch({ cwd });
2371
+ const head = await runProcess({ command: "git", args: ["rev-parse", baseBranch], cwd, timeoutMs: 15e3 });
2372
+ if (!head.success) return { success: false, error: head.stderr || head.error || "failed_to_read_base_head" };
2373
+ const commitHash = String(head.stdout || "").trim();
2374
+ if (!commitHash) return { success: false, error: "missing_commit_hash" };
2375
+ let githubToken = null;
2376
+ let githubScope = null;
2377
+ let githubConnected = false;
2378
+ try {
2379
+ const githubRes = await apiJsonGet({
2380
+ serverUrl: coordination.serverUrl,
2381
+ token: coordination.token,
2382
+ path: "/v1/connect/github/token"
2383
+ });
2384
+ githubToken = typeof githubRes?.token === "string" ? githubRes.token.trim() : null;
2385
+ githubConnected = !!githubRes?.present;
2386
+ githubScope = typeof githubRes?.scope === "string" ? githubRes.scope : null;
2387
+ } catch (e) {
2388
+ return { success: false, error: e instanceof Error ? e.message : "github_token_fetch_failed" };
2389
+ }
2390
+ if (!githubToken) return { success: false, error: githubConnected ? "github_token_unavailable" : "github_not_connected" };
2391
+ const snapshotBranch = `flockbay/snapshot/${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15)}-${randomUUID().slice(0, 8)}`;
2392
+ const askpass = await createGithubAskpassScript();
2393
+ const pushEnv = {
2394
+ GIT_TERMINAL_PROMPT: "0",
2395
+ GIT_ASKPASS: askpass.askpassPath,
2396
+ FLOCKBAY_GITHUB_TOKEN: githubToken
2397
+ };
2398
+ const pushRes = await runProcess({
2399
+ command: "git",
2400
+ args: ["push", repoUrl, `${commitHash}:refs/heads/${snapshotBranch}`],
2401
+ cwd,
2402
+ timeoutMs: Math.max(timeoutMs, 6 * 6e4),
2403
+ env: pushEnv
2404
+ });
2405
+ await askpass.cleanup();
2406
+ if (!pushRes.success) {
2407
+ const detail = String(pushRes.stderr || pushRes.stdout || "").trim();
2408
+ try {
2409
+ await appendCoordinationActivity({
2410
+ coordination,
2411
+ projectId,
2412
+ event: {
2413
+ kind: "github_publish_failed",
2414
+ title: "GitHub publish failed",
2415
+ detail: detail ? detail.slice(0, 2e3) : null,
2416
+ data: { step: "push", repoUrl, baseBranch, snapshotBranch, exitCode: pushRes.exitCode, githubScope }
2417
+ }
2418
+ });
2419
+ } catch (error) {
2420
+ logNonSilent("[registerCommonHandlers] Failed to append coordination activity (github_publish_failed: push)", error);
2421
+ }
2422
+ return { success: false, error: detail || "git_push_failed" };
2423
+ }
2424
+ try {
2425
+ await appendCoordinationActivity({
2426
+ coordination,
2427
+ projectId,
2428
+ event: {
2429
+ kind: "github_snapshot_pushed",
2430
+ title: "Snapshot pushed to GitHub",
2431
+ detail: snapshotBranch,
2432
+ data: { repoUrl, baseBranch, snapshotBranch, commitHash, githubScope }
2433
+ }
2434
+ });
2435
+ } catch (error) {
2436
+ logNonSilent("[registerCommonHandlers] Failed to append coordination activity (github_snapshot_pushed)", error);
2437
+ }
2438
+ if (action === "github_snapshot_push") {
2439
+ return {
2440
+ success: true,
2441
+ stdout: `Pushed ${snapshotBranch}`,
2442
+ stderr: "",
2443
+ exitCode: 0,
2444
+ github: { branch: snapshotBranch, baseBranch, commitHash }
2445
+ };
2446
+ }
2447
+ const repoFullName = parseGithubRepoFullName(repoUrl);
2448
+ if (!repoFullName) return { success: false, error: "invalid_github_repo_url" };
2449
+ const prTitle = asText(data.title).trim() || `Flockbay snapshot (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)})`;
2450
+ const prBody = (() => {
2451
+ const explicit = asText(data.body).trim();
2452
+ if (explicit) return explicit;
2453
+ return [
2454
+ "Flockbay snapshot PR (manual publish).",
2455
+ "",
2456
+ `Base: \`${baseBranch}\``,
2457
+ `Commit: \`${commitHash}\``,
2458
+ `Branch: \`${snapshotBranch}\``
2459
+ ].join("\n");
2460
+ })();
2461
+ const prRes = await fetch(`https://api.github.com/repos/${repoFullName}/pulls`, {
2462
+ method: "POST",
2463
+ headers: {
2464
+ Authorization: `Bearer ${githubToken}`,
2465
+ Accept: "application/vnd.github+json",
2466
+ "Content-Type": "application/json"
2467
+ },
2468
+ body: JSON.stringify({
2469
+ title: prTitle,
2470
+ body: prBody,
2471
+ head: snapshotBranch,
2472
+ base: baseBranch,
2473
+ draft: !!data.draft
2474
+ })
2475
+ });
2476
+ const prData = await prRes.json().catch(() => null);
2477
+ if (!prRes.ok) {
2478
+ const msg = typeof prData?.message === "string" ? prData.message : `GitHub API error (${prRes.status})`;
2479
+ try {
2480
+ await appendCoordinationActivity({
2481
+ coordination,
2482
+ projectId,
2483
+ event: {
2484
+ kind: "github_publish_failed",
2485
+ title: "GitHub publish failed",
2486
+ detail: msg,
2487
+ data: { step: "pr_create", repoFullName, baseBranch, snapshotBranch, status: prRes.status }
2488
+ }
2489
+ });
2490
+ } catch (error) {
2491
+ logNonSilent("[registerCommonHandlers] Failed to append coordination activity (github_publish_failed: pr_create)", error);
2492
+ }
2493
+ return { success: false, error: msg };
2494
+ }
2495
+ const prUrl = typeof prData?.html_url === "string" ? prData.html_url : "";
2496
+ const prNumber = Number.isFinite(Number(prData?.number)) ? Number(prData.number) : void 0;
2497
+ try {
2498
+ await appendCoordinationActivity({
2499
+ coordination,
2500
+ projectId,
2501
+ event: {
2502
+ kind: "github_pr_created",
2503
+ title: "PR created on GitHub",
2504
+ detail: prUrl || `${repoFullName}#${prNumber ?? ""}`.trim(),
2505
+ data: { repoFullName, baseBranch, snapshotBranch, prUrl: prUrl || null, prNumber: prNumber ?? null }
2506
+ }
2507
+ });
2508
+ } catch (error) {
2509
+ logNonSilent("[registerCommonHandlers] Failed to append coordination activity (github_pr_created)", error);
2510
+ }
2511
+ return {
2512
+ success: true,
2513
+ stdout: prUrl || `Created PR on ${repoFullName}`,
2514
+ stderr: "",
2515
+ exitCode: 0,
2516
+ github: { repoFullName, branch: snapshotBranch, prUrl: prUrl || void 0, prNumber, baseBranch, commitHash }
2517
+ };
2518
+ }
2519
+ return { success: false, error: `unknown_action:${action}` };
2520
+ } catch (err) {
2521
+ return { success: false, error: err instanceof Error ? err.message : "git_action_failed" };
2522
+ }
2523
+ });
2524
+ rpcHandlerManager.registerHandler("readFile", async (data) => {
2525
+ logger.debug("Read file request:", data.path);
2526
+ const validation = validatePath(data.path, workingDirectory);
2527
+ if (!validation.valid) {
2528
+ return { success: false, error: validation.error };
2529
+ }
2530
+ const resolvedPath = validation.resolvedPath || data.path;
2531
+ try {
2532
+ const buffer = await readFile$1(resolvedPath);
2533
+ const content = buffer.toString("base64");
2534
+ return { success: true, content };
2535
+ } catch (error) {
2536
+ logger.debug("Failed to read file:", error);
2537
+ return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
2538
+ }
2539
+ });
2540
+ rpcHandlerManager.registerHandler("readFileChunk", async (data) => {
2541
+ logger.debug("Read file chunk request:", { path: data.path, offset: data.offset, maxBytes: data.maxBytes });
2542
+ const validation = validatePath(data.path, workingDirectory);
2543
+ if (!validation.valid) {
2544
+ return { success: false, error: validation.error, done: true };
2545
+ }
2546
+ const resolvedPath = validation.resolvedPath || data.path;
2547
+ const offset = Math.max(0, Math.floor(Number(data.offset ?? 0)));
2548
+ const DEFAULT_MAX_BYTES = 262143;
2549
+ const requested = Number(data.maxBytes ?? DEFAULT_MAX_BYTES);
2550
+ const aligned = Math.floor(requested / 3) * 3;
2551
+ const maxBytes = Math.max(3, Math.min(9e5, aligned || DEFAULT_MAX_BYTES));
2552
+ try {
2553
+ const st = await stat$1(resolvedPath);
2554
+ const totalSize = st.size;
2555
+ if (offset >= totalSize) {
2556
+ return { success: true, content: "", offset, nextOffset: offset, totalSize, done: true };
2557
+ }
2558
+ const remaining = totalSize - offset;
2559
+ const toRead = Math.min(maxBytes, remaining);
2560
+ const handle = await open$1(resolvedPath, "r");
2561
+ try {
2562
+ const buffer = Buffer.allocUnsafe(toRead);
2563
+ const { bytesRead } = await handle.read(buffer, 0, toRead, offset);
2564
+ const slice = bytesRead === buffer.length ? buffer : buffer.subarray(0, Math.max(0, bytesRead));
2565
+ const nextOffset = offset + slice.length;
2566
+ const done = nextOffset >= totalSize;
2567
+ return {
2568
+ success: true,
2569
+ content: slice.toString("base64"),
2570
+ offset,
2571
+ nextOffset,
2572
+ totalSize,
2573
+ done
2574
+ };
2575
+ } finally {
2576
+ await handle.close();
2577
+ }
2578
+ } catch (error) {
2579
+ logger.debug("Failed to read file chunk:", error);
2580
+ return { success: false, error: error instanceof Error ? error.message : "Failed to read file chunk", done: true };
2581
+ }
2582
+ });
2583
+ rpcHandlerManager.registerHandler("coordinationClaimFiles", async (data) => {
2584
+ const projectId = String(coordination?.workspaceProjectId || "").trim();
2585
+ const workItemId = String(coordination?.workItemId || "").trim();
2586
+ const leaseGuard = coordination?.leaseGuard;
2587
+ if (!coordination || !leaseGuard) return { success: true, claimedFiles: [] };
2588
+ if (!projectId || !workItemId) return { success: false, error: "coordination_required" };
2589
+ const rawFiles = Array.isArray(data?.files) ? data.files : [];
2590
+ const files = rawFiles.map((f) => normalizeCoordinationFilePath(String(f || ""))).filter((v) => Boolean(v));
2591
+ if (files.length === 0) return { success: false, error: "missing_files" };
2592
+ try {
2593
+ const repoRootRes = await runProcess({
2594
+ command: "git",
2595
+ args: ["rev-parse", "--show-toplevel"],
2596
+ cwd: workingDirectory,
2597
+ timeoutMs: 1e4,
2598
+ env: { GIT_TERMINAL_PROMPT: "0" }
2599
+ });
2600
+ if (!repoRootRes.success) {
2601
+ return { success: false, error: repoRootRes.stderr || repoRootRes.error || "not_a_git_repo" };
2602
+ }
2603
+ const repoRoot = String(repoRootRes.stdout || "").trim();
2604
+ if (!repoRoot) return { success: false, error: "not_a_git_repo" };
2605
+ const needsRefresh = files.filter((f) => !leaseGuard.has(f));
2606
+ const preferredBaseBranch = null;
2607
+ const showRefExists = async (ref) => {
2608
+ const r = String(ref || "").trim();
2609
+ if (!r) return false;
2610
+ const res2 = await runProcess({
2611
+ command: "git",
2612
+ args: ["show-ref", "--verify", "--quiet", r],
2613
+ cwd: repoRoot,
2614
+ timeoutMs: 1e4,
2615
+ env: { GIT_TERMINAL_PROMPT: "0" }
2616
+ });
2617
+ return res2.success;
2618
+ };
2619
+ const remoteName = await (async () => {
2620
+ const res2 = await runProcess({
2621
+ command: "git",
2622
+ args: ["remote"],
2623
+ cwd: repoRoot,
2624
+ timeoutMs: 1e4,
2625
+ env: { GIT_TERMINAL_PROMPT: "0" }
2626
+ });
2627
+ if (!res2.success) return null;
2628
+ const remotes = String(res2.stdout || "").split("\n").map((l) => l.trim()).filter(Boolean);
2629
+ if (remotes.includes("origin")) return "origin";
2630
+ return remotes[0] ?? null;
2631
+ })();
2632
+ const branchExists = async (branch) => {
2633
+ const b = String(branch || "").trim();
2634
+ if (!b) return false;
2635
+ if (await showRefExists(`refs/heads/${b}`)) return true;
2636
+ if (remoteName && await showRefExists(`refs/remotes/${remoteName}/${b}`)) return true;
2637
+ return false;
2638
+ };
2639
+ const candidates = Array.from(new Set([preferredBaseBranch, "main", "master"].map((b) => String(b || "").trim()).filter(Boolean)));
2640
+ let baseBranch = null;
2641
+ for (const candidate of candidates) {
2642
+ if (await branchExists(candidate)) {
2643
+ baseBranch = candidate;
2644
+ break;
2645
+ }
2646
+ }
2647
+ if (!baseBranch) {
2648
+ const heads = await runProcess({ command: "git", args: ["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd: repoRoot, timeoutMs: 1e4 });
2649
+ const available = String(heads.stdout || "").split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 20).join(", ");
2650
+ return {
2651
+ success: false,
2652
+ error: `Invalid base branch '${preferredBaseBranch || "main"}' (no local branch found). Set coordination settings mergeBaseBranch, or create the branch. Available local branches: ${available || "(none)"}`
2653
+ };
2654
+ }
2655
+ const baseRemoteRef = remoteName ? `${remoteName}/${baseBranch}` : null;
2656
+ const hasRemoteBaseRef = async () => {
2657
+ if (!remoteName) return false;
2658
+ return await showRefExists(`refs/remotes/${remoteName}/${baseBranch}`);
2659
+ };
2660
+ if (remoteName) {
2661
+ const fetched = await runProcess({
2662
+ command: "git",
2663
+ args: ["fetch", "--prune", remoteName, baseBranch],
2664
+ cwd: repoRoot,
2665
+ timeoutMs: 12e4,
2666
+ env: { GIT_TERMINAL_PROMPT: "0" }
2667
+ });
2668
+ if (!fetched.success) {
2669
+ return {
2670
+ success: false,
2671
+ error: `Cannot claim files safely because the base branch could not be refreshed.
2672
+
2673
+ git fetch ${remoteName} ${baseBranch}
2674
+ ${String(fetched.stderr || fetched.error || "").trim() || "fetch failed"}`
2675
+ };
2676
+ }
2677
+ }
2678
+ const baseRef = await hasRemoteBaseRef() && baseRemoteRef ? baseRemoteRef : baseBranch;
2679
+ const refreshable = [];
2680
+ for (const filePath of needsRefresh) {
2681
+ const inBase = await runProcess({
2682
+ command: "git",
2683
+ args: ["cat-file", "-e", `${baseRef}:${filePath}`],
2684
+ cwd: repoRoot,
2685
+ timeoutMs: 1e4,
2686
+ env: { GIT_TERMINAL_PROMPT: "0" }
2687
+ });
2688
+ if (!inBase.success) continue;
2689
+ const st = await runProcess({
2690
+ command: "git",
2691
+ args: ["status", "--porcelain=v1", "--", filePath],
2692
+ cwd: repoRoot,
2693
+ timeoutMs: 1e4,
2694
+ env: { GIT_TERMINAL_PROMPT: "0" }
2695
+ });
2696
+ if (!st.success) {
2697
+ return { success: false, error: st.stderr || st.error || `status_failed: ${filePath}` };
2698
+ }
2699
+ if (String(st.stdout || "").trim()) {
2700
+ return {
2701
+ success: false,
2702
+ error: `Cannot safely refresh "${filePath}" from ${baseBranch} because it has local changes.
2703
+
2704
+ Fix: revert/commit your local changes for this path, then claim again.
2705
+
2706
+ git status -- ${filePath}
2707
+ ${String(st.stdout || "").trim()}`
2708
+ };
2709
+ }
2710
+ refreshable.push(filePath);
2711
+ }
2712
+ const url = `${coordination.serverUrl.replace(/\/+$/, "")}/v1/coordination/work-items/claim-files`;
2713
+ const body = {
2714
+ projectId,
2715
+ workItemId,
2716
+ files,
2717
+ mode: data?.mode ?? "active",
2718
+ leaseMs: typeof data?.leaseMs === "number" ? data.leaseMs : void 0
2719
+ };
2720
+ const res = await fetch(url, {
2721
+ method: "POST",
2722
+ headers: {
2723
+ "Authorization": `Bearer ${coordination.token}`,
2724
+ "Content-Type": "application/json"
2725
+ },
2726
+ body: JSON.stringify(body)
2727
+ });
2728
+ const payload = await res.json().catch(() => null);
2729
+ if (!res.ok) {
2730
+ const msg = typeof payload?.error === "string" ? payload.error : `claim failed (${res.status})`;
2731
+ return { success: false, error: msg };
2732
+ }
2733
+ if (!payload?.success) {
2734
+ return {
2735
+ success: false,
2736
+ error: typeof payload?.error === "string" ? payload.error : "conflict",
2737
+ claimedFiles: Array.isArray(payload?.claimedFiles) ? payload.claimedFiles : files,
2738
+ conflicts: Array.isArray(payload?.conflicts) ? payload.conflicts : void 0
2739
+ };
2740
+ }
2741
+ const claimedFiles = Array.isArray(payload?.claimedFiles) ? payload.claimedFiles.map((v) => normalizeCoordinationFilePath(String(v || ""))).filter((v) => Boolean(v)) : files;
2742
+ const claimedSet = new Set(claimedFiles);
2743
+ const toRefresh = refreshable.filter((p) => claimedSet.has(p));
2744
+ const pre = await runProcess({
2745
+ command: "git",
2746
+ args: ["status", "--porcelain=v1", "--untracked-files=no"],
2747
+ cwd: repoRoot,
2748
+ timeoutMs: 1e4,
2749
+ env: { GIT_TERMINAL_PROMPT: "0" }
2750
+ });
2751
+ if (pre.success && !String(pre.stdout || "").trim()) {
2752
+ await runProcess({
2753
+ command: "git",
2754
+ args: ["merge", "--ff-only", baseRef],
2755
+ cwd: repoRoot,
2756
+ timeoutMs: 12e4,
2757
+ env: { GIT_TERMINAL_PROMPT: "0" }
2758
+ });
2759
+ }
2760
+ if (toRefresh.length > 0) {
2761
+ const restored = await runProcess({
2762
+ command: "git",
2763
+ args: ["restore", "--source", baseRef, "--worktree", "--", ...toRefresh],
2764
+ cwd: repoRoot,
2765
+ timeoutMs: 6e4,
2766
+ env: { GIT_TERMINAL_PROMPT: "0" }
2767
+ });
2768
+ if (!restored.success) {
2769
+ try {
2770
+ await apiJsonPost({
2771
+ serverUrl: coordination.serverUrl,
2772
+ token: coordination.token,
2773
+ path: "/v1/coordination/work-items/update",
2774
+ body: { projectId, workItemId, leaseMs: 6e4 }
2775
+ });
2776
+ } catch (error) {
2777
+ logNonSilent("[registerCommonHandlers] Failed to bump coordination lease after refresh failure", error);
2778
+ }
2779
+ return { success: false, error: restored.stderr || restored.error || "refresh_failed" };
2780
+ }
2781
+ }
2782
+ leaseGuard.grant(claimedFiles, typeof body.leaseMs === "number" ? body.leaseMs : void 0);
2783
+ return { success: true, claimedFiles };
2784
+ } catch (err) {
2785
+ return { success: false, error: err instanceof Error ? err.message : "claim_failed" };
2786
+ }
2787
+ });
2788
+ rpcHandlerManager.registerHandler("writeFile", async (data) => {
2789
+ logger.debug("Write file request:", data.path);
2790
+ const validation = validatePath(data.path, workingDirectory);
2791
+ if (!validation.valid) {
2792
+ return { success: false, error: validation.error };
2793
+ }
2794
+ const resolvedPath = validation.resolvedPath || data.path;
2795
+ const relPath = relative(workingDirectory, resolvedPath).replace(/\\/g, "/");
2796
+ try {
2797
+ const leaseGuard = coordination?.leaseGuard;
2798
+ const isUserUpload = relPath === ".flockbay/user-uploads" || relPath.startsWith(".flockbay/user-uploads/");
2799
+ if (leaseGuard && !isUserUpload && !leaseGuard.has(relPath)) {
2800
+ return {
2801
+ success: false,
2802
+ error: `File write blocked (coordination claim required).
2803
+
2804
+ Before editing a file, call \`mcp__flockbay__coordination_claim_files\` with:
2805
+ files: ["${relPath}"]
2806
+
2807
+ If the claim conflicts, call \`mcp__flockbay__coordination_check_files\` and wait/retry.`
2808
+ };
2809
+ }
2810
+ const expectedHash = data.expectedHash;
2811
+ if (expectedHash === void 0) {
2812
+ try {
2813
+ await stat$1(resolvedPath);
2814
+ return {
2815
+ success: false,
2816
+ error: `Missing expectedHash for existing file.
2817
+
2818
+ Fix: read the current file contents, compute sha256, and retry with expectedHash set.
2819
+ This prevents stale-overwrite bugs when the base branch advances.`
2820
+ };
2821
+ } catch (error) {
2822
+ const nodeError = error;
2823
+ if (nodeError.code !== "ENOENT") throw error;
2824
+ }
2825
+ } else if (expectedHash === null) {
2826
+ try {
2827
+ await stat$1(resolvedPath);
2828
+ return { success: false, error: "File already exists but was expected to be new" };
2829
+ } catch (error) {
2830
+ const nodeError = error;
2831
+ if (nodeError.code !== "ENOENT") throw error;
2832
+ }
2833
+ } else {
2834
+ try {
2835
+ const existingBuffer = await readFile$1(resolvedPath);
2836
+ const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
2837
+ if (existingHash !== expectedHash) {
2838
+ return {
2839
+ success: false,
2840
+ error: `File hash mismatch.
2841
+
2842
+ Expected: ${expectedHash}
2843
+ Actual: ${existingHash}
2844
+
2845
+ Fix: re-read the file and retry (the file changed since you last read it).`
2846
+ };
2847
+ }
2848
+ } catch (error) {
2849
+ const nodeError = error;
2850
+ if (nodeError.code !== "ENOENT") throw error;
2851
+ return { success: false, error: "File does not exist but hash was provided" };
2852
+ }
2853
+ }
2854
+ await mkdir$1(dirname(resolvedPath), { recursive: true });
2855
+ const buffer = Buffer.from(data.content, "base64");
2856
+ await writeFile$1(resolvedPath, buffer);
2857
+ const hash = createHash("sha256").update(buffer).digest("hex");
2858
+ return { success: true, hash };
2859
+ } catch (error) {
2860
+ logger.debug("Failed to write file:", error);
2861
+ return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
2862
+ }
2863
+ });
2864
+ rpcHandlerManager.registerHandler("listDirectory", async (data) => {
2865
+ logger.debug("List directory request:", data.path);
2866
+ const validation = validatePath(data.path, workingDirectory);
2867
+ if (!validation.valid) {
2868
+ return { success: false, error: validation.error };
2869
+ }
2870
+ const resolvedPath = validation.resolvedPath || data.path;
2871
+ try {
2872
+ const entries = await readdir(resolvedPath, { withFileTypes: true });
2873
+ const directoryEntries = await Promise.all(
2874
+ entries.map(async (entry) => {
2875
+ const fullPath = join$1(resolvedPath, entry.name);
2876
+ let type = "other";
2877
+ let size;
2878
+ let modified;
2879
+ if (entry.isDirectory()) {
2880
+ type = "directory";
2881
+ } else if (entry.isFile()) {
2882
+ type = "file";
2883
+ }
2884
+ try {
2885
+ const stats = await stat$1(fullPath);
2886
+ size = stats.size;
2887
+ modified = stats.mtime.getTime();
2888
+ } catch (error) {
2889
+ logger.debug(`Failed to stat ${fullPath}:`, error);
2890
+ }
2891
+ return {
2892
+ name: entry.name,
2893
+ type,
2894
+ size,
2895
+ modified
2896
+ };
2897
+ })
2898
+ );
2899
+ directoryEntries.sort((a, b) => {
2900
+ if (a.type === "directory" && b.type !== "directory") return -1;
2901
+ if (a.type !== "directory" && b.type === "directory") return 1;
2902
+ return a.name.localeCompare(b.name);
2903
+ });
2904
+ return { success: true, entries: directoryEntries };
2905
+ } catch (error) {
2906
+ logger.debug("Failed to list directory:", error);
2907
+ return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
2908
+ }
2909
+ });
2910
+ rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
2911
+ logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
2912
+ const validation = validatePath(data.path, workingDirectory);
2913
+ if (!validation.valid) {
2914
+ return { success: false, error: validation.error };
2915
+ }
2916
+ const rootPath = validation.resolvedPath || data.path;
2917
+ async function buildTree(path, name, currentDepth) {
2918
+ try {
2919
+ const stats = await stat$1(path);
2920
+ const node = {
2921
+ name,
2922
+ path,
2923
+ type: stats.isDirectory() ? "directory" : "file",
2924
+ size: stats.size,
2925
+ modified: stats.mtime.getTime()
2926
+ };
2927
+ if (stats.isDirectory() && currentDepth < data.maxDepth) {
2928
+ const entries = await readdir(path, { withFileTypes: true });
2929
+ const children = [];
2930
+ await Promise.all(
2931
+ entries.map(async (entry) => {
2932
+ if (entry.isSymbolicLink()) {
2933
+ logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
2934
+ return;
2935
+ }
2936
+ const childPath = join$1(path, entry.name);
2937
+ const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
2938
+ if (childNode) {
2939
+ children.push(childNode);
2940
+ }
2941
+ })
2942
+ );
2943
+ children.sort((a, b) => {
2944
+ if (a.type === "directory" && b.type !== "directory") return -1;
2945
+ if (a.type !== "directory" && b.type === "directory") return 1;
2946
+ return a.name.localeCompare(b.name);
2947
+ });
2948
+ node.children = children;
2949
+ }
2950
+ return node;
2951
+ } catch (error) {
2952
+ logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
2953
+ return null;
2954
+ }
2955
+ }
2956
+ try {
2957
+ if (data.maxDepth < 0) {
2958
+ return { success: false, error: "maxDepth must be non-negative" };
2959
+ }
2960
+ const baseName = rootPath === "/" ? "/" : rootPath.split("/").pop() || rootPath;
2961
+ const tree = await buildTree(rootPath, baseName, 0);
2962
+ if (!tree) {
2963
+ return { success: false, error: "Failed to access the specified path" };
2964
+ }
2965
+ return { success: true, tree };
2966
+ } catch (error) {
2967
+ logger.debug("Failed to get directory tree:", error);
2968
+ return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
2969
+ }
2970
+ });
2971
+ rpcHandlerManager.registerHandler("ripgrep", async (data) => {
2972
+ logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
2973
+ let cwd = workingDirectory;
2974
+ if (data.cwd) {
2975
+ const validation = validatePath(data.cwd, workingDirectory);
2976
+ if (!validation.valid) {
2977
+ return { success: false, error: validation.error };
2978
+ }
2979
+ cwd = validation.resolvedPath || data.cwd;
2980
+ }
2981
+ try {
2982
+ const result = await run$1(data.args, { cwd });
2983
+ return {
2984
+ success: true,
2985
+ exitCode: result.exitCode,
2986
+ stdout: result.stdout.toString(),
2987
+ stderr: result.stderr.toString()
2988
+ };
2989
+ } catch (error) {
2990
+ logger.debug("Failed to run ripgrep:", error);
2991
+ return {
2992
+ success: false,
2993
+ error: error instanceof Error ? error.message : "Failed to run ripgrep"
2994
+ };
2995
+ }
2996
+ });
2997
+ rpcHandlerManager.registerHandler("difftastic", async (data) => {
2998
+ logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
2999
+ if (data.cwd) {
3000
+ const validation = validatePath(data.cwd, workingDirectory);
3001
+ if (!validation.valid) {
3002
+ return { success: false, error: validation.error };
3003
+ }
3004
+ }
3005
+ try {
3006
+ const result = await run(data.args, { cwd: data.cwd });
3007
+ return {
3008
+ success: true,
3009
+ exitCode: result.exitCode,
3010
+ stdout: result.stdout.toString(),
3011
+ stderr: result.stderr.toString()
3012
+ };
3013
+ } catch (error) {
3014
+ logger.debug("Failed to run difftastic:", error);
3015
+ return {
3016
+ success: false,
3017
+ error: error instanceof Error ? error.message : "Failed to run difftastic"
3018
+ };
3019
+ }
3020
+ });
3021
+ }
3022
+
3023
+ const DEFAULT_TTL_MS = 2 * 60 * 6e4;
3024
+ function normalizeLedgerFilePath(input) {
3025
+ const raw = String(input || "").trim();
3026
+ if (!raw) return null;
3027
+ let candidate = raw.replace(/\\/g, "/");
3028
+ candidate = candidate.replace(/^\.\/+/, "").replace(/^\/+/, "");
3029
+ if (!candidate) return null;
3030
+ const parts = candidate.split("/").filter(Boolean);
3031
+ const out = [];
3032
+ for (const part of parts) {
3033
+ if (part === "." || part === "") continue;
3034
+ if (part === "..") return null;
3035
+ out.push(part);
3036
+ }
3037
+ return out.join("/") || null;
3038
+ }
3039
+ class CoordinationLeaseGuard {
3040
+ expiresByFile = /* @__PURE__ */ new Map();
3041
+ grant(files, ttlMs) {
3042
+ const clampedTtl = Math.max(6e4, Math.min(24 * 60 * 6e4, Number(ttlMs ?? DEFAULT_TTL_MS) || DEFAULT_TTL_MS));
3043
+ const expiresAt = Date.now() + clampedTtl;
3044
+ const granted = [];
3045
+ for (const f of Array.isArray(files) ? files : []) {
3046
+ const normalized = normalizeLedgerFilePath(String(f || ""));
3047
+ if (!normalized) continue;
3048
+ const existing = Number(this.expiresByFile.get(normalized) || 0);
3049
+ this.expiresByFile.set(normalized, Math.max(existing, expiresAt));
3050
+ granted.push(normalized);
3051
+ }
3052
+ granted.sort((a, b) => a.localeCompare(b));
3053
+ return { granted, expiresAt };
3054
+ }
3055
+ has(filePath, nowMs = Date.now()) {
3056
+ const normalized = normalizeLedgerFilePath(filePath);
3057
+ if (!normalized) return false;
3058
+ const expiresAt = Number(this.expiresByFile.get(normalized) || 0);
3059
+ if (!Number.isFinite(expiresAt) || expiresAt <= nowMs) {
3060
+ this.expiresByFile.delete(normalized);
3061
+ return false;
3062
+ }
3063
+ return true;
3064
+ }
3065
+ revoke(files) {
3066
+ const revoked = [];
3067
+ for (const f of Array.isArray(files) ? files : []) {
3068
+ const normalized = normalizeLedgerFilePath(String(f || ""));
3069
+ if (!normalized) continue;
3070
+ if (this.expiresByFile.delete(normalized)) {
3071
+ revoked.push(normalized);
3072
+ }
3073
+ }
3074
+ revoked.sort((a, b) => a.localeCompare(b));
3075
+ return { revoked };
3076
+ }
3077
+ clear() {
3078
+ this.expiresByFile.clear();
3079
+ }
3080
+ /**
3081
+ * Debug-only: return current granted files and expiry.
3082
+ * This intentionally leaks coordination state for diagnosis (safe for prelaunch).
3083
+ */
3084
+ snapshot(nowMs = Date.now()) {
3085
+ const out = [];
3086
+ for (const [file, expiresAtRaw] of this.expiresByFile.entries()) {
3087
+ const expiresAt = Number(expiresAtRaw || 0);
3088
+ if (!Number.isFinite(expiresAt) || expiresAt <= nowMs) continue;
3089
+ out.push({ file, expiresAt, expiresInMs: expiresAt - nowMs });
3090
+ }
3091
+ out.sort((a, b) => a.file.localeCompare(b.file));
3092
+ return out;
3093
+ }
3094
+ }
3095
+
3096
+ class ApiSessionClient extends EventEmitter {
3097
+ token;
3098
+ sessionId;
3099
+ metadata;
3100
+ metadataVersion;
3101
+ agentState;
3102
+ agentStateVersion;
3103
+ socket;
3104
+ pendingMessages = [];
3105
+ pendingMessageCallback = null;
3106
+ pendingOutboundMessages = [];
3107
+ rpcHandlerManager;
3108
+ agentStateLock = new AsyncLock();
3109
+ metadataLock = new AsyncLock();
3110
+ encryptionKey;
3111
+ coordinationLeaseGuard;
3112
+ coordinationLedgerReadAt = 0;
3113
+ docsIndexReadAt = 0;
3114
+ constructor(token, session) {
3115
+ super();
3116
+ this.token = token;
3117
+ this.sessionId = session.id;
3118
+ this.metadata = session.metadata;
3119
+ this.metadataVersion = session.metadataVersion;
3120
+ this.agentState = session.agentState;
3121
+ this.agentStateVersion = session.agentStateVersion;
3122
+ this.encryptionKey = session.encryptionKey;
3123
+ this.coordinationLeaseGuard = new CoordinationLeaseGuard();
3124
+ this.rpcHandlerManager = new RpcHandlerManager({
3125
+ scopePrefix: this.sessionId,
3126
+ encryptionKey: this.encryptionKey,
3127
+ logger: (msg, data) => logger.debug(msg, data)
3128
+ });
3129
+ registerCommonHandlers(this.rpcHandlerManager, this.metadata.path, {
3130
+ serverUrl: configuration.serverUrl,
3131
+ token: this.token,
3132
+ sessionId: this.sessionId,
3133
+ machineId: this.metadata.machineId || "",
3134
+ projectRootPath: this.metadata?.projectRootPath ? String(this.metadata.projectRootPath) : this.metadata.path,
3135
+ workItemId: this.metadata?.workItemId ? String(this.metadata.workItemId) : null,
3136
+ workspaceProjectId: this.metadata?.workspaceProjectId ? String(this.metadata.workspaceProjectId) : null,
3137
+ leaseGuard: this.coordinationLeaseGuard
3138
+ }, { bashEnabled: false });
3139
+ this.startCoordinationAutopilot();
3140
+ this.socket = io(configuration.serverUrl, {
3141
+ auth: {
3142
+ token: this.token,
3143
+ clientType: "session-scoped",
3144
+ sessionId: this.sessionId
3145
+ },
3146
+ path: "/v1/updates",
3147
+ reconnection: true,
3148
+ reconnectionAttempts: Infinity,
3149
+ reconnectionDelay: 1e3,
3150
+ reconnectionDelayMax: 5e3,
3151
+ transports: ["websocket"],
3152
+ withCredentials: true,
3153
+ autoConnect: false
3154
+ });
3155
+ this.socket.on("connect", () => {
3156
+ logger.debug("Socket connected successfully");
3157
+ this.rpcHandlerManager.onSocketConnect(this.socket);
3158
+ this.flushOutboundQueue();
3159
+ });
3160
+ this.socket.on("rpc-request", async (data, callback) => {
3161
+ callback(await this.rpcHandlerManager.handleRequest(data));
3162
+ });
3163
+ this.socket.on("disconnect", (reason) => {
3164
+ logger.debug("[API] Socket disconnected:", reason);
3165
+ this.rpcHandlerManager.onSocketDisconnect();
3166
+ });
3167
+ this.socket.on("connect_error", (error) => {
3168
+ logger.debug("[API] Socket connection error:", error);
3169
+ this.rpcHandlerManager.onSocketDisconnect();
3170
+ });
3171
+ this.socket.on("update", (data) => {
3172
+ try {
3173
+ logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
3174
+ if (!data.body) {
3175
+ logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
3176
+ return;
3177
+ }
3178
+ if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
3179
+ const body = decrypt(this.encryptionKey, decodeBase64(data.body.message.content.c));
3180
+ logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
3181
+ const userResult = UserMessageSchema.safeParse(body);
3182
+ if (userResult.success) {
3183
+ if (this.pendingMessageCallback) {
3184
+ this.pendingMessageCallback(userResult.data);
3185
+ } else {
3186
+ this.pendingMessages.push(userResult.data);
3187
+ }
3188
+ } else {
3189
+ this.emit("message", body);
3190
+ }
3191
+ } else if (data.body.t === "update-session") {
3192
+ if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
3193
+ this.metadata = decrypt(this.encryptionKey, decodeBase64(data.body.metadata.value));
3194
+ this.metadataVersion = data.body.metadata.version;
3195
+ }
3196
+ if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
3197
+ this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, decodeBase64(data.body.agentState.value)) : null;
3198
+ this.agentStateVersion = data.body.agentState.version;
3199
+ }
3200
+ } else if (data.body.t === "update-machine") {
3201
+ logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
3202
+ } else {
3203
+ this.emit("message", data.body);
3204
+ }
3205
+ } catch (error) {
3206
+ logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
3207
+ }
3208
+ });
3209
+ this.socket.on("error", (error) => {
3210
+ logger.debug("[API] Socket error:", error);
3211
+ });
3212
+ this.socket.connect();
3213
+ }
3214
+ /**
3215
+ * Returns the session client's bearer token for calling authenticated HTTP endpoints.
3216
+ * Intended for internal integrations (e.g. uploading artifacts for this session).
3217
+ */
3218
+ getAuthToken() {
3219
+ return this.token;
3220
+ }
3221
+ /**
3222
+ * True when this session has workspaceProjectId/workItemId metadata
3223
+ * (i.e. ledger tools are available and claim enforcement makes sense).
3224
+ */
3225
+ hasCoordinationContext() {
3226
+ const meta = this.metadata;
3227
+ if (!meta) return false;
3228
+ const projectId = String(meta.workspaceProjectId || "").trim();
3229
+ const workItemId = String(meta.workItemId || "").trim();
3230
+ return Boolean(projectId && workItemId);
3231
+ }
3232
+ markCoordinationLedgerRead(atMs = Date.now()) {
3233
+ this.coordinationLedgerReadAt = Number.isFinite(atMs) ? atMs : Date.now();
3234
+ }
3235
+ getCoordinationLedgerReadAt() {
3236
+ return Number(this.coordinationLedgerReadAt || 0);
3237
+ }
3238
+ didReadCoordinationLedgerWithin(ms) {
3239
+ const last = Number(this.coordinationLedgerReadAt || 0);
3240
+ if (!Number.isFinite(last) || last <= 0) return false;
3241
+ return Date.now() - last <= ms;
3242
+ }
3243
+ markDocsIndexRead(atMs = Date.now()) {
3244
+ this.docsIndexReadAt = Number.isFinite(atMs) ? atMs : Date.now();
3245
+ }
3246
+ getDocsIndexReadAt() {
3247
+ return Number(this.docsIndexReadAt || 0);
3248
+ }
3249
+ didReadDocsIndexWithin(ms) {
3250
+ const last = Number(this.docsIndexReadAt || 0);
3251
+ if (!Number.isFinite(last) || last <= 0) return false;
3252
+ return Date.now() - last <= ms;
3253
+ }
3254
+ startCoordinationAutopilot() {
3255
+ const meta = this.metadata;
3256
+ if (!meta) return;
3257
+ const projectId = String(meta.workspaceProjectId || "").trim();
3258
+ const workItemId = String(meta.workItemId || "").trim();
3259
+ const machineId = String(meta.machineId || "").trim();
3260
+ const projectRootPath = String(meta.projectRootPath || meta.path || "").trim();
3261
+ const cwd = String(meta.path || "").trim();
3262
+ if (!projectId || !workItemId || !machineId || !projectRootPath || !cwd) return;
3263
+ String(process.env.FLOCKBAY_COORDINATION_AUTO_SHIP_ON_COMMIT || "").trim() === "1";
3264
+ const serverUrl = configuration.serverUrl.replace(/\/+$/, "");
3265
+ const token = this.token;
3266
+ const sessionId = this.sessionId;
3267
+ const postJson = async (path, body) => {
3268
+ const res = await fetch(`${serverUrl}${path}`, {
3269
+ method: "POST",
3270
+ headers: {
3271
+ Authorization: `Bearer ${token}`,
3272
+ "Content-Type": "application/json"
3273
+ },
3274
+ body: JSON.stringify(body ?? {})
3275
+ });
3276
+ const data = await res.json().catch(() => null);
3277
+ if (!res.ok) {
3278
+ const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
3279
+ throw new Error(msg);
3280
+ }
3281
+ return data;
3282
+ };
3283
+ void postJson(
3284
+ "/v1/coordination/work-items/update",
3285
+ { projectId, workItemId, sessionId }
3286
+ ).catch((err) => logger.debug("[coordination-autopilot] Failed to attach session to work item:", err));
3287
+ logger.debug("[coordination] Enabled", { projectId, workItemId, cwd });
3288
+ return;
3289
+ }
3290
+ flushOutboundQueue() {
3291
+ if (!this.socket.connected) return;
3292
+ if (this.pendingOutboundMessages.length === 0) return;
3293
+ const batch = this.pendingOutboundMessages;
3294
+ this.pendingOutboundMessages = [];
3295
+ logger.debug("[API] Flushing queued outbound messages", { count: batch.length });
3296
+ for (const item of batch) {
3297
+ this.socket.emit("message", { sid: item.sid, message: item.message });
3298
+ }
3299
+ }
3300
+ emitMessageOrQueue(args) {
3301
+ if (this.socket.connected) {
3302
+ this.socket.emit("message", { sid: args.sid, message: args.message });
3303
+ return;
3304
+ }
3305
+ const MAX_QUEUED = 200;
3306
+ if (this.pendingOutboundMessages.length >= MAX_QUEUED) {
3307
+ const head = this.pendingOutboundMessages[0];
3308
+ throw new Error(
3309
+ `Socket not connected; outbound queue full (${MAX_QUEUED}). Oldest=${head?.kind ?? "unknown"} queuedAt=${head?.queuedAt ?? "unknown"}`
3310
+ );
3311
+ }
3312
+ this.pendingOutboundMessages.push({ ...args, queuedAt: Date.now() });
3313
+ logger.debug("[API] Socket not connected; queued outbound message", {
3314
+ kind: args.kind,
3315
+ queued: this.pendingOutboundMessages.length
3316
+ });
3317
+ }
3318
+ onUserMessage(callback) {
3319
+ this.pendingMessageCallback = callback;
3320
+ while (this.pendingMessages.length > 0) {
3321
+ callback(this.pendingMessages.shift());
3322
+ }
3323
+ }
3324
+ /**
3325
+ * Send message to session
3326
+ * @param body - Message body (can be MessageContent or raw content for agent messages)
3327
+ */
3328
+ sendClaudeSessionMessage(body) {
3329
+ let content;
3330
+ if (body.type === "user" && typeof body.message.content === "string" && body.isSidechain !== true && body.isMeta !== true) {
3331
+ content = {
3332
+ role: "user",
3333
+ content: {
3334
+ type: "text",
3335
+ text: body.message.content
3336
+ },
3337
+ meta: {
3338
+ sentFrom: "cli"
3339
+ }
3340
+ };
3341
+ } else {
3342
+ content = {
3343
+ role: "agent",
3344
+ content: {
3345
+ type: "output",
3346
+ data: body
3347
+ // This wraps the entire Claude message
3348
+ },
3349
+ meta: {
3350
+ sentFrom: "cli"
3351
+ }
3352
+ };
3353
+ }
3354
+ logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
3355
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3356
+ this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: "claude" });
3357
+ if (body.type === "assistant" && body.message?.usage) {
3358
+ try {
3359
+ this.sendUsageData(body.message.usage);
3360
+ } catch (error) {
3361
+ logger.debug("[SOCKET] Failed to send usage data:", error);
3362
+ }
3363
+ }
3364
+ if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
3365
+ this.updateMetadata((metadata) => ({
3366
+ ...metadata,
3367
+ summary: {
3368
+ text: body.summary,
3369
+ updatedAt: Date.now()
3370
+ }
3371
+ }));
3372
+ }
3373
+ }
3374
+ sendCodexMessage(body) {
3375
+ let content = {
3376
+ role: "agent",
3377
+ content: {
3378
+ type: "codex",
3379
+ data: body
3380
+ // This wraps the entire Claude message
3381
+ },
3382
+ meta: {
3383
+ sentFrom: "cli"
3384
+ }
3385
+ };
3386
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3387
+ this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `codex:${String(body?.type ?? "unknown")}` });
3388
+ }
3389
+ /**
3390
+ * Send a generic agent message to the session.
3391
+ * Works for any agent type (Gemini, Codex, Claude, etc.)
3392
+ *
3393
+ * @param agentType - The type of agent sending the message (e.g., 'gemini', 'codex', 'claude')
3394
+ * @param body - The message payload
3395
+ */
3396
+ sendAgentMessage(agentType, body) {
3397
+ let content = {
3398
+ role: "agent",
3399
+ content: {
3400
+ type: agentType,
3401
+ data: body
3402
+ },
3403
+ meta: {
3404
+ sentFrom: "cli"
3405
+ }
3406
+ };
3407
+ logger.debug(`[SOCKET] Sending ${agentType} message:`, { type: body.type, hasMessage: !!body.message });
3408
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3409
+ this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `${agentType}:${String(body?.type ?? "unknown")}` });
3410
+ }
3411
+ sendSessionEvent(event, id) {
3412
+ let content = {
3413
+ role: "agent",
3414
+ content: {
3415
+ id: id ?? randomUUID$1(),
3416
+ type: "event",
3417
+ data: event
3418
+ }
3419
+ };
3420
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3421
+ this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `event:${event.type}` });
3422
+ }
3423
+ /**
3424
+ * Send a ping message to keep the connection alive
3425
+ */
3426
+ keepAlive(thinking, mode) {
3427
+ if (process.env.DEBUG) {
3428
+ logger.debug(`[API] Sending keep alive message: ${thinking}`);
3429
+ }
3430
+ this.socket.volatile.emit("session-alive", {
3431
+ sid: this.sessionId,
3432
+ time: Date.now(),
3433
+ thinking,
3434
+ mode
3435
+ });
3436
+ }
3437
+ /**
3438
+ * Send session death message
3439
+ */
3440
+ sendSessionDeath() {
3441
+ this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
3442
+ }
3443
+ /**
3444
+ * Send usage data to the server
3445
+ */
3446
+ sendUsageData(usage) {
3447
+ const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
3448
+ const usageReport = {
3449
+ key: "claude-session",
3450
+ sessionId: this.sessionId,
3451
+ tokens: {
3452
+ total: totalTokens,
3453
+ input: usage.input_tokens,
3454
+ output: usage.output_tokens,
3455
+ cache_creation: usage.cache_creation_input_tokens || 0,
3456
+ cache_read: usage.cache_read_input_tokens || 0
3457
+ },
3458
+ cost: {
3459
+ // TODO: Calculate actual costs based on pricing
3460
+ // For now, using placeholder values
3461
+ total: 0,
3462
+ input: 0,
3463
+ output: 0
3464
+ }
3465
+ };
3466
+ logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
3467
+ this.socket.emit("usage-report", usageReport);
3468
+ }
3469
+ /**
3470
+ * Update session metadata
3471
+ * @param handler - Handler function that returns the updated metadata
3472
+ */
3473
+ updateMetadata(handler) {
3474
+ this.metadataLock.inLock(async () => {
3475
+ await backoff(async () => {
3476
+ let updated = handler(this.metadata);
3477
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, updated)) });
3478
+ if (answer.result === "success") {
3479
+ this.metadata = decrypt(this.encryptionKey, decodeBase64(answer.metadata));
3480
+ this.metadataVersion = answer.version;
3481
+ } else if (answer.result === "version-mismatch") {
3482
+ if (answer.version > this.metadataVersion) {
3483
+ this.metadataVersion = answer.version;
3484
+ this.metadata = decrypt(this.encryptionKey, decodeBase64(answer.metadata));
3485
+ }
3486
+ throw new Error("Metadata version mismatch");
3487
+ } else if (answer.result === "error") ;
3488
+ });
3489
+ });
3490
+ }
3491
+ /**
3492
+ * Update session agent state
3493
+ * @param handler - Handler function that returns the updated agent state
3494
+ */
3495
+ updateAgentState(handler) {
3496
+ logger.debugLargeJson("Updating agent state", this.agentState);
3497
+ this.agentStateLock.inLock(async () => {
3498
+ await backoff(async () => {
3499
+ let updated = handler(this.agentState || {});
3500
+ const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(this.encryptionKey, updated)) : null });
3501
+ if (answer.result === "success") {
3502
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, decodeBase64(answer.agentState)) : null;
3503
+ this.agentStateVersion = answer.version;
3504
+ logger.debug("Agent state updated", this.agentState);
3505
+ } else if (answer.result === "version-mismatch") {
3506
+ if (answer.version > this.agentStateVersion) {
3507
+ this.agentStateVersion = answer.version;
3508
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, decodeBase64(answer.agentState)) : null;
3509
+ }
3510
+ throw new Error("Agent state version mismatch");
3511
+ } else if (answer.result === "error") ;
3512
+ });
3513
+ });
3514
+ }
3515
+ /**
3516
+ * Wait for socket buffer to flush
3517
+ */
3518
+ async flush() {
3519
+ if (!this.socket.connected) {
3520
+ return;
3521
+ }
3522
+ return new Promise((resolve) => {
3523
+ this.socket.emit("ping", () => {
3524
+ resolve();
3525
+ });
3526
+ setTimeout(() => {
3527
+ resolve();
3528
+ }, 1e4);
3529
+ });
3530
+ }
3531
+ async close() {
3532
+ logger.debug("[API] socket.close() called");
3533
+ this.socket.close();
3534
+ }
3535
+ }
3536
+
3537
+ function looksLikeEngineRoot(engineRoot) {
3538
+ if (!engineRoot) return false;
3539
+ return fs__default.existsSync(path.join(engineRoot, "Engine")) && fs__default.existsSync(path.join(engineRoot, "Engine", "Plugins"));
3540
+ }
3541
+ function findFilesNamedUnderDir(options) {
3542
+ const rootDir = options.rootDir;
3543
+ const filename = options.filename;
3544
+ const maxDepth = Math.max(0, Math.floor(options.maxDepth));
3545
+ const results = [];
3546
+ const visit = (dir, depth) => {
3547
+ if (depth > maxDepth) return;
3548
+ let entries = [];
3549
+ try {
3550
+ entries = fs__default.readdirSync(dir, { withFileTypes: true });
3551
+ } catch {
3552
+ return;
3553
+ }
3554
+ for (const entry of entries) {
3555
+ if (entry.name === ".DS_Store") continue;
3556
+ const full = path.join(dir, entry.name);
3557
+ if (entry.isDirectory()) {
3558
+ visit(full, depth + 1);
3559
+ continue;
3560
+ }
3561
+ if (entry.isFile() && entry.name === filename) results.push(full);
3562
+ }
3563
+ };
3564
+ visit(rootDir, 0);
3565
+ return results;
3566
+ }
3567
+ function installUnrealMcpPluginToEngine(engineRootRaw) {
3568
+ const engineRoot = (engineRootRaw || "").trim();
3569
+ if (!looksLikeEngineRoot(engineRoot)) {
3570
+ return {
3571
+ ok: false,
3572
+ errorMessage: `Invalid engine root (expected an Unreal Engine install folder containing Engine/\u2026): ${engineRoot || "(empty)"}`
3573
+ };
3574
+ }
3575
+ const srcDir = unrealMcpPluginSourceDir();
3576
+ if (!fs__default.existsSync(srcDir)) {
3577
+ return { ok: false, errorMessage: `Missing UnrealMCP plugin source folder: ${srcDir}` };
3578
+ }
3579
+ const destDir = path.join(engineRoot, "Engine", "Plugins", "UnrealMCP");
3580
+ try {
3581
+ const enginePluginsDir = path.join(engineRoot, "Engine", "Plugins");
3582
+ const candidates = findFilesNamedUnderDir({ rootDir: enginePluginsDir, filename: "UnrealMCP.uplugin", maxDepth: 6 });
3583
+ const otherCopies = candidates.map((p) => path.resolve(p)).filter((p) => !p.startsWith(path.resolve(destDir) + path.sep));
3584
+ if (otherCopies.length > 0) {
3585
+ return {
3586
+ ok: false,
3587
+ errorMessage: `Found another UnrealMCP plugin install inside this engine folder.
3588
+
3589
+ Unreal cannot load two plugins with the same name, so another copy can shadow Flockbay's plugin and cause errors like:
3590
+ - Unknown command: play_in_editor
3591
+
3592
+ Engine: ${engineRoot}
3593
+ Conflicting plugin(s):
3594
+ ` + otherCopies.map((p) => `- ${p}`).join("\n") + `
3595
+
3596
+ Fix:
3597
+ - Close Unreal Editor
3598
+ - Delete/rename the conflicting plugin folder(s) above (common: Engine/Plugins/Marketplace/UnrealMCP)
3599
+ - Re-run the UnrealMCP install, then restart Unreal Editor so it rebuilds the plugin`
3600
+ };
3601
+ }
3602
+ if (fs__default.existsSync(destDir)) {
3603
+ fs__default.rmSync(destDir, { recursive: true, force: true });
3604
+ }
3605
+ fs__default.mkdirSync(destDir, { recursive: true });
3606
+ fs__default.cpSync(srcDir, destDir, { recursive: true, force: true });
3607
+ const destUpluginPath = path.join(destDir, "UnrealMCP.uplugin");
3608
+ if (!fs__default.existsSync(destUpluginPath)) {
3609
+ return {
3610
+ ok: false,
3611
+ errorMessage: `UnrealMCP install completed but the plugin descriptor is missing.
3612
+ Expected: ${destUpluginPath}`
3613
+ };
3614
+ }
3615
+ const raw = fs__default.readFileSync(destUpluginPath, "utf8");
3616
+ let parsed = null;
3617
+ try {
3618
+ parsed = JSON.parse(raw);
3619
+ } catch {
3620
+ }
3621
+ const friendlyName = typeof parsed?.FriendlyName === "string" ? parsed.FriendlyName : null;
3622
+ const createdBy = typeof parsed?.CreatedBy === "string" ? parsed.CreatedBy : null;
3623
+ if (friendlyName !== "Flockbay MCP" || createdBy !== "Respaced Inc.") {
3624
+ return {
3625
+ ok: false,
3626
+ errorMessage: `UnrealMCP install verification failed: the engine plugin descriptor does not match what Flockbay ships.
3627
+
3628
+ Path: ${destUpluginPath}
3629
+ Observed:
3630
+ - FriendlyName: ${friendlyName ?? "(missing)"}
3631
+ - CreatedBy: ${createdBy ?? "(missing)"}
3632
+
3633
+ Expected:
3634
+ - FriendlyName: Flockbay MCP
3635
+ - CreatedBy: Respaced Inc.
3636
+
3637
+ This usually means you're running an older Flockbay CLI build, or the engine folder wasn't updated.`
3638
+ };
3639
+ }
3640
+ return { ok: true, engineRoot, destDir };
3641
+ } catch (err) {
3642
+ const message = err instanceof Error ? err.message : String(err);
3643
+ return {
3644
+ ok: false,
3645
+ errorMessage: `Failed to copy UnrealMCP plugin into the engine install.
3646
+ From: ${srcDir}
3647
+ To: ${destDir}
3648
+ Error: ${message}`
3649
+ };
3650
+ }
3651
+ }
3652
+
3653
+ function setEnabledByDefault(upluginPath, enabled) {
3654
+ try {
3655
+ const raw = fs__default.readFileSync(upluginPath, "utf8");
3656
+ const json = JSON.parse(raw);
3657
+ json.EnabledByDefault = enabled;
3658
+ fs__default.writeFileSync(upluginPath, JSON.stringify(json, null, " ") + "\n", "utf8");
3659
+ return { ok: true };
3660
+ } catch (err) {
3661
+ const message = err instanceof Error ? err.message : String(err);
3662
+ return { ok: false, error: message };
3663
+ }
3664
+ }
3665
+ function runUatPath(engineRoot) {
3666
+ if (process.platform === "win32") {
3667
+ return path.join(engineRoot, "Engine", "Build", "BatchFiles", "RunUAT.bat");
3668
+ }
3669
+ return path.join(engineRoot, "Engine", "Build", "BatchFiles", "RunUAT.sh");
3670
+ }
3671
+ function targetPlatform() {
3672
+ if (process.platform === "win32") return "Win64";
3673
+ if (process.platform === "darwin") return "Mac";
3674
+ return "Linux";
3675
+ }
3676
+ function resolvePackagedPluginDir(packageDir) {
3677
+ const nested = path.join(packageDir, "UnrealMCP");
3678
+ if (fs__default.existsSync(path.join(nested, "UnrealMCP.uplugin"))) return { ok: true, dir: nested };
3679
+ if (fs__default.existsSync(path.join(packageDir, "UnrealMCP.uplugin"))) return { ok: true, dir: packageDir };
3680
+ return {
3681
+ ok: false,
3682
+ error: `Build completed, but the packaged plugin output was not found.
3683
+ Expected either:
3684
+ - ${path.join(nested, "UnrealMCP.uplugin")}
3685
+ - ${path.join(packageDir, "UnrealMCP.uplugin")}`
3686
+ };
3687
+ }
3688
+ async function buildAndInstallUnrealMcpPlugin(options) {
3689
+ const engineRoot = options.engineRoot.trim();
3690
+ const pluginDir = path.join(engineRoot, "Engine", "Plugins", "UnrealMCP");
3691
+ const pluginUpluginPath = path.join(pluginDir, "UnrealMCP.uplugin");
3692
+ if (!fs__default.existsSync(pluginUpluginPath)) {
3693
+ return { ok: false, errorMessage: `Missing plugin file: ${pluginUpluginPath}` };
3694
+ }
3695
+ const uat = runUatPath(engineRoot);
3696
+ if (!fs__default.existsSync(uat)) {
3697
+ return { ok: false, errorMessage: `Missing Unreal build tool (RunUAT): ${uat}` };
3698
+ }
3699
+ const runtimeDir = path.join(options.flockbayHomeDir, "unreal-mcp");
3700
+ fs__default.mkdirSync(runtimeDir, { recursive: true });
3701
+ const buildLogPath = path.join(runtimeDir, `unreal_mcp_build_${Date.now()}.log`);
3702
+ const packageDir = path.join(runtimeDir, `plugin_package_${Date.now()}`);
3703
+ const args = [
3704
+ "BuildPlugin",
3705
+ `-Plugin=${pluginUpluginPath}`,
3706
+ `-Package=${packageDir}`,
3707
+ "-Rocket",
3708
+ `-TargetPlatforms=${targetPlatform()}`
3709
+ ];
3710
+ const logStream = fs__default.createWriteStream(buildLogPath, { flags: "a" });
3711
+ logStream.write(`engineRoot: ${engineRoot}
3712
+ `);
3713
+ logStream.write(`uat: ${uat}
3714
+ `);
3715
+ logStream.write(`args: ${JSON.stringify(args)}
3716
+
3717
+ `);
3718
+ const child = process.platform === "win32" ? spawn$1("cmd.exe", ["/c", uat, ...args], { stdio: ["ignore", "pipe", "pipe"] }) : spawn$1(uat, args, { stdio: ["ignore", "pipe", "pipe"] });
3719
+ child.stdout?.on("data", (chunk) => logStream.write(chunk));
3720
+ child.stderr?.on("data", (chunk) => logStream.write(chunk));
3721
+ const exitCode = await new Promise((resolve) => child.on("close", resolve));
3722
+ logStream.end(`
3723
+ exitCode: ${exitCode}
3724
+ `);
3725
+ if (exitCode !== 0) {
3726
+ return {
3727
+ ok: false,
3728
+ buildLogPath,
3729
+ errorMessage: `Failed to build the Unreal bridge.
3730
+ Log: ${buildLogPath}
3731
+ Make sure your compiler tools are installed (Xcode on macOS, Visual Studio Build Tools on Windows).`
3732
+ };
3733
+ }
3734
+ const resolved = resolvePackagedPluginDir(packageDir);
3735
+ if (!resolved.ok) return { ok: false, buildLogPath, errorMessage: resolved.error };
3736
+ const packagedPluginDir = resolved.dir;
3737
+ try {
3738
+ fs__default.cpSync(packagedPluginDir, pluginDir, { recursive: true, force: true });
3739
+ } catch (err) {
3740
+ const message = err instanceof Error ? err.message : String(err);
3741
+ return {
3742
+ ok: false,
3743
+ buildLogPath,
3744
+ errorMessage: `Built the plugin, but failed to copy the compiled output into the engine install.
3745
+ From: ${packagedPluginDir}
3746
+ To: ${pluginDir}
3747
+ Error: ${message}`
3748
+ };
3749
+ }
3750
+ const binariesDir = path.join(pluginDir, "Binaries");
3751
+ if (!fs__default.existsSync(binariesDir)) {
3752
+ return {
3753
+ ok: false,
3754
+ buildLogPath,
3755
+ errorMessage: `Build reported success, but the plugin binaries folder is missing.
3756
+ Expected: ${binariesDir}
3757
+ Log: ${buildLogPath}`
3758
+ };
3759
+ }
3760
+ const setEnabled = setEnabledByDefault(pluginUpluginPath, true);
3761
+ if (!setEnabled.ok) {
3762
+ return {
3763
+ ok: false,
3764
+ buildLogPath,
3765
+ errorMessage: `Built the plugin, but failed to enable it.
3766
+ Plugin: ${pluginUpluginPath}
3767
+ Error: ${setEnabled.error}
3768
+ Log: ${buildLogPath}`
3769
+ };
3770
+ }
3771
+ return { ok: true, buildLogPath };
3772
+ }
3773
+
3774
+ class ApiMachineClient {
3775
+ constructor(token, machine) {
3776
+ this.token = token;
3777
+ this.machine = machine;
3778
+ this.rpcHandlerManager = new RpcHandlerManager({
3779
+ scopePrefix: this.machine.id,
3780
+ encryptionKey: this.machine.encryptionKey,
3781
+ logger: (msg, data) => logger.debug(msg, data)
3782
+ });
3783
+ const rootDir = this.machine?.metadata?.homeDir || os.homedir() || process.cwd();
3784
+ registerCommonHandlers(this.rpcHandlerManager, rootDir, {
3785
+ serverUrl: configuration.serverUrl,
3786
+ token: this.token,
3787
+ sessionId: "",
3788
+ machineId: this.machine.id,
3789
+ projectRootPath: rootDir
3790
+ });
3791
+ this.rpcHandlerManager.registerHandler(
3792
+ "unreal-mcp-install-engine-plugin",
3793
+ async (params) => {
3794
+ const engineRoot = String(params?.engineRoot || "").trim();
3795
+ const result = installUnrealMcpPluginToEngine(engineRoot);
3796
+ if (result.ok) {
3797
+ if (Boolean(params?.build)) {
3798
+ const built = await buildAndInstallUnrealMcpPlugin({
3799
+ engineRoot: result.engineRoot,
3800
+ flockbayHomeDir: this.machine?.metadata?.flockbayHomeDir || os.homedir()
3801
+ });
3802
+ if (!built.ok) {
3803
+ return { success: false, error: built.errorMessage };
3804
+ }
3805
+ return {
3806
+ success: true,
3807
+ engineRoot: result.engineRoot,
3808
+ destDir: result.destDir,
3809
+ buildLogPath: built.buildLogPath
3810
+ };
3811
+ }
3812
+ return { success: true, engineRoot: result.engineRoot, destDir: result.destDir };
3813
+ }
3814
+ return { success: false, error: result.errorMessage };
3815
+ }
3816
+ );
3817
+ }
3818
+ socket;
3819
+ keepAliveInterval = null;
3820
+ rpcHandlerManager;
3821
+ setRPCHandlers({
3822
+ spawnSession,
3823
+ stopSession,
3824
+ requestShutdown
3825
+ }) {
3826
+ const spawnHandler = async (params) => {
3827
+ const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token, coordination } = params || {};
3828
+ logger.debug(`[API MACHINE] Spawning session with params: ${JSON.stringify(params)}`);
3829
+ if (!directory) {
3830
+ throw new Error("Directory is required");
3831
+ }
3832
+ const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token, coordination });
3833
+ switch (result.type) {
3834
+ case "success":
3835
+ logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
3836
+ return { type: "success", sessionId: result.sessionId };
3837
+ case "requestToApproveDirectoryCreation":
3838
+ logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
3839
+ return { type: "requestToApproveDirectoryCreation", directory: result.directory };
3840
+ case "error":
3841
+ throw new Error(result.errorMessage);
3842
+ }
3843
+ };
3844
+ this.rpcHandlerManager.registerHandler("spawn-session", spawnHandler);
3845
+ this.rpcHandlerManager.registerHandler("stop-session", (params) => {
3846
+ const { sessionId } = params || {};
3847
+ if (!sessionId) {
3848
+ throw new Error("Session ID is required");
3849
+ }
3850
+ const success = stopSession(sessionId);
3851
+ if (!success) {
3852
+ throw new Error("Session not found or failed to stop");
3853
+ }
3854
+ logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
3855
+ return { message: "Session stopped" };
3856
+ });
3857
+ this.rpcHandlerManager.registerHandler("stop-daemon", () => {
3858
+ logger.debug("[API MACHINE] Received stop-daemon RPC request");
3859
+ setTimeout(() => {
3860
+ logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
3861
+ requestShutdown();
3862
+ }, 100);
3863
+ return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
3864
+ });
3865
+ }
3866
+ /**
3867
+ * Update machine metadata
3868
+ * Currently unused, changes from the mobile client are more likely
3869
+ * for example to set a custom name.
3870
+ */
3871
+ async updateMachineMetadata(handler) {
3872
+ await backoff(async () => {
3873
+ const updated = handler(this.machine.metadata);
3874
+ const answer = await this.socket.emitWithAck("machine-update-metadata", {
3875
+ machineId: this.machine.id,
3876
+ metadata: encodeBase64(encrypt(this.machine.encryptionKey, updated)),
3877
+ expectedVersion: this.machine.metadataVersion
3878
+ });
3879
+ if (answer.result === "success") {
3880
+ this.machine.metadata = decrypt(this.machine.encryptionKey, decodeBase64(answer.metadata));
3881
+ this.machine.metadataVersion = answer.version;
3882
+ logger.debug("[API MACHINE] Metadata updated successfully");
3883
+ } else if (answer.result === "version-mismatch") {
3884
+ if (answer.version > this.machine.metadataVersion) {
3885
+ this.machine.metadataVersion = answer.version;
3886
+ this.machine.metadata = decrypt(this.machine.encryptionKey, decodeBase64(answer.metadata));
3887
+ }
3888
+ throw new Error("Metadata version mismatch");
3889
+ }
3890
+ });
3891
+ }
3892
+ /**
3893
+ * Update daemon state (runtime info) - similar to session updateAgentState
3894
+ * Simplified without lock - relies on backoff for retry
3895
+ */
3896
+ async updateDaemonState(handler) {
3897
+ await backoff(async () => {
3898
+ const updated = handler(this.machine.daemonState);
3899
+ const answer = await this.socket.emitWithAck("machine-update-state", {
3900
+ machineId: this.machine.id,
3901
+ daemonState: encodeBase64(encrypt(this.machine.encryptionKey, updated)),
3902
+ expectedVersion: this.machine.daemonStateVersion
3903
+ });
3904
+ if (answer.result === "success") {
3905
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(answer.daemonState));
3906
+ this.machine.daemonStateVersion = answer.version;
3907
+ logger.debug("[API MACHINE] Daemon state updated successfully");
3908
+ } else if (answer.result === "version-mismatch") {
3909
+ if (answer.version > this.machine.daemonStateVersion) {
3910
+ this.machine.daemonStateVersion = answer.version;
3911
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(answer.daemonState));
3912
+ }
3913
+ throw new Error("Daemon state version mismatch");
3914
+ }
3915
+ });
3916
+ }
3917
+ connect() {
3918
+ const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
3919
+ logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
3920
+ const machineMetadataEncrypted = encodeBase64(encrypt(this.machine.encryptionKey, this.machine.metadata));
3921
+ const daemonStateEncrypted = this.machine.daemonState ? encodeBase64(encrypt(this.machine.encryptionKey, this.machine.daemonState)) : "";
3922
+ const dataEncryptionKey = String(this.machine.dataEncryptionKey || "").trim();
3923
+ if (!dataEncryptionKey) {
3924
+ throw new Error("Missing machine dataEncryptionKey (DEK)");
3925
+ }
3926
+ this.socket = io(serverUrl, {
3927
+ transports: ["websocket"],
3928
+ auth: {
3929
+ token: this.token,
3930
+ clientType: "machine-scoped",
3931
+ machineId: this.machine.id,
3932
+ dataEncryptionKey,
3933
+ machineMetadata: machineMetadataEncrypted,
3934
+ daemonState: daemonStateEncrypted
3935
+ },
3936
+ path: "/v1/updates",
3937
+ reconnection: true,
3938
+ reconnectionDelay: 1e3,
3939
+ reconnectionDelayMax: 5e3
3940
+ });
3941
+ this.socket.on("connect", () => {
3942
+ logger.debug("[API MACHINE] Connected to server");
3943
+ this.updateDaemonState((state) => ({
3944
+ ...state,
3945
+ status: "running",
3946
+ pid: process.pid,
3947
+ httpPort: this.machine.daemonState?.httpPort,
3948
+ startedAt: Date.now()
3949
+ }));
3950
+ this.rpcHandlerManager.onSocketConnect(this.socket);
3951
+ this.startKeepAlive();
3952
+ });
3953
+ this.socket.on("disconnect", () => {
3954
+ logger.debug("[API MACHINE] Disconnected from server");
3955
+ this.rpcHandlerManager.onSocketDisconnect();
3956
+ this.stopKeepAlive();
3957
+ });
3958
+ this.socket.on("rpc-request", async (data, callback) => {
3959
+ logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
3960
+ callback(await this.rpcHandlerManager.handleRequest(data));
3961
+ });
3962
+ this.socket.on("update", (data) => {
3963
+ if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
3964
+ const update = data.body;
3965
+ if (update.metadata) {
3966
+ logger.debug("[API MACHINE] Received external metadata update");
3967
+ this.machine.metadata = decrypt(this.machine.encryptionKey, decodeBase64(update.metadata.value));
3968
+ this.machine.metadataVersion = update.metadata.version;
3969
+ }
3970
+ if (update.daemonState) {
3971
+ logger.debug("[API MACHINE] Received external daemon state update");
3972
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(update.daemonState.value));
3973
+ this.machine.daemonStateVersion = update.daemonState.version;
3974
+ }
3975
+ } else {
3976
+ logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
3977
+ }
3978
+ });
3979
+ this.socket.on("connect_error", (error) => {
3980
+ logger.debug(`[API MACHINE] Connection error: ${error.message}`);
3981
+ });
3982
+ this.socket.io.on("error", (error) => {
3983
+ logger.debug("[API MACHINE] Socket error:", error);
3984
+ });
3985
+ }
3986
+ startKeepAlive() {
3987
+ this.stopKeepAlive();
3988
+ this.keepAliveInterval = setInterval(() => {
3989
+ const payload = {
3990
+ machineId: this.machine.id,
3991
+ time: Date.now()
3992
+ };
3993
+ if (process.env.DEBUG) {
3994
+ logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
3995
+ }
3996
+ this.socket.emit("machine-alive", payload);
3997
+ }, 2e4);
3998
+ logger.debug("[API MACHINE] Keep-alive started (20s interval)");
3999
+ }
4000
+ stopKeepAlive() {
4001
+ if (this.keepAliveInterval) {
4002
+ clearInterval(this.keepAliveInterval);
4003
+ this.keepAliveInterval = null;
4004
+ logger.debug("[API MACHINE] Keep-alive stopped");
4005
+ }
4006
+ }
4007
+ shutdown() {
4008
+ logger.debug("[API MACHINE] Shutting down");
4009
+ this.stopKeepAlive();
4010
+ if (this.socket) {
4011
+ this.socket.close();
4012
+ logger.debug("[API MACHINE] Socket closed");
4013
+ }
4014
+ }
4015
+ }
4016
+
4017
+ class PushNotificationClient {
4018
+ token;
4019
+ baseUrl;
4020
+ expo;
4021
+ constructor(token, baseUrl = "https://api-internal.flockbay.com") {
4022
+ this.token = token;
4023
+ this.baseUrl = baseUrl;
4024
+ this.expo = new Expo();
4025
+ }
4026
+ /**
4027
+ * Fetch all push tokens for the authenticated user
4028
+ */
4029
+ async fetchPushTokens() {
4030
+ try {
4031
+ const response = await axios.get(
4032
+ `${this.baseUrl}/v1/push-tokens`,
4033
+ {
4034
+ headers: {
4035
+ "Authorization": `Bearer ${this.token}`,
4036
+ "Content-Type": "application/json"
4037
+ }
4038
+ }
4039
+ );
4040
+ logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
4041
+ response.data.tokens.forEach((token, index) => {
4042
+ logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
4043
+ });
4044
+ return response.data.tokens;
4045
+ } catch (error) {
4046
+ logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
4047
+ throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
4048
+ }
4049
+ }
4050
+ /**
4051
+ * Send push notification via Expo Push API with retry
4052
+ * @param messages - Array of push messages to send
4053
+ */
4054
+ async sendPushNotifications(messages) {
4055
+ logger.debug(`Sending ${messages.length} push notifications`);
4056
+ const validMessages = messages.filter((message) => {
4057
+ if (Array.isArray(message.to)) {
4058
+ return message.to.every((token) => Expo.isExpoPushToken(token));
4059
+ }
4060
+ return Expo.isExpoPushToken(message.to);
4061
+ });
4062
+ if (validMessages.length === 0) {
4063
+ logger.debug("No valid Expo push tokens found");
4064
+ return;
4065
+ }
4066
+ const chunks = this.expo.chunkPushNotifications(validMessages);
4067
+ for (const chunk of chunks) {
4068
+ const startTime = Date.now();
4069
+ const timeout = 3e5;
4070
+ let attempt = 0;
4071
+ while (true) {
4072
+ try {
4073
+ const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
4074
+ const errors = ticketChunk.filter((ticket) => ticket.status === "error");
4075
+ if (errors.length > 0) {
4076
+ const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
4077
+ logger.debug("[PUSH] Some notifications failed:", errorDetails);
4078
+ }
4079
+ if (errors.length === ticketChunk.length) {
4080
+ throw new Error("All push notifications in chunk failed");
4081
+ }
4082
+ break;
4083
+ } catch (error) {
4084
+ const elapsed = Date.now() - startTime;
4085
+ if (elapsed >= timeout) {
4086
+ logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
4087
+ break;
4088
+ }
4089
+ attempt++;
4090
+ const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
4091
+ const remainingTime = timeout - elapsed;
4092
+ const waitTime = Math.min(delay, remainingTime);
4093
+ if (waitTime > 0) {
4094
+ logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
4095
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
4096
+ }
4097
+ }
4098
+ }
4099
+ }
4100
+ logger.debug(`Push notifications sent successfully`);
4101
+ }
4102
+ /**
4103
+ * Send a push notification to all registered devices for the user
4104
+ * @param title - Notification title
4105
+ * @param body - Notification body
4106
+ * @param data - Additional data to send with the notification
4107
+ */
4108
+ sendToAllDevices(title, body, data) {
4109
+ logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
4110
+ (async () => {
4111
+ try {
4112
+ logger.debug("[PUSH] Fetching push tokens...");
4113
+ const tokens = await this.fetchPushTokens();
4114
+ logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
4115
+ tokens.forEach((token, index) => {
4116
+ logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
4117
+ });
4118
+ if (tokens.length === 0) {
4119
+ logger.debug("No push tokens found for user");
4120
+ return;
4121
+ }
4122
+ const messages = tokens.map((token, index) => {
4123
+ logger.debug(`[PUSH] Creating message ${index + 1} for token`);
4124
+ return {
4125
+ to: token.token,
4126
+ title,
4127
+ body,
4128
+ data,
4129
+ sound: "default",
4130
+ priority: "high"
4131
+ };
4132
+ });
4133
+ logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
4134
+ await this.sendPushNotifications(messages);
4135
+ logger.debug("[PUSH] Push notifications sent successfully");
4136
+ } catch (error) {
4137
+ logger.debug("[PUSH] Error sending to all devices:", error);
4138
+ }
4139
+ })();
4140
+ }
4141
+ }
4142
+
4143
+ class ApiClient {
4144
+ static async create(credential) {
4145
+ return new ApiClient(credential);
4146
+ }
4147
+ credential;
4148
+ pushClient;
4149
+ constructor(credential) {
4150
+ this.credential = credential;
4151
+ this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
4152
+ }
4153
+ /**
4154
+ * Create a new session or load existing one with the given tag
4155
+ */
4156
+ async getOrCreateSession(opts) {
4157
+ const encryptionKey = getRandomBytes(32);
4158
+ const encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
4159
+ const dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
4160
+ dataEncryptionKey.set([0], 0);
4161
+ dataEncryptionKey.set(encryptedDataKey, 1);
4162
+ try {
4163
+ const response = await axios.post(
4164
+ `${configuration.serverUrl}/v1/sessions`,
4165
+ {
4166
+ tag: opts.tag,
4167
+ metadata: encodeBase64(encrypt(encryptionKey, opts.metadata)),
4168
+ agentState: opts.state ? encodeBase64(encrypt(encryptionKey, opts.state)) : null,
4169
+ dataEncryptionKey: encodeBase64(dataEncryptionKey)
4170
+ },
4171
+ {
4172
+ headers: {
4173
+ "Authorization": `Bearer ${this.credential.token}`,
4174
+ "Content-Type": "application/json"
4175
+ },
4176
+ timeout: 6e4
4177
+ // 1 minute timeout for very bad network connections
4178
+ }
4179
+ );
4180
+ logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
4181
+ let raw = response.data.session;
4182
+ let session = {
4183
+ id: raw.id,
4184
+ seq: raw.seq,
4185
+ metadata: decrypt(encryptionKey, decodeBase64(raw.metadata)),
4186
+ metadataVersion: raw.metadataVersion,
4187
+ agentState: raw.agentState ? decrypt(encryptionKey, decodeBase64(raw.agentState)) : null,
4188
+ agentStateVersion: raw.agentStateVersion,
4189
+ encryptionKey
4190
+ };
4191
+ return session;
4192
+ } catch (error) {
4193
+ logger.debug("[API] [ERROR] Failed to get or create session:", error);
4194
+ throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
4195
+ }
4196
+ }
4197
+ /**
4198
+ * Register or update machine with the server
4199
+ * Returns the current machine state from the server with decrypted metadata and daemonState
4200
+ */
4201
+ async getOrCreateMachine(opts) {
4202
+ const encryptionKey = this.credential.encryption.machineKey;
4203
+ const encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
4204
+ const dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
4205
+ dataEncryptionKey.set([0], 0);
4206
+ dataEncryptionKey.set(encryptedDataKey, 1);
4207
+ const response = await axios.post(
4208
+ `${configuration.serverUrl}/v1/machines`,
4209
+ {
4210
+ id: opts.machineId,
4211
+ metadata: encodeBase64(encrypt(encryptionKey, opts.metadata)),
4212
+ daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, opts.daemonState)) : void 0,
4213
+ dataEncryptionKey: encodeBase64(dataEncryptionKey)
4214
+ },
4215
+ {
4216
+ headers: {
4217
+ "Authorization": `Bearer ${this.credential.token}`,
4218
+ "Content-Type": "application/json"
4219
+ },
4220
+ timeout: 6e4
4221
+ // 1 minute timeout for very bad network connections
4222
+ }
4223
+ );
4224
+ if (response.status !== 200) {
4225
+ console.error(chalk.red(`[API] Failed to create machine: ${response.statusText}`));
4226
+ console.log(chalk.yellow(`[API] Failed to create machine: ${response.statusText}, most likely you have re-authenticated, but you still have a machine associated with the old account. Now we are trying to re-associate the machine with the new account. That is not allowed. Please run 'flockbay doctor clean' to clean up your local state, and try your original command again. Please create an issue on GitHub if this is causing you problems. We apologize for the inconvenience.`));
4227
+ process.exit(1);
4228
+ }
4229
+ const raw = response.data.machine;
4230
+ logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
4231
+ const machine = {
4232
+ id: raw.id,
4233
+ dataEncryptionKey: String(raw.dataEncryptionKey || ""),
4234
+ encryptionKey,
4235
+ metadata: raw.metadata ? decrypt(encryptionKey, decodeBase64(raw.metadata)) : null,
4236
+ metadataVersion: raw.metadataVersion || 0,
4237
+ daemonState: raw.daemonState ? decrypt(encryptionKey, decodeBase64(raw.daemonState)) : null,
4238
+ daemonStateVersion: raw.daemonStateVersion || 0
4239
+ };
4240
+ return machine;
4241
+ }
4242
+ /**
4243
+ * Fetch an existing machine by id.
4244
+ * Useful when the daemon needs the latest server-side machine metadata (e.g. dev toggles).
4245
+ */
4246
+ async getMachine(machineId) {
4247
+ const id = String(machineId || "").trim();
4248
+ if (!id) {
4249
+ throw new Error("Machine id is required");
4250
+ }
4251
+ const encryptionKey = this.credential.encryption.machineKey;
4252
+ const response = await axios.get(
4253
+ `${configuration.serverUrl}/v1/machines/${encodeURIComponent(id)}`,
4254
+ {
4255
+ headers: {
4256
+ "Authorization": `Bearer ${this.credential.token}`,
4257
+ "Content-Type": "application/json"
4258
+ },
4259
+ timeout: 6e4
4260
+ }
4261
+ );
4262
+ if (response.status !== 200) {
4263
+ throw new Error(`Failed to fetch machine: ${response.statusText}`);
4264
+ }
4265
+ const raw = response.data?.machine;
4266
+ if (!raw?.id) {
4267
+ throw new Error("Invalid machine response");
4268
+ }
4269
+ const machine = {
4270
+ id: raw.id,
4271
+ dataEncryptionKey: String(raw.dataEncryptionKey || ""),
4272
+ encryptionKey,
4273
+ metadata: raw.metadata ? decrypt(encryptionKey, decodeBase64(raw.metadata)) : null,
4274
+ metadataVersion: raw.metadataVersion || 0,
4275
+ daemonState: raw.daemonState ? decrypt(encryptionKey, decodeBase64(raw.daemonState)) : null,
4276
+ daemonStateVersion: raw.daemonStateVersion || 0
4277
+ };
4278
+ return machine;
4279
+ }
4280
+ sessionSyncClient(session) {
4281
+ return new ApiSessionClient(this.credential.token, session);
4282
+ }
4283
+ machineSyncClient(machine) {
4284
+ return new ApiMachineClient(this.credential.token, machine);
4285
+ }
4286
+ push() {
4287
+ return this.pushClient;
4288
+ }
4289
+ /**
4290
+ * Register a vendor API token with the server
4291
+ * The token is sent as a JSON string - server handles encryption
4292
+ */
4293
+ async registerVendorToken(vendor, apiKey) {
4294
+ try {
4295
+ const response = await axios.post(
4296
+ `${configuration.serverUrl}/v1/connect/${vendor}/register`,
4297
+ {
4298
+ token: JSON.stringify(apiKey)
4299
+ },
4300
+ {
4301
+ headers: {
4302
+ "Authorization": `Bearer ${this.credential.token}`,
4303
+ "Content-Type": "application/json"
4304
+ },
4305
+ timeout: 5e3
4306
+ }
4307
+ );
4308
+ if (response.status !== 200 && response.status !== 201) {
4309
+ throw new Error(`Server returned status ${response.status}`);
4310
+ }
4311
+ logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
4312
+ } catch (error) {
4313
+ logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
4314
+ throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
4315
+ }
4316
+ }
4317
+ /**
4318
+ * Get vendor API token from the server
4319
+ * Returns the token if it exists, null otherwise
4320
+ */
4321
+ async getVendorToken(vendor) {
4322
+ try {
4323
+ const response = await axios.get(
4324
+ `${configuration.serverUrl}/v1/connect/${vendor}/token`,
4325
+ {
4326
+ headers: {
4327
+ "Authorization": `Bearer ${this.credential.token}`,
4328
+ "Content-Type": "application/json"
4329
+ },
4330
+ timeout: 5e3
4331
+ }
4332
+ );
4333
+ if (response.status === 404) {
4334
+ logger.debug(`[API] No vendor token found for ${vendor}`);
4335
+ return null;
4336
+ }
4337
+ if (response.status !== 200) {
4338
+ throw new Error(`Server returned status ${response.status}`);
4339
+ }
4340
+ logger.debug(`[API] Raw vendor token response:`, {
4341
+ status: response.status,
4342
+ dataKeys: Object.keys(response.data || {}),
4343
+ hasToken: "token" in (response.data || {}),
4344
+ tokenType: typeof response.data?.token,
4345
+ present: response.data?.present
4346
+ });
4347
+ const present = typeof response.data?.present === "boolean" ? response.data.present : null;
4348
+ const tokenField = response.data?.token;
4349
+ if (present === true && (tokenField === null || tokenField === void 0 || tokenField === "")) {
4350
+ logger.debug(`[API] Vendor token for ${vendor} is present but redacted by the server (FLOCKBAY_REDACT_CONNECTED_TOKENS=1).`);
4351
+ return null;
4352
+ }
4353
+ let tokenData = null;
4354
+ if (response.data?.token) {
4355
+ if (typeof response.data.token === "string") {
4356
+ try {
4357
+ tokenData = JSON.parse(response.data.token);
4358
+ } catch (parseError) {
4359
+ logger.debug(`[API] Failed to parse token as JSON, using as string:`, parseError);
4360
+ tokenData = response.data.token;
4361
+ }
4362
+ } else if (response.data.token !== null) {
4363
+ tokenData = response.data.token;
4364
+ } else {
4365
+ logger.debug(`[API] Token is null for ${vendor}, treating as not found`);
4366
+ return null;
4367
+ }
4368
+ } else if (response.data && typeof response.data === "object") {
4369
+ if (response.data.token === null && response.data.present === false) {
4370
+ logger.debug(`[API] Response contains present=false and null token for ${vendor}, treating as not found`);
4371
+ return null;
4372
+ }
4373
+ if (response.data.token === null && Object.keys(response.data).length === 1) {
4374
+ logger.debug(`[API] Response contains only null token for ${vendor}, treating as not found`);
4375
+ return null;
4376
+ }
4377
+ tokenData = response.data;
4378
+ }
4379
+ if (tokenData === null || tokenData && typeof tokenData === "object" && tokenData.token === null && Object.keys(tokenData).length === 1) {
4380
+ logger.debug(`[API] Token data is null for ${vendor}`);
4381
+ return null;
4382
+ }
4383
+ if (tokenData && typeof tokenData === "object" && "token" in tokenData && "present" in tokenData) {
4384
+ logger.debug(`[API] Received token metadata object for ${vendor}; returning parsed token field only.`);
4385
+ return tokenData.token ? tokenData.token : null;
4386
+ }
4387
+ logger.debug(`[API] Vendor token for ${vendor} retrieved successfully`, {
4388
+ tokenDataType: typeof tokenData,
4389
+ tokenDataKeys: tokenData && typeof tokenData === "object" ? Object.keys(tokenData) : "not an object"
4390
+ });
4391
+ return tokenData;
4392
+ } catch (error) {
4393
+ if (error.response?.status === 404) {
4394
+ logger.debug(`[API] No vendor token found for ${vendor}`);
4395
+ return null;
4396
+ }
4397
+ logger.debug(`[API] [ERROR] Failed to get vendor token:`, error);
4398
+ return null;
4399
+ }
4400
+ }
4401
+ }
4402
+
4403
+ const UsageSchema = z$1.object({
4404
+ input_tokens: z$1.number().int().nonnegative(),
4405
+ cache_creation_input_tokens: z$1.number().int().nonnegative().optional(),
4406
+ cache_read_input_tokens: z$1.number().int().nonnegative().optional(),
4407
+ output_tokens: z$1.number().int().nonnegative(),
4408
+ service_tier: z$1.string().nullish()
4409
+ }).passthrough();
4410
+ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
4411
+ // User message - validates uuid and message.content
4412
+ z$1.object({
4413
+ type: z$1.literal("user"),
4414
+ isSidechain: z$1.boolean().optional(),
4415
+ isMeta: z$1.boolean().optional(),
4416
+ uuid: z$1.string(),
4417
+ // Used in getMessageKey()
4418
+ message: z$1.object({
4419
+ content: z$1.union([z$1.string(), z$1.any()])
4420
+ // Used in sessionScanner.ts
4421
+ }).passthrough()
4422
+ }).passthrough(),
4423
+ // Assistant message - only validates uuid and type
4424
+ // message object is optional to handle synthetic error messages (isApiErrorMessage: true)
4425
+ // which may have different structure than normal assistant messages
4426
+ z$1.object({
4427
+ uuid: z$1.string(),
4428
+ type: z$1.literal("assistant"),
4429
+ message: z$1.object({
4430
+ usage: UsageSchema.optional()
4431
+ // Used in apiSession.ts
4432
+ }).passthrough().optional()
4433
+ }).passthrough(),
4434
+ // Summary message - validates summary and leafUuid
4435
+ z$1.object({
4436
+ type: z$1.literal("summary"),
4437
+ summary: z$1.string(),
4438
+ // Used in apiSession.ts
4439
+ leafUuid: z$1.string()
4440
+ // Used in getMessageKey()
4441
+ }).passthrough(),
4442
+ // System message - validates uuid
4443
+ z$1.object({
4444
+ type: z$1.literal("system"),
4445
+ uuid: z$1.string()
4446
+ // Used in getMessageKey()
4447
+ }).passthrough()
4448
+ ]);
4449
+
4450
+ export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, AsyncLock as f, readDaemonState as g, clearDaemonState as h, readCredentials as i, encodeBase64 as j, encodeBase64Url as k, logger as l, decodeBase64 as m, unrealMcpPythonDir as n, acquireDaemonLock as o, projectPath as p, writeDaemonState as q, readSettings as r, releaseDaemonLock as s, sendUnrealMcpTcpCommand as t, updateSettings as u, clearCredentials as v, writeCredentialsDataKey as w, clearMachineId as x, installUnrealMcpPluginToEngine as y, getLatestDaemonLog as z };