flockbay 0.10.15 → 0.10.16

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 (31) hide show
  1. package/dist/codex/flockbayMcpStdioBridge.cjs +339 -0
  2. package/dist/codex/flockbayMcpStdioBridge.mjs +339 -0
  3. package/dist/{index--o4BPz5o.cjs → index-Cau-_Qvn.cjs} +2683 -609
  4. package/dist/{index-CUp3juDS.mjs → index-DtmFQzXY.mjs} +2684 -611
  5. package/dist/index.cjs +3 -5
  6. package/dist/index.mjs +3 -5
  7. package/dist/lib.cjs +7 -9
  8. package/dist/lib.d.cts +219 -531
  9. package/dist/lib.d.mts +219 -531
  10. package/dist/lib.mjs +7 -9
  11. package/dist/{runCodex-o6PCbHQ7.mjs → runCodex-Di9eHddq.mjs} +263 -42
  12. package/dist/{runCodex-D3eT-TvB.cjs → runCodex-DzP3VUa-.cjs} +264 -43
  13. package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-BS6sBU_V.mjs} +63 -28
  14. package/dist/{runGemini-CBxZp6I7.cjs → runGemini-CpmehDQ2.cjs} +64 -29
  15. package/dist/{types-DGd6ea2Z.mjs → types-CwzNqYEx.mjs} +465 -1142
  16. package/dist/{types-C-jnUdn_.cjs → types-SUAKq-K0.cjs} +466 -1146
  17. package/package.json +1 -1
  18. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +195 -6
  19. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +376 -5
  20. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommandSchema.cpp +731 -0
  21. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +476 -8
  22. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +1518 -94
  23. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +7 -4
  24. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +150 -112
  25. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +2 -1
  26. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +4 -1
  27. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommandSchema.h +42 -0
  28. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +21 -0
  29. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +4 -1
  30. package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +0 -136
  31. package/dist/flockbayScreenshotGate-DkxU24cR.cjs +0 -138
@@ -1,17 +1,15 @@
1
1
  import axios from 'axios';
2
- import chalk from 'chalk';
3
- import { appendFileSync } from 'fs';
4
2
  import fs__default, { existsSync, readFileSync, mkdirSync, constants, unlinkSync, writeFileSync, readdirSync, statSync, createWriteStream } from 'node:fs';
5
3
  import os, { homedir } from 'node:os';
6
4
  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
5
  import { EventEmitter } from 'node:events';
6
+ import { randomUUID as randomUUID$1 } from 'node:crypto';
13
7
  import { io } from 'socket.io-client';
14
- import { spawn as spawn$1 } from 'node:child_process';
8
+ import chalk from 'chalk';
9
+ import { appendFileSync } from 'fs';
10
+ import { readFile, unlink, open, stat, mkdir, writeFile, rename } from 'node:fs/promises';
11
+ import * as z from 'zod';
12
+ import { z as z$1 } from 'zod';
15
13
  import { spawn } from 'child_process';
16
14
  import { realpath, stat as stat$1, readdir, rm, mkdir as mkdir$1, readFile as readFile$1, open as open$1, writeFile as writeFile$1, chmod } from 'fs/promises';
17
15
  import { randomUUID, createHash } from 'crypto';
@@ -20,10 +18,10 @@ import { fileURLToPath } from 'url';
20
18
  import process$1 from 'node:process';
21
19
  import { platform } from 'os';
22
20
  import net from 'node:net';
23
- import { Expo } from 'expo-server-sdk';
21
+ import { spawn as spawn$1 } from 'node:child_process';
24
22
 
25
23
  var name = "flockbay";
26
- var version = "0.10.15";
24
+ var version = "0.10.16";
27
25
  var description = "Flockbay CLI (local agent + daemon)";
28
26
  var author = "Eduardo Orellana";
29
27
  var license = "UNLICENSED";
@@ -174,6 +172,26 @@ var packageJson = {
174
172
  packageManager: packageManager
175
173
  };
176
174
 
175
+ function parseProfileFromProcessArgs() {
176
+ const args = process.argv.slice(2);
177
+ for (let i = 0; i < args.length; i++) {
178
+ const a = String(args[i] || "");
179
+ if (a === "--profile") {
180
+ const v = String(args[i + 1] || "").trim();
181
+ if (v && !v.startsWith("-")) return v;
182
+ }
183
+ if (a.startsWith("--profile=")) {
184
+ const v = a.slice("--profile=".length).trim();
185
+ if (v) return v;
186
+ }
187
+ }
188
+ return "";
189
+ }
190
+ function sanitizeProfileName(input) {
191
+ const raw = String(input).trim();
192
+ const cleaned = raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
193
+ return cleaned || "default";
194
+ }
177
195
  function normalizeServerUrlForNode(url) {
178
196
  try {
179
197
  const u = new URL(url);
@@ -187,6 +205,7 @@ class Configuration {
187
205
  serverUrl;
188
206
  webappUrl;
189
207
  isDaemonProcess;
208
+ profile;
190
209
  // Directories and paths (from persistence)
191
210
  flockbayHomeDir;
192
211
  logsDir;
@@ -200,12 +219,15 @@ class Configuration {
200
219
  constructor() {
201
220
  const args = process.argv.slice(2);
202
221
  this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
222
+ const profileFromArgs = parseProfileFromProcessArgs();
223
+ const profileFromEnv = process.env.FLOCKBAY_PROFILE;
224
+ this.profile = sanitizeProfileName(profileFromEnv || profileFromArgs || "default");
203
225
  const homeOverride = process.env.FLOCKBAY_HOME_DIR;
204
226
  if (homeOverride) {
205
227
  const expandedPath = homeOverride.replace(/^~/, homedir());
206
- this.flockbayHomeDir = expandedPath;
228
+ this.flockbayHomeDir = join(expandedPath, "profiles", this.profile);
207
229
  } else {
208
- this.flockbayHomeDir = join(homedir(), ".flockbay");
230
+ this.flockbayHomeDir = join(homedir(), ".flockbay", "profiles", this.profile);
209
231
  }
210
232
  this.logsDir = join(this.flockbayHomeDir, "logs");
211
233
  this.settingsFile = join(this.flockbayHomeDir, "settings.json");
@@ -239,83 +261,6 @@ class Configuration {
239
261
  }
240
262
  const configuration = new Configuration();
241
263
 
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
264
  const defaultSettings = {
320
265
  onboardingCompleted: false
321
266
  };
@@ -381,40 +326,30 @@ async function updateSettings(updater) {
381
326
  });
382
327
  }
383
328
  }
384
- const credentialsSchema = z.object({
385
- token: z.string(),
386
- encryption: z.object({
387
- publicKey: z.string().base64(),
388
- machineKey: z.string().base64()
389
- })
329
+ const workspaceAuthSchema = z.object({
330
+ machineToken: z.string().min(1),
331
+ orgId: z.string().min(1),
332
+ createdAtMs: z.number().optional()
390
333
  });
391
334
  async function readCredentials() {
392
- if (!existsSync(configuration.privateKeyFile)) {
393
- return null;
394
- }
335
+ if (!existsSync(configuration.privateKeyFile)) return null;
395
336
  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
- };
337
+ const raw = await readFile(configuration.privateKeyFile, "utf8");
338
+ const parsed = workspaceAuthSchema.parse(JSON.parse(raw));
339
+ return { machineToken: parsed.machineToken, orgId: parsed.orgId, createdAtMs: parsed.createdAtMs };
406
340
  } catch {
407
341
  return null;
408
342
  }
409
343
  }
410
- async function writeCredentialsDataKey(credentials) {
344
+ async function writeCredentials(auth) {
411
345
  if (!existsSync(configuration.flockbayHomeDir)) {
412
346
  await mkdir(configuration.flockbayHomeDir, { recursive: true });
413
347
  }
414
- await writeFile(configuration.privateKeyFile, JSON.stringify({
415
- encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
416
- token: credentials.token
417
- }, null, 2));
348
+ await writeFile(
349
+ configuration.privateKeyFile,
350
+ JSON.stringify({ machineToken: auth.machineToken, orgId: auth.orgId, createdAtMs: auth.createdAtMs ?? Date.now() }, null, 2),
351
+ "utf8"
352
+ );
418
353
  }
419
354
  async function clearCredentials() {
420
355
  if (existsSync(configuration.privateKeyFile)) {
@@ -720,222 +655,13 @@ async function getLatestDaemonLog() {
720
655
  return latest || null;
721
656
  }
722
657
 
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
658
  class RpcHandlerManager {
931
659
  handlers = /* @__PURE__ */ new Map();
932
660
  scopePrefix;
933
- encryptionKey;
934
661
  logger;
935
662
  socket = null;
936
663
  constructor(config) {
937
664
  this.scopePrefix = config.scopePrefix;
938
- this.encryptionKey = config.encryptionKey;
939
665
  this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
940
666
  }
941
667
  /**
@@ -960,20 +686,24 @@ class RpcHandlerManager {
960
686
  const handler = this.handlers.get(request.method);
961
687
  if (!handler) {
962
688
  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;
689
+ return { error: "Method not found" };
690
+ }
691
+ const rawParams = request.params;
692
+ const parsedParams = (() => {
693
+ if (typeof rawParams !== "string") return rawParams;
694
+ const s = rawParams.trim();
695
+ if (!s) return rawParams;
696
+ if (!(s.startsWith("{") || s.startsWith("["))) return rawParams;
697
+ try {
698
+ return JSON.parse(s);
699
+ } catch {
700
+ return rawParams;
701
+ }
702
+ })();
703
+ return await handler(parsedParams);
971
704
  } catch (error) {
972
705
  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));
706
+ return { error: error instanceof Error ? error.message : "Unknown error" };
977
707
  }
978
708
  }
979
709
  onSocketConnect(socket) {
@@ -1179,6 +909,19 @@ async function sendUnrealMcpTcpCommand(options) {
1179
909
  const paramName = missingParamMatch[1];
1180
910
  hints.push(`Tip: include the required parameter \`${paramName}\` in the request params (UnrealMCP does not infer defaults).`);
1181
911
  }
912
+ const details = response?.details;
913
+ if (details && typeof details === "object") {
914
+ const kind = typeof details.kind === "string" ? details.kind : null;
915
+ const missing = Array.isArray(details.missing) ? details.missing.filter((x) => typeof x === "string") : [];
916
+ if (kind === "missing_params" && missing.length > 0) {
917
+ hints.push(`Missing params: ${missing.map((m) => `\`${m}\``).join(", ")}`);
918
+ }
919
+ const exampleCall = details.exampleCall;
920
+ if (exampleCall && typeof exampleCall === "object") {
921
+ hints.push(`Example call:
922
+ ${JSON.stringify(exampleCall, null, 2)}`);
923
+ }
924
+ }
1182
925
  const hint = hints.length > 0 ? `
1183
926
 
1184
927
  ${hints.join("\n\n")}` : "";
@@ -1457,7 +1200,7 @@ async function apiJsonGet(params) {
1457
1200
  const res = await fetch(url, {
1458
1201
  method: "GET",
1459
1202
  headers: {
1460
- Authorization: `Bearer ${params.token}`,
1203
+ Authorization: `Machine ${params.token}`,
1461
1204
  Accept: "application/json"
1462
1205
  }
1463
1206
  });
@@ -1473,7 +1216,7 @@ async function apiJsonPost(params) {
1473
1216
  const res = await fetch(url, {
1474
1217
  method: "POST",
1475
1218
  headers: {
1476
- Authorization: `Bearer ${params.token}`,
1219
+ Authorization: `Machine ${params.token}`,
1477
1220
  "Content-Type": "application/json",
1478
1221
  Accept: "application/json"
1479
1222
  },
@@ -1838,8 +1581,20 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordinatio
1838
1581
  "play_in_editor_windowed",
1839
1582
  "stop_play_in_editor",
1840
1583
  "take_screenshot",
1584
+ "map_check",
1585
+ "compile_blueprints_all",
1586
+ "get_editor_context",
1587
+ "get_player_context",
1588
+ "raycast_from_camera",
1589
+ "raycast_down",
1590
+ "get_actor_transform",
1591
+ "get_actor_bounds",
1841
1592
  "spawn_actor",
1842
- "set_actor_transform"
1593
+ "set_actor_transform",
1594
+ "search_assets",
1595
+ "get_asset_info",
1596
+ "list_asset_packs",
1597
+ "place_asset"
1843
1598
  ];
1844
1599
  const expected = await readExpectedUnrealMcpUplugin().catch((err) => {
1845
1600
  const message = err instanceof Error ? err.message : String(err);
@@ -3101,48 +2856,38 @@ class ApiSessionClient extends EventEmitter {
3101
2856
  agentState;
3102
2857
  agentStateVersion;
3103
2858
  socket;
3104
- pendingMessages = [];
3105
- pendingMessageCallback = null;
3106
- pendingOutboundMessages = [];
3107
2859
  rpcHandlerManager;
3108
- agentStateLock = new AsyncLock();
3109
- metadataLock = new AsyncLock();
3110
- encryptionKey;
3111
- coordinationLeaseGuard;
3112
- coordinationLedgerReadAt = 0;
3113
- docsIndexReadAt = 0;
2860
+ coordinationLeaseGuard = new CoordinationLeaseGuard();
2861
+ coordinationLedgerLastReadAtMs = null;
2862
+ outboundQueue = [];
3114
2863
  constructor(token, session) {
3115
2864
  super();
3116
2865
  this.token = token;
3117
2866
  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();
2867
+ this.metadata = session.metadata ?? null;
2868
+ this.metadataVersion = Number(session.metadataVersion || 0);
2869
+ this.agentState = session.agentState ?? null;
2870
+ this.agentStateVersion = Number(session.agentStateVersion || 0);
3124
2871
  this.rpcHandlerManager = new RpcHandlerManager({
3125
2872
  scopePrefix: this.sessionId,
3126
- encryptionKey: this.encryptionKey,
3127
2873
  logger: (msg, data) => logger.debug(msg, data)
3128
2874
  });
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: {
2875
+ const cwd = String(this.metadata?.path || process.cwd()).trim() || process.cwd();
2876
+ const machineId = String(this.metadata?.machineId || "").trim();
2877
+ registerCommonHandlers(
2878
+ this.rpcHandlerManager,
2879
+ cwd,
2880
+ {
2881
+ serverUrl: configuration.serverUrl,
3142
2882
  token: this.token,
3143
- clientType: "session-scoped",
3144
- sessionId: this.sessionId
2883
+ sessionId: this.sessionId,
2884
+ machineId,
2885
+ projectRootPath: String(this.metadata?.projectRootPath || cwd)
3145
2886
  },
2887
+ { bashEnabled: false }
2888
+ );
2889
+ this.socket = io(configuration.serverUrl, {
2890
+ auth: { token: this.token, clientType: "session-scoped", sessionId: this.sessionId, machineId },
3146
2891
  path: "/v1/updates",
3147
2892
  reconnection: true,
3148
2893
  reconnectionAttempts: Infinity,
@@ -3153,386 +2898,196 @@ class ApiSessionClient extends EventEmitter {
3153
2898
  autoConnect: false
3154
2899
  });
3155
2900
  this.socket.on("connect", () => {
3156
- logger.debug("Socket connected successfully");
2901
+ logger.debug("[session] socket connected");
3157
2902
  this.rpcHandlerManager.onSocketConnect(this.socket);
3158
- this.flushOutboundQueue();
2903
+ this.flush();
2904
+ this.emit("connect");
3159
2905
  });
3160
- this.socket.on("rpc-request", async (data, callback) => {
3161
- callback(await this.rpcHandlerManager.handleRequest(data));
2906
+ this.socket.on("connect_error", (error) => {
2907
+ const message = error instanceof Error ? error.message : String(error?.message || error || "connect_error");
2908
+ logger.debug("[session] socket connect_error", { message });
3162
2909
  });
3163
2910
  this.socket.on("disconnect", (reason) => {
3164
- logger.debug("[API] Socket disconnected:", reason);
2911
+ logger.debug("[session] socket disconnected", reason);
3165
2912
  this.rpcHandlerManager.onSocketDisconnect();
2913
+ this.emit("disconnect", reason);
3166
2914
  });
3167
- this.socket.on("connect_error", (error) => {
3168
- logger.debug("[API] Socket connection error:", error);
3169
- this.rpcHandlerManager.onSocketDisconnect();
2915
+ this.socket.on("rpc-request", async (data, callback) => {
2916
+ callback(await this.rpcHandlerManager.handleRequest(data));
3170
2917
  });
3171
2918
  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
- }
2919
+ this.emit("update", data);
3208
2920
  });
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
2921
  }
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);
2922
+ connect() {
2923
+ this.socket.connect();
3231
2924
  }
3232
- markCoordinationLedgerRead(atMs = Date.now()) {
3233
- this.coordinationLedgerReadAt = Number.isFinite(atMs) ? atMs : Date.now();
2925
+ async connectAndWait(timeoutMs = 15e3) {
2926
+ if (this.socket.connected) return;
2927
+ await new Promise((resolve, reject) => {
2928
+ let timeoutHandle = null;
2929
+ const cleanup = () => {
2930
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2931
+ this.socket.off("connect", onConnect);
2932
+ this.socket.off("connect_error", onError);
2933
+ this.socket.off("disconnect", onDisconnect);
2934
+ };
2935
+ const onConnect = () => {
2936
+ cleanup();
2937
+ resolve();
2938
+ };
2939
+ const onError = (err) => {
2940
+ cleanup();
2941
+ const message = err instanceof Error ? err.message : String(err?.message || err || "connect_error");
2942
+ reject(new Error(message));
2943
+ };
2944
+ const onDisconnect = (reason) => {
2945
+ cleanup();
2946
+ reject(new Error(`disconnected:${String(reason || "unknown")}`));
2947
+ };
2948
+ timeoutHandle = setTimeout(() => {
2949
+ cleanup();
2950
+ reject(new Error("connect_timeout"));
2951
+ }, timeoutMs);
2952
+ this.socket.once("connect", onConnect);
2953
+ this.socket.once("connect_error", onError);
2954
+ this.socket.once("disconnect", onDisconnect);
2955
+ this.connect();
2956
+ });
3234
2957
  }
3235
- getCoordinationLedgerReadAt() {
3236
- return Number(this.coordinationLedgerReadAt || 0);
2958
+ disconnect() {
2959
+ this.socket.disconnect();
3237
2960
  }
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;
2961
+ close() {
2962
+ this.disconnect();
3242
2963
  }
3243
- markDocsIndexRead(atMs = Date.now()) {
3244
- this.docsIndexReadAt = Number.isFinite(atMs) ? atMs : Date.now();
2964
+ getAuthToken() {
2965
+ return this.token;
3245
2966
  }
3246
- getDocsIndexReadAt() {
3247
- return Number(this.docsIndexReadAt || 0);
2967
+ async listMessages() {
2968
+ const baseUrl = configuration.serverUrl.replace(/\/+$/, "");
2969
+ const url = `${baseUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/messages`;
2970
+ const res = await axios.get(url, {
2971
+ headers: { Authorization: `Machine ${this.token}`, "Content-Type": "application/json" },
2972
+ timeout: 6e4
2973
+ });
2974
+ return Array.isArray(res.data?.messages) ? res.data.messages : [];
3248
2975
  }
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;
2976
+ markCoordinationLedgerRead(timeMs = Date.now()) {
2977
+ this.coordinationLedgerLastReadAtMs = timeMs;
3253
2978
  }
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;
2979
+ getCoordinationLedgerLastReadAtMs() {
2980
+ return this.coordinationLedgerLastReadAtMs;
3289
2981
  }
3290
- flushOutboundQueue() {
2982
+ keepAlive(thinking, mode) {
3291
2983
  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
- });
2984
+ this.socket.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking, mode });
3317
2985
  }
3318
- onUserMessage(callback) {
3319
- this.pendingMessageCallback = callback;
3320
- while (this.pendingMessages.length > 0) {
3321
- callback(this.pendingMessages.shift());
3322
- }
2986
+ sendSessionDeath() {
2987
+ this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
3323
2988
  }
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
- }
2989
+ sendSessionEvent(event) {
2990
+ this.emitMessageOrQueue({
2991
+ role: "agent",
2992
+ content: { type: "event", id: randomUUID$1(), data: event },
2993
+ meta: { sentFrom: "cli" }
2994
+ });
3373
2995
  }
3374
2996
  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")}` });
2997
+ this.emitMessageOrQueue({ role: "agent", content: { type: "codex", data: body }, meta: { sentFrom: "cli" } });
3388
2998
  }
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
2999
  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")}` });
3000
+ this.emitMessageOrQueue({ role: "agent", content: { type: agentType, data: body }, meta: { sentFrom: "cli" } });
3410
3001
  }
3411
- sendSessionEvent(event, id) {
3412
- let content = {
3002
+ sendClaudeSessionMessage(body) {
3003
+ this.emitMessageOrQueue({
3413
3004
  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
3005
+ content: { type: "output", data: body },
3006
+ meta: { sentFrom: "cli" }
3435
3007
  });
3436
3008
  }
3437
- /**
3438
- * Send session death message
3439
- */
3440
- sendSessionDeath() {
3441
- this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
3009
+ emitMessageOrQueue(content) {
3010
+ const payload = { sid: this.sessionId, content };
3011
+ if (this.socket.connected) {
3012
+ this.socket.emit("message", payload);
3013
+ return;
3014
+ }
3015
+ this.outboundQueue.push(payload);
3442
3016
  }
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
- }
3017
+ flush() {
3018
+ if (!this.socket.connected) return;
3019
+ while (this.outboundQueue.length > 0) {
3020
+ const next = this.outboundQueue.shift();
3021
+ if (!next) continue;
3022
+ this.socket.emit("message", next);
3023
+ }
3024
+ }
3025
+ onUserMessage(handler) {
3026
+ const listener = (u) => {
3027
+ if (u?.body?.t !== "new-message") return;
3028
+ const body = u.body;
3029
+ const raw = body?.message ?? null;
3030
+ if (!raw || typeof raw !== "object") return;
3031
+ const record = raw?.content;
3032
+ if (!record || typeof record !== "object") return;
3033
+ if (record.role !== "user") return;
3034
+ handler(record);
3465
3035
  };
3466
- logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
3467
- this.socket.emit("usage-report", usageReport);
3036
+ this.on("update", listener);
3037
+ return () => this.off("update", listener);
3468
3038
  }
3469
- /**
3470
- * Update session metadata
3471
- * @param handler - Handler function that returns the updated metadata
3472
- */
3473
3039
  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
- });
3040
+ const current = this.metadata || {};
3041
+ const next = handler(current);
3042
+ void this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: next }).then((answer) => {
3043
+ if (answer?.result === "success") {
3044
+ this.metadata = next;
3045
+ this.metadataVersion += 1;
3046
+ }
3047
+ }).catch(() => null);
3490
3048
  }
3491
- /**
3492
- * Update session agent state
3493
- * @param handler - Handler function that returns the updated agent state
3494
- */
3495
3049
  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
- });
3050
+ const current = this.agentState || {};
3051
+ const next = handler(current);
3052
+ void this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: next }).then((answer) => {
3053
+ if (answer?.result === "success") {
3054
+ this.agentState = next;
3055
+ this.agentStateVersion += 1;
3056
+ }
3057
+ }).catch(() => null);
3514
3058
  }
3515
- /**
3516
- * Wait for socket buffer to flush
3517
- */
3518
- async flush() {
3519
- if (!this.socket.connected) {
3520
- return;
3059
+ }
3060
+
3061
+ async function delay(ms) {
3062
+ return new Promise((resolve) => setTimeout(resolve, ms));
3063
+ }
3064
+ function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
3065
+ let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
3066
+ return Math.round(Math.random() * maxDelayRet);
3067
+ }
3068
+ function createBackoff(opts) {
3069
+ return async (callback) => {
3070
+ let currentFailureCount = 0;
3071
+ const minDelay = opts && opts.minDelay !== void 0 ? opts.minDelay : 250;
3072
+ const maxDelay = opts && opts.maxDelay !== void 0 ? opts.maxDelay : 1e3;
3073
+ const maxFailureCount = opts && opts.maxFailureCount !== void 0 ? opts.maxFailureCount : 50;
3074
+ while (true) {
3075
+ try {
3076
+ return await callback();
3077
+ } catch (e) {
3078
+ if (currentFailureCount < maxFailureCount) {
3079
+ currentFailureCount++;
3080
+ }
3081
+ if (opts && opts.onError) {
3082
+ opts.onError(e, currentFailureCount);
3083
+ }
3084
+ let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
3085
+ await delay(waitForRequest);
3086
+ }
3521
3087
  }
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
- }
3088
+ };
3535
3089
  }
3090
+ let backoff = createBackoff();
3536
3091
 
3537
3092
  function looksLikeEngineRoot(engineRoot) {
3538
3093
  if (!engineRoot) return false;
@@ -3705,6 +3260,11 @@ async function buildAndInstallUnrealMcpPlugin(options) {
3705
3260
  `-Plugin=${pluginUpluginPath}`,
3706
3261
  `-Package=${packageDir}`,
3707
3262
  "-Rocket",
3263
+ // UAT enforces a global single-instance mutex. On macOS/Linux, the named mutex can
3264
+ // appear "already created" even when no process is actually holding it, which makes
3265
+ // repeated installs fail with: "A conflicting instance of AutomationTool is already running."
3266
+ // `-WaitForUATMutex` makes UAT acquire the mutex if it's free (or wait if another build is in flight).
3267
+ "-WaitForUATMutex",
3708
3268
  `-TargetPlatforms=${targetPlatform()}`
3709
3269
  ];
3710
3270
  const logStream = fs__default.createWriteStream(buildLogPath, { flags: "a" });
@@ -3777,7 +3337,6 @@ class ApiMachineClient {
3777
3337
  this.machine = machine;
3778
3338
  this.rpcHandlerManager = new RpcHandlerManager({
3779
3339
  scopePrefix: this.machine.id,
3780
- encryptionKey: this.machine.encryptionKey,
3781
3340
  logger: (msg, data) => logger.debug(msg, data)
3782
3341
  });
3783
3342
  const rootDir = this.machine?.metadata?.homeDir || os.homedir() || process.cwd();
@@ -3818,6 +3377,24 @@ class ApiMachineClient {
3818
3377
  socket;
3819
3378
  keepAliveInterval = null;
3820
3379
  rpcHandlerManager;
3380
+ connected = false;
3381
+ lastConnectError = null;
3382
+ lastDisconnectReason = null;
3383
+ lastHttpUpsertError = null;
3384
+ lastHttpUpsertStatus = null;
3385
+ lastHttpUpsertAt = null;
3386
+ upsertBackoff = createBackoff({
3387
+ minDelay: 1e3,
3388
+ maxDelay: 3e4,
3389
+ maxFailureCount: 200,
3390
+ onError: (e, failures) => {
3391
+ const msg = e instanceof Error ? e.message : String(e);
3392
+ this.lastHttpUpsertError = msg;
3393
+ if (process.env.DEBUG) {
3394
+ logger.debug(`[API MACHINE] Machine upsert retry (${failures}): ${msg}`);
3395
+ }
3396
+ }
3397
+ });
3821
3398
  setRPCHandlers({
3822
3399
  spawnSession,
3823
3400
  stopSession,
@@ -3864,74 +3441,86 @@ class ApiMachineClient {
3864
3441
  });
3865
3442
  }
3866
3443
  /**
3867
- * Update machine metadata
3868
- * Currently unused, changes from the mobile client are more likely
3869
- * for example to set a custom name.
3444
+ * Upsert machine record (metadata + daemon state) via HTTP (workspace-native V1).
3870
3445
  */
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
- }
3446
+ async upsertMachineHttp(params) {
3447
+ const attemptAt = Date.now();
3448
+ const endpoint = configuration.serverUrl.replace(/\/+$/, "");
3449
+ const timeoutMs = Math.max(1e3, Math.min(12e4, Number.parseInt(process.env.FLOCKBAY_MACHINE_UPSERT_TIMEOUT_MS || "30000", 10) || 3e4));
3450
+ const res = await fetch(`${endpoint}/v1/machines`, {
3451
+ method: "POST",
3452
+ headers: {
3453
+ Authorization: `Machine ${this.token}`,
3454
+ "Content-Type": "application/json"
3455
+ },
3456
+ body: JSON.stringify({
3457
+ id: this.machine.id,
3458
+ ...params.metadata !== void 0 ? { metadata: params.metadata } : {},
3459
+ ...params.daemonState !== void 0 ? { daemonState: params.daemonState } : {}
3460
+ }),
3461
+ signal: AbortSignal.timeout(timeoutMs)
3890
3462
  });
3463
+ if (!res.ok) {
3464
+ const detail = await res.text().catch(() => "");
3465
+ const msg = `machine_upsert_failed:${res.status}${detail ? `:${detail.slice(0, 2e3)}` : ""}`;
3466
+ this.lastHttpUpsertStatus = res.status;
3467
+ this.lastHttpUpsertError = msg;
3468
+ throw new Error(msg);
3469
+ }
3470
+ const data = await res.json().catch(() => null);
3471
+ const machine = data?.machine && typeof data.machine === "object" ? data.machine : null;
3472
+ if (machine) {
3473
+ this.machine.metadata = machine.metadata ?? null;
3474
+ this.machine.daemonState = machine.daemonState ?? null;
3475
+ this.machine.seq = Number(machine.seq || this.machine.seq || 0);
3476
+ }
3477
+ this.lastHttpUpsertStatus = res.status;
3478
+ this.lastHttpUpsertError = null;
3479
+ this.lastHttpUpsertAt = attemptAt;
3891
3480
  }
3892
3481
  /**
3893
- * Update daemon state (runtime info) - similar to session updateAgentState
3894
- * Simplified without lock - relies on backoff for retry
3482
+ * Update daemon state (runtime info) via HTTP upsert.
3895
3483
  */
3896
3484
  async updateDaemonState(handler) {
3897
- await backoff(async () => {
3485
+ await this.upsertBackoff(async () => {
3898
3486
  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
- }
3487
+ await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
3488
+ logger.debug("[API MACHINE] Daemon state updated successfully");
3915
3489
  });
3916
3490
  }
3491
+ /**
3492
+ * Best-effort single attempt (no retries). Useful during shutdown.
3493
+ */
3494
+ async updateDaemonStateOnce(handler) {
3495
+ try {
3496
+ const updated = handler(this.machine.daemonState);
3497
+ await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
3498
+ return true;
3499
+ } catch (e) {
3500
+ const msg = e instanceof Error ? e.message : String(e);
3501
+ logger.debug("[API MACHINE] Daemon state update failed (best-effort):", msg);
3502
+ return false;
3503
+ }
3504
+ }
3505
+ getStatusSnapshot() {
3506
+ return {
3507
+ connected: this.connected,
3508
+ lastConnectError: this.lastConnectError,
3509
+ lastDisconnectReason: this.lastDisconnectReason,
3510
+ lastHttpUpsertError: this.lastHttpUpsertError,
3511
+ lastHttpUpsertStatus: this.lastHttpUpsertStatus,
3512
+ lastHttpUpsertAt: this.lastHttpUpsertAt
3513
+ };
3514
+ }
3917
3515
  connect() {
3918
3516
  const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
3919
3517
  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
3518
  this.socket = io(serverUrl, {
3927
3519
  transports: ["websocket"],
3928
3520
  auth: {
3929
3521
  token: this.token,
3930
3522
  clientType: "machine-scoped",
3931
- machineId: this.machine.id,
3932
- dataEncryptionKey,
3933
- machineMetadata: machineMetadataEncrypted,
3934
- daemonState: daemonStateEncrypted
3523
+ machineId: this.machine.id
3935
3524
  },
3936
3525
  path: "/v1/updates",
3937
3526
  reconnection: true,
@@ -3940,6 +3529,9 @@ class ApiMachineClient {
3940
3529
  });
3941
3530
  this.socket.on("connect", () => {
3942
3531
  logger.debug("[API MACHINE] Connected to server");
3532
+ this.connected = true;
3533
+ this.lastConnectError = null;
3534
+ this.lastDisconnectReason = null;
3943
3535
  this.updateDaemonState((state) => ({
3944
3536
  ...state,
3945
3537
  status: "running",
@@ -3950,8 +3542,10 @@ class ApiMachineClient {
3950
3542
  this.rpcHandlerManager.onSocketConnect(this.socket);
3951
3543
  this.startKeepAlive();
3952
3544
  });
3953
- this.socket.on("disconnect", () => {
3954
- logger.debug("[API MACHINE] Disconnected from server");
3545
+ this.socket.on("disconnect", (reason) => {
3546
+ logger.debug("[API MACHINE] Disconnected from server", reason);
3547
+ this.connected = false;
3548
+ this.lastDisconnectReason = typeof reason === "string" ? reason : "disconnected";
3955
3549
  this.rpcHandlerManager.onSocketDisconnect();
3956
3550
  this.stopKeepAlive();
3957
3551
  });
@@ -3960,24 +3554,19 @@ class ApiMachineClient {
3960
3554
  callback(await this.rpcHandlerManager.handleRequest(data));
3961
3555
  });
3962
3556
  this.socket.on("update", (data) => {
3963
- if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
3557
+ if (data?.body?.t === "update-machine" && data.body.machineId === this.machine.id) {
3964
3558
  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;
3559
+ if (update.machine) {
3560
+ this.machine.metadata = update.machine.metadata ?? null;
3561
+ this.machine.daemonState = update.machine.daemonState ?? null;
3562
+ this.machine.seq = Number(update.machine.seq || this.machine.seq || 0);
3969
3563
  }
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
3564
  }
3978
3565
  });
3979
3566
  this.socket.on("connect_error", (error) => {
3980
3567
  logger.debug(`[API MACHINE] Connection error: ${error.message}`);
3568
+ this.connected = false;
3569
+ this.lastConnectError = String(error?.message || "connect_error");
3981
3570
  });
3982
3571
  this.socket.io.on("error", (error) => {
3983
3572
  logger.debug("[API MACHINE] Socket error:", error);
@@ -4014,389 +3603,123 @@ class ApiMachineClient {
4014
3603
  }
4015
3604
  }
4016
3605
 
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
- }
3606
+ function machineAuthHeaders(machineToken) {
3607
+ return { Authorization: `Machine ${machineToken}`, "Content-Type": "application/json" };
3608
+ }
3609
+ function normalizeSession(raw) {
3610
+ return {
3611
+ id: String(raw?.id || ""),
3612
+ seq: Number(raw?.seq || 0),
3613
+ active: Boolean(raw?.active),
3614
+ activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
3615
+ createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
3616
+ updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
3617
+ metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
3618
+ metadataVersion: Number(raw?.metadataVersion || 0),
3619
+ agentState: raw?.agentState && typeof raw.agentState === "object" ? raw.agentState : null,
3620
+ agentStateVersion: Number(raw?.agentStateVersion || 0)
3621
+ };
3622
+ }
3623
+ function normalizeMachine(raw) {
3624
+ return {
3625
+ id: String(raw?.id || ""),
3626
+ seq: Number(raw?.seq || 0),
3627
+ active: Boolean(raw?.active),
3628
+ activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
3629
+ createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
3630
+ updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
3631
+ metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
3632
+ daemonState: raw?.daemonState && typeof raw.daemonState === "object" ? raw.daemonState : null
3633
+ };
4141
3634
  }
4142
-
4143
3635
  class ApiClient {
4144
- static async create(credential) {
4145
- return new ApiClient(credential);
3636
+ constructor(auth) {
3637
+ this.auth = auth;
4146
3638
  }
4147
- credential;
4148
- pushClient;
4149
- constructor(credential) {
4150
- this.credential = credential;
4151
- this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
3639
+ static async create(auth) {
3640
+ return new ApiClient(auth);
3641
+ }
3642
+ baseUrl() {
3643
+ return configuration.serverUrl.replace(/\/+$/, "");
3644
+ }
3645
+ async createSession(opts) {
3646
+ const response = await axios.post(
3647
+ `${this.baseUrl()}/v1/sessions`,
3648
+ { metadata: opts.metadata ?? {}, agentState: opts.state ?? null },
3649
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
3650
+ );
3651
+ return normalizeSession(response.data?.session);
4152
3652
  }
4153
- /**
4154
- * Create a new session or load existing one with the given tag
4155
- */
4156
3653
  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
- }
3654
+ const tag = String(opts?.tag || "").trim();
3655
+ const metadata = opts?.metadata && typeof opts.metadata === "object" ? opts.metadata : {};
3656
+ const merged = tag ? { ...metadata, tag } : metadata;
3657
+ return this.createSession({ metadata: merged, state: opts.state ?? null });
3658
+ }
3659
+ async getSessionById(sessionId) {
3660
+ const id = String(sessionId || "").trim();
3661
+ if (!id) throw new Error("Session id is required");
3662
+ const response = await axios.get(
3663
+ `${this.baseUrl()}/v1/sessions/${encodeURIComponent(id)}`,
3664
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
3665
+ );
3666
+ return normalizeSession(response.data?.session);
4196
3667
  }
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);
3668
+ async upsertMachine(opts) {
3669
+ const machineId = String(opts.machineId || "").trim();
3670
+ if (!machineId) throw new Error("Machine id is required");
4207
3671
  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
- }
3672
+ `${this.baseUrl()}/v1/machines`,
3673
+ { id: machineId, metadata: opts.metadata ?? {}, daemonState: opts.daemonState ?? null },
3674
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
4223
3675
  );
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;
3676
+ return normalizeMachine(response.data?.machine);
3677
+ }
3678
+ async getOrCreateMachine(opts) {
3679
+ return this.upsertMachine(opts);
4241
3680
  }
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
3681
  async getMachine(machineId) {
4247
3682
  const id = String(machineId || "").trim();
4248
- if (!id) {
4249
- throw new Error("Machine id is required");
4250
- }
4251
- const encryptionKey = this.credential.encryption.machineKey;
3683
+ if (!id) throw new Error("Machine id is required");
4252
3684
  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
- }
3685
+ `${this.baseUrl()}/v1/machines/${encodeURIComponent(id)}`,
3686
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
4261
3687
  );
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);
3688
+ return normalizeMachine(response.data?.machine);
3689
+ }
3690
+ async registerVendorToken(vendor, token) {
3691
+ const v = String(vendor || "").trim().toLowerCase();
3692
+ if (!v) throw new Error("Vendor is required");
3693
+ await axios.post(
3694
+ `${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`,
3695
+ { token },
3696
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
3697
+ );
3698
+ return { ok: true };
4282
3699
  }
4283
- machineSyncClient(machine) {
4284
- return new ApiMachineClient(this.credential.token, machine);
3700
+ async getVendorToken(vendor) {
3701
+ const v = String(vendor || "").trim().toLowerCase();
3702
+ if (!v) throw new Error("Vendor is required");
3703
+ const response = await axios.get(`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`, {
3704
+ headers: machineAuthHeaders(this.auth.machineToken),
3705
+ timeout: 6e4,
3706
+ validateStatus: (s) => s >= 200 && s < 300 || s === 404
3707
+ });
3708
+ if (response.status === 404) return null;
3709
+ return response.data?.token ?? null;
4285
3710
  }
4286
3711
  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}`);
3712
+ return {
3713
+ sendToAllDevices: async () => {
3714
+ return;
4310
3715
  }
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
- }
3716
+ };
4316
3717
  }
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
- }
3718
+ sessionSyncClient(session) {
3719
+ return new ApiSessionClient(this.auth.machineToken, session);
3720
+ }
3721
+ machineSyncClient(machine) {
3722
+ return new ApiMachineClient(this.auth.machineToken, machine);
4400
3723
  }
4401
3724
  }
4402
3725
 
@@ -4447,4 +3770,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
4447
3770
  }).passthrough()
4448
3771
  ]);
4449
3772
 
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 };
3773
+ export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, readDaemonState as f, clearDaemonState as g, readCredentials as h, unrealMcpPythonDir as i, acquireDaemonLock as j, writeDaemonState as k, logger as l, ApiMachineClient as m, releaseDaemonLock as n, clearCredentials as o, projectPath as p, clearMachineId as q, readSettings as r, sendUnrealMcpTcpCommand as s, installUnrealMcpPluginToEngine as t, updateSettings as u, getLatestDaemonLog as v, writeCredentials as w };