flockbay 0.10.15 → 0.10.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codex/flockbayMcpStdioBridge.cjs +339 -0
- package/dist/codex/flockbayMcpStdioBridge.mjs +339 -0
- package/dist/{index--o4BPz5o.cjs → index-Cau-_Qvn.cjs} +2683 -609
- package/dist/{index-CUp3juDS.mjs → index-DtmFQzXY.mjs} +2684 -611
- package/dist/index.cjs +3 -5
- package/dist/index.mjs +3 -5
- package/dist/lib.cjs +7 -9
- package/dist/lib.d.cts +219 -531
- package/dist/lib.d.mts +219 -531
- package/dist/lib.mjs +7 -9
- package/dist/{runCodex-o6PCbHQ7.mjs → runCodex-Di9eHddq.mjs} +263 -42
- package/dist/{runCodex-D3eT-TvB.cjs → runCodex-DzP3VUa-.cjs} +264 -43
- package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-BS6sBU_V.mjs} +63 -28
- package/dist/{runGemini-CBxZp6I7.cjs → runGemini-CpmehDQ2.cjs} +64 -29
- package/dist/{types-DGd6ea2Z.mjs → types-CwzNqYEx.mjs} +465 -1142
- package/dist/{types-C-jnUdn_.cjs → types-SUAKq-K0.cjs} +466 -1146
- package/package.json +1 -1
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +195 -6
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +376 -5
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommandSchema.cpp +731 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +476 -8
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +1518 -94
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +7 -4
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +150 -112
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +2 -1
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +4 -1
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommandSchema.h +42 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +21 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +4 -1
- package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +0 -136
- 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
|
|
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
|
|
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.
|
|
45
|
+
var version = "0.10.16";
|
|
48
46
|
var description = "Flockbay CLI (local agent + daemon)";
|
|
49
47
|
var author = "Eduardo Orellana";
|
|
50
48
|
var license = "UNLICENSED";
|
|
@@ -195,6 +193,26 @@ 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") {
|
|
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
|
+
}
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
function sanitizeProfileName(input) {
|
|
212
|
+
const raw = String(input).trim();
|
|
213
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
214
|
+
return cleaned || "default";
|
|
215
|
+
}
|
|
198
216
|
function normalizeServerUrlForNode(url) {
|
|
199
217
|
try {
|
|
200
218
|
const u = new URL(url);
|
|
@@ -208,6 +226,7 @@ class Configuration {
|
|
|
208
226
|
serverUrl;
|
|
209
227
|
webappUrl;
|
|
210
228
|
isDaemonProcess;
|
|
229
|
+
profile;
|
|
211
230
|
// Directories and paths (from persistence)
|
|
212
231
|
flockbayHomeDir;
|
|
213
232
|
logsDir;
|
|
@@ -221,12 +240,15 @@ class Configuration {
|
|
|
221
240
|
constructor() {
|
|
222
241
|
const args = process.argv.slice(2);
|
|
223
242
|
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
243
|
+
const profileFromArgs = parseProfileFromProcessArgs();
|
|
244
|
+
const profileFromEnv = process.env.FLOCKBAY_PROFILE;
|
|
245
|
+
this.profile = sanitizeProfileName(profileFromEnv || profileFromArgs || "default");
|
|
224
246
|
const homeOverride = process.env.FLOCKBAY_HOME_DIR;
|
|
225
247
|
if (homeOverride) {
|
|
226
248
|
const expandedPath = homeOverride.replace(/^~/, os.homedir());
|
|
227
|
-
this.flockbayHomeDir = expandedPath;
|
|
249
|
+
this.flockbayHomeDir = path.join(expandedPath, "profiles", this.profile);
|
|
228
250
|
} else {
|
|
229
|
-
this.flockbayHomeDir = path.join(os.homedir(), ".flockbay");
|
|
251
|
+
this.flockbayHomeDir = path.join(os.homedir(), ".flockbay", "profiles", this.profile);
|
|
230
252
|
}
|
|
231
253
|
this.logsDir = path.join(this.flockbayHomeDir, "logs");
|
|
232
254
|
this.settingsFile = path.join(this.flockbayHomeDir, "settings.json");
|
|
@@ -260,83 +282,6 @@ class Configuration {
|
|
|
260
282
|
}
|
|
261
283
|
const configuration = new Configuration();
|
|
262
284
|
|
|
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
285
|
const defaultSettings = {
|
|
341
286
|
onboardingCompleted: false
|
|
342
287
|
};
|
|
@@ -402,40 +347,30 @@ async function updateSettings(updater) {
|
|
|
402
347
|
});
|
|
403
348
|
}
|
|
404
349
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
machineKey: z__namespace.string().base64()
|
|
410
|
-
})
|
|
350
|
+
const workspaceAuthSchema = z__namespace.object({
|
|
351
|
+
machineToken: z__namespace.string().min(1),
|
|
352
|
+
orgId: z__namespace.string().min(1),
|
|
353
|
+
createdAtMs: z__namespace.number().optional()
|
|
411
354
|
});
|
|
412
355
|
async function readCredentials() {
|
|
413
|
-
if (!fs.existsSync(configuration.privateKeyFile))
|
|
414
|
-
return null;
|
|
415
|
-
}
|
|
356
|
+
if (!fs.existsSync(configuration.privateKeyFile)) return null;
|
|
416
357
|
try {
|
|
417
|
-
const
|
|
418
|
-
const
|
|
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
|
-
};
|
|
358
|
+
const raw = await fs$1.readFile(configuration.privateKeyFile, "utf8");
|
|
359
|
+
const parsed = workspaceAuthSchema.parse(JSON.parse(raw));
|
|
360
|
+
return { machineToken: parsed.machineToken, orgId: parsed.orgId, createdAtMs: parsed.createdAtMs };
|
|
427
361
|
} catch {
|
|
428
362
|
return null;
|
|
429
363
|
}
|
|
430
364
|
}
|
|
431
|
-
async function
|
|
365
|
+
async function writeCredentials(auth) {
|
|
432
366
|
if (!fs.existsSync(configuration.flockbayHomeDir)) {
|
|
433
367
|
await fs$1.mkdir(configuration.flockbayHomeDir, { recursive: true });
|
|
434
368
|
}
|
|
435
|
-
await fs$1.writeFile(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
369
|
+
await fs$1.writeFile(
|
|
370
|
+
configuration.privateKeyFile,
|
|
371
|
+
JSON.stringify({ machineToken: auth.machineToken, orgId: auth.orgId, createdAtMs: auth.createdAtMs ?? Date.now() }, null, 2),
|
|
372
|
+
"utf8"
|
|
373
|
+
);
|
|
439
374
|
}
|
|
440
375
|
async function clearCredentials() {
|
|
441
376
|
if (fs.existsSync(configuration.privateKeyFile)) {
|
|
@@ -741,222 +676,13 @@ async function getLatestDaemonLog() {
|
|
|
741
676
|
return latest || null;
|
|
742
677
|
}
|
|
743
678
|
|
|
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
679
|
class RpcHandlerManager {
|
|
952
680
|
handlers = /* @__PURE__ */ new Map();
|
|
953
681
|
scopePrefix;
|
|
954
|
-
encryptionKey;
|
|
955
682
|
logger;
|
|
956
683
|
socket = null;
|
|
957
684
|
constructor(config) {
|
|
958
685
|
this.scopePrefix = config.scopePrefix;
|
|
959
|
-
this.encryptionKey = config.encryptionKey;
|
|
960
686
|
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
|
|
961
687
|
}
|
|
962
688
|
/**
|
|
@@ -981,20 +707,24 @@ class RpcHandlerManager {
|
|
|
981
707
|
const handler = this.handlers.get(request.method);
|
|
982
708
|
if (!handler) {
|
|
983
709
|
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
710
|
+
return { error: "Method not found" };
|
|
711
|
+
}
|
|
712
|
+
const rawParams = request.params;
|
|
713
|
+
const parsedParams = (() => {
|
|
714
|
+
if (typeof rawParams !== "string") return rawParams;
|
|
715
|
+
const s = rawParams.trim();
|
|
716
|
+
if (!s) return rawParams;
|
|
717
|
+
if (!(s.startsWith("{") || s.startsWith("["))) return rawParams;
|
|
718
|
+
try {
|
|
719
|
+
return JSON.parse(s);
|
|
720
|
+
} catch {
|
|
721
|
+
return rawParams;
|
|
722
|
+
}
|
|
723
|
+
})();
|
|
724
|
+
return await handler(parsedParams);
|
|
992
725
|
} catch (error) {
|
|
993
726
|
this.logger("[RPC] [ERROR] Error handling request", { error });
|
|
994
|
-
|
|
995
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
996
|
-
};
|
|
997
|
-
return encodeBase64(encrypt(this.encryptionKey, errorResponse));
|
|
727
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
998
728
|
}
|
|
999
729
|
}
|
|
1000
730
|
onSocketConnect(socket) {
|
|
@@ -1036,7 +766,7 @@ class RpcHandlerManager {
|
|
|
1036
766
|
}
|
|
1037
767
|
}
|
|
1038
768
|
|
|
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-
|
|
769
|
+
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-SUAKq-K0.cjs', document.baseURI).href))));
|
|
1040
770
|
function projectPath() {
|
|
1041
771
|
const path = path$1.resolve(__dirname$1, "..");
|
|
1042
772
|
return path;
|
|
@@ -1200,6 +930,19 @@ async function sendUnrealMcpTcpCommand(options) {
|
|
|
1200
930
|
const paramName = missingParamMatch[1];
|
|
1201
931
|
hints.push(`Tip: include the required parameter \`${paramName}\` in the request params (UnrealMCP does not infer defaults).`);
|
|
1202
932
|
}
|
|
933
|
+
const details = response?.details;
|
|
934
|
+
if (details && typeof details === "object") {
|
|
935
|
+
const kind = typeof details.kind === "string" ? details.kind : null;
|
|
936
|
+
const missing = Array.isArray(details.missing) ? details.missing.filter((x) => typeof x === "string") : [];
|
|
937
|
+
if (kind === "missing_params" && missing.length > 0) {
|
|
938
|
+
hints.push(`Missing params: ${missing.map((m) => `\`${m}\``).join(", ")}`);
|
|
939
|
+
}
|
|
940
|
+
const exampleCall = details.exampleCall;
|
|
941
|
+
if (exampleCall && typeof exampleCall === "object") {
|
|
942
|
+
hints.push(`Example call:
|
|
943
|
+
${JSON.stringify(exampleCall, null, 2)}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
1203
946
|
const hint = hints.length > 0 ? `
|
|
1204
947
|
|
|
1205
948
|
${hints.join("\n\n")}` : "";
|
|
@@ -1478,7 +1221,7 @@ async function apiJsonGet(params) {
|
|
|
1478
1221
|
const res = await fetch(url, {
|
|
1479
1222
|
method: "GET",
|
|
1480
1223
|
headers: {
|
|
1481
|
-
Authorization: `
|
|
1224
|
+
Authorization: `Machine ${params.token}`,
|
|
1482
1225
|
Accept: "application/json"
|
|
1483
1226
|
}
|
|
1484
1227
|
});
|
|
@@ -1494,7 +1237,7 @@ async function apiJsonPost(params) {
|
|
|
1494
1237
|
const res = await fetch(url, {
|
|
1495
1238
|
method: "POST",
|
|
1496
1239
|
headers: {
|
|
1497
|
-
Authorization: `
|
|
1240
|
+
Authorization: `Machine ${params.token}`,
|
|
1498
1241
|
"Content-Type": "application/json",
|
|
1499
1242
|
Accept: "application/json"
|
|
1500
1243
|
},
|
|
@@ -1859,8 +1602,20 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordinatio
|
|
|
1859
1602
|
"play_in_editor_windowed",
|
|
1860
1603
|
"stop_play_in_editor",
|
|
1861
1604
|
"take_screenshot",
|
|
1605
|
+
"map_check",
|
|
1606
|
+
"compile_blueprints_all",
|
|
1607
|
+
"get_editor_context",
|
|
1608
|
+
"get_player_context",
|
|
1609
|
+
"raycast_from_camera",
|
|
1610
|
+
"raycast_down",
|
|
1611
|
+
"get_actor_transform",
|
|
1612
|
+
"get_actor_bounds",
|
|
1862
1613
|
"spawn_actor",
|
|
1863
|
-
"set_actor_transform"
|
|
1614
|
+
"set_actor_transform",
|
|
1615
|
+
"search_assets",
|
|
1616
|
+
"get_asset_info",
|
|
1617
|
+
"list_asset_packs",
|
|
1618
|
+
"place_asset"
|
|
1864
1619
|
];
|
|
1865
1620
|
const expected = await readExpectedUnrealMcpUplugin().catch((err) => {
|
|
1866
1621
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3122,48 +2877,38 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3122
2877
|
agentState;
|
|
3123
2878
|
agentStateVersion;
|
|
3124
2879
|
socket;
|
|
3125
|
-
pendingMessages = [];
|
|
3126
|
-
pendingMessageCallback = null;
|
|
3127
|
-
pendingOutboundMessages = [];
|
|
3128
2880
|
rpcHandlerManager;
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
coordinationLeaseGuard;
|
|
3133
|
-
coordinationLedgerReadAt = 0;
|
|
3134
|
-
docsIndexReadAt = 0;
|
|
2881
|
+
coordinationLeaseGuard = new CoordinationLeaseGuard();
|
|
2882
|
+
coordinationLedgerLastReadAtMs = null;
|
|
2883
|
+
outboundQueue = [];
|
|
3135
2884
|
constructor(token, session) {
|
|
3136
2885
|
super();
|
|
3137
2886
|
this.token = token;
|
|
3138
2887
|
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();
|
|
2888
|
+
this.metadata = session.metadata ?? null;
|
|
2889
|
+
this.metadataVersion = Number(session.metadataVersion || 0);
|
|
2890
|
+
this.agentState = session.agentState ?? null;
|
|
2891
|
+
this.agentStateVersion = Number(session.agentStateVersion || 0);
|
|
3145
2892
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
3146
2893
|
scopePrefix: this.sessionId,
|
|
3147
|
-
encryptionKey: this.encryptionKey,
|
|
3148
2894
|
logger: (msg, data) => logger.debug(msg, data)
|
|
3149
2895
|
});
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
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: {
|
|
2896
|
+
const cwd = String(this.metadata?.path || process.cwd()).trim() || process.cwd();
|
|
2897
|
+
const machineId = String(this.metadata?.machineId || "").trim();
|
|
2898
|
+
registerCommonHandlers(
|
|
2899
|
+
this.rpcHandlerManager,
|
|
2900
|
+
cwd,
|
|
2901
|
+
{
|
|
2902
|
+
serverUrl: configuration.serverUrl,
|
|
3163
2903
|
token: this.token,
|
|
3164
|
-
|
|
3165
|
-
|
|
2904
|
+
sessionId: this.sessionId,
|
|
2905
|
+
machineId,
|
|
2906
|
+
projectRootPath: String(this.metadata?.projectRootPath || cwd)
|
|
3166
2907
|
},
|
|
2908
|
+
{ bashEnabled: false }
|
|
2909
|
+
);
|
|
2910
|
+
this.socket = socket_ioClient.io(configuration.serverUrl, {
|
|
2911
|
+
auth: { token: this.token, clientType: "session-scoped", sessionId: this.sessionId, machineId },
|
|
3167
2912
|
path: "/v1/updates",
|
|
3168
2913
|
reconnection: true,
|
|
3169
2914
|
reconnectionAttempts: Infinity,
|
|
@@ -3174,386 +2919,196 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3174
2919
|
autoConnect: false
|
|
3175
2920
|
});
|
|
3176
2921
|
this.socket.on("connect", () => {
|
|
3177
|
-
logger.debug("
|
|
2922
|
+
logger.debug("[session] socket connected");
|
|
3178
2923
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
3179
|
-
this.
|
|
2924
|
+
this.flush();
|
|
2925
|
+
this.emit("connect");
|
|
3180
2926
|
});
|
|
3181
|
-
this.socket.on("
|
|
3182
|
-
|
|
2927
|
+
this.socket.on("connect_error", (error) => {
|
|
2928
|
+
const message = error instanceof Error ? error.message : String(error?.message || error || "connect_error");
|
|
2929
|
+
logger.debug("[session] socket connect_error", { message });
|
|
3183
2930
|
});
|
|
3184
2931
|
this.socket.on("disconnect", (reason) => {
|
|
3185
|
-
logger.debug("[
|
|
2932
|
+
logger.debug("[session] socket disconnected", reason);
|
|
3186
2933
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
2934
|
+
this.emit("disconnect", reason);
|
|
3187
2935
|
});
|
|
3188
|
-
this.socket.on("
|
|
3189
|
-
|
|
3190
|
-
this.rpcHandlerManager.onSocketDisconnect();
|
|
2936
|
+
this.socket.on("rpc-request", async (data, callback) => {
|
|
2937
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
3191
2938
|
});
|
|
3192
2939
|
this.socket.on("update", (data) => {
|
|
3193
|
-
|
|
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
|
-
}
|
|
3229
|
-
});
|
|
3230
|
-
this.socket.on("error", (error) => {
|
|
3231
|
-
logger.debug("[API] Socket error:", error);
|
|
2940
|
+
this.emit("update", data);
|
|
3232
2941
|
});
|
|
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
2942
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
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);
|
|
2943
|
+
connect() {
|
|
2944
|
+
this.socket.connect();
|
|
3252
2945
|
}
|
|
3253
|
-
|
|
3254
|
-
this.
|
|
2946
|
+
async connectAndWait(timeoutMs = 15e3) {
|
|
2947
|
+
if (this.socket.connected) return;
|
|
2948
|
+
await new Promise((resolve, reject) => {
|
|
2949
|
+
let timeoutHandle = null;
|
|
2950
|
+
const cleanup = () => {
|
|
2951
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2952
|
+
this.socket.off("connect", onConnect);
|
|
2953
|
+
this.socket.off("connect_error", onError);
|
|
2954
|
+
this.socket.off("disconnect", onDisconnect);
|
|
2955
|
+
};
|
|
2956
|
+
const onConnect = () => {
|
|
2957
|
+
cleanup();
|
|
2958
|
+
resolve();
|
|
2959
|
+
};
|
|
2960
|
+
const onError = (err) => {
|
|
2961
|
+
cleanup();
|
|
2962
|
+
const message = err instanceof Error ? err.message : String(err?.message || err || "connect_error");
|
|
2963
|
+
reject(new Error(message));
|
|
2964
|
+
};
|
|
2965
|
+
const onDisconnect = (reason) => {
|
|
2966
|
+
cleanup();
|
|
2967
|
+
reject(new Error(`disconnected:${String(reason || "unknown")}`));
|
|
2968
|
+
};
|
|
2969
|
+
timeoutHandle = setTimeout(() => {
|
|
2970
|
+
cleanup();
|
|
2971
|
+
reject(new Error("connect_timeout"));
|
|
2972
|
+
}, timeoutMs);
|
|
2973
|
+
this.socket.once("connect", onConnect);
|
|
2974
|
+
this.socket.once("connect_error", onError);
|
|
2975
|
+
this.socket.once("disconnect", onDisconnect);
|
|
2976
|
+
this.connect();
|
|
2977
|
+
});
|
|
3255
2978
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
2979
|
+
disconnect() {
|
|
2980
|
+
this.socket.disconnect();
|
|
3258
2981
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
if (!Number.isFinite(last) || last <= 0) return false;
|
|
3262
|
-
return Date.now() - last <= ms;
|
|
2982
|
+
close() {
|
|
2983
|
+
this.disconnect();
|
|
3263
2984
|
}
|
|
3264
|
-
|
|
3265
|
-
this.
|
|
2985
|
+
getAuthToken() {
|
|
2986
|
+
return this.token;
|
|
3266
2987
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
2988
|
+
async listMessages() {
|
|
2989
|
+
const baseUrl = configuration.serverUrl.replace(/\/+$/, "");
|
|
2990
|
+
const url = `${baseUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/messages`;
|
|
2991
|
+
const res = await axios.get(url, {
|
|
2992
|
+
headers: { Authorization: `Machine ${this.token}`, "Content-Type": "application/json" },
|
|
2993
|
+
timeout: 6e4
|
|
2994
|
+
});
|
|
2995
|
+
return Array.isArray(res.data?.messages) ? res.data.messages : [];
|
|
3269
2996
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
if (!Number.isFinite(last) || last <= 0) return false;
|
|
3273
|
-
return Date.now() - last <= ms;
|
|
2997
|
+
markCoordinationLedgerRead(timeMs = Date.now()) {
|
|
2998
|
+
this.coordinationLedgerLastReadAtMs = timeMs;
|
|
3274
2999
|
}
|
|
3275
|
-
|
|
3276
|
-
|
|
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;
|
|
3000
|
+
getCoordinationLedgerLastReadAtMs() {
|
|
3001
|
+
return this.coordinationLedgerLastReadAtMs;
|
|
3310
3002
|
}
|
|
3311
|
-
|
|
3003
|
+
keepAlive(thinking, mode) {
|
|
3312
3004
|
if (!this.socket.connected) return;
|
|
3313
|
-
|
|
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
|
-
});
|
|
3005
|
+
this.socket.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking, mode });
|
|
3338
3006
|
}
|
|
3339
|
-
|
|
3340
|
-
this.
|
|
3341
|
-
while (this.pendingMessages.length > 0) {
|
|
3342
|
-
callback(this.pendingMessages.shift());
|
|
3343
|
-
}
|
|
3007
|
+
sendSessionDeath() {
|
|
3008
|
+
this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
|
|
3344
3009
|
}
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
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
|
-
}
|
|
3010
|
+
sendSessionEvent(event) {
|
|
3011
|
+
this.emitMessageOrQueue({
|
|
3012
|
+
role: "agent",
|
|
3013
|
+
content: { type: "event", id: node_crypto.randomUUID(), data: event },
|
|
3014
|
+
meta: { sentFrom: "cli" }
|
|
3015
|
+
});
|
|
3394
3016
|
}
|
|
3395
3017
|
sendCodexMessage(body) {
|
|
3396
|
-
|
|
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")}` });
|
|
3018
|
+
this.emitMessageOrQueue({ role: "agent", content: { type: "codex", data: body }, meta: { sentFrom: "cli" } });
|
|
3409
3019
|
}
|
|
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
3020
|
sendAgentMessage(agentType, body) {
|
|
3418
|
-
|
|
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")}` });
|
|
3021
|
+
this.emitMessageOrQueue({ role: "agent", content: { type: agentType, data: body }, meta: { sentFrom: "cli" } });
|
|
3431
3022
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3023
|
+
sendClaudeSessionMessage(body) {
|
|
3024
|
+
this.emitMessageOrQueue({
|
|
3434
3025
|
role: "agent",
|
|
3435
|
-
content: {
|
|
3436
|
-
|
|
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
|
|
3026
|
+
content: { type: "output", data: body },
|
|
3027
|
+
meta: { sentFrom: "cli" }
|
|
3456
3028
|
});
|
|
3457
3029
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3030
|
+
emitMessageOrQueue(content) {
|
|
3031
|
+
const payload = { sid: this.sessionId, content };
|
|
3032
|
+
if (this.socket.connected) {
|
|
3033
|
+
this.socket.emit("message", payload);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
this.outboundQueue.push(payload);
|
|
3463
3037
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
total: 0,
|
|
3483
|
-
input: 0,
|
|
3484
|
-
output: 0
|
|
3485
|
-
}
|
|
3038
|
+
flush() {
|
|
3039
|
+
if (!this.socket.connected) return;
|
|
3040
|
+
while (this.outboundQueue.length > 0) {
|
|
3041
|
+
const next = this.outboundQueue.shift();
|
|
3042
|
+
if (!next) continue;
|
|
3043
|
+
this.socket.emit("message", next);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
onUserMessage(handler) {
|
|
3047
|
+
const listener = (u) => {
|
|
3048
|
+
if (u?.body?.t !== "new-message") return;
|
|
3049
|
+
const body = u.body;
|
|
3050
|
+
const raw = body?.message ?? null;
|
|
3051
|
+
if (!raw || typeof raw !== "object") return;
|
|
3052
|
+
const record = raw?.content;
|
|
3053
|
+
if (!record || typeof record !== "object") return;
|
|
3054
|
+
if (record.role !== "user") return;
|
|
3055
|
+
handler(record);
|
|
3486
3056
|
};
|
|
3487
|
-
|
|
3488
|
-
this.
|
|
3057
|
+
this.on("update", listener);
|
|
3058
|
+
return () => this.off("update", listener);
|
|
3489
3059
|
}
|
|
3490
|
-
/**
|
|
3491
|
-
* Update session metadata
|
|
3492
|
-
* @param handler - Handler function that returns the updated metadata
|
|
3493
|
-
*/
|
|
3494
3060
|
updateMetadata(handler) {
|
|
3495
|
-
this.
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
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
|
-
});
|
|
3061
|
+
const current = this.metadata || {};
|
|
3062
|
+
const next = handler(current);
|
|
3063
|
+
void this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: next }).then((answer) => {
|
|
3064
|
+
if (answer?.result === "success") {
|
|
3065
|
+
this.metadata = next;
|
|
3066
|
+
this.metadataVersion += 1;
|
|
3067
|
+
}
|
|
3068
|
+
}).catch(() => null);
|
|
3511
3069
|
}
|
|
3512
|
-
/**
|
|
3513
|
-
* Update session agent state
|
|
3514
|
-
* @param handler - Handler function that returns the updated agent state
|
|
3515
|
-
*/
|
|
3516
3070
|
updateAgentState(handler) {
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
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
|
-
});
|
|
3071
|
+
const current = this.agentState || {};
|
|
3072
|
+
const next = handler(current);
|
|
3073
|
+
void this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: next }).then((answer) => {
|
|
3074
|
+
if (answer?.result === "success") {
|
|
3075
|
+
this.agentState = next;
|
|
3076
|
+
this.agentStateVersion += 1;
|
|
3077
|
+
}
|
|
3078
|
+
}).catch(() => null);
|
|
3535
3079
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
async function delay(ms) {
|
|
3083
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3084
|
+
}
|
|
3085
|
+
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
3086
|
+
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
|
|
3087
|
+
return Math.round(Math.random() * maxDelayRet);
|
|
3088
|
+
}
|
|
3089
|
+
function createBackoff(opts) {
|
|
3090
|
+
return async (callback) => {
|
|
3091
|
+
let currentFailureCount = 0;
|
|
3092
|
+
const minDelay = opts && opts.minDelay !== void 0 ? opts.minDelay : 250;
|
|
3093
|
+
const maxDelay = opts && opts.maxDelay !== void 0 ? opts.maxDelay : 1e3;
|
|
3094
|
+
const maxFailureCount = opts && opts.maxFailureCount !== void 0 ? opts.maxFailureCount : 50;
|
|
3095
|
+
while (true) {
|
|
3096
|
+
try {
|
|
3097
|
+
return await callback();
|
|
3098
|
+
} catch (e) {
|
|
3099
|
+
if (currentFailureCount < maxFailureCount) {
|
|
3100
|
+
currentFailureCount++;
|
|
3101
|
+
}
|
|
3102
|
+
if (opts && opts.onError) {
|
|
3103
|
+
opts.onError(e, currentFailureCount);
|
|
3104
|
+
}
|
|
3105
|
+
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
3106
|
+
await delay(waitForRequest);
|
|
3107
|
+
}
|
|
3542
3108
|
}
|
|
3543
|
-
|
|
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
|
-
}
|
|
3109
|
+
};
|
|
3556
3110
|
}
|
|
3111
|
+
let backoff = createBackoff();
|
|
3557
3112
|
|
|
3558
3113
|
function looksLikeEngineRoot(engineRoot) {
|
|
3559
3114
|
if (!engineRoot) return false;
|
|
@@ -3726,6 +3281,11 @@ async function buildAndInstallUnrealMcpPlugin(options) {
|
|
|
3726
3281
|
`-Plugin=${pluginUpluginPath}`,
|
|
3727
3282
|
`-Package=${packageDir}`,
|
|
3728
3283
|
"-Rocket",
|
|
3284
|
+
// UAT enforces a global single-instance mutex. On macOS/Linux, the named mutex can
|
|
3285
|
+
// appear "already created" even when no process is actually holding it, which makes
|
|
3286
|
+
// repeated installs fail with: "A conflicting instance of AutomationTool is already running."
|
|
3287
|
+
// `-WaitForUATMutex` makes UAT acquire the mutex if it's free (or wait if another build is in flight).
|
|
3288
|
+
"-WaitForUATMutex",
|
|
3729
3289
|
`-TargetPlatforms=${targetPlatform()}`
|
|
3730
3290
|
];
|
|
3731
3291
|
const logStream = fs.createWriteStream(buildLogPath, { flags: "a" });
|
|
@@ -3798,7 +3358,6 @@ class ApiMachineClient {
|
|
|
3798
3358
|
this.machine = machine;
|
|
3799
3359
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
3800
3360
|
scopePrefix: this.machine.id,
|
|
3801
|
-
encryptionKey: this.machine.encryptionKey,
|
|
3802
3361
|
logger: (msg, data) => logger.debug(msg, data)
|
|
3803
3362
|
});
|
|
3804
3363
|
const rootDir = this.machine?.metadata?.homeDir || os.homedir() || process.cwd();
|
|
@@ -3839,6 +3398,24 @@ class ApiMachineClient {
|
|
|
3839
3398
|
socket;
|
|
3840
3399
|
keepAliveInterval = null;
|
|
3841
3400
|
rpcHandlerManager;
|
|
3401
|
+
connected = false;
|
|
3402
|
+
lastConnectError = null;
|
|
3403
|
+
lastDisconnectReason = null;
|
|
3404
|
+
lastHttpUpsertError = null;
|
|
3405
|
+
lastHttpUpsertStatus = null;
|
|
3406
|
+
lastHttpUpsertAt = null;
|
|
3407
|
+
upsertBackoff = createBackoff({
|
|
3408
|
+
minDelay: 1e3,
|
|
3409
|
+
maxDelay: 3e4,
|
|
3410
|
+
maxFailureCount: 200,
|
|
3411
|
+
onError: (e, failures) => {
|
|
3412
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3413
|
+
this.lastHttpUpsertError = msg;
|
|
3414
|
+
if (process.env.DEBUG) {
|
|
3415
|
+
logger.debug(`[API MACHINE] Machine upsert retry (${failures}): ${msg}`);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3842
3419
|
setRPCHandlers({
|
|
3843
3420
|
spawnSession,
|
|
3844
3421
|
stopSession,
|
|
@@ -3885,74 +3462,86 @@ class ApiMachineClient {
|
|
|
3885
3462
|
});
|
|
3886
3463
|
}
|
|
3887
3464
|
/**
|
|
3888
|
-
*
|
|
3889
|
-
* Currently unused, changes from the mobile client are more likely
|
|
3890
|
-
* for example to set a custom name.
|
|
3465
|
+
* Upsert machine record (metadata + daemon state) via HTTP (workspace-native V1).
|
|
3891
3466
|
*/
|
|
3892
|
-
async
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
}
|
|
3909
|
-
throw new Error("Metadata version mismatch");
|
|
3910
|
-
}
|
|
3467
|
+
async upsertMachineHttp(params) {
|
|
3468
|
+
const attemptAt = Date.now();
|
|
3469
|
+
const endpoint = configuration.serverUrl.replace(/\/+$/, "");
|
|
3470
|
+
const timeoutMs = Math.max(1e3, Math.min(12e4, Number.parseInt(process.env.FLOCKBAY_MACHINE_UPSERT_TIMEOUT_MS || "30000", 10) || 3e4));
|
|
3471
|
+
const res = await fetch(`${endpoint}/v1/machines`, {
|
|
3472
|
+
method: "POST",
|
|
3473
|
+
headers: {
|
|
3474
|
+
Authorization: `Machine ${this.token}`,
|
|
3475
|
+
"Content-Type": "application/json"
|
|
3476
|
+
},
|
|
3477
|
+
body: JSON.stringify({
|
|
3478
|
+
id: this.machine.id,
|
|
3479
|
+
...params.metadata !== void 0 ? { metadata: params.metadata } : {},
|
|
3480
|
+
...params.daemonState !== void 0 ? { daemonState: params.daemonState } : {}
|
|
3481
|
+
}),
|
|
3482
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3911
3483
|
});
|
|
3484
|
+
if (!res.ok) {
|
|
3485
|
+
const detail = await res.text().catch(() => "");
|
|
3486
|
+
const msg = `machine_upsert_failed:${res.status}${detail ? `:${detail.slice(0, 2e3)}` : ""}`;
|
|
3487
|
+
this.lastHttpUpsertStatus = res.status;
|
|
3488
|
+
this.lastHttpUpsertError = msg;
|
|
3489
|
+
throw new Error(msg);
|
|
3490
|
+
}
|
|
3491
|
+
const data = await res.json().catch(() => null);
|
|
3492
|
+
const machine = data?.machine && typeof data.machine === "object" ? data.machine : null;
|
|
3493
|
+
if (machine) {
|
|
3494
|
+
this.machine.metadata = machine.metadata ?? null;
|
|
3495
|
+
this.machine.daemonState = machine.daemonState ?? null;
|
|
3496
|
+
this.machine.seq = Number(machine.seq || this.machine.seq || 0);
|
|
3497
|
+
}
|
|
3498
|
+
this.lastHttpUpsertStatus = res.status;
|
|
3499
|
+
this.lastHttpUpsertError = null;
|
|
3500
|
+
this.lastHttpUpsertAt = attemptAt;
|
|
3912
3501
|
}
|
|
3913
3502
|
/**
|
|
3914
|
-
* Update daemon state (runtime info)
|
|
3915
|
-
* Simplified without lock - relies on backoff for retry
|
|
3503
|
+
* Update daemon state (runtime info) via HTTP upsert.
|
|
3916
3504
|
*/
|
|
3917
3505
|
async updateDaemonState(handler) {
|
|
3918
|
-
await
|
|
3506
|
+
await this.upsertBackoff(async () => {
|
|
3919
3507
|
const updated = handler(this.machine.daemonState);
|
|
3920
|
-
|
|
3921
|
-
|
|
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
|
-
}
|
|
3508
|
+
await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
|
|
3509
|
+
logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
3936
3510
|
});
|
|
3937
3511
|
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Best-effort single attempt (no retries). Useful during shutdown.
|
|
3514
|
+
*/
|
|
3515
|
+
async updateDaemonStateOnce(handler) {
|
|
3516
|
+
try {
|
|
3517
|
+
const updated = handler(this.machine.daemonState);
|
|
3518
|
+
await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
|
|
3519
|
+
return true;
|
|
3520
|
+
} catch (e) {
|
|
3521
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3522
|
+
logger.debug("[API MACHINE] Daemon state update failed (best-effort):", msg);
|
|
3523
|
+
return false;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
getStatusSnapshot() {
|
|
3527
|
+
return {
|
|
3528
|
+
connected: this.connected,
|
|
3529
|
+
lastConnectError: this.lastConnectError,
|
|
3530
|
+
lastDisconnectReason: this.lastDisconnectReason,
|
|
3531
|
+
lastHttpUpsertError: this.lastHttpUpsertError,
|
|
3532
|
+
lastHttpUpsertStatus: this.lastHttpUpsertStatus,
|
|
3533
|
+
lastHttpUpsertAt: this.lastHttpUpsertAt
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3938
3536
|
connect() {
|
|
3939
3537
|
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
3940
3538
|
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
3539
|
this.socket = socket_ioClient.io(serverUrl, {
|
|
3948
3540
|
transports: ["websocket"],
|
|
3949
3541
|
auth: {
|
|
3950
3542
|
token: this.token,
|
|
3951
3543
|
clientType: "machine-scoped",
|
|
3952
|
-
machineId: this.machine.id
|
|
3953
|
-
dataEncryptionKey,
|
|
3954
|
-
machineMetadata: machineMetadataEncrypted,
|
|
3955
|
-
daemonState: daemonStateEncrypted
|
|
3544
|
+
machineId: this.machine.id
|
|
3956
3545
|
},
|
|
3957
3546
|
path: "/v1/updates",
|
|
3958
3547
|
reconnection: true,
|
|
@@ -3961,6 +3550,9 @@ class ApiMachineClient {
|
|
|
3961
3550
|
});
|
|
3962
3551
|
this.socket.on("connect", () => {
|
|
3963
3552
|
logger.debug("[API MACHINE] Connected to server");
|
|
3553
|
+
this.connected = true;
|
|
3554
|
+
this.lastConnectError = null;
|
|
3555
|
+
this.lastDisconnectReason = null;
|
|
3964
3556
|
this.updateDaemonState((state) => ({
|
|
3965
3557
|
...state,
|
|
3966
3558
|
status: "running",
|
|
@@ -3971,8 +3563,10 @@ class ApiMachineClient {
|
|
|
3971
3563
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
3972
3564
|
this.startKeepAlive();
|
|
3973
3565
|
});
|
|
3974
|
-
this.socket.on("disconnect", () => {
|
|
3975
|
-
logger.debug("[API MACHINE] Disconnected from server");
|
|
3566
|
+
this.socket.on("disconnect", (reason) => {
|
|
3567
|
+
logger.debug("[API MACHINE] Disconnected from server", reason);
|
|
3568
|
+
this.connected = false;
|
|
3569
|
+
this.lastDisconnectReason = typeof reason === "string" ? reason : "disconnected";
|
|
3976
3570
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
3977
3571
|
this.stopKeepAlive();
|
|
3978
3572
|
});
|
|
@@ -3981,24 +3575,19 @@ class ApiMachineClient {
|
|
|
3981
3575
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
3982
3576
|
});
|
|
3983
3577
|
this.socket.on("update", (data) => {
|
|
3984
|
-
if (data
|
|
3578
|
+
if (data?.body?.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
3985
3579
|
const update = data.body;
|
|
3986
|
-
if (update.
|
|
3987
|
-
|
|
3988
|
-
this.machine.
|
|
3989
|
-
this.machine.
|
|
3580
|
+
if (update.machine) {
|
|
3581
|
+
this.machine.metadata = update.machine.metadata ?? null;
|
|
3582
|
+
this.machine.daemonState = update.machine.daemonState ?? null;
|
|
3583
|
+
this.machine.seq = Number(update.machine.seq || this.machine.seq || 0);
|
|
3990
3584
|
}
|
|
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
3585
|
}
|
|
3999
3586
|
});
|
|
4000
3587
|
this.socket.on("connect_error", (error) => {
|
|
4001
3588
|
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
3589
|
+
this.connected = false;
|
|
3590
|
+
this.lastConnectError = String(error?.message || "connect_error");
|
|
4002
3591
|
});
|
|
4003
3592
|
this.socket.io.on("error", (error) => {
|
|
4004
3593
|
logger.debug("[API MACHINE] Socket error:", error);
|
|
@@ -4035,389 +3624,123 @@ class ApiMachineClient {
|
|
|
4035
3624
|
}
|
|
4036
3625
|
}
|
|
4037
3626
|
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
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
|
-
}
|
|
3627
|
+
function machineAuthHeaders(machineToken) {
|
|
3628
|
+
return { Authorization: `Machine ${machineToken}`, "Content-Type": "application/json" };
|
|
3629
|
+
}
|
|
3630
|
+
function normalizeSession(raw) {
|
|
3631
|
+
return {
|
|
3632
|
+
id: String(raw?.id || ""),
|
|
3633
|
+
seq: Number(raw?.seq || 0),
|
|
3634
|
+
active: Boolean(raw?.active),
|
|
3635
|
+
activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
|
|
3636
|
+
createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
|
|
3637
|
+
updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
|
|
3638
|
+
metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
|
|
3639
|
+
metadataVersion: Number(raw?.metadataVersion || 0),
|
|
3640
|
+
agentState: raw?.agentState && typeof raw.agentState === "object" ? raw.agentState : null,
|
|
3641
|
+
agentStateVersion: Number(raw?.agentStateVersion || 0)
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
function normalizeMachine(raw) {
|
|
3645
|
+
return {
|
|
3646
|
+
id: String(raw?.id || ""),
|
|
3647
|
+
seq: Number(raw?.seq || 0),
|
|
3648
|
+
active: Boolean(raw?.active),
|
|
3649
|
+
activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
|
|
3650
|
+
createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
|
|
3651
|
+
updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
|
|
3652
|
+
metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
|
|
3653
|
+
daemonState: raw?.daemonState && typeof raw.daemonState === "object" ? raw.daemonState : null
|
|
3654
|
+
};
|
|
4162
3655
|
}
|
|
4163
|
-
|
|
4164
3656
|
class ApiClient {
|
|
4165
|
-
|
|
4166
|
-
|
|
3657
|
+
constructor(auth) {
|
|
3658
|
+
this.auth = auth;
|
|
4167
3659
|
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
3660
|
+
static async create(auth) {
|
|
3661
|
+
return new ApiClient(auth);
|
|
3662
|
+
}
|
|
3663
|
+
baseUrl() {
|
|
3664
|
+
return configuration.serverUrl.replace(/\/+$/, "");
|
|
3665
|
+
}
|
|
3666
|
+
async createSession(opts) {
|
|
3667
|
+
const response = await axios.post(
|
|
3668
|
+
`${this.baseUrl()}/v1/sessions`,
|
|
3669
|
+
{ metadata: opts.metadata ?? {}, agentState: opts.state ?? null },
|
|
3670
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3671
|
+
);
|
|
3672
|
+
return normalizeSession(response.data?.session);
|
|
4173
3673
|
}
|
|
4174
|
-
/**
|
|
4175
|
-
* Create a new session or load existing one with the given tag
|
|
4176
|
-
*/
|
|
4177
3674
|
async getOrCreateSession(opts) {
|
|
4178
|
-
const
|
|
4179
|
-
const
|
|
4180
|
-
const
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
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
|
-
}
|
|
3675
|
+
const tag = String(opts?.tag || "").trim();
|
|
3676
|
+
const metadata = opts?.metadata && typeof opts.metadata === "object" ? opts.metadata : {};
|
|
3677
|
+
const merged = tag ? { ...metadata, tag } : metadata;
|
|
3678
|
+
return this.createSession({ metadata: merged, state: opts.state ?? null });
|
|
3679
|
+
}
|
|
3680
|
+
async getSessionById(sessionId) {
|
|
3681
|
+
const id = String(sessionId || "").trim();
|
|
3682
|
+
if (!id) throw new Error("Session id is required");
|
|
3683
|
+
const response = await axios.get(
|
|
3684
|
+
`${this.baseUrl()}/v1/sessions/${encodeURIComponent(id)}`,
|
|
3685
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3686
|
+
);
|
|
3687
|
+
return normalizeSession(response.data?.session);
|
|
4217
3688
|
}
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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);
|
|
3689
|
+
async upsertMachine(opts) {
|
|
3690
|
+
const machineId = String(opts.machineId || "").trim();
|
|
3691
|
+
if (!machineId) throw new Error("Machine id is required");
|
|
4228
3692
|
const response = await axios.post(
|
|
4229
|
-
`${
|
|
4230
|
-
{
|
|
4231
|
-
|
|
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
|
-
}
|
|
3693
|
+
`${this.baseUrl()}/v1/machines`,
|
|
3694
|
+
{ id: machineId, metadata: opts.metadata ?? {}, daemonState: opts.daemonState ?? null },
|
|
3695
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
4244
3696
|
);
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
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;
|
|
3697
|
+
return normalizeMachine(response.data?.machine);
|
|
3698
|
+
}
|
|
3699
|
+
async getOrCreateMachine(opts) {
|
|
3700
|
+
return this.upsertMachine(opts);
|
|
4262
3701
|
}
|
|
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
3702
|
async getMachine(machineId) {
|
|
4268
3703
|
const id = String(machineId || "").trim();
|
|
4269
|
-
if (!id)
|
|
4270
|
-
throw new Error("Machine id is required");
|
|
4271
|
-
}
|
|
4272
|
-
const encryptionKey = this.credential.encryption.machineKey;
|
|
3704
|
+
if (!id) throw new Error("Machine id is required");
|
|
4273
3705
|
const response = await axios.get(
|
|
4274
|
-
`${
|
|
4275
|
-
{
|
|
4276
|
-
headers: {
|
|
4277
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4278
|
-
"Content-Type": "application/json"
|
|
4279
|
-
},
|
|
4280
|
-
timeout: 6e4
|
|
4281
|
-
}
|
|
3706
|
+
`${this.baseUrl()}/v1/machines/${encodeURIComponent(id)}`,
|
|
3707
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
4282
3708
|
);
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
const
|
|
4287
|
-
if (!
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
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);
|
|
3709
|
+
return normalizeMachine(response.data?.machine);
|
|
3710
|
+
}
|
|
3711
|
+
async registerVendorToken(vendor, token) {
|
|
3712
|
+
const v = String(vendor || "").trim().toLowerCase();
|
|
3713
|
+
if (!v) throw new Error("Vendor is required");
|
|
3714
|
+
await axios.post(
|
|
3715
|
+
`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`,
|
|
3716
|
+
{ token },
|
|
3717
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3718
|
+
);
|
|
3719
|
+
return { ok: true };
|
|
4303
3720
|
}
|
|
4304
|
-
|
|
4305
|
-
|
|
3721
|
+
async getVendorToken(vendor) {
|
|
3722
|
+
const v = String(vendor || "").trim().toLowerCase();
|
|
3723
|
+
if (!v) throw new Error("Vendor is required");
|
|
3724
|
+
const response = await axios.get(`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`, {
|
|
3725
|
+
headers: machineAuthHeaders(this.auth.machineToken),
|
|
3726
|
+
timeout: 6e4,
|
|
3727
|
+
validateStatus: (s) => s >= 200 && s < 300 || s === 404
|
|
3728
|
+
});
|
|
3729
|
+
if (response.status === 404) return null;
|
|
3730
|
+
return response.data?.token ?? null;
|
|
4306
3731
|
}
|
|
4307
3732
|
push() {
|
|
4308
|
-
return
|
|
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}`);
|
|
3733
|
+
return {
|
|
3734
|
+
sendToAllDevices: async () => {
|
|
3735
|
+
return;
|
|
4331
3736
|
}
|
|
4332
|
-
|
|
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
|
-
}
|
|
3737
|
+
};
|
|
4337
3738
|
}
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
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
|
-
}
|
|
3739
|
+
sessionSyncClient(session) {
|
|
3740
|
+
return new ApiSessionClient(this.auth.machineToken, session);
|
|
3741
|
+
}
|
|
3742
|
+
machineSyncClient(machine) {
|
|
3743
|
+
return new ApiMachineClient(this.auth.machineToken, machine);
|
|
4421
3744
|
}
|
|
4422
3745
|
}
|
|
4423
3746
|
|
|
@@ -4469,8 +3792,8 @@ const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
|
|
|
4469
3792
|
]);
|
|
4470
3793
|
|
|
4471
3794
|
exports.ApiClient = ApiClient;
|
|
3795
|
+
exports.ApiMachineClient = ApiMachineClient;
|
|
4472
3796
|
exports.ApiSessionClient = ApiSessionClient;
|
|
4473
|
-
exports.AsyncLock = AsyncLock;
|
|
4474
3797
|
exports.RawJSONLinesSchema = RawJSONLinesSchema;
|
|
4475
3798
|
exports.acquireDaemonLock = acquireDaemonLock;
|
|
4476
3799
|
exports.backoff = backoff;
|
|
@@ -4478,10 +3801,7 @@ exports.clearCredentials = clearCredentials;
|
|
|
4478
3801
|
exports.clearDaemonState = clearDaemonState;
|
|
4479
3802
|
exports.clearMachineId = clearMachineId;
|
|
4480
3803
|
exports.configuration = configuration;
|
|
4481
|
-
exports.decodeBase64 = decodeBase64;
|
|
4482
3804
|
exports.delay = delay;
|
|
4483
|
-
exports.encodeBase64 = encodeBase64;
|
|
4484
|
-
exports.encodeBase64Url = encodeBase64Url;
|
|
4485
3805
|
exports.getLatestDaemonLog = getLatestDaemonLog;
|
|
4486
3806
|
exports.installUnrealMcpPluginToEngine = installUnrealMcpPluginToEngine;
|
|
4487
3807
|
exports.logger = logger;
|
|
@@ -4494,5 +3814,5 @@ exports.releaseDaemonLock = releaseDaemonLock;
|
|
|
4494
3814
|
exports.sendUnrealMcpTcpCommand = sendUnrealMcpTcpCommand;
|
|
4495
3815
|
exports.unrealMcpPythonDir = unrealMcpPythonDir;
|
|
4496
3816
|
exports.updateSettings = updateSettings;
|
|
4497
|
-
exports.
|
|
3817
|
+
exports.writeCredentials = writeCredentials;
|
|
4498
3818
|
exports.writeDaemonState = writeDaemonState;
|