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.
- package/dist/codex/flockbayMcpStdioBridge.cjs +339 -0
- package/dist/codex/flockbayMcpStdioBridge.mjs +339 -0
- package/dist/{index--o4BPz5o.cjs → index-BxBuBx7C.cjs} +2706 -609
- package/dist/{index-CUp3juDS.mjs → index-CHm9r89K.mjs} +2707 -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-D3eT-TvB.cjs → runCodex-DuCGwO2K.cjs} +264 -43
- package/dist/{runCodex-o6PCbHQ7.mjs → runCodex-DudVDqNh.mjs} +263 -42
- package/dist/{runGemini-CBxZp6I7.cjs → runGemini-B25LZ4Cw.cjs} +64 -29
- package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-Ddu8UCOS.mjs} +63 -28
- package/dist/{types-C-jnUdn_.cjs → types-CGQhv7Z-.cjs} +470 -1146
- package/dist/{types-DGd6ea2Z.mjs → types-DuhcLxar.mjs} +469 -1142
- 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.17";
|
|
48
46
|
var description = "Flockbay CLI (local agent + daemon)";
|
|
49
47
|
var author = "Eduardo Orellana";
|
|
50
48
|
var license = "UNLICENSED";
|
|
@@ -195,6 +193,30 @@ var packageJson = {
|
|
|
195
193
|
packageManager: packageManager
|
|
196
194
|
};
|
|
197
195
|
|
|
196
|
+
function parseProfileFromProcessArgs() {
|
|
197
|
+
const args = process.argv.slice(2);
|
|
198
|
+
for (let i = 0; i < args.length; i++) {
|
|
199
|
+
const a = String(args[i] || "");
|
|
200
|
+
if (a === "--profile" || a === "-profile" || a === "-p") {
|
|
201
|
+
const v = String(args[i + 1] || "").trim();
|
|
202
|
+
if (v && !v.startsWith("-")) return v;
|
|
203
|
+
}
|
|
204
|
+
if (a.startsWith("--profile=")) {
|
|
205
|
+
const v = a.slice("--profile=".length).trim();
|
|
206
|
+
if (v) return v;
|
|
207
|
+
}
|
|
208
|
+
if (a.startsWith("-profile=")) {
|
|
209
|
+
const v = a.slice("-profile=".length).trim();
|
|
210
|
+
if (v) return v;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
function sanitizeProfileName(input) {
|
|
216
|
+
const raw = String(input).trim();
|
|
217
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
218
|
+
return cleaned || "default";
|
|
219
|
+
}
|
|
198
220
|
function normalizeServerUrlForNode(url) {
|
|
199
221
|
try {
|
|
200
222
|
const u = new URL(url);
|
|
@@ -208,6 +230,7 @@ class Configuration {
|
|
|
208
230
|
serverUrl;
|
|
209
231
|
webappUrl;
|
|
210
232
|
isDaemonProcess;
|
|
233
|
+
profile;
|
|
211
234
|
// Directories and paths (from persistence)
|
|
212
235
|
flockbayHomeDir;
|
|
213
236
|
logsDir;
|
|
@@ -221,12 +244,15 @@ class Configuration {
|
|
|
221
244
|
constructor() {
|
|
222
245
|
const args = process.argv.slice(2);
|
|
223
246
|
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
247
|
+
const profileFromArgs = parseProfileFromProcessArgs();
|
|
248
|
+
const profileFromEnv = process.env.FLOCKBAY_PROFILE;
|
|
249
|
+
this.profile = sanitizeProfileName(profileFromEnv || profileFromArgs || "default");
|
|
224
250
|
const homeOverride = process.env.FLOCKBAY_HOME_DIR;
|
|
225
251
|
if (homeOverride) {
|
|
226
252
|
const expandedPath = homeOverride.replace(/^~/, os.homedir());
|
|
227
|
-
this.flockbayHomeDir = expandedPath;
|
|
253
|
+
this.flockbayHomeDir = path.join(expandedPath, "profiles", this.profile);
|
|
228
254
|
} else {
|
|
229
|
-
this.flockbayHomeDir = path.join(os.homedir(), ".flockbay");
|
|
255
|
+
this.flockbayHomeDir = path.join(os.homedir(), ".flockbay", "profiles", this.profile);
|
|
230
256
|
}
|
|
231
257
|
this.logsDir = path.join(this.flockbayHomeDir, "logs");
|
|
232
258
|
this.settingsFile = path.join(this.flockbayHomeDir, "settings.json");
|
|
@@ -260,83 +286,6 @@ class Configuration {
|
|
|
260
286
|
}
|
|
261
287
|
const configuration = new Configuration();
|
|
262
288
|
|
|
263
|
-
function encodeBase64(buffer, variant = "base64") {
|
|
264
|
-
if (variant === "base64url") {
|
|
265
|
-
return encodeBase64Url(buffer);
|
|
266
|
-
}
|
|
267
|
-
return Buffer.from(buffer).toString("base64");
|
|
268
|
-
}
|
|
269
|
-
function encodeBase64Url(buffer) {
|
|
270
|
-
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
271
|
-
}
|
|
272
|
-
function decodeBase64(base64, variant = "base64") {
|
|
273
|
-
if (variant === "base64url") {
|
|
274
|
-
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
275
|
-
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
276
|
-
}
|
|
277
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
278
|
-
}
|
|
279
|
-
function getRandomBytes(size) {
|
|
280
|
-
return new Uint8Array(node_crypto.randomBytes(size));
|
|
281
|
-
}
|
|
282
|
-
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
283
|
-
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
284
|
-
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
285
|
-
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
286
|
-
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
|
|
287
|
-
result.set(ephemeralKeyPair.publicKey, 0);
|
|
288
|
-
result.set(nonce, ephemeralKeyPair.publicKey.length);
|
|
289
|
-
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
|
-
function encryptWithDataKey(data, dataKey) {
|
|
293
|
-
const nonce = getRandomBytes(12);
|
|
294
|
-
const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
295
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
296
|
-
const encrypted = Buffer.concat([
|
|
297
|
-
cipher.update(plaintext),
|
|
298
|
-
cipher.final()
|
|
299
|
-
]);
|
|
300
|
-
const authTag = cipher.getAuthTag();
|
|
301
|
-
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
302
|
-
bundle.set([0], 0);
|
|
303
|
-
bundle.set(nonce, 1);
|
|
304
|
-
bundle.set(new Uint8Array(encrypted), 13);
|
|
305
|
-
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
306
|
-
return bundle;
|
|
307
|
-
}
|
|
308
|
-
function decryptWithDataKey(bundle, dataKey) {
|
|
309
|
-
if (bundle.length < 1) {
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
if (bundle[0] !== 0) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
if (bundle.length < 12 + 16 + 1) {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
const nonce = bundle.slice(1, 13);
|
|
319
|
-
const authTag = bundle.slice(bundle.length - 16);
|
|
320
|
-
const ciphertext = bundle.slice(13, bundle.length - 16);
|
|
321
|
-
try {
|
|
322
|
-
const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
|
|
323
|
-
decipher.setAuthTag(authTag);
|
|
324
|
-
const decrypted = Buffer.concat([
|
|
325
|
-
decipher.update(ciphertext),
|
|
326
|
-
decipher.final()
|
|
327
|
-
]);
|
|
328
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
329
|
-
} catch (error) {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function encrypt(key, data) {
|
|
334
|
-
return encryptWithDataKey(data, key);
|
|
335
|
-
}
|
|
336
|
-
function decrypt(key, data) {
|
|
337
|
-
return decryptWithDataKey(data, key);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
289
|
const defaultSettings = {
|
|
341
290
|
onboardingCompleted: false
|
|
342
291
|
};
|
|
@@ -402,40 +351,30 @@ async function updateSettings(updater) {
|
|
|
402
351
|
});
|
|
403
352
|
}
|
|
404
353
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
machineKey: z__namespace.string().base64()
|
|
410
|
-
})
|
|
354
|
+
const workspaceAuthSchema = z__namespace.object({
|
|
355
|
+
machineToken: z__namespace.string().min(1),
|
|
356
|
+
orgId: z__namespace.string().min(1),
|
|
357
|
+
createdAtMs: z__namespace.number().optional()
|
|
411
358
|
});
|
|
412
359
|
async function readCredentials() {
|
|
413
|
-
if (!fs.existsSync(configuration.privateKeyFile))
|
|
414
|
-
return null;
|
|
415
|
-
}
|
|
360
|
+
if (!fs.existsSync(configuration.privateKeyFile)) return null;
|
|
416
361
|
try {
|
|
417
|
-
const
|
|
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
|
-
};
|
|
362
|
+
const raw = await fs$1.readFile(configuration.privateKeyFile, "utf8");
|
|
363
|
+
const parsed = workspaceAuthSchema.parse(JSON.parse(raw));
|
|
364
|
+
return { machineToken: parsed.machineToken, orgId: parsed.orgId, createdAtMs: parsed.createdAtMs };
|
|
427
365
|
} catch {
|
|
428
366
|
return null;
|
|
429
367
|
}
|
|
430
368
|
}
|
|
431
|
-
async function
|
|
369
|
+
async function writeCredentials(auth) {
|
|
432
370
|
if (!fs.existsSync(configuration.flockbayHomeDir)) {
|
|
433
371
|
await fs$1.mkdir(configuration.flockbayHomeDir, { recursive: true });
|
|
434
372
|
}
|
|
435
|
-
await fs$1.writeFile(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
373
|
+
await fs$1.writeFile(
|
|
374
|
+
configuration.privateKeyFile,
|
|
375
|
+
JSON.stringify({ machineToken: auth.machineToken, orgId: auth.orgId, createdAtMs: auth.createdAtMs ?? Date.now() }, null, 2),
|
|
376
|
+
"utf8"
|
|
377
|
+
);
|
|
439
378
|
}
|
|
440
379
|
async function clearCredentials() {
|
|
441
380
|
if (fs.existsSync(configuration.privateKeyFile)) {
|
|
@@ -741,222 +680,13 @@ async function getLatestDaemonLog() {
|
|
|
741
680
|
return latest || null;
|
|
742
681
|
}
|
|
743
682
|
|
|
744
|
-
const SessionMessageContentSchema = z.z.object({
|
|
745
|
-
c: z.z.string(),
|
|
746
|
-
// Base64 encoded encrypted content
|
|
747
|
-
t: z.z.literal("encrypted")
|
|
748
|
-
});
|
|
749
|
-
const UpdateBodySchema = z.z.object({
|
|
750
|
-
message: z.z.object({
|
|
751
|
-
id: z.z.string(),
|
|
752
|
-
seq: z.z.number(),
|
|
753
|
-
content: SessionMessageContentSchema
|
|
754
|
-
}),
|
|
755
|
-
sid: z.z.string(),
|
|
756
|
-
// Session ID
|
|
757
|
-
t: z.z.literal("new-message")
|
|
758
|
-
});
|
|
759
|
-
const UpdateSessionBodySchema = z.z.object({
|
|
760
|
-
t: z.z.literal("update-session"),
|
|
761
|
-
sid: z.z.string(),
|
|
762
|
-
metadata: z.z.object({
|
|
763
|
-
version: z.z.number(),
|
|
764
|
-
value: z.z.string()
|
|
765
|
-
}).nullish(),
|
|
766
|
-
agentState: z.z.object({
|
|
767
|
-
version: z.z.number(),
|
|
768
|
-
value: z.z.string()
|
|
769
|
-
}).nullish()
|
|
770
|
-
});
|
|
771
|
-
const UpdateMachineBodySchema = z.z.object({
|
|
772
|
-
t: z.z.literal("update-machine"),
|
|
773
|
-
machineId: z.z.string(),
|
|
774
|
-
metadata: z.z.object({
|
|
775
|
-
version: z.z.number(),
|
|
776
|
-
value: z.z.string()
|
|
777
|
-
}).nullish(),
|
|
778
|
-
daemonState: z.z.object({
|
|
779
|
-
version: z.z.number(),
|
|
780
|
-
value: z.z.string()
|
|
781
|
-
}).nullish()
|
|
782
|
-
});
|
|
783
|
-
z.z.object({
|
|
784
|
-
id: z.z.string(),
|
|
785
|
-
seq: z.z.number(),
|
|
786
|
-
body: z.z.union([
|
|
787
|
-
UpdateBodySchema,
|
|
788
|
-
UpdateSessionBodySchema,
|
|
789
|
-
UpdateMachineBodySchema
|
|
790
|
-
]),
|
|
791
|
-
createdAt: z.z.number()
|
|
792
|
-
});
|
|
793
|
-
z.z.object({
|
|
794
|
-
host: z.z.string(),
|
|
795
|
-
platform: z.z.string(),
|
|
796
|
-
flockbayCliVersion: z.z.string(),
|
|
797
|
-
homeDir: z.z.string(),
|
|
798
|
-
flockbayHomeDir: z.z.string(),
|
|
799
|
-
flockbayLibDir: z.z.string(),
|
|
800
|
-
// Dev-only: bypass Unreal project + SDK gating (so sessions can run in arbitrary folders).
|
|
801
|
-
flockbayDevBypassUeGates: z.z.boolean().optional()
|
|
802
|
-
});
|
|
803
|
-
z.z.object({
|
|
804
|
-
status: z.z.union([
|
|
805
|
-
z.z.enum(["running", "shutting-down"]),
|
|
806
|
-
z.z.string()
|
|
807
|
-
// Forward compatibility
|
|
808
|
-
]),
|
|
809
|
-
pid: z.z.number().optional(),
|
|
810
|
-
httpPort: z.z.number().optional(),
|
|
811
|
-
startedAt: z.z.number().optional(),
|
|
812
|
-
shutdownRequestedAt: z.z.number().optional(),
|
|
813
|
-
shutdownSource: z.z.union([
|
|
814
|
-
z.z.enum(["mobile-app", "cli", "os-signal", "unknown"]),
|
|
815
|
-
z.z.string()
|
|
816
|
-
// Forward compatibility
|
|
817
|
-
]).optional()
|
|
818
|
-
});
|
|
819
|
-
z.z.object({
|
|
820
|
-
content: SessionMessageContentSchema,
|
|
821
|
-
createdAt: z.z.number(),
|
|
822
|
-
id: z.z.string(),
|
|
823
|
-
seq: z.z.number(),
|
|
824
|
-
updatedAt: z.z.number()
|
|
825
|
-
});
|
|
826
|
-
const MessageMetaSchema = z.z.object({
|
|
827
|
-
sentFrom: z.z.string().optional(),
|
|
828
|
-
// Source identifier
|
|
829
|
-
permissionMode: z.z.string().optional(),
|
|
830
|
-
// Permission mode for this message
|
|
831
|
-
model: z.z.string().nullable().optional(),
|
|
832
|
-
// Model name for this message (null = reset)
|
|
833
|
-
customSystemPrompt: z.z.string().nullable().optional(),
|
|
834
|
-
// Custom system prompt for this message (null = reset)
|
|
835
|
-
appendSystemPrompt: z.z.string().nullable().optional(),
|
|
836
|
-
// Append to system prompt for this message (null = reset)
|
|
837
|
-
allowedTools: z.z.array(z.z.string()).nullable().optional(),
|
|
838
|
-
// Allowed tools for this message (null = reset)
|
|
839
|
-
disallowedTools: z.z.array(z.z.string()).nullable().optional(),
|
|
840
|
-
// Disallowed tools for this message (null = reset)
|
|
841
|
-
// Optional text to show in UI instead of the raw prompt (mobile/web feature).
|
|
842
|
-
displayText: z.z.string().optional(),
|
|
843
|
-
// Optional multimodal attachments (base64-embedded; used for vision-capable agents).
|
|
844
|
-
attachments: z.z.object({
|
|
845
|
-
images: z.z.array(
|
|
846
|
-
z.z.object({
|
|
847
|
-
mimeType: z.z.string(),
|
|
848
|
-
base64: z.z.string(),
|
|
849
|
-
name: z.z.string().optional(),
|
|
850
|
-
width: z.z.number().optional(),
|
|
851
|
-
height: z.z.number().optional()
|
|
852
|
-
})
|
|
853
|
-
).optional()
|
|
854
|
-
}).optional()
|
|
855
|
-
});
|
|
856
|
-
z.z.object({
|
|
857
|
-
session: z.z.object({
|
|
858
|
-
id: z.z.string(),
|
|
859
|
-
tag: z.z.string(),
|
|
860
|
-
seq: z.z.number(),
|
|
861
|
-
createdAt: z.z.number(),
|
|
862
|
-
updatedAt: z.z.number(),
|
|
863
|
-
metadata: z.z.string(),
|
|
864
|
-
metadataVersion: z.z.number(),
|
|
865
|
-
agentState: z.z.string().nullable(),
|
|
866
|
-
agentStateVersion: z.z.number()
|
|
867
|
-
})
|
|
868
|
-
});
|
|
869
|
-
const UserMessageSchema = z.z.object({
|
|
870
|
-
role: z.z.literal("user"),
|
|
871
|
-
content: z.z.object({
|
|
872
|
-
type: z.z.literal("text"),
|
|
873
|
-
text: z.z.string()
|
|
874
|
-
}),
|
|
875
|
-
localKey: z.z.string().optional(),
|
|
876
|
-
// Mobile messages include this
|
|
877
|
-
meta: MessageMetaSchema.optional()
|
|
878
|
-
});
|
|
879
|
-
const AgentMessageSchema = z.z.object({
|
|
880
|
-
role: z.z.literal("agent"),
|
|
881
|
-
content: z.z.object({
|
|
882
|
-
type: z.z.literal("output"),
|
|
883
|
-
data: z.z.any()
|
|
884
|
-
}),
|
|
885
|
-
meta: MessageMetaSchema.optional()
|
|
886
|
-
});
|
|
887
|
-
z.z.union([UserMessageSchema, AgentMessageSchema]);
|
|
888
|
-
|
|
889
|
-
async function delay(ms) {
|
|
890
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
891
|
-
}
|
|
892
|
-
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
893
|
-
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
|
|
894
|
-
return Math.round(Math.random() * maxDelayRet);
|
|
895
|
-
}
|
|
896
|
-
function createBackoff(opts) {
|
|
897
|
-
return async (callback) => {
|
|
898
|
-
let currentFailureCount = 0;
|
|
899
|
-
const minDelay = 250;
|
|
900
|
-
const maxDelay = 1e3;
|
|
901
|
-
const maxFailureCount = 50;
|
|
902
|
-
while (true) {
|
|
903
|
-
try {
|
|
904
|
-
return await callback();
|
|
905
|
-
} catch (e) {
|
|
906
|
-
if (currentFailureCount < maxFailureCount) {
|
|
907
|
-
currentFailureCount++;
|
|
908
|
-
}
|
|
909
|
-
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
910
|
-
await delay(waitForRequest);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
let backoff = createBackoff();
|
|
916
|
-
|
|
917
|
-
class AsyncLock {
|
|
918
|
-
permits = 1;
|
|
919
|
-
promiseResolverQueue = [];
|
|
920
|
-
async inLock(func) {
|
|
921
|
-
try {
|
|
922
|
-
await this.lock();
|
|
923
|
-
return await func();
|
|
924
|
-
} finally {
|
|
925
|
-
this.unlock();
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
async lock() {
|
|
929
|
-
if (this.permits > 0) {
|
|
930
|
-
this.permits = this.permits - 1;
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
|
|
934
|
-
}
|
|
935
|
-
unlock() {
|
|
936
|
-
this.permits += 1;
|
|
937
|
-
if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
|
|
938
|
-
throw new Error("this.permits should never be > 0 when there is someone waiting.");
|
|
939
|
-
} else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
|
|
940
|
-
this.permits -= 1;
|
|
941
|
-
const nextResolver = this.promiseResolverQueue.shift();
|
|
942
|
-
if (nextResolver) {
|
|
943
|
-
setTimeout(() => {
|
|
944
|
-
nextResolver(true);
|
|
945
|
-
}, 0);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
683
|
class RpcHandlerManager {
|
|
952
684
|
handlers = /* @__PURE__ */ new Map();
|
|
953
685
|
scopePrefix;
|
|
954
|
-
encryptionKey;
|
|
955
686
|
logger;
|
|
956
687
|
socket = null;
|
|
957
688
|
constructor(config) {
|
|
958
689
|
this.scopePrefix = config.scopePrefix;
|
|
959
|
-
this.encryptionKey = config.encryptionKey;
|
|
960
690
|
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
|
|
961
691
|
}
|
|
962
692
|
/**
|
|
@@ -981,20 +711,24 @@ class RpcHandlerManager {
|
|
|
981
711
|
const handler = this.handlers.get(request.method);
|
|
982
712
|
if (!handler) {
|
|
983
713
|
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
714
|
+
return { error: "Method not found" };
|
|
715
|
+
}
|
|
716
|
+
const rawParams = request.params;
|
|
717
|
+
const parsedParams = (() => {
|
|
718
|
+
if (typeof rawParams !== "string") return rawParams;
|
|
719
|
+
const s = rawParams.trim();
|
|
720
|
+
if (!s) return rawParams;
|
|
721
|
+
if (!(s.startsWith("{") || s.startsWith("["))) return rawParams;
|
|
722
|
+
try {
|
|
723
|
+
return JSON.parse(s);
|
|
724
|
+
} catch {
|
|
725
|
+
return rawParams;
|
|
726
|
+
}
|
|
727
|
+
})();
|
|
728
|
+
return await handler(parsedParams);
|
|
992
729
|
} catch (error) {
|
|
993
730
|
this.logger("[RPC] [ERROR] Error handling request", { error });
|
|
994
|
-
|
|
995
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
996
|
-
};
|
|
997
|
-
return encodeBase64(encrypt(this.encryptionKey, errorResponse));
|
|
731
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
998
732
|
}
|
|
999
733
|
}
|
|
1000
734
|
onSocketConnect(socket) {
|
|
@@ -1036,7 +770,7 @@ class RpcHandlerManager {
|
|
|
1036
770
|
}
|
|
1037
771
|
}
|
|
1038
772
|
|
|
1039
|
-
const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-
|
|
773
|
+
const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CGQhv7Z-.cjs', document.baseURI).href))));
|
|
1040
774
|
function projectPath() {
|
|
1041
775
|
const path = path$1.resolve(__dirname$1, "..");
|
|
1042
776
|
return path;
|
|
@@ -1200,6 +934,19 @@ async function sendUnrealMcpTcpCommand(options) {
|
|
|
1200
934
|
const paramName = missingParamMatch[1];
|
|
1201
935
|
hints.push(`Tip: include the required parameter \`${paramName}\` in the request params (UnrealMCP does not infer defaults).`);
|
|
1202
936
|
}
|
|
937
|
+
const details = response?.details;
|
|
938
|
+
if (details && typeof details === "object") {
|
|
939
|
+
const kind = typeof details.kind === "string" ? details.kind : null;
|
|
940
|
+
const missing = Array.isArray(details.missing) ? details.missing.filter((x) => typeof x === "string") : [];
|
|
941
|
+
if (kind === "missing_params" && missing.length > 0) {
|
|
942
|
+
hints.push(`Missing params: ${missing.map((m) => `\`${m}\``).join(", ")}`);
|
|
943
|
+
}
|
|
944
|
+
const exampleCall = details.exampleCall;
|
|
945
|
+
if (exampleCall && typeof exampleCall === "object") {
|
|
946
|
+
hints.push(`Example call:
|
|
947
|
+
${JSON.stringify(exampleCall, null, 2)}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
1203
950
|
const hint = hints.length > 0 ? `
|
|
1204
951
|
|
|
1205
952
|
${hints.join("\n\n")}` : "";
|
|
@@ -1478,7 +1225,7 @@ async function apiJsonGet(params) {
|
|
|
1478
1225
|
const res = await fetch(url, {
|
|
1479
1226
|
method: "GET",
|
|
1480
1227
|
headers: {
|
|
1481
|
-
Authorization: `
|
|
1228
|
+
Authorization: `Machine ${params.token}`,
|
|
1482
1229
|
Accept: "application/json"
|
|
1483
1230
|
}
|
|
1484
1231
|
});
|
|
@@ -1494,7 +1241,7 @@ async function apiJsonPost(params) {
|
|
|
1494
1241
|
const res = await fetch(url, {
|
|
1495
1242
|
method: "POST",
|
|
1496
1243
|
headers: {
|
|
1497
|
-
Authorization: `
|
|
1244
|
+
Authorization: `Machine ${params.token}`,
|
|
1498
1245
|
"Content-Type": "application/json",
|
|
1499
1246
|
Accept: "application/json"
|
|
1500
1247
|
},
|
|
@@ -1859,8 +1606,20 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordinatio
|
|
|
1859
1606
|
"play_in_editor_windowed",
|
|
1860
1607
|
"stop_play_in_editor",
|
|
1861
1608
|
"take_screenshot",
|
|
1609
|
+
"map_check",
|
|
1610
|
+
"compile_blueprints_all",
|
|
1611
|
+
"get_editor_context",
|
|
1612
|
+
"get_player_context",
|
|
1613
|
+
"raycast_from_camera",
|
|
1614
|
+
"raycast_down",
|
|
1615
|
+
"get_actor_transform",
|
|
1616
|
+
"get_actor_bounds",
|
|
1862
1617
|
"spawn_actor",
|
|
1863
|
-
"set_actor_transform"
|
|
1618
|
+
"set_actor_transform",
|
|
1619
|
+
"search_assets",
|
|
1620
|
+
"get_asset_info",
|
|
1621
|
+
"list_asset_packs",
|
|
1622
|
+
"place_asset"
|
|
1864
1623
|
];
|
|
1865
1624
|
const expected = await readExpectedUnrealMcpUplugin().catch((err) => {
|
|
1866
1625
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3122,48 +2881,38 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3122
2881
|
agentState;
|
|
3123
2882
|
agentStateVersion;
|
|
3124
2883
|
socket;
|
|
3125
|
-
pendingMessages = [];
|
|
3126
|
-
pendingMessageCallback = null;
|
|
3127
|
-
pendingOutboundMessages = [];
|
|
3128
2884
|
rpcHandlerManager;
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
coordinationLeaseGuard;
|
|
3133
|
-
coordinationLedgerReadAt = 0;
|
|
3134
|
-
docsIndexReadAt = 0;
|
|
2885
|
+
coordinationLeaseGuard = new CoordinationLeaseGuard();
|
|
2886
|
+
coordinationLedgerLastReadAtMs = null;
|
|
2887
|
+
outboundQueue = [];
|
|
3135
2888
|
constructor(token, session) {
|
|
3136
2889
|
super();
|
|
3137
2890
|
this.token = token;
|
|
3138
2891
|
this.sessionId = session.id;
|
|
3139
|
-
this.metadata = session.metadata;
|
|
3140
|
-
this.metadataVersion = session.metadataVersion;
|
|
3141
|
-
this.agentState = session.agentState;
|
|
3142
|
-
this.agentStateVersion = session.agentStateVersion;
|
|
3143
|
-
this.encryptionKey = session.encryptionKey;
|
|
3144
|
-
this.coordinationLeaseGuard = new CoordinationLeaseGuard();
|
|
2892
|
+
this.metadata = session.metadata ?? null;
|
|
2893
|
+
this.metadataVersion = Number(session.metadataVersion || 0);
|
|
2894
|
+
this.agentState = session.agentState ?? null;
|
|
2895
|
+
this.agentStateVersion = Number(session.agentStateVersion || 0);
|
|
3145
2896
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
3146
2897
|
scopePrefix: this.sessionId,
|
|
3147
|
-
encryptionKey: this.encryptionKey,
|
|
3148
2898
|
logger: (msg, data) => logger.debug(msg, data)
|
|
3149
2899
|
});
|
|
3150
|
-
|
|
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: {
|
|
2900
|
+
const cwd = String(this.metadata?.path || process.cwd()).trim() || process.cwd();
|
|
2901
|
+
const machineId = String(this.metadata?.machineId || "").trim();
|
|
2902
|
+
registerCommonHandlers(
|
|
2903
|
+
this.rpcHandlerManager,
|
|
2904
|
+
cwd,
|
|
2905
|
+
{
|
|
2906
|
+
serverUrl: configuration.serverUrl,
|
|
3163
2907
|
token: this.token,
|
|
3164
|
-
|
|
3165
|
-
|
|
2908
|
+
sessionId: this.sessionId,
|
|
2909
|
+
machineId,
|
|
2910
|
+
projectRootPath: String(this.metadata?.projectRootPath || cwd)
|
|
3166
2911
|
},
|
|
2912
|
+
{ bashEnabled: false }
|
|
2913
|
+
);
|
|
2914
|
+
this.socket = socket_ioClient.io(configuration.serverUrl, {
|
|
2915
|
+
auth: { token: this.token, clientType: "session-scoped", sessionId: this.sessionId, machineId },
|
|
3167
2916
|
path: "/v1/updates",
|
|
3168
2917
|
reconnection: true,
|
|
3169
2918
|
reconnectionAttempts: Infinity,
|
|
@@ -3174,386 +2923,196 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3174
2923
|
autoConnect: false
|
|
3175
2924
|
});
|
|
3176
2925
|
this.socket.on("connect", () => {
|
|
3177
|
-
logger.debug("
|
|
2926
|
+
logger.debug("[session] socket connected");
|
|
3178
2927
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
3179
|
-
this.
|
|
2928
|
+
this.flush();
|
|
2929
|
+
this.emit("connect");
|
|
3180
2930
|
});
|
|
3181
|
-
this.socket.on("
|
|
3182
|
-
|
|
2931
|
+
this.socket.on("connect_error", (error) => {
|
|
2932
|
+
const message = error instanceof Error ? error.message : String(error?.message || error || "connect_error");
|
|
2933
|
+
logger.debug("[session] socket connect_error", { message });
|
|
3183
2934
|
});
|
|
3184
2935
|
this.socket.on("disconnect", (reason) => {
|
|
3185
|
-
logger.debug("[
|
|
2936
|
+
logger.debug("[session] socket disconnected", reason);
|
|
3186
2937
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
2938
|
+
this.emit("disconnect", reason);
|
|
3187
2939
|
});
|
|
3188
|
-
this.socket.on("
|
|
3189
|
-
|
|
3190
|
-
this.rpcHandlerManager.onSocketDisconnect();
|
|
2940
|
+
this.socket.on("rpc-request", async (data, callback) => {
|
|
2941
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
3191
2942
|
});
|
|
3192
2943
|
this.socket.on("update", (data) => {
|
|
3193
|
-
|
|
3194
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
|
|
3195
|
-
if (!data.body) {
|
|
3196
|
-
logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
|
|
3197
|
-
return;
|
|
3198
|
-
}
|
|
3199
|
-
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
3200
|
-
const body = decrypt(this.encryptionKey, decodeBase64(data.body.message.content.c));
|
|
3201
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
3202
|
-
const userResult = UserMessageSchema.safeParse(body);
|
|
3203
|
-
if (userResult.success) {
|
|
3204
|
-
if (this.pendingMessageCallback) {
|
|
3205
|
-
this.pendingMessageCallback(userResult.data);
|
|
3206
|
-
} else {
|
|
3207
|
-
this.pendingMessages.push(userResult.data);
|
|
3208
|
-
}
|
|
3209
|
-
} else {
|
|
3210
|
-
this.emit("message", body);
|
|
3211
|
-
}
|
|
3212
|
-
} else if (data.body.t === "update-session") {
|
|
3213
|
-
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
|
|
3214
|
-
this.metadata = decrypt(this.encryptionKey, decodeBase64(data.body.metadata.value));
|
|
3215
|
-
this.metadataVersion = data.body.metadata.version;
|
|
3216
|
-
}
|
|
3217
|
-
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
|
|
3218
|
-
this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, decodeBase64(data.body.agentState.value)) : null;
|
|
3219
|
-
this.agentStateVersion = data.body.agentState.version;
|
|
3220
|
-
}
|
|
3221
|
-
} else if (data.body.t === "update-machine") {
|
|
3222
|
-
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
3223
|
-
} else {
|
|
3224
|
-
this.emit("message", data.body);
|
|
3225
|
-
}
|
|
3226
|
-
} catch (error) {
|
|
3227
|
-
logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
|
|
3228
|
-
}
|
|
2944
|
+
this.emit("update", data);
|
|
3229
2945
|
});
|
|
3230
|
-
this.socket.on("error", (error) => {
|
|
3231
|
-
logger.debug("[API] Socket error:", error);
|
|
3232
|
-
});
|
|
3233
|
-
this.socket.connect();
|
|
3234
|
-
}
|
|
3235
|
-
/**
|
|
3236
|
-
* Returns the session client's bearer token for calling authenticated HTTP endpoints.
|
|
3237
|
-
* Intended for internal integrations (e.g. uploading artifacts for this session).
|
|
3238
|
-
*/
|
|
3239
|
-
getAuthToken() {
|
|
3240
|
-
return this.token;
|
|
3241
2946
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
* (i.e. ledger tools are available and claim enforcement makes sense).
|
|
3245
|
-
*/
|
|
3246
|
-
hasCoordinationContext() {
|
|
3247
|
-
const meta = this.metadata;
|
|
3248
|
-
if (!meta) return false;
|
|
3249
|
-
const projectId = String(meta.workspaceProjectId || "").trim();
|
|
3250
|
-
const workItemId = String(meta.workItemId || "").trim();
|
|
3251
|
-
return Boolean(projectId && workItemId);
|
|
2947
|
+
connect() {
|
|
2948
|
+
this.socket.connect();
|
|
3252
2949
|
}
|
|
3253
|
-
|
|
3254
|
-
this.
|
|
2950
|
+
async connectAndWait(timeoutMs = 15e3) {
|
|
2951
|
+
if (this.socket.connected) return;
|
|
2952
|
+
await new Promise((resolve, reject) => {
|
|
2953
|
+
let timeoutHandle = null;
|
|
2954
|
+
const cleanup = () => {
|
|
2955
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2956
|
+
this.socket.off("connect", onConnect);
|
|
2957
|
+
this.socket.off("connect_error", onError);
|
|
2958
|
+
this.socket.off("disconnect", onDisconnect);
|
|
2959
|
+
};
|
|
2960
|
+
const onConnect = () => {
|
|
2961
|
+
cleanup();
|
|
2962
|
+
resolve();
|
|
2963
|
+
};
|
|
2964
|
+
const onError = (err) => {
|
|
2965
|
+
cleanup();
|
|
2966
|
+
const message = err instanceof Error ? err.message : String(err?.message || err || "connect_error");
|
|
2967
|
+
reject(new Error(message));
|
|
2968
|
+
};
|
|
2969
|
+
const onDisconnect = (reason) => {
|
|
2970
|
+
cleanup();
|
|
2971
|
+
reject(new Error(`disconnected:${String(reason || "unknown")}`));
|
|
2972
|
+
};
|
|
2973
|
+
timeoutHandle = setTimeout(() => {
|
|
2974
|
+
cleanup();
|
|
2975
|
+
reject(new Error("connect_timeout"));
|
|
2976
|
+
}, timeoutMs);
|
|
2977
|
+
this.socket.once("connect", onConnect);
|
|
2978
|
+
this.socket.once("connect_error", onError);
|
|
2979
|
+
this.socket.once("disconnect", onDisconnect);
|
|
2980
|
+
this.connect();
|
|
2981
|
+
});
|
|
3255
2982
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
2983
|
+
disconnect() {
|
|
2984
|
+
this.socket.disconnect();
|
|
3258
2985
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
if (!Number.isFinite(last) || last <= 0) return false;
|
|
3262
|
-
return Date.now() - last <= ms;
|
|
2986
|
+
close() {
|
|
2987
|
+
this.disconnect();
|
|
3263
2988
|
}
|
|
3264
|
-
|
|
3265
|
-
this.
|
|
2989
|
+
getAuthToken() {
|
|
2990
|
+
return this.token;
|
|
3266
2991
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
2992
|
+
async listMessages() {
|
|
2993
|
+
const baseUrl = configuration.serverUrl.replace(/\/+$/, "");
|
|
2994
|
+
const url = `${baseUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/messages`;
|
|
2995
|
+
const res = await axios.get(url, {
|
|
2996
|
+
headers: { Authorization: `Machine ${this.token}`, "Content-Type": "application/json" },
|
|
2997
|
+
timeout: 6e4
|
|
2998
|
+
});
|
|
2999
|
+
return Array.isArray(res.data?.messages) ? res.data.messages : [];
|
|
3269
3000
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
if (!Number.isFinite(last) || last <= 0) return false;
|
|
3273
|
-
return Date.now() - last <= ms;
|
|
3001
|
+
markCoordinationLedgerRead(timeMs = Date.now()) {
|
|
3002
|
+
this.coordinationLedgerLastReadAtMs = timeMs;
|
|
3274
3003
|
}
|
|
3275
|
-
|
|
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;
|
|
3004
|
+
getCoordinationLedgerLastReadAtMs() {
|
|
3005
|
+
return this.coordinationLedgerLastReadAtMs;
|
|
3310
3006
|
}
|
|
3311
|
-
|
|
3007
|
+
keepAlive(thinking, mode) {
|
|
3312
3008
|
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
|
-
});
|
|
3009
|
+
this.socket.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking, mode });
|
|
3338
3010
|
}
|
|
3339
|
-
|
|
3340
|
-
this.
|
|
3341
|
-
while (this.pendingMessages.length > 0) {
|
|
3342
|
-
callback(this.pendingMessages.shift());
|
|
3343
|
-
}
|
|
3011
|
+
sendSessionDeath() {
|
|
3012
|
+
this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
|
|
3344
3013
|
}
|
|
3345
|
-
|
|
3346
|
-
|
|
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
|
-
}
|
|
3014
|
+
sendSessionEvent(event) {
|
|
3015
|
+
this.emitMessageOrQueue({
|
|
3016
|
+
role: "agent",
|
|
3017
|
+
content: { type: "event", id: node_crypto.randomUUID(), data: event },
|
|
3018
|
+
meta: { sentFrom: "cli" }
|
|
3019
|
+
});
|
|
3394
3020
|
}
|
|
3395
3021
|
sendCodexMessage(body) {
|
|
3396
|
-
|
|
3397
|
-
role: "agent",
|
|
3398
|
-
content: {
|
|
3399
|
-
type: "codex",
|
|
3400
|
-
data: body
|
|
3401
|
-
// This wraps the entire Claude message
|
|
3402
|
-
},
|
|
3403
|
-
meta: {
|
|
3404
|
-
sentFrom: "cli"
|
|
3405
|
-
}
|
|
3406
|
-
};
|
|
3407
|
-
const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
|
|
3408
|
-
this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `codex:${String(body?.type ?? "unknown")}` });
|
|
3022
|
+
this.emitMessageOrQueue({ role: "agent", content: { type: "codex", data: body }, meta: { sentFrom: "cli" } });
|
|
3409
3023
|
}
|
|
3410
|
-
/**
|
|
3411
|
-
* Send a generic agent message to the session.
|
|
3412
|
-
* Works for any agent type (Gemini, Codex, Claude, etc.)
|
|
3413
|
-
*
|
|
3414
|
-
* @param agentType - The type of agent sending the message (e.g., 'gemini', 'codex', 'claude')
|
|
3415
|
-
* @param body - The message payload
|
|
3416
|
-
*/
|
|
3417
3024
|
sendAgentMessage(agentType, body) {
|
|
3418
|
-
|
|
3419
|
-
role: "agent",
|
|
3420
|
-
content: {
|
|
3421
|
-
type: agentType,
|
|
3422
|
-
data: body
|
|
3423
|
-
},
|
|
3424
|
-
meta: {
|
|
3425
|
-
sentFrom: "cli"
|
|
3426
|
-
}
|
|
3427
|
-
};
|
|
3428
|
-
logger.debug(`[SOCKET] Sending ${agentType} message:`, { type: body.type, hasMessage: !!body.message });
|
|
3429
|
-
const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
|
|
3430
|
-
this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `${agentType}:${String(body?.type ?? "unknown")}` });
|
|
3025
|
+
this.emitMessageOrQueue({ role: "agent", content: { type: agentType, data: body }, meta: { sentFrom: "cli" } });
|
|
3431
3026
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3027
|
+
sendClaudeSessionMessage(body) {
|
|
3028
|
+
this.emitMessageOrQueue({
|
|
3434
3029
|
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
|
|
3030
|
+
content: { type: "output", data: body },
|
|
3031
|
+
meta: { sentFrom: "cli" }
|
|
3456
3032
|
});
|
|
3457
3033
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3034
|
+
emitMessageOrQueue(content) {
|
|
3035
|
+
const payload = { sid: this.sessionId, content };
|
|
3036
|
+
if (this.socket.connected) {
|
|
3037
|
+
this.socket.emit("message", payload);
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
this.outboundQueue.push(payload);
|
|
3463
3041
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
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
|
-
}
|
|
3042
|
+
flush() {
|
|
3043
|
+
if (!this.socket.connected) return;
|
|
3044
|
+
while (this.outboundQueue.length > 0) {
|
|
3045
|
+
const next = this.outboundQueue.shift();
|
|
3046
|
+
if (!next) continue;
|
|
3047
|
+
this.socket.emit("message", next);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
onUserMessage(handler) {
|
|
3051
|
+
const listener = (u) => {
|
|
3052
|
+
if (u?.body?.t !== "new-message") return;
|
|
3053
|
+
const body = u.body;
|
|
3054
|
+
const raw = body?.message ?? null;
|
|
3055
|
+
if (!raw || typeof raw !== "object") return;
|
|
3056
|
+
const record = raw?.content;
|
|
3057
|
+
if (!record || typeof record !== "object") return;
|
|
3058
|
+
if (record.role !== "user") return;
|
|
3059
|
+
handler(record);
|
|
3486
3060
|
};
|
|
3487
|
-
|
|
3488
|
-
this.
|
|
3061
|
+
this.on("update", listener);
|
|
3062
|
+
return () => this.off("update", listener);
|
|
3489
3063
|
}
|
|
3490
|
-
/**
|
|
3491
|
-
* Update session metadata
|
|
3492
|
-
* @param handler - Handler function that returns the updated metadata
|
|
3493
|
-
*/
|
|
3494
3064
|
updateMetadata(handler) {
|
|
3495
|
-
this.
|
|
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
|
-
});
|
|
3065
|
+
const current = this.metadata || {};
|
|
3066
|
+
const next = handler(current);
|
|
3067
|
+
void this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: next }).then((answer) => {
|
|
3068
|
+
if (answer?.result === "success") {
|
|
3069
|
+
this.metadata = next;
|
|
3070
|
+
this.metadataVersion += 1;
|
|
3071
|
+
}
|
|
3072
|
+
}).catch(() => null);
|
|
3511
3073
|
}
|
|
3512
|
-
/**
|
|
3513
|
-
* Update session agent state
|
|
3514
|
-
* @param handler - Handler function that returns the updated agent state
|
|
3515
|
-
*/
|
|
3516
3074
|
updateAgentState(handler) {
|
|
3517
|
-
|
|
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
|
-
});
|
|
3075
|
+
const current = this.agentState || {};
|
|
3076
|
+
const next = handler(current);
|
|
3077
|
+
void this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: next }).then((answer) => {
|
|
3078
|
+
if (answer?.result === "success") {
|
|
3079
|
+
this.agentState = next;
|
|
3080
|
+
this.agentStateVersion += 1;
|
|
3081
|
+
}
|
|
3082
|
+
}).catch(() => null);
|
|
3535
3083
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
async function delay(ms) {
|
|
3087
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3088
|
+
}
|
|
3089
|
+
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
3090
|
+
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
|
|
3091
|
+
return Math.round(Math.random() * maxDelayRet);
|
|
3092
|
+
}
|
|
3093
|
+
function createBackoff(opts) {
|
|
3094
|
+
return async (callback) => {
|
|
3095
|
+
let currentFailureCount = 0;
|
|
3096
|
+
const minDelay = opts && opts.minDelay !== void 0 ? opts.minDelay : 250;
|
|
3097
|
+
const maxDelay = opts && opts.maxDelay !== void 0 ? opts.maxDelay : 1e3;
|
|
3098
|
+
const maxFailureCount = opts && opts.maxFailureCount !== void 0 ? opts.maxFailureCount : 50;
|
|
3099
|
+
while (true) {
|
|
3100
|
+
try {
|
|
3101
|
+
return await callback();
|
|
3102
|
+
} catch (e) {
|
|
3103
|
+
if (currentFailureCount < maxFailureCount) {
|
|
3104
|
+
currentFailureCount++;
|
|
3105
|
+
}
|
|
3106
|
+
if (opts && opts.onError) {
|
|
3107
|
+
opts.onError(e, currentFailureCount);
|
|
3108
|
+
}
|
|
3109
|
+
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
3110
|
+
await delay(waitForRequest);
|
|
3111
|
+
}
|
|
3542
3112
|
}
|
|
3543
|
-
|
|
3544
|
-
this.socket.emit("ping", () => {
|
|
3545
|
-
resolve();
|
|
3546
|
-
});
|
|
3547
|
-
setTimeout(() => {
|
|
3548
|
-
resolve();
|
|
3549
|
-
}, 1e4);
|
|
3550
|
-
});
|
|
3551
|
-
}
|
|
3552
|
-
async close() {
|
|
3553
|
-
logger.debug("[API] socket.close() called");
|
|
3554
|
-
this.socket.close();
|
|
3555
|
-
}
|
|
3113
|
+
};
|
|
3556
3114
|
}
|
|
3115
|
+
let backoff = createBackoff();
|
|
3557
3116
|
|
|
3558
3117
|
function looksLikeEngineRoot(engineRoot) {
|
|
3559
3118
|
if (!engineRoot) return false;
|
|
@@ -3726,6 +3285,11 @@ async function buildAndInstallUnrealMcpPlugin(options) {
|
|
|
3726
3285
|
`-Plugin=${pluginUpluginPath}`,
|
|
3727
3286
|
`-Package=${packageDir}`,
|
|
3728
3287
|
"-Rocket",
|
|
3288
|
+
// UAT enforces a global single-instance mutex. On macOS/Linux, the named mutex can
|
|
3289
|
+
// appear "already created" even when no process is actually holding it, which makes
|
|
3290
|
+
// repeated installs fail with: "A conflicting instance of AutomationTool is already running."
|
|
3291
|
+
// `-WaitForUATMutex` makes UAT acquire the mutex if it's free (or wait if another build is in flight).
|
|
3292
|
+
"-WaitForUATMutex",
|
|
3729
3293
|
`-TargetPlatforms=${targetPlatform()}`
|
|
3730
3294
|
];
|
|
3731
3295
|
const logStream = fs.createWriteStream(buildLogPath, { flags: "a" });
|
|
@@ -3798,7 +3362,6 @@ class ApiMachineClient {
|
|
|
3798
3362
|
this.machine = machine;
|
|
3799
3363
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
3800
3364
|
scopePrefix: this.machine.id,
|
|
3801
|
-
encryptionKey: this.machine.encryptionKey,
|
|
3802
3365
|
logger: (msg, data) => logger.debug(msg, data)
|
|
3803
3366
|
});
|
|
3804
3367
|
const rootDir = this.machine?.metadata?.homeDir || os.homedir() || process.cwd();
|
|
@@ -3839,6 +3402,24 @@ class ApiMachineClient {
|
|
|
3839
3402
|
socket;
|
|
3840
3403
|
keepAliveInterval = null;
|
|
3841
3404
|
rpcHandlerManager;
|
|
3405
|
+
connected = false;
|
|
3406
|
+
lastConnectError = null;
|
|
3407
|
+
lastDisconnectReason = null;
|
|
3408
|
+
lastHttpUpsertError = null;
|
|
3409
|
+
lastHttpUpsertStatus = null;
|
|
3410
|
+
lastHttpUpsertAt = null;
|
|
3411
|
+
upsertBackoff = createBackoff({
|
|
3412
|
+
minDelay: 1e3,
|
|
3413
|
+
maxDelay: 3e4,
|
|
3414
|
+
maxFailureCount: 200,
|
|
3415
|
+
onError: (e, failures) => {
|
|
3416
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3417
|
+
this.lastHttpUpsertError = msg;
|
|
3418
|
+
if (process.env.DEBUG) {
|
|
3419
|
+
logger.debug(`[API MACHINE] Machine upsert retry (${failures}): ${msg}`);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3842
3423
|
setRPCHandlers({
|
|
3843
3424
|
spawnSession,
|
|
3844
3425
|
stopSession,
|
|
@@ -3885,74 +3466,86 @@ class ApiMachineClient {
|
|
|
3885
3466
|
});
|
|
3886
3467
|
}
|
|
3887
3468
|
/**
|
|
3888
|
-
*
|
|
3889
|
-
* Currently unused, changes from the mobile client are more likely
|
|
3890
|
-
* for example to set a custom name.
|
|
3469
|
+
* Upsert machine record (metadata + daemon state) via HTTP (workspace-native V1).
|
|
3891
3470
|
*/
|
|
3892
|
-
async
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
}
|
|
3909
|
-
throw new Error("Metadata version mismatch");
|
|
3910
|
-
}
|
|
3471
|
+
async upsertMachineHttp(params) {
|
|
3472
|
+
const attemptAt = Date.now();
|
|
3473
|
+
const endpoint = configuration.serverUrl.replace(/\/+$/, "");
|
|
3474
|
+
const timeoutMs = Math.max(1e3, Math.min(12e4, Number.parseInt(process.env.FLOCKBAY_MACHINE_UPSERT_TIMEOUT_MS || "30000", 10) || 3e4));
|
|
3475
|
+
const res = await fetch(`${endpoint}/v1/machines`, {
|
|
3476
|
+
method: "POST",
|
|
3477
|
+
headers: {
|
|
3478
|
+
Authorization: `Machine ${this.token}`,
|
|
3479
|
+
"Content-Type": "application/json"
|
|
3480
|
+
},
|
|
3481
|
+
body: JSON.stringify({
|
|
3482
|
+
id: this.machine.id,
|
|
3483
|
+
...params.metadata !== void 0 ? { metadata: params.metadata } : {},
|
|
3484
|
+
...params.daemonState !== void 0 ? { daemonState: params.daemonState } : {}
|
|
3485
|
+
}),
|
|
3486
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3911
3487
|
});
|
|
3488
|
+
if (!res.ok) {
|
|
3489
|
+
const detail = await res.text().catch(() => "");
|
|
3490
|
+
const msg = `machine_upsert_failed:${res.status}${detail ? `:${detail.slice(0, 2e3)}` : ""}`;
|
|
3491
|
+
this.lastHttpUpsertStatus = res.status;
|
|
3492
|
+
this.lastHttpUpsertError = msg;
|
|
3493
|
+
throw new Error(msg);
|
|
3494
|
+
}
|
|
3495
|
+
const data = await res.json().catch(() => null);
|
|
3496
|
+
const machine = data?.machine && typeof data.machine === "object" ? data.machine : null;
|
|
3497
|
+
if (machine) {
|
|
3498
|
+
this.machine.metadata = machine.metadata ?? null;
|
|
3499
|
+
this.machine.daemonState = machine.daemonState ?? null;
|
|
3500
|
+
this.machine.seq = Number(machine.seq || this.machine.seq || 0);
|
|
3501
|
+
}
|
|
3502
|
+
this.lastHttpUpsertStatus = res.status;
|
|
3503
|
+
this.lastHttpUpsertError = null;
|
|
3504
|
+
this.lastHttpUpsertAt = attemptAt;
|
|
3912
3505
|
}
|
|
3913
3506
|
/**
|
|
3914
|
-
* Update daemon state (runtime info)
|
|
3915
|
-
* Simplified without lock - relies on backoff for retry
|
|
3507
|
+
* Update daemon state (runtime info) via HTTP upsert.
|
|
3916
3508
|
*/
|
|
3917
3509
|
async updateDaemonState(handler) {
|
|
3918
|
-
await
|
|
3510
|
+
await this.upsertBackoff(async () => {
|
|
3919
3511
|
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
|
-
}
|
|
3512
|
+
await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
|
|
3513
|
+
logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
3936
3514
|
});
|
|
3937
3515
|
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Best-effort single attempt (no retries). Useful during shutdown.
|
|
3518
|
+
*/
|
|
3519
|
+
async updateDaemonStateOnce(handler) {
|
|
3520
|
+
try {
|
|
3521
|
+
const updated = handler(this.machine.daemonState);
|
|
3522
|
+
await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
|
|
3523
|
+
return true;
|
|
3524
|
+
} catch (e) {
|
|
3525
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3526
|
+
logger.debug("[API MACHINE] Daemon state update failed (best-effort):", msg);
|
|
3527
|
+
return false;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
getStatusSnapshot() {
|
|
3531
|
+
return {
|
|
3532
|
+
connected: this.connected,
|
|
3533
|
+
lastConnectError: this.lastConnectError,
|
|
3534
|
+
lastDisconnectReason: this.lastDisconnectReason,
|
|
3535
|
+
lastHttpUpsertError: this.lastHttpUpsertError,
|
|
3536
|
+
lastHttpUpsertStatus: this.lastHttpUpsertStatus,
|
|
3537
|
+
lastHttpUpsertAt: this.lastHttpUpsertAt
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3938
3540
|
connect() {
|
|
3939
3541
|
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
3940
3542
|
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
3941
|
-
const machineMetadataEncrypted = encodeBase64(encrypt(this.machine.encryptionKey, this.machine.metadata));
|
|
3942
|
-
const daemonStateEncrypted = this.machine.daemonState ? encodeBase64(encrypt(this.machine.encryptionKey, this.machine.daemonState)) : "";
|
|
3943
|
-
const dataEncryptionKey = String(this.machine.dataEncryptionKey || "").trim();
|
|
3944
|
-
if (!dataEncryptionKey) {
|
|
3945
|
-
throw new Error("Missing machine dataEncryptionKey (DEK)");
|
|
3946
|
-
}
|
|
3947
3543
|
this.socket = socket_ioClient.io(serverUrl, {
|
|
3948
3544
|
transports: ["websocket"],
|
|
3949
3545
|
auth: {
|
|
3950
3546
|
token: this.token,
|
|
3951
3547
|
clientType: "machine-scoped",
|
|
3952
|
-
machineId: this.machine.id
|
|
3953
|
-
dataEncryptionKey,
|
|
3954
|
-
machineMetadata: machineMetadataEncrypted,
|
|
3955
|
-
daemonState: daemonStateEncrypted
|
|
3548
|
+
machineId: this.machine.id
|
|
3956
3549
|
},
|
|
3957
3550
|
path: "/v1/updates",
|
|
3958
3551
|
reconnection: true,
|
|
@@ -3961,6 +3554,9 @@ class ApiMachineClient {
|
|
|
3961
3554
|
});
|
|
3962
3555
|
this.socket.on("connect", () => {
|
|
3963
3556
|
logger.debug("[API MACHINE] Connected to server");
|
|
3557
|
+
this.connected = true;
|
|
3558
|
+
this.lastConnectError = null;
|
|
3559
|
+
this.lastDisconnectReason = null;
|
|
3964
3560
|
this.updateDaemonState((state) => ({
|
|
3965
3561
|
...state,
|
|
3966
3562
|
status: "running",
|
|
@@ -3971,8 +3567,10 @@ class ApiMachineClient {
|
|
|
3971
3567
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
3972
3568
|
this.startKeepAlive();
|
|
3973
3569
|
});
|
|
3974
|
-
this.socket.on("disconnect", () => {
|
|
3975
|
-
logger.debug("[API MACHINE] Disconnected from server");
|
|
3570
|
+
this.socket.on("disconnect", (reason) => {
|
|
3571
|
+
logger.debug("[API MACHINE] Disconnected from server", reason);
|
|
3572
|
+
this.connected = false;
|
|
3573
|
+
this.lastDisconnectReason = typeof reason === "string" ? reason : "disconnected";
|
|
3976
3574
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
3977
3575
|
this.stopKeepAlive();
|
|
3978
3576
|
});
|
|
@@ -3981,24 +3579,19 @@ class ApiMachineClient {
|
|
|
3981
3579
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
3982
3580
|
});
|
|
3983
3581
|
this.socket.on("update", (data) => {
|
|
3984
|
-
if (data
|
|
3582
|
+
if (data?.body?.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
3985
3583
|
const update = data.body;
|
|
3986
|
-
if (update.
|
|
3987
|
-
|
|
3988
|
-
this.machine.
|
|
3989
|
-
this.machine.
|
|
3584
|
+
if (update.machine) {
|
|
3585
|
+
this.machine.metadata = update.machine.metadata ?? null;
|
|
3586
|
+
this.machine.daemonState = update.machine.daemonState ?? null;
|
|
3587
|
+
this.machine.seq = Number(update.machine.seq || this.machine.seq || 0);
|
|
3990
3588
|
}
|
|
3991
|
-
if (update.daemonState) {
|
|
3992
|
-
logger.debug("[API MACHINE] Received external daemon state update");
|
|
3993
|
-
this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(update.daemonState.value));
|
|
3994
|
-
this.machine.daemonStateVersion = update.daemonState.version;
|
|
3995
|
-
}
|
|
3996
|
-
} else {
|
|
3997
|
-
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
3998
3589
|
}
|
|
3999
3590
|
});
|
|
4000
3591
|
this.socket.on("connect_error", (error) => {
|
|
4001
3592
|
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
3593
|
+
this.connected = false;
|
|
3594
|
+
this.lastConnectError = String(error?.message || "connect_error");
|
|
4002
3595
|
});
|
|
4003
3596
|
this.socket.io.on("error", (error) => {
|
|
4004
3597
|
logger.debug("[API MACHINE] Socket error:", error);
|
|
@@ -4035,389 +3628,123 @@ class ApiMachineClient {
|
|
|
4035
3628
|
}
|
|
4036
3629
|
}
|
|
4037
3630
|
|
|
4038
|
-
|
|
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
|
-
}
|
|
3631
|
+
function machineAuthHeaders(machineToken) {
|
|
3632
|
+
return { Authorization: `Machine ${machineToken}`, "Content-Type": "application/json" };
|
|
3633
|
+
}
|
|
3634
|
+
function normalizeSession(raw) {
|
|
3635
|
+
return {
|
|
3636
|
+
id: String(raw?.id || ""),
|
|
3637
|
+
seq: Number(raw?.seq || 0),
|
|
3638
|
+
active: Boolean(raw?.active),
|
|
3639
|
+
activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
|
|
3640
|
+
createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
|
|
3641
|
+
updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
|
|
3642
|
+
metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
|
|
3643
|
+
metadataVersion: Number(raw?.metadataVersion || 0),
|
|
3644
|
+
agentState: raw?.agentState && typeof raw.agentState === "object" ? raw.agentState : null,
|
|
3645
|
+
agentStateVersion: Number(raw?.agentStateVersion || 0)
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
function normalizeMachine(raw) {
|
|
3649
|
+
return {
|
|
3650
|
+
id: String(raw?.id || ""),
|
|
3651
|
+
seq: Number(raw?.seq || 0),
|
|
3652
|
+
active: Boolean(raw?.active),
|
|
3653
|
+
activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
|
|
3654
|
+
createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
|
|
3655
|
+
updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
|
|
3656
|
+
metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
|
|
3657
|
+
daemonState: raw?.daemonState && typeof raw.daemonState === "object" ? raw.daemonState : null
|
|
3658
|
+
};
|
|
4162
3659
|
}
|
|
4163
|
-
|
|
4164
3660
|
class ApiClient {
|
|
4165
|
-
|
|
4166
|
-
|
|
3661
|
+
constructor(auth) {
|
|
3662
|
+
this.auth = auth;
|
|
4167
3663
|
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
3664
|
+
static async create(auth) {
|
|
3665
|
+
return new ApiClient(auth);
|
|
3666
|
+
}
|
|
3667
|
+
baseUrl() {
|
|
3668
|
+
return configuration.serverUrl.replace(/\/+$/, "");
|
|
3669
|
+
}
|
|
3670
|
+
async createSession(opts) {
|
|
3671
|
+
const response = await axios.post(
|
|
3672
|
+
`${this.baseUrl()}/v1/sessions`,
|
|
3673
|
+
{ metadata: opts.metadata ?? {}, agentState: opts.state ?? null },
|
|
3674
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3675
|
+
);
|
|
3676
|
+
return normalizeSession(response.data?.session);
|
|
4173
3677
|
}
|
|
4174
|
-
/**
|
|
4175
|
-
* Create a new session or load existing one with the given tag
|
|
4176
|
-
*/
|
|
4177
3678
|
async getOrCreateSession(opts) {
|
|
4178
|
-
const
|
|
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
|
-
}
|
|
3679
|
+
const tag = String(opts?.tag || "").trim();
|
|
3680
|
+
const metadata = opts?.metadata && typeof opts.metadata === "object" ? opts.metadata : {};
|
|
3681
|
+
const merged = tag ? { ...metadata, tag } : metadata;
|
|
3682
|
+
return this.createSession({ metadata: merged, state: opts.state ?? null });
|
|
3683
|
+
}
|
|
3684
|
+
async getSessionById(sessionId) {
|
|
3685
|
+
const id = String(sessionId || "").trim();
|
|
3686
|
+
if (!id) throw new Error("Session id is required");
|
|
3687
|
+
const response = await axios.get(
|
|
3688
|
+
`${this.baseUrl()}/v1/sessions/${encodeURIComponent(id)}`,
|
|
3689
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3690
|
+
);
|
|
3691
|
+
return normalizeSession(response.data?.session);
|
|
4217
3692
|
}
|
|
4218
|
-
|
|
4219
|
-
|
|
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);
|
|
3693
|
+
async upsertMachine(opts) {
|
|
3694
|
+
const machineId = String(opts.machineId || "").trim();
|
|
3695
|
+
if (!machineId) throw new Error("Machine id is required");
|
|
4228
3696
|
const response = await axios.post(
|
|
4229
|
-
`${
|
|
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
|
-
}
|
|
3697
|
+
`${this.baseUrl()}/v1/machines`,
|
|
3698
|
+
{ id: machineId, metadata: opts.metadata ?? {}, daemonState: opts.daemonState ?? null },
|
|
3699
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
4244
3700
|
);
|
|
4245
|
-
|
|
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;
|
|
3701
|
+
return normalizeMachine(response.data?.machine);
|
|
3702
|
+
}
|
|
3703
|
+
async getOrCreateMachine(opts) {
|
|
3704
|
+
return this.upsertMachine(opts);
|
|
4262
3705
|
}
|
|
4263
|
-
/**
|
|
4264
|
-
* Fetch an existing machine by id.
|
|
4265
|
-
* Useful when the daemon needs the latest server-side machine metadata (e.g. dev toggles).
|
|
4266
|
-
*/
|
|
4267
3706
|
async getMachine(machineId) {
|
|
4268
3707
|
const id = String(machineId || "").trim();
|
|
4269
|
-
if (!id)
|
|
4270
|
-
throw new Error("Machine id is required");
|
|
4271
|
-
}
|
|
4272
|
-
const encryptionKey = this.credential.encryption.machineKey;
|
|
3708
|
+
if (!id) throw new Error("Machine id is required");
|
|
4273
3709
|
const response = await axios.get(
|
|
4274
|
-
`${
|
|
4275
|
-
{
|
|
4276
|
-
headers: {
|
|
4277
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4278
|
-
"Content-Type": "application/json"
|
|
4279
|
-
},
|
|
4280
|
-
timeout: 6e4
|
|
4281
|
-
}
|
|
3710
|
+
`${this.baseUrl()}/v1/machines/${encodeURIComponent(id)}`,
|
|
3711
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
4282
3712
|
);
|
|
4283
|
-
|
|
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);
|
|
3713
|
+
return normalizeMachine(response.data?.machine);
|
|
3714
|
+
}
|
|
3715
|
+
async registerVendorToken(vendor, token) {
|
|
3716
|
+
const v = String(vendor || "").trim().toLowerCase();
|
|
3717
|
+
if (!v) throw new Error("Vendor is required");
|
|
3718
|
+
await axios.post(
|
|
3719
|
+
`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`,
|
|
3720
|
+
{ token },
|
|
3721
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3722
|
+
);
|
|
3723
|
+
return { ok: true };
|
|
4303
3724
|
}
|
|
4304
|
-
|
|
4305
|
-
|
|
3725
|
+
async getVendorToken(vendor) {
|
|
3726
|
+
const v = String(vendor || "").trim().toLowerCase();
|
|
3727
|
+
if (!v) throw new Error("Vendor is required");
|
|
3728
|
+
const response = await axios.get(`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`, {
|
|
3729
|
+
headers: machineAuthHeaders(this.auth.machineToken),
|
|
3730
|
+
timeout: 6e4,
|
|
3731
|
+
validateStatus: (s) => s >= 200 && s < 300 || s === 404
|
|
3732
|
+
});
|
|
3733
|
+
if (response.status === 404) return null;
|
|
3734
|
+
return response.data?.token ?? null;
|
|
4306
3735
|
}
|
|
4307
3736
|
push() {
|
|
4308
|
-
return
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
* Register a vendor API token with the server
|
|
4312
|
-
* The token is sent as a JSON string - server handles encryption
|
|
4313
|
-
*/
|
|
4314
|
-
async registerVendorToken(vendor, apiKey) {
|
|
4315
|
-
try {
|
|
4316
|
-
const response = await axios.post(
|
|
4317
|
-
`${configuration.serverUrl}/v1/connect/${vendor}/register`,
|
|
4318
|
-
{
|
|
4319
|
-
token: JSON.stringify(apiKey)
|
|
4320
|
-
},
|
|
4321
|
-
{
|
|
4322
|
-
headers: {
|
|
4323
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4324
|
-
"Content-Type": "application/json"
|
|
4325
|
-
},
|
|
4326
|
-
timeout: 5e3
|
|
4327
|
-
}
|
|
4328
|
-
);
|
|
4329
|
-
if (response.status !== 200 && response.status !== 201) {
|
|
4330
|
-
throw new Error(`Server returned status ${response.status}`);
|
|
3737
|
+
return {
|
|
3738
|
+
sendToAllDevices: async () => {
|
|
3739
|
+
return;
|
|
4331
3740
|
}
|
|
4332
|
-
|
|
4333
|
-
} catch (error) {
|
|
4334
|
-
logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
|
|
4335
|
-
throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4336
|
-
}
|
|
3741
|
+
};
|
|
4337
3742
|
}
|
|
4338
|
-
|
|
4339
|
-
|
|
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
|
-
}
|
|
3743
|
+
sessionSyncClient(session) {
|
|
3744
|
+
return new ApiSessionClient(this.auth.machineToken, session);
|
|
3745
|
+
}
|
|
3746
|
+
machineSyncClient(machine) {
|
|
3747
|
+
return new ApiMachineClient(this.auth.machineToken, machine);
|
|
4421
3748
|
}
|
|
4422
3749
|
}
|
|
4423
3750
|
|
|
@@ -4469,8 +3796,8 @@ const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
|
|
|
4469
3796
|
]);
|
|
4470
3797
|
|
|
4471
3798
|
exports.ApiClient = ApiClient;
|
|
3799
|
+
exports.ApiMachineClient = ApiMachineClient;
|
|
4472
3800
|
exports.ApiSessionClient = ApiSessionClient;
|
|
4473
|
-
exports.AsyncLock = AsyncLock;
|
|
4474
3801
|
exports.RawJSONLinesSchema = RawJSONLinesSchema;
|
|
4475
3802
|
exports.acquireDaemonLock = acquireDaemonLock;
|
|
4476
3803
|
exports.backoff = backoff;
|
|
@@ -4478,10 +3805,7 @@ exports.clearCredentials = clearCredentials;
|
|
|
4478
3805
|
exports.clearDaemonState = clearDaemonState;
|
|
4479
3806
|
exports.clearMachineId = clearMachineId;
|
|
4480
3807
|
exports.configuration = configuration;
|
|
4481
|
-
exports.decodeBase64 = decodeBase64;
|
|
4482
3808
|
exports.delay = delay;
|
|
4483
|
-
exports.encodeBase64 = encodeBase64;
|
|
4484
|
-
exports.encodeBase64Url = encodeBase64Url;
|
|
4485
3809
|
exports.getLatestDaemonLog = getLatestDaemonLog;
|
|
4486
3810
|
exports.installUnrealMcpPluginToEngine = installUnrealMcpPluginToEngine;
|
|
4487
3811
|
exports.logger = logger;
|
|
@@ -4494,5 +3818,5 @@ exports.releaseDaemonLock = releaseDaemonLock;
|
|
|
4494
3818
|
exports.sendUnrealMcpTcpCommand = sendUnrealMcpTcpCommand;
|
|
4495
3819
|
exports.unrealMcpPythonDir = unrealMcpPythonDir;
|
|
4496
3820
|
exports.updateSettings = updateSettings;
|
|
4497
|
-
exports.
|
|
3821
|
+
exports.writeCredentials = writeCredentials;
|
|
4498
3822
|
exports.writeDaemonState = writeDaemonState;
|