flockbay 0.10.15 → 0.10.17

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-BxBuBx7C.cjs} +2706 -609
  4. package/dist/{index-CUp3juDS.mjs → index-CHm9r89K.mjs} +2707 -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-D3eT-TvB.cjs → runCodex-DuCGwO2K.cjs} +264 -43
  12. package/dist/{runCodex-o6PCbHQ7.mjs → runCodex-DudVDqNh.mjs} +263 -42
  13. package/dist/{runGemini-CBxZp6I7.cjs → runGemini-B25LZ4Cw.cjs} +64 -29
  14. package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-Ddu8UCOS.mjs} +63 -28
  15. package/dist/{types-C-jnUdn_.cjs → types-CGQhv7Z-.cjs} +470 -1146
  16. package/dist/{types-DGd6ea2Z.mjs → types-DuhcLxar.mjs} +469 -1142
  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,18 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  var axios = require('axios');
4
- var chalk = require('chalk');
5
- var fs$2 = require('fs');
6
4
  var fs = require('node:fs');
7
5
  var os = require('node:os');
8
6
  var path = require('node:path');
9
- var fs$1 = require('node:fs/promises');
10
- var z = require('zod');
11
- var node_crypto = require('node:crypto');
12
- var tweetnacl = require('tweetnacl');
13
7
  var node_events = require('node:events');
8
+ var node_crypto = require('node:crypto');
14
9
  var socket_ioClient = require('socket.io-client');
15
- var node_child_process = require('node:child_process');
10
+ var chalk = require('chalk');
11
+ var fs$2 = require('fs');
12
+ var fs$1 = require('node:fs/promises');
13
+ var z = require('zod');
16
14
  var child_process = require('child_process');
17
15
  var fs$3 = require('fs/promises');
18
16
  var crypto = require('crypto');
@@ -21,7 +19,7 @@ var url = require('url');
21
19
  var process$1 = require('node:process');
22
20
  var os$1 = require('os');
23
21
  var net = require('node:net');
24
- var expoServerSdk = require('expo-server-sdk');
22
+ var node_child_process = require('node:child_process');
25
23
 
26
24
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
27
25
  function _interopNamespaceDefault(e) {
@@ -44,7 +42,7 @@ function _interopNamespaceDefault(e) {
44
42
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
45
43
 
46
44
  var name = "flockbay";
47
- var version = "0.10.15";
45
+ var version = "0.10.17";
48
46
  var description = "Flockbay CLI (local agent + daemon)";
49
47
  var author = "Eduardo Orellana";
50
48
  var license = "UNLICENSED";
@@ -195,6 +193,30 @@ var packageJson = {
195
193
  packageManager: packageManager
196
194
  };
197
195
 
196
+ function parseProfileFromProcessArgs() {
197
+ const args = process.argv.slice(2);
198
+ for (let i = 0; i < args.length; i++) {
199
+ const a = String(args[i] || "");
200
+ if (a === "--profile" || a === "-profile" || a === "-p") {
201
+ const v = String(args[i + 1] || "").trim();
202
+ if (v && !v.startsWith("-")) return v;
203
+ }
204
+ if (a.startsWith("--profile=")) {
205
+ const v = a.slice("--profile=".length).trim();
206
+ if (v) return v;
207
+ }
208
+ if (a.startsWith("-profile=")) {
209
+ const v = a.slice("-profile=".length).trim();
210
+ if (v) return v;
211
+ }
212
+ }
213
+ return "";
214
+ }
215
+ function sanitizeProfileName(input) {
216
+ const raw = String(input).trim();
217
+ const cleaned = raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
218
+ return cleaned || "default";
219
+ }
198
220
  function normalizeServerUrlForNode(url) {
199
221
  try {
200
222
  const u = new URL(url);
@@ -208,6 +230,7 @@ class Configuration {
208
230
  serverUrl;
209
231
  webappUrl;
210
232
  isDaemonProcess;
233
+ profile;
211
234
  // Directories and paths (from persistence)
212
235
  flockbayHomeDir;
213
236
  logsDir;
@@ -221,12 +244,15 @@ class Configuration {
221
244
  constructor() {
222
245
  const args = process.argv.slice(2);
223
246
  this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
247
+ const profileFromArgs = parseProfileFromProcessArgs();
248
+ const profileFromEnv = process.env.FLOCKBAY_PROFILE;
249
+ this.profile = sanitizeProfileName(profileFromEnv || profileFromArgs || "default");
224
250
  const homeOverride = process.env.FLOCKBAY_HOME_DIR;
225
251
  if (homeOverride) {
226
252
  const expandedPath = homeOverride.replace(/^~/, os.homedir());
227
- this.flockbayHomeDir = expandedPath;
253
+ this.flockbayHomeDir = path.join(expandedPath, "profiles", this.profile);
228
254
  } else {
229
- this.flockbayHomeDir = path.join(os.homedir(), ".flockbay");
255
+ this.flockbayHomeDir = path.join(os.homedir(), ".flockbay", "profiles", this.profile);
230
256
  }
231
257
  this.logsDir = path.join(this.flockbayHomeDir, "logs");
232
258
  this.settingsFile = path.join(this.flockbayHomeDir, "settings.json");
@@ -260,83 +286,6 @@ class Configuration {
260
286
  }
261
287
  const configuration = new Configuration();
262
288
 
263
- function encodeBase64(buffer, variant = "base64") {
264
- if (variant === "base64url") {
265
- return encodeBase64Url(buffer);
266
- }
267
- return Buffer.from(buffer).toString("base64");
268
- }
269
- function encodeBase64Url(buffer) {
270
- return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
271
- }
272
- function decodeBase64(base64, variant = "base64") {
273
- if (variant === "base64url") {
274
- const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
275
- return new Uint8Array(Buffer.from(base64Standard, "base64"));
276
- }
277
- return new Uint8Array(Buffer.from(base64, "base64"));
278
- }
279
- function getRandomBytes(size) {
280
- return new Uint8Array(node_crypto.randomBytes(size));
281
- }
282
- function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
283
- const ephemeralKeyPair = tweetnacl.box.keyPair();
284
- const nonce = getRandomBytes(tweetnacl.box.nonceLength);
285
- const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
286
- const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
287
- result.set(ephemeralKeyPair.publicKey, 0);
288
- result.set(nonce, ephemeralKeyPair.publicKey.length);
289
- result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
290
- return result;
291
- }
292
- function encryptWithDataKey(data, dataKey) {
293
- const nonce = getRandomBytes(12);
294
- const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
295
- const plaintext = new TextEncoder().encode(JSON.stringify(data));
296
- const encrypted = Buffer.concat([
297
- cipher.update(plaintext),
298
- cipher.final()
299
- ]);
300
- const authTag = cipher.getAuthTag();
301
- const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
302
- bundle.set([0], 0);
303
- bundle.set(nonce, 1);
304
- bundle.set(new Uint8Array(encrypted), 13);
305
- bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
306
- return bundle;
307
- }
308
- function decryptWithDataKey(bundle, dataKey) {
309
- if (bundle.length < 1) {
310
- return null;
311
- }
312
- if (bundle[0] !== 0) {
313
- return null;
314
- }
315
- if (bundle.length < 12 + 16 + 1) {
316
- return null;
317
- }
318
- const nonce = bundle.slice(1, 13);
319
- const authTag = bundle.slice(bundle.length - 16);
320
- const ciphertext = bundle.slice(13, bundle.length - 16);
321
- try {
322
- const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
323
- decipher.setAuthTag(authTag);
324
- const decrypted = Buffer.concat([
325
- decipher.update(ciphertext),
326
- decipher.final()
327
- ]);
328
- return JSON.parse(new TextDecoder().decode(decrypted));
329
- } catch (error) {
330
- return null;
331
- }
332
- }
333
- function encrypt(key, data) {
334
- return encryptWithDataKey(data, key);
335
- }
336
- function decrypt(key, data) {
337
- return decryptWithDataKey(data, key);
338
- }
339
-
340
289
  const defaultSettings = {
341
290
  onboardingCompleted: false
342
291
  };
@@ -402,40 +351,30 @@ async function updateSettings(updater) {
402
351
  });
403
352
  }
404
353
  }
405
- const credentialsSchema = z__namespace.object({
406
- token: z__namespace.string(),
407
- encryption: z__namespace.object({
408
- publicKey: z__namespace.string().base64(),
409
- machineKey: z__namespace.string().base64()
410
- })
354
+ const workspaceAuthSchema = z__namespace.object({
355
+ machineToken: z__namespace.string().min(1),
356
+ orgId: z__namespace.string().min(1),
357
+ createdAtMs: z__namespace.number().optional()
411
358
  });
412
359
  async function readCredentials() {
413
- if (!fs.existsSync(configuration.privateKeyFile)) {
414
- return null;
415
- }
360
+ if (!fs.existsSync(configuration.privateKeyFile)) return null;
416
361
  try {
417
- const keyBase64 = await fs$1.readFile(configuration.privateKeyFile, "utf8");
418
- const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
419
- return {
420
- token: credentials.token,
421
- encryption: {
422
- type: "dataKey",
423
- publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
424
- machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
425
- }
426
- };
362
+ const raw = await fs$1.readFile(configuration.privateKeyFile, "utf8");
363
+ const parsed = workspaceAuthSchema.parse(JSON.parse(raw));
364
+ return { machineToken: parsed.machineToken, orgId: parsed.orgId, createdAtMs: parsed.createdAtMs };
427
365
  } catch {
428
366
  return null;
429
367
  }
430
368
  }
431
- async function writeCredentialsDataKey(credentials) {
369
+ async function writeCredentials(auth) {
432
370
  if (!fs.existsSync(configuration.flockbayHomeDir)) {
433
371
  await fs$1.mkdir(configuration.flockbayHomeDir, { recursive: true });
434
372
  }
435
- await fs$1.writeFile(configuration.privateKeyFile, JSON.stringify({
436
- encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
437
- token: credentials.token
438
- }, null, 2));
373
+ await fs$1.writeFile(
374
+ configuration.privateKeyFile,
375
+ JSON.stringify({ machineToken: auth.machineToken, orgId: auth.orgId, createdAtMs: auth.createdAtMs ?? Date.now() }, null, 2),
376
+ "utf8"
377
+ );
439
378
  }
440
379
  async function clearCredentials() {
441
380
  if (fs.existsSync(configuration.privateKeyFile)) {
@@ -741,222 +680,13 @@ async function getLatestDaemonLog() {
741
680
  return latest || null;
742
681
  }
743
682
 
744
- const SessionMessageContentSchema = z.z.object({
745
- c: z.z.string(),
746
- // Base64 encoded encrypted content
747
- t: z.z.literal("encrypted")
748
- });
749
- const UpdateBodySchema = z.z.object({
750
- message: z.z.object({
751
- id: z.z.string(),
752
- seq: z.z.number(),
753
- content: SessionMessageContentSchema
754
- }),
755
- sid: z.z.string(),
756
- // Session ID
757
- t: z.z.literal("new-message")
758
- });
759
- const UpdateSessionBodySchema = z.z.object({
760
- t: z.z.literal("update-session"),
761
- sid: z.z.string(),
762
- metadata: z.z.object({
763
- version: z.z.number(),
764
- value: z.z.string()
765
- }).nullish(),
766
- agentState: z.z.object({
767
- version: z.z.number(),
768
- value: z.z.string()
769
- }).nullish()
770
- });
771
- const UpdateMachineBodySchema = z.z.object({
772
- t: z.z.literal("update-machine"),
773
- machineId: z.z.string(),
774
- metadata: z.z.object({
775
- version: z.z.number(),
776
- value: z.z.string()
777
- }).nullish(),
778
- daemonState: z.z.object({
779
- version: z.z.number(),
780
- value: z.z.string()
781
- }).nullish()
782
- });
783
- z.z.object({
784
- id: z.z.string(),
785
- seq: z.z.number(),
786
- body: z.z.union([
787
- UpdateBodySchema,
788
- UpdateSessionBodySchema,
789
- UpdateMachineBodySchema
790
- ]),
791
- createdAt: z.z.number()
792
- });
793
- z.z.object({
794
- host: z.z.string(),
795
- platform: z.z.string(),
796
- flockbayCliVersion: z.z.string(),
797
- homeDir: z.z.string(),
798
- flockbayHomeDir: z.z.string(),
799
- flockbayLibDir: z.z.string(),
800
- // Dev-only: bypass Unreal project + SDK gating (so sessions can run in arbitrary folders).
801
- flockbayDevBypassUeGates: z.z.boolean().optional()
802
- });
803
- z.z.object({
804
- status: z.z.union([
805
- z.z.enum(["running", "shutting-down"]),
806
- z.z.string()
807
- // Forward compatibility
808
- ]),
809
- pid: z.z.number().optional(),
810
- httpPort: z.z.number().optional(),
811
- startedAt: z.z.number().optional(),
812
- shutdownRequestedAt: z.z.number().optional(),
813
- shutdownSource: z.z.union([
814
- z.z.enum(["mobile-app", "cli", "os-signal", "unknown"]),
815
- z.z.string()
816
- // Forward compatibility
817
- ]).optional()
818
- });
819
- z.z.object({
820
- content: SessionMessageContentSchema,
821
- createdAt: z.z.number(),
822
- id: z.z.string(),
823
- seq: z.z.number(),
824
- updatedAt: z.z.number()
825
- });
826
- const MessageMetaSchema = z.z.object({
827
- sentFrom: z.z.string().optional(),
828
- // Source identifier
829
- permissionMode: z.z.string().optional(),
830
- // Permission mode for this message
831
- model: z.z.string().nullable().optional(),
832
- // Model name for this message (null = reset)
833
- customSystemPrompt: z.z.string().nullable().optional(),
834
- // Custom system prompt for this message (null = reset)
835
- appendSystemPrompt: z.z.string().nullable().optional(),
836
- // Append to system prompt for this message (null = reset)
837
- allowedTools: z.z.array(z.z.string()).nullable().optional(),
838
- // Allowed tools for this message (null = reset)
839
- disallowedTools: z.z.array(z.z.string()).nullable().optional(),
840
- // Disallowed tools for this message (null = reset)
841
- // Optional text to show in UI instead of the raw prompt (mobile/web feature).
842
- displayText: z.z.string().optional(),
843
- // Optional multimodal attachments (base64-embedded; used for vision-capable agents).
844
- attachments: z.z.object({
845
- images: z.z.array(
846
- z.z.object({
847
- mimeType: z.z.string(),
848
- base64: z.z.string(),
849
- name: z.z.string().optional(),
850
- width: z.z.number().optional(),
851
- height: z.z.number().optional()
852
- })
853
- ).optional()
854
- }).optional()
855
- });
856
- z.z.object({
857
- session: z.z.object({
858
- id: z.z.string(),
859
- tag: z.z.string(),
860
- seq: z.z.number(),
861
- createdAt: z.z.number(),
862
- updatedAt: z.z.number(),
863
- metadata: z.z.string(),
864
- metadataVersion: z.z.number(),
865
- agentState: z.z.string().nullable(),
866
- agentStateVersion: z.z.number()
867
- })
868
- });
869
- const UserMessageSchema = z.z.object({
870
- role: z.z.literal("user"),
871
- content: z.z.object({
872
- type: z.z.literal("text"),
873
- text: z.z.string()
874
- }),
875
- localKey: z.z.string().optional(),
876
- // Mobile messages include this
877
- meta: MessageMetaSchema.optional()
878
- });
879
- const AgentMessageSchema = z.z.object({
880
- role: z.z.literal("agent"),
881
- content: z.z.object({
882
- type: z.z.literal("output"),
883
- data: z.z.any()
884
- }),
885
- meta: MessageMetaSchema.optional()
886
- });
887
- z.z.union([UserMessageSchema, AgentMessageSchema]);
888
-
889
- async function delay(ms) {
890
- return new Promise((resolve) => setTimeout(resolve, ms));
891
- }
892
- function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
893
- let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
894
- return Math.round(Math.random() * maxDelayRet);
895
- }
896
- function createBackoff(opts) {
897
- return async (callback) => {
898
- let currentFailureCount = 0;
899
- const minDelay = 250;
900
- const maxDelay = 1e3;
901
- const maxFailureCount = 50;
902
- while (true) {
903
- try {
904
- return await callback();
905
- } catch (e) {
906
- if (currentFailureCount < maxFailureCount) {
907
- currentFailureCount++;
908
- }
909
- let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
910
- await delay(waitForRequest);
911
- }
912
- }
913
- };
914
- }
915
- let backoff = createBackoff();
916
-
917
- class AsyncLock {
918
- permits = 1;
919
- promiseResolverQueue = [];
920
- async inLock(func) {
921
- try {
922
- await this.lock();
923
- return await func();
924
- } finally {
925
- this.unlock();
926
- }
927
- }
928
- async lock() {
929
- if (this.permits > 0) {
930
- this.permits = this.permits - 1;
931
- return;
932
- }
933
- await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
934
- }
935
- unlock() {
936
- this.permits += 1;
937
- if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
938
- throw new Error("this.permits should never be > 0 when there is someone waiting.");
939
- } else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
940
- this.permits -= 1;
941
- const nextResolver = this.promiseResolverQueue.shift();
942
- if (nextResolver) {
943
- setTimeout(() => {
944
- nextResolver(true);
945
- }, 0);
946
- }
947
- }
948
- }
949
- }
950
-
951
683
  class RpcHandlerManager {
952
684
  handlers = /* @__PURE__ */ new Map();
953
685
  scopePrefix;
954
- encryptionKey;
955
686
  logger;
956
687
  socket = null;
957
688
  constructor(config) {
958
689
  this.scopePrefix = config.scopePrefix;
959
- this.encryptionKey = config.encryptionKey;
960
690
  this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
961
691
  }
962
692
  /**
@@ -981,20 +711,24 @@ class RpcHandlerManager {
981
711
  const handler = this.handlers.get(request.method);
982
712
  if (!handler) {
983
713
  this.logger("[RPC] [ERROR] Method not found", { method: request.method });
984
- const errorResponse = { error: "Method not found" };
985
- const encryptedError = encodeBase64(encrypt(this.encryptionKey, errorResponse));
986
- return encryptedError;
987
- }
988
- const decryptedParams = decrypt(this.encryptionKey, decodeBase64(request.params));
989
- const result = await handler(decryptedParams);
990
- const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, result));
991
- return encryptedResponse;
714
+ return { error: "Method not found" };
715
+ }
716
+ const rawParams = request.params;
717
+ const parsedParams = (() => {
718
+ if (typeof rawParams !== "string") return rawParams;
719
+ const s = rawParams.trim();
720
+ if (!s) return rawParams;
721
+ if (!(s.startsWith("{") || s.startsWith("["))) return rawParams;
722
+ try {
723
+ return JSON.parse(s);
724
+ } catch {
725
+ return rawParams;
726
+ }
727
+ })();
728
+ return await handler(parsedParams);
992
729
  } catch (error) {
993
730
  this.logger("[RPC] [ERROR] Error handling request", { error });
994
- const errorResponse = {
995
- error: error instanceof Error ? error.message : "Unknown error"
996
- };
997
- return encodeBase64(encrypt(this.encryptionKey, errorResponse));
731
+ return { error: error instanceof Error ? error.message : "Unknown error" };
998
732
  }
999
733
  }
1000
734
  onSocketConnect(socket) {
@@ -1036,7 +770,7 @@ class RpcHandlerManager {
1036
770
  }
1037
771
  }
1038
772
 
1039
- const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-C-jnUdn_.cjs', document.baseURI).href))));
773
+ const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CGQhv7Z-.cjs', document.baseURI).href))));
1040
774
  function projectPath() {
1041
775
  const path = path$1.resolve(__dirname$1, "..");
1042
776
  return path;
@@ -1200,6 +934,19 @@ async function sendUnrealMcpTcpCommand(options) {
1200
934
  const paramName = missingParamMatch[1];
1201
935
  hints.push(`Tip: include the required parameter \`${paramName}\` in the request params (UnrealMCP does not infer defaults).`);
1202
936
  }
937
+ const details = response?.details;
938
+ if (details && typeof details === "object") {
939
+ const kind = typeof details.kind === "string" ? details.kind : null;
940
+ const missing = Array.isArray(details.missing) ? details.missing.filter((x) => typeof x === "string") : [];
941
+ if (kind === "missing_params" && missing.length > 0) {
942
+ hints.push(`Missing params: ${missing.map((m) => `\`${m}\``).join(", ")}`);
943
+ }
944
+ const exampleCall = details.exampleCall;
945
+ if (exampleCall && typeof exampleCall === "object") {
946
+ hints.push(`Example call:
947
+ ${JSON.stringify(exampleCall, null, 2)}`);
948
+ }
949
+ }
1203
950
  const hint = hints.length > 0 ? `
1204
951
 
1205
952
  ${hints.join("\n\n")}` : "";
@@ -1478,7 +1225,7 @@ async function apiJsonGet(params) {
1478
1225
  const res = await fetch(url, {
1479
1226
  method: "GET",
1480
1227
  headers: {
1481
- Authorization: `Bearer ${params.token}`,
1228
+ Authorization: `Machine ${params.token}`,
1482
1229
  Accept: "application/json"
1483
1230
  }
1484
1231
  });
@@ -1494,7 +1241,7 @@ async function apiJsonPost(params) {
1494
1241
  const res = await fetch(url, {
1495
1242
  method: "POST",
1496
1243
  headers: {
1497
- Authorization: `Bearer ${params.token}`,
1244
+ Authorization: `Machine ${params.token}`,
1498
1245
  "Content-Type": "application/json",
1499
1246
  Accept: "application/json"
1500
1247
  },
@@ -1859,8 +1606,20 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordinatio
1859
1606
  "play_in_editor_windowed",
1860
1607
  "stop_play_in_editor",
1861
1608
  "take_screenshot",
1609
+ "map_check",
1610
+ "compile_blueprints_all",
1611
+ "get_editor_context",
1612
+ "get_player_context",
1613
+ "raycast_from_camera",
1614
+ "raycast_down",
1615
+ "get_actor_transform",
1616
+ "get_actor_bounds",
1862
1617
  "spawn_actor",
1863
- "set_actor_transform"
1618
+ "set_actor_transform",
1619
+ "search_assets",
1620
+ "get_asset_info",
1621
+ "list_asset_packs",
1622
+ "place_asset"
1864
1623
  ];
1865
1624
  const expected = await readExpectedUnrealMcpUplugin().catch((err) => {
1866
1625
  const message = err instanceof Error ? err.message : String(err);
@@ -3122,48 +2881,38 @@ class ApiSessionClient extends node_events.EventEmitter {
3122
2881
  agentState;
3123
2882
  agentStateVersion;
3124
2883
  socket;
3125
- pendingMessages = [];
3126
- pendingMessageCallback = null;
3127
- pendingOutboundMessages = [];
3128
2884
  rpcHandlerManager;
3129
- agentStateLock = new AsyncLock();
3130
- metadataLock = new AsyncLock();
3131
- encryptionKey;
3132
- coordinationLeaseGuard;
3133
- coordinationLedgerReadAt = 0;
3134
- docsIndexReadAt = 0;
2885
+ coordinationLeaseGuard = new CoordinationLeaseGuard();
2886
+ coordinationLedgerLastReadAtMs = null;
2887
+ outboundQueue = [];
3135
2888
  constructor(token, session) {
3136
2889
  super();
3137
2890
  this.token = token;
3138
2891
  this.sessionId = session.id;
3139
- this.metadata = session.metadata;
3140
- this.metadataVersion = session.metadataVersion;
3141
- this.agentState = session.agentState;
3142
- this.agentStateVersion = session.agentStateVersion;
3143
- this.encryptionKey = session.encryptionKey;
3144
- this.coordinationLeaseGuard = new CoordinationLeaseGuard();
2892
+ this.metadata = session.metadata ?? null;
2893
+ this.metadataVersion = Number(session.metadataVersion || 0);
2894
+ this.agentState = session.agentState ?? null;
2895
+ this.agentStateVersion = Number(session.agentStateVersion || 0);
3145
2896
  this.rpcHandlerManager = new RpcHandlerManager({
3146
2897
  scopePrefix: this.sessionId,
3147
- encryptionKey: this.encryptionKey,
3148
2898
  logger: (msg, data) => logger.debug(msg, data)
3149
2899
  });
3150
- registerCommonHandlers(this.rpcHandlerManager, this.metadata.path, {
3151
- serverUrl: configuration.serverUrl,
3152
- token: this.token,
3153
- sessionId: this.sessionId,
3154
- machineId: this.metadata.machineId || "",
3155
- projectRootPath: this.metadata?.projectRootPath ? String(this.metadata.projectRootPath) : this.metadata.path,
3156
- workItemId: this.metadata?.workItemId ? String(this.metadata.workItemId) : null,
3157
- workspaceProjectId: this.metadata?.workspaceProjectId ? String(this.metadata.workspaceProjectId) : null,
3158
- leaseGuard: this.coordinationLeaseGuard
3159
- }, { bashEnabled: false });
3160
- this.startCoordinationAutopilot();
3161
- this.socket = socket_ioClient.io(configuration.serverUrl, {
3162
- auth: {
2900
+ const cwd = String(this.metadata?.path || process.cwd()).trim() || process.cwd();
2901
+ const machineId = String(this.metadata?.machineId || "").trim();
2902
+ registerCommonHandlers(
2903
+ this.rpcHandlerManager,
2904
+ cwd,
2905
+ {
2906
+ serverUrl: configuration.serverUrl,
3163
2907
  token: this.token,
3164
- clientType: "session-scoped",
3165
- sessionId: this.sessionId
2908
+ sessionId: this.sessionId,
2909
+ machineId,
2910
+ projectRootPath: String(this.metadata?.projectRootPath || cwd)
3166
2911
  },
2912
+ { bashEnabled: false }
2913
+ );
2914
+ this.socket = socket_ioClient.io(configuration.serverUrl, {
2915
+ auth: { token: this.token, clientType: "session-scoped", sessionId: this.sessionId, machineId },
3167
2916
  path: "/v1/updates",
3168
2917
  reconnection: true,
3169
2918
  reconnectionAttempts: Infinity,
@@ -3174,386 +2923,196 @@ class ApiSessionClient extends node_events.EventEmitter {
3174
2923
  autoConnect: false
3175
2924
  });
3176
2925
  this.socket.on("connect", () => {
3177
- logger.debug("Socket connected successfully");
2926
+ logger.debug("[session] socket connected");
3178
2927
  this.rpcHandlerManager.onSocketConnect(this.socket);
3179
- this.flushOutboundQueue();
2928
+ this.flush();
2929
+ this.emit("connect");
3180
2930
  });
3181
- this.socket.on("rpc-request", async (data, callback) => {
3182
- callback(await this.rpcHandlerManager.handleRequest(data));
2931
+ this.socket.on("connect_error", (error) => {
2932
+ const message = error instanceof Error ? error.message : String(error?.message || error || "connect_error");
2933
+ logger.debug("[session] socket connect_error", { message });
3183
2934
  });
3184
2935
  this.socket.on("disconnect", (reason) => {
3185
- logger.debug("[API] Socket disconnected:", reason);
2936
+ logger.debug("[session] socket disconnected", reason);
3186
2937
  this.rpcHandlerManager.onSocketDisconnect();
2938
+ this.emit("disconnect", reason);
3187
2939
  });
3188
- this.socket.on("connect_error", (error) => {
3189
- logger.debug("[API] Socket connection error:", error);
3190
- this.rpcHandlerManager.onSocketDisconnect();
2940
+ this.socket.on("rpc-request", async (data, callback) => {
2941
+ callback(await this.rpcHandlerManager.handleRequest(data));
3191
2942
  });
3192
2943
  this.socket.on("update", (data) => {
3193
- try {
3194
- logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
3195
- if (!data.body) {
3196
- logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
3197
- return;
3198
- }
3199
- if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
3200
- const body = decrypt(this.encryptionKey, decodeBase64(data.body.message.content.c));
3201
- logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
3202
- const userResult = UserMessageSchema.safeParse(body);
3203
- if (userResult.success) {
3204
- if (this.pendingMessageCallback) {
3205
- this.pendingMessageCallback(userResult.data);
3206
- } else {
3207
- this.pendingMessages.push(userResult.data);
3208
- }
3209
- } else {
3210
- this.emit("message", body);
3211
- }
3212
- } else if (data.body.t === "update-session") {
3213
- if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
3214
- this.metadata = decrypt(this.encryptionKey, decodeBase64(data.body.metadata.value));
3215
- this.metadataVersion = data.body.metadata.version;
3216
- }
3217
- if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
3218
- this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, decodeBase64(data.body.agentState.value)) : null;
3219
- this.agentStateVersion = data.body.agentState.version;
3220
- }
3221
- } else if (data.body.t === "update-machine") {
3222
- logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
3223
- } else {
3224
- this.emit("message", data.body);
3225
- }
3226
- } catch (error) {
3227
- logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
3228
- }
2944
+ this.emit("update", data);
3229
2945
  });
3230
- this.socket.on("error", (error) => {
3231
- logger.debug("[API] Socket error:", error);
3232
- });
3233
- this.socket.connect();
3234
- }
3235
- /**
3236
- * Returns the session client's bearer token for calling authenticated HTTP endpoints.
3237
- * Intended for internal integrations (e.g. uploading artifacts for this session).
3238
- */
3239
- getAuthToken() {
3240
- return this.token;
3241
2946
  }
3242
- /**
3243
- * True when this session has workspaceProjectId/workItemId metadata
3244
- * (i.e. ledger tools are available and claim enforcement makes sense).
3245
- */
3246
- hasCoordinationContext() {
3247
- const meta = this.metadata;
3248
- if (!meta) return false;
3249
- const projectId = String(meta.workspaceProjectId || "").trim();
3250
- const workItemId = String(meta.workItemId || "").trim();
3251
- return Boolean(projectId && workItemId);
2947
+ connect() {
2948
+ this.socket.connect();
3252
2949
  }
3253
- markCoordinationLedgerRead(atMs = Date.now()) {
3254
- this.coordinationLedgerReadAt = Number.isFinite(atMs) ? atMs : Date.now();
2950
+ async connectAndWait(timeoutMs = 15e3) {
2951
+ if (this.socket.connected) return;
2952
+ await new Promise((resolve, reject) => {
2953
+ let timeoutHandle = null;
2954
+ const cleanup = () => {
2955
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2956
+ this.socket.off("connect", onConnect);
2957
+ this.socket.off("connect_error", onError);
2958
+ this.socket.off("disconnect", onDisconnect);
2959
+ };
2960
+ const onConnect = () => {
2961
+ cleanup();
2962
+ resolve();
2963
+ };
2964
+ const onError = (err) => {
2965
+ cleanup();
2966
+ const message = err instanceof Error ? err.message : String(err?.message || err || "connect_error");
2967
+ reject(new Error(message));
2968
+ };
2969
+ const onDisconnect = (reason) => {
2970
+ cleanup();
2971
+ reject(new Error(`disconnected:${String(reason || "unknown")}`));
2972
+ };
2973
+ timeoutHandle = setTimeout(() => {
2974
+ cleanup();
2975
+ reject(new Error("connect_timeout"));
2976
+ }, timeoutMs);
2977
+ this.socket.once("connect", onConnect);
2978
+ this.socket.once("connect_error", onError);
2979
+ this.socket.once("disconnect", onDisconnect);
2980
+ this.connect();
2981
+ });
3255
2982
  }
3256
- getCoordinationLedgerReadAt() {
3257
- return Number(this.coordinationLedgerReadAt || 0);
2983
+ disconnect() {
2984
+ this.socket.disconnect();
3258
2985
  }
3259
- didReadCoordinationLedgerWithin(ms) {
3260
- const last = Number(this.coordinationLedgerReadAt || 0);
3261
- if (!Number.isFinite(last) || last <= 0) return false;
3262
- return Date.now() - last <= ms;
2986
+ close() {
2987
+ this.disconnect();
3263
2988
  }
3264
- markDocsIndexRead(atMs = Date.now()) {
3265
- this.docsIndexReadAt = Number.isFinite(atMs) ? atMs : Date.now();
2989
+ getAuthToken() {
2990
+ return this.token;
3266
2991
  }
3267
- getDocsIndexReadAt() {
3268
- return Number(this.docsIndexReadAt || 0);
2992
+ async listMessages() {
2993
+ const baseUrl = configuration.serverUrl.replace(/\/+$/, "");
2994
+ const url = `${baseUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/messages`;
2995
+ const res = await axios.get(url, {
2996
+ headers: { Authorization: `Machine ${this.token}`, "Content-Type": "application/json" },
2997
+ timeout: 6e4
2998
+ });
2999
+ return Array.isArray(res.data?.messages) ? res.data.messages : [];
3269
3000
  }
3270
- didReadDocsIndexWithin(ms) {
3271
- const last = Number(this.docsIndexReadAt || 0);
3272
- if (!Number.isFinite(last) || last <= 0) return false;
3273
- return Date.now() - last <= ms;
3001
+ markCoordinationLedgerRead(timeMs = Date.now()) {
3002
+ this.coordinationLedgerLastReadAtMs = timeMs;
3274
3003
  }
3275
- startCoordinationAutopilot() {
3276
- const meta = this.metadata;
3277
- if (!meta) return;
3278
- const projectId = String(meta.workspaceProjectId || "").trim();
3279
- const workItemId = String(meta.workItemId || "").trim();
3280
- const machineId = String(meta.machineId || "").trim();
3281
- const projectRootPath = String(meta.projectRootPath || meta.path || "").trim();
3282
- const cwd = String(meta.path || "").trim();
3283
- if (!projectId || !workItemId || !machineId || !projectRootPath || !cwd) return;
3284
- String(process.env.FLOCKBAY_COORDINATION_AUTO_SHIP_ON_COMMIT || "").trim() === "1";
3285
- const serverUrl = configuration.serverUrl.replace(/\/+$/, "");
3286
- const token = this.token;
3287
- const sessionId = this.sessionId;
3288
- const postJson = async (path, body) => {
3289
- const res = await fetch(`${serverUrl}${path}`, {
3290
- method: "POST",
3291
- headers: {
3292
- Authorization: `Bearer ${token}`,
3293
- "Content-Type": "application/json"
3294
- },
3295
- body: JSON.stringify(body ?? {})
3296
- });
3297
- const data = await res.json().catch(() => null);
3298
- if (!res.ok) {
3299
- const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
3300
- throw new Error(msg);
3301
- }
3302
- return data;
3303
- };
3304
- void postJson(
3305
- "/v1/coordination/work-items/update",
3306
- { projectId, workItemId, sessionId }
3307
- ).catch((err) => logger.debug("[coordination-autopilot] Failed to attach session to work item:", err));
3308
- logger.debug("[coordination] Enabled", { projectId, workItemId, cwd });
3309
- return;
3004
+ getCoordinationLedgerLastReadAtMs() {
3005
+ return this.coordinationLedgerLastReadAtMs;
3310
3006
  }
3311
- flushOutboundQueue() {
3007
+ keepAlive(thinking, mode) {
3312
3008
  if (!this.socket.connected) return;
3313
- if (this.pendingOutboundMessages.length === 0) return;
3314
- const batch = this.pendingOutboundMessages;
3315
- this.pendingOutboundMessages = [];
3316
- logger.debug("[API] Flushing queued outbound messages", { count: batch.length });
3317
- for (const item of batch) {
3318
- this.socket.emit("message", { sid: item.sid, message: item.message });
3319
- }
3320
- }
3321
- emitMessageOrQueue(args) {
3322
- if (this.socket.connected) {
3323
- this.socket.emit("message", { sid: args.sid, message: args.message });
3324
- return;
3325
- }
3326
- const MAX_QUEUED = 200;
3327
- if (this.pendingOutboundMessages.length >= MAX_QUEUED) {
3328
- const head = this.pendingOutboundMessages[0];
3329
- throw new Error(
3330
- `Socket not connected; outbound queue full (${MAX_QUEUED}). Oldest=${head?.kind ?? "unknown"} queuedAt=${head?.queuedAt ?? "unknown"}`
3331
- );
3332
- }
3333
- this.pendingOutboundMessages.push({ ...args, queuedAt: Date.now() });
3334
- logger.debug("[API] Socket not connected; queued outbound message", {
3335
- kind: args.kind,
3336
- queued: this.pendingOutboundMessages.length
3337
- });
3009
+ this.socket.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking, mode });
3338
3010
  }
3339
- onUserMessage(callback) {
3340
- this.pendingMessageCallback = callback;
3341
- while (this.pendingMessages.length > 0) {
3342
- callback(this.pendingMessages.shift());
3343
- }
3011
+ sendSessionDeath() {
3012
+ this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
3344
3013
  }
3345
- /**
3346
- * Send message to session
3347
- * @param body - Message body (can be MessageContent or raw content for agent messages)
3348
- */
3349
- sendClaudeSessionMessage(body) {
3350
- let content;
3351
- if (body.type === "user" && typeof body.message.content === "string" && body.isSidechain !== true && body.isMeta !== true) {
3352
- content = {
3353
- role: "user",
3354
- content: {
3355
- type: "text",
3356
- text: body.message.content
3357
- },
3358
- meta: {
3359
- sentFrom: "cli"
3360
- }
3361
- };
3362
- } else {
3363
- content = {
3364
- role: "agent",
3365
- content: {
3366
- type: "output",
3367
- data: body
3368
- // This wraps the entire Claude message
3369
- },
3370
- meta: {
3371
- sentFrom: "cli"
3372
- }
3373
- };
3374
- }
3375
- logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
3376
- const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3377
- this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: "claude" });
3378
- if (body.type === "assistant" && body.message?.usage) {
3379
- try {
3380
- this.sendUsageData(body.message.usage);
3381
- } catch (error) {
3382
- logger.debug("[SOCKET] Failed to send usage data:", error);
3383
- }
3384
- }
3385
- if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
3386
- this.updateMetadata((metadata) => ({
3387
- ...metadata,
3388
- summary: {
3389
- text: body.summary,
3390
- updatedAt: Date.now()
3391
- }
3392
- }));
3393
- }
3014
+ sendSessionEvent(event) {
3015
+ this.emitMessageOrQueue({
3016
+ role: "agent",
3017
+ content: { type: "event", id: node_crypto.randomUUID(), data: event },
3018
+ meta: { sentFrom: "cli" }
3019
+ });
3394
3020
  }
3395
3021
  sendCodexMessage(body) {
3396
- let content = {
3397
- role: "agent",
3398
- content: {
3399
- type: "codex",
3400
- data: body
3401
- // This wraps the entire Claude message
3402
- },
3403
- meta: {
3404
- sentFrom: "cli"
3405
- }
3406
- };
3407
- const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3408
- this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `codex:${String(body?.type ?? "unknown")}` });
3022
+ this.emitMessageOrQueue({ role: "agent", content: { type: "codex", data: body }, meta: { sentFrom: "cli" } });
3409
3023
  }
3410
- /**
3411
- * Send a generic agent message to the session.
3412
- * Works for any agent type (Gemini, Codex, Claude, etc.)
3413
- *
3414
- * @param agentType - The type of agent sending the message (e.g., 'gemini', 'codex', 'claude')
3415
- * @param body - The message payload
3416
- */
3417
3024
  sendAgentMessage(agentType, body) {
3418
- let content = {
3419
- role: "agent",
3420
- content: {
3421
- type: agentType,
3422
- data: body
3423
- },
3424
- meta: {
3425
- sentFrom: "cli"
3426
- }
3427
- };
3428
- logger.debug(`[SOCKET] Sending ${agentType} message:`, { type: body.type, hasMessage: !!body.message });
3429
- const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3430
- this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `${agentType}:${String(body?.type ?? "unknown")}` });
3025
+ this.emitMessageOrQueue({ role: "agent", content: { type: agentType, data: body }, meta: { sentFrom: "cli" } });
3431
3026
  }
3432
- sendSessionEvent(event, id) {
3433
- let content = {
3027
+ sendClaudeSessionMessage(body) {
3028
+ this.emitMessageOrQueue({
3434
3029
  role: "agent",
3435
- content: {
3436
- id: id ?? node_crypto.randomUUID(),
3437
- type: "event",
3438
- data: event
3439
- }
3440
- };
3441
- const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
3442
- this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `event:${event.type}` });
3443
- }
3444
- /**
3445
- * Send a ping message to keep the connection alive
3446
- */
3447
- keepAlive(thinking, mode) {
3448
- if (process.env.DEBUG) {
3449
- logger.debug(`[API] Sending keep alive message: ${thinking}`);
3450
- }
3451
- this.socket.volatile.emit("session-alive", {
3452
- sid: this.sessionId,
3453
- time: Date.now(),
3454
- thinking,
3455
- mode
3030
+ content: { type: "output", data: body },
3031
+ meta: { sentFrom: "cli" }
3456
3032
  });
3457
3033
  }
3458
- /**
3459
- * Send session death message
3460
- */
3461
- sendSessionDeath() {
3462
- this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
3034
+ emitMessageOrQueue(content) {
3035
+ const payload = { sid: this.sessionId, content };
3036
+ if (this.socket.connected) {
3037
+ this.socket.emit("message", payload);
3038
+ return;
3039
+ }
3040
+ this.outboundQueue.push(payload);
3463
3041
  }
3464
- /**
3465
- * Send usage data to the server
3466
- */
3467
- sendUsageData(usage) {
3468
- const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
3469
- const usageReport = {
3470
- key: "claude-session",
3471
- sessionId: this.sessionId,
3472
- tokens: {
3473
- total: totalTokens,
3474
- input: usage.input_tokens,
3475
- output: usage.output_tokens,
3476
- cache_creation: usage.cache_creation_input_tokens || 0,
3477
- cache_read: usage.cache_read_input_tokens || 0
3478
- },
3479
- cost: {
3480
- // TODO: Calculate actual costs based on pricing
3481
- // For now, using placeholder values
3482
- total: 0,
3483
- input: 0,
3484
- output: 0
3485
- }
3042
+ flush() {
3043
+ if (!this.socket.connected) return;
3044
+ while (this.outboundQueue.length > 0) {
3045
+ const next = this.outboundQueue.shift();
3046
+ if (!next) continue;
3047
+ this.socket.emit("message", next);
3048
+ }
3049
+ }
3050
+ onUserMessage(handler) {
3051
+ const listener = (u) => {
3052
+ if (u?.body?.t !== "new-message") return;
3053
+ const body = u.body;
3054
+ const raw = body?.message ?? null;
3055
+ if (!raw || typeof raw !== "object") return;
3056
+ const record = raw?.content;
3057
+ if (!record || typeof record !== "object") return;
3058
+ if (record.role !== "user") return;
3059
+ handler(record);
3486
3060
  };
3487
- logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
3488
- this.socket.emit("usage-report", usageReport);
3061
+ this.on("update", listener);
3062
+ return () => this.off("update", listener);
3489
3063
  }
3490
- /**
3491
- * Update session metadata
3492
- * @param handler - Handler function that returns the updated metadata
3493
- */
3494
3064
  updateMetadata(handler) {
3495
- this.metadataLock.inLock(async () => {
3496
- await backoff(async () => {
3497
- let updated = handler(this.metadata);
3498
- const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, updated)) });
3499
- if (answer.result === "success") {
3500
- this.metadata = decrypt(this.encryptionKey, decodeBase64(answer.metadata));
3501
- this.metadataVersion = answer.version;
3502
- } else if (answer.result === "version-mismatch") {
3503
- if (answer.version > this.metadataVersion) {
3504
- this.metadataVersion = answer.version;
3505
- this.metadata = decrypt(this.encryptionKey, decodeBase64(answer.metadata));
3506
- }
3507
- throw new Error("Metadata version mismatch");
3508
- } else if (answer.result === "error") ;
3509
- });
3510
- });
3065
+ const current = this.metadata || {};
3066
+ const next = handler(current);
3067
+ void this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: next }).then((answer) => {
3068
+ if (answer?.result === "success") {
3069
+ this.metadata = next;
3070
+ this.metadataVersion += 1;
3071
+ }
3072
+ }).catch(() => null);
3511
3073
  }
3512
- /**
3513
- * Update session agent state
3514
- * @param handler - Handler function that returns the updated agent state
3515
- */
3516
3074
  updateAgentState(handler) {
3517
- logger.debugLargeJson("Updating agent state", this.agentState);
3518
- this.agentStateLock.inLock(async () => {
3519
- await backoff(async () => {
3520
- let updated = handler(this.agentState || {});
3521
- const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(this.encryptionKey, updated)) : null });
3522
- if (answer.result === "success") {
3523
- this.agentState = answer.agentState ? decrypt(this.encryptionKey, decodeBase64(answer.agentState)) : null;
3524
- this.agentStateVersion = answer.version;
3525
- logger.debug("Agent state updated", this.agentState);
3526
- } else if (answer.result === "version-mismatch") {
3527
- if (answer.version > this.agentStateVersion) {
3528
- this.agentStateVersion = answer.version;
3529
- this.agentState = answer.agentState ? decrypt(this.encryptionKey, decodeBase64(answer.agentState)) : null;
3530
- }
3531
- throw new Error("Agent state version mismatch");
3532
- } else if (answer.result === "error") ;
3533
- });
3534
- });
3075
+ const current = this.agentState || {};
3076
+ const next = handler(current);
3077
+ void this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: next }).then((answer) => {
3078
+ if (answer?.result === "success") {
3079
+ this.agentState = next;
3080
+ this.agentStateVersion += 1;
3081
+ }
3082
+ }).catch(() => null);
3535
3083
  }
3536
- /**
3537
- * Wait for socket buffer to flush
3538
- */
3539
- async flush() {
3540
- if (!this.socket.connected) {
3541
- return;
3084
+ }
3085
+
3086
+ async function delay(ms) {
3087
+ return new Promise((resolve) => setTimeout(resolve, ms));
3088
+ }
3089
+ function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
3090
+ let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
3091
+ return Math.round(Math.random() * maxDelayRet);
3092
+ }
3093
+ function createBackoff(opts) {
3094
+ return async (callback) => {
3095
+ let currentFailureCount = 0;
3096
+ const minDelay = opts && opts.minDelay !== void 0 ? opts.minDelay : 250;
3097
+ const maxDelay = opts && opts.maxDelay !== void 0 ? opts.maxDelay : 1e3;
3098
+ const maxFailureCount = opts && opts.maxFailureCount !== void 0 ? opts.maxFailureCount : 50;
3099
+ while (true) {
3100
+ try {
3101
+ return await callback();
3102
+ } catch (e) {
3103
+ if (currentFailureCount < maxFailureCount) {
3104
+ currentFailureCount++;
3105
+ }
3106
+ if (opts && opts.onError) {
3107
+ opts.onError(e, currentFailureCount);
3108
+ }
3109
+ let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
3110
+ await delay(waitForRequest);
3111
+ }
3542
3112
  }
3543
- return new Promise((resolve) => {
3544
- this.socket.emit("ping", () => {
3545
- resolve();
3546
- });
3547
- setTimeout(() => {
3548
- resolve();
3549
- }, 1e4);
3550
- });
3551
- }
3552
- async close() {
3553
- logger.debug("[API] socket.close() called");
3554
- this.socket.close();
3555
- }
3113
+ };
3556
3114
  }
3115
+ let backoff = createBackoff();
3557
3116
 
3558
3117
  function looksLikeEngineRoot(engineRoot) {
3559
3118
  if (!engineRoot) return false;
@@ -3726,6 +3285,11 @@ async function buildAndInstallUnrealMcpPlugin(options) {
3726
3285
  `-Plugin=${pluginUpluginPath}`,
3727
3286
  `-Package=${packageDir}`,
3728
3287
  "-Rocket",
3288
+ // UAT enforces a global single-instance mutex. On macOS/Linux, the named mutex can
3289
+ // appear "already created" even when no process is actually holding it, which makes
3290
+ // repeated installs fail with: "A conflicting instance of AutomationTool is already running."
3291
+ // `-WaitForUATMutex` makes UAT acquire the mutex if it's free (or wait if another build is in flight).
3292
+ "-WaitForUATMutex",
3729
3293
  `-TargetPlatforms=${targetPlatform()}`
3730
3294
  ];
3731
3295
  const logStream = fs.createWriteStream(buildLogPath, { flags: "a" });
@@ -3798,7 +3362,6 @@ class ApiMachineClient {
3798
3362
  this.machine = machine;
3799
3363
  this.rpcHandlerManager = new RpcHandlerManager({
3800
3364
  scopePrefix: this.machine.id,
3801
- encryptionKey: this.machine.encryptionKey,
3802
3365
  logger: (msg, data) => logger.debug(msg, data)
3803
3366
  });
3804
3367
  const rootDir = this.machine?.metadata?.homeDir || os.homedir() || process.cwd();
@@ -3839,6 +3402,24 @@ class ApiMachineClient {
3839
3402
  socket;
3840
3403
  keepAliveInterval = null;
3841
3404
  rpcHandlerManager;
3405
+ connected = false;
3406
+ lastConnectError = null;
3407
+ lastDisconnectReason = null;
3408
+ lastHttpUpsertError = null;
3409
+ lastHttpUpsertStatus = null;
3410
+ lastHttpUpsertAt = null;
3411
+ upsertBackoff = createBackoff({
3412
+ minDelay: 1e3,
3413
+ maxDelay: 3e4,
3414
+ maxFailureCount: 200,
3415
+ onError: (e, failures) => {
3416
+ const msg = e instanceof Error ? e.message : String(e);
3417
+ this.lastHttpUpsertError = msg;
3418
+ if (process.env.DEBUG) {
3419
+ logger.debug(`[API MACHINE] Machine upsert retry (${failures}): ${msg}`);
3420
+ }
3421
+ }
3422
+ });
3842
3423
  setRPCHandlers({
3843
3424
  spawnSession,
3844
3425
  stopSession,
@@ -3885,74 +3466,86 @@ class ApiMachineClient {
3885
3466
  });
3886
3467
  }
3887
3468
  /**
3888
- * Update machine metadata
3889
- * Currently unused, changes from the mobile client are more likely
3890
- * for example to set a custom name.
3469
+ * Upsert machine record (metadata + daemon state) via HTTP (workspace-native V1).
3891
3470
  */
3892
- async updateMachineMetadata(handler) {
3893
- await backoff(async () => {
3894
- const updated = handler(this.machine.metadata);
3895
- const answer = await this.socket.emitWithAck("machine-update-metadata", {
3896
- machineId: this.machine.id,
3897
- metadata: encodeBase64(encrypt(this.machine.encryptionKey, updated)),
3898
- expectedVersion: this.machine.metadataVersion
3899
- });
3900
- if (answer.result === "success") {
3901
- this.machine.metadata = decrypt(this.machine.encryptionKey, decodeBase64(answer.metadata));
3902
- this.machine.metadataVersion = answer.version;
3903
- logger.debug("[API MACHINE] Metadata updated successfully");
3904
- } else if (answer.result === "version-mismatch") {
3905
- if (answer.version > this.machine.metadataVersion) {
3906
- this.machine.metadataVersion = answer.version;
3907
- this.machine.metadata = decrypt(this.machine.encryptionKey, decodeBase64(answer.metadata));
3908
- }
3909
- throw new Error("Metadata version mismatch");
3910
- }
3471
+ async upsertMachineHttp(params) {
3472
+ const attemptAt = Date.now();
3473
+ const endpoint = configuration.serverUrl.replace(/\/+$/, "");
3474
+ const timeoutMs = Math.max(1e3, Math.min(12e4, Number.parseInt(process.env.FLOCKBAY_MACHINE_UPSERT_TIMEOUT_MS || "30000", 10) || 3e4));
3475
+ const res = await fetch(`${endpoint}/v1/machines`, {
3476
+ method: "POST",
3477
+ headers: {
3478
+ Authorization: `Machine ${this.token}`,
3479
+ "Content-Type": "application/json"
3480
+ },
3481
+ body: JSON.stringify({
3482
+ id: this.machine.id,
3483
+ ...params.metadata !== void 0 ? { metadata: params.metadata } : {},
3484
+ ...params.daemonState !== void 0 ? { daemonState: params.daemonState } : {}
3485
+ }),
3486
+ signal: AbortSignal.timeout(timeoutMs)
3911
3487
  });
3488
+ if (!res.ok) {
3489
+ const detail = await res.text().catch(() => "");
3490
+ const msg = `machine_upsert_failed:${res.status}${detail ? `:${detail.slice(0, 2e3)}` : ""}`;
3491
+ this.lastHttpUpsertStatus = res.status;
3492
+ this.lastHttpUpsertError = msg;
3493
+ throw new Error(msg);
3494
+ }
3495
+ const data = await res.json().catch(() => null);
3496
+ const machine = data?.machine && typeof data.machine === "object" ? data.machine : null;
3497
+ if (machine) {
3498
+ this.machine.metadata = machine.metadata ?? null;
3499
+ this.machine.daemonState = machine.daemonState ?? null;
3500
+ this.machine.seq = Number(machine.seq || this.machine.seq || 0);
3501
+ }
3502
+ this.lastHttpUpsertStatus = res.status;
3503
+ this.lastHttpUpsertError = null;
3504
+ this.lastHttpUpsertAt = attemptAt;
3912
3505
  }
3913
3506
  /**
3914
- * Update daemon state (runtime info) - similar to session updateAgentState
3915
- * Simplified without lock - relies on backoff for retry
3507
+ * Update daemon state (runtime info) via HTTP upsert.
3916
3508
  */
3917
3509
  async updateDaemonState(handler) {
3918
- await backoff(async () => {
3510
+ await this.upsertBackoff(async () => {
3919
3511
  const updated = handler(this.machine.daemonState);
3920
- const answer = await this.socket.emitWithAck("machine-update-state", {
3921
- machineId: this.machine.id,
3922
- daemonState: encodeBase64(encrypt(this.machine.encryptionKey, updated)),
3923
- expectedVersion: this.machine.daemonStateVersion
3924
- });
3925
- if (answer.result === "success") {
3926
- this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(answer.daemonState));
3927
- this.machine.daemonStateVersion = answer.version;
3928
- logger.debug("[API MACHINE] Daemon state updated successfully");
3929
- } else if (answer.result === "version-mismatch") {
3930
- if (answer.version > this.machine.daemonStateVersion) {
3931
- this.machine.daemonStateVersion = answer.version;
3932
- this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(answer.daemonState));
3933
- }
3934
- throw new Error("Daemon state version mismatch");
3935
- }
3512
+ await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
3513
+ logger.debug("[API MACHINE] Daemon state updated successfully");
3936
3514
  });
3937
3515
  }
3516
+ /**
3517
+ * Best-effort single attempt (no retries). Useful during shutdown.
3518
+ */
3519
+ async updateDaemonStateOnce(handler) {
3520
+ try {
3521
+ const updated = handler(this.machine.daemonState);
3522
+ await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
3523
+ return true;
3524
+ } catch (e) {
3525
+ const msg = e instanceof Error ? e.message : String(e);
3526
+ logger.debug("[API MACHINE] Daemon state update failed (best-effort):", msg);
3527
+ return false;
3528
+ }
3529
+ }
3530
+ getStatusSnapshot() {
3531
+ return {
3532
+ connected: this.connected,
3533
+ lastConnectError: this.lastConnectError,
3534
+ lastDisconnectReason: this.lastDisconnectReason,
3535
+ lastHttpUpsertError: this.lastHttpUpsertError,
3536
+ lastHttpUpsertStatus: this.lastHttpUpsertStatus,
3537
+ lastHttpUpsertAt: this.lastHttpUpsertAt
3538
+ };
3539
+ }
3938
3540
  connect() {
3939
3541
  const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
3940
3542
  logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
3941
- const machineMetadataEncrypted = encodeBase64(encrypt(this.machine.encryptionKey, this.machine.metadata));
3942
- const daemonStateEncrypted = this.machine.daemonState ? encodeBase64(encrypt(this.machine.encryptionKey, this.machine.daemonState)) : "";
3943
- const dataEncryptionKey = String(this.machine.dataEncryptionKey || "").trim();
3944
- if (!dataEncryptionKey) {
3945
- throw new Error("Missing machine dataEncryptionKey (DEK)");
3946
- }
3947
3543
  this.socket = socket_ioClient.io(serverUrl, {
3948
3544
  transports: ["websocket"],
3949
3545
  auth: {
3950
3546
  token: this.token,
3951
3547
  clientType: "machine-scoped",
3952
- machineId: this.machine.id,
3953
- dataEncryptionKey,
3954
- machineMetadata: machineMetadataEncrypted,
3955
- daemonState: daemonStateEncrypted
3548
+ machineId: this.machine.id
3956
3549
  },
3957
3550
  path: "/v1/updates",
3958
3551
  reconnection: true,
@@ -3961,6 +3554,9 @@ class ApiMachineClient {
3961
3554
  });
3962
3555
  this.socket.on("connect", () => {
3963
3556
  logger.debug("[API MACHINE] Connected to server");
3557
+ this.connected = true;
3558
+ this.lastConnectError = null;
3559
+ this.lastDisconnectReason = null;
3964
3560
  this.updateDaemonState((state) => ({
3965
3561
  ...state,
3966
3562
  status: "running",
@@ -3971,8 +3567,10 @@ class ApiMachineClient {
3971
3567
  this.rpcHandlerManager.onSocketConnect(this.socket);
3972
3568
  this.startKeepAlive();
3973
3569
  });
3974
- this.socket.on("disconnect", () => {
3975
- logger.debug("[API MACHINE] Disconnected from server");
3570
+ this.socket.on("disconnect", (reason) => {
3571
+ logger.debug("[API MACHINE] Disconnected from server", reason);
3572
+ this.connected = false;
3573
+ this.lastDisconnectReason = typeof reason === "string" ? reason : "disconnected";
3976
3574
  this.rpcHandlerManager.onSocketDisconnect();
3977
3575
  this.stopKeepAlive();
3978
3576
  });
@@ -3981,24 +3579,19 @@ class ApiMachineClient {
3981
3579
  callback(await this.rpcHandlerManager.handleRequest(data));
3982
3580
  });
3983
3581
  this.socket.on("update", (data) => {
3984
- if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
3582
+ if (data?.body?.t === "update-machine" && data.body.machineId === this.machine.id) {
3985
3583
  const update = data.body;
3986
- if (update.metadata) {
3987
- logger.debug("[API MACHINE] Received external metadata update");
3988
- this.machine.metadata = decrypt(this.machine.encryptionKey, decodeBase64(update.metadata.value));
3989
- this.machine.metadataVersion = update.metadata.version;
3584
+ if (update.machine) {
3585
+ this.machine.metadata = update.machine.metadata ?? null;
3586
+ this.machine.daemonState = update.machine.daemonState ?? null;
3587
+ this.machine.seq = Number(update.machine.seq || this.machine.seq || 0);
3990
3588
  }
3991
- if (update.daemonState) {
3992
- logger.debug("[API MACHINE] Received external daemon state update");
3993
- this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(update.daemonState.value));
3994
- this.machine.daemonStateVersion = update.daemonState.version;
3995
- }
3996
- } else {
3997
- logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
3998
3589
  }
3999
3590
  });
4000
3591
  this.socket.on("connect_error", (error) => {
4001
3592
  logger.debug(`[API MACHINE] Connection error: ${error.message}`);
3593
+ this.connected = false;
3594
+ this.lastConnectError = String(error?.message || "connect_error");
4002
3595
  });
4003
3596
  this.socket.io.on("error", (error) => {
4004
3597
  logger.debug("[API MACHINE] Socket error:", error);
@@ -4035,389 +3628,123 @@ class ApiMachineClient {
4035
3628
  }
4036
3629
  }
4037
3630
 
4038
- class PushNotificationClient {
4039
- token;
4040
- baseUrl;
4041
- expo;
4042
- constructor(token, baseUrl = "https://api-internal.flockbay.com") {
4043
- this.token = token;
4044
- this.baseUrl = baseUrl;
4045
- this.expo = new expoServerSdk.Expo();
4046
- }
4047
- /**
4048
- * Fetch all push tokens for the authenticated user
4049
- */
4050
- async fetchPushTokens() {
4051
- try {
4052
- const response = await axios.get(
4053
- `${this.baseUrl}/v1/push-tokens`,
4054
- {
4055
- headers: {
4056
- "Authorization": `Bearer ${this.token}`,
4057
- "Content-Type": "application/json"
4058
- }
4059
- }
4060
- );
4061
- logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
4062
- response.data.tokens.forEach((token, index) => {
4063
- logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
4064
- });
4065
- return response.data.tokens;
4066
- } catch (error) {
4067
- logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
4068
- throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
4069
- }
4070
- }
4071
- /**
4072
- * Send push notification via Expo Push API with retry
4073
- * @param messages - Array of push messages to send
4074
- */
4075
- async sendPushNotifications(messages) {
4076
- logger.debug(`Sending ${messages.length} push notifications`);
4077
- const validMessages = messages.filter((message) => {
4078
- if (Array.isArray(message.to)) {
4079
- return message.to.every((token) => expoServerSdk.Expo.isExpoPushToken(token));
4080
- }
4081
- return expoServerSdk.Expo.isExpoPushToken(message.to);
4082
- });
4083
- if (validMessages.length === 0) {
4084
- logger.debug("No valid Expo push tokens found");
4085
- return;
4086
- }
4087
- const chunks = this.expo.chunkPushNotifications(validMessages);
4088
- for (const chunk of chunks) {
4089
- const startTime = Date.now();
4090
- const timeout = 3e5;
4091
- let attempt = 0;
4092
- while (true) {
4093
- try {
4094
- const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
4095
- const errors = ticketChunk.filter((ticket) => ticket.status === "error");
4096
- if (errors.length > 0) {
4097
- const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
4098
- logger.debug("[PUSH] Some notifications failed:", errorDetails);
4099
- }
4100
- if (errors.length === ticketChunk.length) {
4101
- throw new Error("All push notifications in chunk failed");
4102
- }
4103
- break;
4104
- } catch (error) {
4105
- const elapsed = Date.now() - startTime;
4106
- if (elapsed >= timeout) {
4107
- logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
4108
- break;
4109
- }
4110
- attempt++;
4111
- const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
4112
- const remainingTime = timeout - elapsed;
4113
- const waitTime = Math.min(delay, remainingTime);
4114
- if (waitTime > 0) {
4115
- logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
4116
- await new Promise((resolve) => setTimeout(resolve, waitTime));
4117
- }
4118
- }
4119
- }
4120
- }
4121
- logger.debug(`Push notifications sent successfully`);
4122
- }
4123
- /**
4124
- * Send a push notification to all registered devices for the user
4125
- * @param title - Notification title
4126
- * @param body - Notification body
4127
- * @param data - Additional data to send with the notification
4128
- */
4129
- sendToAllDevices(title, body, data) {
4130
- logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
4131
- (async () => {
4132
- try {
4133
- logger.debug("[PUSH] Fetching push tokens...");
4134
- const tokens = await this.fetchPushTokens();
4135
- logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
4136
- tokens.forEach((token, index) => {
4137
- logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
4138
- });
4139
- if (tokens.length === 0) {
4140
- logger.debug("No push tokens found for user");
4141
- return;
4142
- }
4143
- const messages = tokens.map((token, index) => {
4144
- logger.debug(`[PUSH] Creating message ${index + 1} for token`);
4145
- return {
4146
- to: token.token,
4147
- title,
4148
- body,
4149
- data,
4150
- sound: "default",
4151
- priority: "high"
4152
- };
4153
- });
4154
- logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
4155
- await this.sendPushNotifications(messages);
4156
- logger.debug("[PUSH] Push notifications sent successfully");
4157
- } catch (error) {
4158
- logger.debug("[PUSH] Error sending to all devices:", error);
4159
- }
4160
- })();
4161
- }
3631
+ function machineAuthHeaders(machineToken) {
3632
+ return { Authorization: `Machine ${machineToken}`, "Content-Type": "application/json" };
3633
+ }
3634
+ function normalizeSession(raw) {
3635
+ return {
3636
+ id: String(raw?.id || ""),
3637
+ seq: Number(raw?.seq || 0),
3638
+ active: Boolean(raw?.active),
3639
+ activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
3640
+ createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
3641
+ updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
3642
+ metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
3643
+ metadataVersion: Number(raw?.metadataVersion || 0),
3644
+ agentState: raw?.agentState && typeof raw.agentState === "object" ? raw.agentState : null,
3645
+ agentStateVersion: Number(raw?.agentStateVersion || 0)
3646
+ };
3647
+ }
3648
+ function normalizeMachine(raw) {
3649
+ return {
3650
+ id: String(raw?.id || ""),
3651
+ seq: Number(raw?.seq || 0),
3652
+ active: Boolean(raw?.active),
3653
+ activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
3654
+ createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
3655
+ updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
3656
+ metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
3657
+ daemonState: raw?.daemonState && typeof raw.daemonState === "object" ? raw.daemonState : null
3658
+ };
4162
3659
  }
4163
-
4164
3660
  class ApiClient {
4165
- static async create(credential) {
4166
- return new ApiClient(credential);
3661
+ constructor(auth) {
3662
+ this.auth = auth;
4167
3663
  }
4168
- credential;
4169
- pushClient;
4170
- constructor(credential) {
4171
- this.credential = credential;
4172
- this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
3664
+ static async create(auth) {
3665
+ return new ApiClient(auth);
3666
+ }
3667
+ baseUrl() {
3668
+ return configuration.serverUrl.replace(/\/+$/, "");
3669
+ }
3670
+ async createSession(opts) {
3671
+ const response = await axios.post(
3672
+ `${this.baseUrl()}/v1/sessions`,
3673
+ { metadata: opts.metadata ?? {}, agentState: opts.state ?? null },
3674
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
3675
+ );
3676
+ return normalizeSession(response.data?.session);
4173
3677
  }
4174
- /**
4175
- * Create a new session or load existing one with the given tag
4176
- */
4177
3678
  async getOrCreateSession(opts) {
4178
- const encryptionKey = getRandomBytes(32);
4179
- const encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
4180
- const dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
4181
- dataEncryptionKey.set([0], 0);
4182
- dataEncryptionKey.set(encryptedDataKey, 1);
4183
- try {
4184
- const response = await axios.post(
4185
- `${configuration.serverUrl}/v1/sessions`,
4186
- {
4187
- tag: opts.tag,
4188
- metadata: encodeBase64(encrypt(encryptionKey, opts.metadata)),
4189
- agentState: opts.state ? encodeBase64(encrypt(encryptionKey, opts.state)) : null,
4190
- dataEncryptionKey: encodeBase64(dataEncryptionKey)
4191
- },
4192
- {
4193
- headers: {
4194
- "Authorization": `Bearer ${this.credential.token}`,
4195
- "Content-Type": "application/json"
4196
- },
4197
- timeout: 6e4
4198
- // 1 minute timeout for very bad network connections
4199
- }
4200
- );
4201
- logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
4202
- let raw = response.data.session;
4203
- let session = {
4204
- id: raw.id,
4205
- seq: raw.seq,
4206
- metadata: decrypt(encryptionKey, decodeBase64(raw.metadata)),
4207
- metadataVersion: raw.metadataVersion,
4208
- agentState: raw.agentState ? decrypt(encryptionKey, decodeBase64(raw.agentState)) : null,
4209
- agentStateVersion: raw.agentStateVersion,
4210
- encryptionKey
4211
- };
4212
- return session;
4213
- } catch (error) {
4214
- logger.debug("[API] [ERROR] Failed to get or create session:", error);
4215
- throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
4216
- }
3679
+ const tag = String(opts?.tag || "").trim();
3680
+ const metadata = opts?.metadata && typeof opts.metadata === "object" ? opts.metadata : {};
3681
+ const merged = tag ? { ...metadata, tag } : metadata;
3682
+ return this.createSession({ metadata: merged, state: opts.state ?? null });
3683
+ }
3684
+ async getSessionById(sessionId) {
3685
+ const id = String(sessionId || "").trim();
3686
+ if (!id) throw new Error("Session id is required");
3687
+ const response = await axios.get(
3688
+ `${this.baseUrl()}/v1/sessions/${encodeURIComponent(id)}`,
3689
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
3690
+ );
3691
+ return normalizeSession(response.data?.session);
4217
3692
  }
4218
- /**
4219
- * Register or update machine with the server
4220
- * Returns the current machine state from the server with decrypted metadata and daemonState
4221
- */
4222
- async getOrCreateMachine(opts) {
4223
- const encryptionKey = this.credential.encryption.machineKey;
4224
- const encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
4225
- const dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
4226
- dataEncryptionKey.set([0], 0);
4227
- dataEncryptionKey.set(encryptedDataKey, 1);
3693
+ async upsertMachine(opts) {
3694
+ const machineId = String(opts.machineId || "").trim();
3695
+ if (!machineId) throw new Error("Machine id is required");
4228
3696
  const response = await axios.post(
4229
- `${configuration.serverUrl}/v1/machines`,
4230
- {
4231
- id: opts.machineId,
4232
- metadata: encodeBase64(encrypt(encryptionKey, opts.metadata)),
4233
- daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, opts.daemonState)) : void 0,
4234
- dataEncryptionKey: encodeBase64(dataEncryptionKey)
4235
- },
4236
- {
4237
- headers: {
4238
- "Authorization": `Bearer ${this.credential.token}`,
4239
- "Content-Type": "application/json"
4240
- },
4241
- timeout: 6e4
4242
- // 1 minute timeout for very bad network connections
4243
- }
3697
+ `${this.baseUrl()}/v1/machines`,
3698
+ { id: machineId, metadata: opts.metadata ?? {}, daemonState: opts.daemonState ?? null },
3699
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
4244
3700
  );
4245
- if (response.status !== 200) {
4246
- console.error(chalk.red(`[API] Failed to create machine: ${response.statusText}`));
4247
- 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.`));
4248
- process.exit(1);
4249
- }
4250
- const raw = response.data.machine;
4251
- logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
4252
- const machine = {
4253
- id: raw.id,
4254
- dataEncryptionKey: String(raw.dataEncryptionKey || ""),
4255
- encryptionKey,
4256
- metadata: raw.metadata ? decrypt(encryptionKey, decodeBase64(raw.metadata)) : null,
4257
- metadataVersion: raw.metadataVersion || 0,
4258
- daemonState: raw.daemonState ? decrypt(encryptionKey, decodeBase64(raw.daemonState)) : null,
4259
- daemonStateVersion: raw.daemonStateVersion || 0
4260
- };
4261
- return machine;
3701
+ return normalizeMachine(response.data?.machine);
3702
+ }
3703
+ async getOrCreateMachine(opts) {
3704
+ return this.upsertMachine(opts);
4262
3705
  }
4263
- /**
4264
- * Fetch an existing machine by id.
4265
- * Useful when the daemon needs the latest server-side machine metadata (e.g. dev toggles).
4266
- */
4267
3706
  async getMachine(machineId) {
4268
3707
  const id = String(machineId || "").trim();
4269
- if (!id) {
4270
- throw new Error("Machine id is required");
4271
- }
4272
- const encryptionKey = this.credential.encryption.machineKey;
3708
+ if (!id) throw new Error("Machine id is required");
4273
3709
  const response = await axios.get(
4274
- `${configuration.serverUrl}/v1/machines/${encodeURIComponent(id)}`,
4275
- {
4276
- headers: {
4277
- "Authorization": `Bearer ${this.credential.token}`,
4278
- "Content-Type": "application/json"
4279
- },
4280
- timeout: 6e4
4281
- }
3710
+ `${this.baseUrl()}/v1/machines/${encodeURIComponent(id)}`,
3711
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
4282
3712
  );
4283
- if (response.status !== 200) {
4284
- throw new Error(`Failed to fetch machine: ${response.statusText}`);
4285
- }
4286
- const raw = response.data?.machine;
4287
- if (!raw?.id) {
4288
- throw new Error("Invalid machine response");
4289
- }
4290
- const machine = {
4291
- id: raw.id,
4292
- dataEncryptionKey: String(raw.dataEncryptionKey || ""),
4293
- encryptionKey,
4294
- metadata: raw.metadata ? decrypt(encryptionKey, decodeBase64(raw.metadata)) : null,
4295
- metadataVersion: raw.metadataVersion || 0,
4296
- daemonState: raw.daemonState ? decrypt(encryptionKey, decodeBase64(raw.daemonState)) : null,
4297
- daemonStateVersion: raw.daemonStateVersion || 0
4298
- };
4299
- return machine;
4300
- }
4301
- sessionSyncClient(session) {
4302
- return new ApiSessionClient(this.credential.token, session);
3713
+ return normalizeMachine(response.data?.machine);
3714
+ }
3715
+ async registerVendorToken(vendor, token) {
3716
+ const v = String(vendor || "").trim().toLowerCase();
3717
+ if (!v) throw new Error("Vendor is required");
3718
+ await axios.post(
3719
+ `${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`,
3720
+ { token },
3721
+ { headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
3722
+ );
3723
+ return { ok: true };
4303
3724
  }
4304
- machineSyncClient(machine) {
4305
- return new ApiMachineClient(this.credential.token, machine);
3725
+ async getVendorToken(vendor) {
3726
+ const v = String(vendor || "").trim().toLowerCase();
3727
+ if (!v) throw new Error("Vendor is required");
3728
+ const response = await axios.get(`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`, {
3729
+ headers: machineAuthHeaders(this.auth.machineToken),
3730
+ timeout: 6e4,
3731
+ validateStatus: (s) => s >= 200 && s < 300 || s === 404
3732
+ });
3733
+ if (response.status === 404) return null;
3734
+ return response.data?.token ?? null;
4306
3735
  }
4307
3736
  push() {
4308
- return this.pushClient;
4309
- }
4310
- /**
4311
- * Register a vendor API token with the server
4312
- * The token is sent as a JSON string - server handles encryption
4313
- */
4314
- async registerVendorToken(vendor, apiKey) {
4315
- try {
4316
- const response = await axios.post(
4317
- `${configuration.serverUrl}/v1/connect/${vendor}/register`,
4318
- {
4319
- token: JSON.stringify(apiKey)
4320
- },
4321
- {
4322
- headers: {
4323
- "Authorization": `Bearer ${this.credential.token}`,
4324
- "Content-Type": "application/json"
4325
- },
4326
- timeout: 5e3
4327
- }
4328
- );
4329
- if (response.status !== 200 && response.status !== 201) {
4330
- throw new Error(`Server returned status ${response.status}`);
3737
+ return {
3738
+ sendToAllDevices: async () => {
3739
+ return;
4331
3740
  }
4332
- logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
4333
- } catch (error) {
4334
- logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
4335
- throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
4336
- }
3741
+ };
4337
3742
  }
4338
- /**
4339
- * Get vendor API token from the server
4340
- * Returns the token if it exists, null otherwise
4341
- */
4342
- async getVendorToken(vendor) {
4343
- try {
4344
- const response = await axios.get(
4345
- `${configuration.serverUrl}/v1/connect/${vendor}/token`,
4346
- {
4347
- headers: {
4348
- "Authorization": `Bearer ${this.credential.token}`,
4349
- "Content-Type": "application/json"
4350
- },
4351
- timeout: 5e3
4352
- }
4353
- );
4354
- if (response.status === 404) {
4355
- logger.debug(`[API] No vendor token found for ${vendor}`);
4356
- return null;
4357
- }
4358
- if (response.status !== 200) {
4359
- throw new Error(`Server returned status ${response.status}`);
4360
- }
4361
- logger.debug(`[API] Raw vendor token response:`, {
4362
- status: response.status,
4363
- dataKeys: Object.keys(response.data || {}),
4364
- hasToken: "token" in (response.data || {}),
4365
- tokenType: typeof response.data?.token,
4366
- present: response.data?.present
4367
- });
4368
- const present = typeof response.data?.present === "boolean" ? response.data.present : null;
4369
- const tokenField = response.data?.token;
4370
- if (present === true && (tokenField === null || tokenField === void 0 || tokenField === "")) {
4371
- logger.debug(`[API] Vendor token for ${vendor} is present but redacted by the server (FLOCKBAY_REDACT_CONNECTED_TOKENS=1).`);
4372
- return null;
4373
- }
4374
- let tokenData = null;
4375
- if (response.data?.token) {
4376
- if (typeof response.data.token === "string") {
4377
- try {
4378
- tokenData = JSON.parse(response.data.token);
4379
- } catch (parseError) {
4380
- logger.debug(`[API] Failed to parse token as JSON, using as string:`, parseError);
4381
- tokenData = response.data.token;
4382
- }
4383
- } else if (response.data.token !== null) {
4384
- tokenData = response.data.token;
4385
- } else {
4386
- logger.debug(`[API] Token is null for ${vendor}, treating as not found`);
4387
- return null;
4388
- }
4389
- } else if (response.data && typeof response.data === "object") {
4390
- if (response.data.token === null && response.data.present === false) {
4391
- logger.debug(`[API] Response contains present=false and null token for ${vendor}, treating as not found`);
4392
- return null;
4393
- }
4394
- if (response.data.token === null && Object.keys(response.data).length === 1) {
4395
- logger.debug(`[API] Response contains only null token for ${vendor}, treating as not found`);
4396
- return null;
4397
- }
4398
- tokenData = response.data;
4399
- }
4400
- if (tokenData === null || tokenData && typeof tokenData === "object" && tokenData.token === null && Object.keys(tokenData).length === 1) {
4401
- logger.debug(`[API] Token data is null for ${vendor}`);
4402
- return null;
4403
- }
4404
- if (tokenData && typeof tokenData === "object" && "token" in tokenData && "present" in tokenData) {
4405
- logger.debug(`[API] Received token metadata object for ${vendor}; returning parsed token field only.`);
4406
- return tokenData.token ? tokenData.token : null;
4407
- }
4408
- logger.debug(`[API] Vendor token for ${vendor} retrieved successfully`, {
4409
- tokenDataType: typeof tokenData,
4410
- tokenDataKeys: tokenData && typeof tokenData === "object" ? Object.keys(tokenData) : "not an object"
4411
- });
4412
- return tokenData;
4413
- } catch (error) {
4414
- if (error.response?.status === 404) {
4415
- logger.debug(`[API] No vendor token found for ${vendor}`);
4416
- return null;
4417
- }
4418
- logger.debug(`[API] [ERROR] Failed to get vendor token:`, error);
4419
- return null;
4420
- }
3743
+ sessionSyncClient(session) {
3744
+ return new ApiSessionClient(this.auth.machineToken, session);
3745
+ }
3746
+ machineSyncClient(machine) {
3747
+ return new ApiMachineClient(this.auth.machineToken, machine);
4421
3748
  }
4422
3749
  }
4423
3750
 
@@ -4469,8 +3796,8 @@ const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
4469
3796
  ]);
4470
3797
 
4471
3798
  exports.ApiClient = ApiClient;
3799
+ exports.ApiMachineClient = ApiMachineClient;
4472
3800
  exports.ApiSessionClient = ApiSessionClient;
4473
- exports.AsyncLock = AsyncLock;
4474
3801
  exports.RawJSONLinesSchema = RawJSONLinesSchema;
4475
3802
  exports.acquireDaemonLock = acquireDaemonLock;
4476
3803
  exports.backoff = backoff;
@@ -4478,10 +3805,7 @@ exports.clearCredentials = clearCredentials;
4478
3805
  exports.clearDaemonState = clearDaemonState;
4479
3806
  exports.clearMachineId = clearMachineId;
4480
3807
  exports.configuration = configuration;
4481
- exports.decodeBase64 = decodeBase64;
4482
3808
  exports.delay = delay;
4483
- exports.encodeBase64 = encodeBase64;
4484
- exports.encodeBase64Url = encodeBase64Url;
4485
3809
  exports.getLatestDaemonLog = getLatestDaemonLog;
4486
3810
  exports.installUnrealMcpPluginToEngine = installUnrealMcpPluginToEngine;
4487
3811
  exports.logger = logger;
@@ -4494,5 +3818,5 @@ exports.releaseDaemonLock = releaseDaemonLock;
4494
3818
  exports.sendUnrealMcpTcpCommand = sendUnrealMcpTcpCommand;
4495
3819
  exports.unrealMcpPythonDir = unrealMcpPythonDir;
4496
3820
  exports.updateSettings = updateSettings;
4497
- exports.writeCredentialsDataKey = writeCredentialsDataKey;
3821
+ exports.writeCredentials = writeCredentials;
4498
3822
  exports.writeDaemonState = writeDaemonState;