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