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,17 +1,15 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { appendFileSync } from 'fs';
|
|
4
2
|
import fs__default, { existsSync, readFileSync, mkdirSync, constants, unlinkSync, writeFileSync, readdirSync, statSync, createWriteStream } from 'node:fs';
|
|
5
3
|
import os, { homedir } from 'node:os';
|
|
6
4
|
import path, { join, basename } from 'node:path';
|
|
7
|
-
import { readFile, open, stat, unlink, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
8
|
-
import * as z from 'zod';
|
|
9
|
-
import { z as z$1 } from 'zod';
|
|
10
|
-
import { randomBytes, createCipheriv, createDecipheriv, randomUUID as randomUUID$1 } from 'node:crypto';
|
|
11
|
-
import tweetnacl from 'tweetnacl';
|
|
12
5
|
import { EventEmitter } from 'node:events';
|
|
6
|
+
import { randomUUID as randomUUID$1 } from 'node:crypto';
|
|
13
7
|
import { io } from 'socket.io-client';
|
|
14
|
-
import
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { appendFileSync } from 'fs';
|
|
10
|
+
import { readFile, unlink, open, stat, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
11
|
+
import * as z from 'zod';
|
|
12
|
+
import { z as z$1 } from 'zod';
|
|
15
13
|
import { spawn } from 'child_process';
|
|
16
14
|
import { realpath, stat as stat$1, readdir, rm, mkdir as mkdir$1, readFile as readFile$1, open as open$1, writeFile as writeFile$1, chmod } from 'fs/promises';
|
|
17
15
|
import { randomUUID, createHash } from 'crypto';
|
|
@@ -20,10 +18,10 @@ import { fileURLToPath } from 'url';
|
|
|
20
18
|
import process$1 from 'node:process';
|
|
21
19
|
import { platform } from 'os';
|
|
22
20
|
import net from 'node:net';
|
|
23
|
-
import {
|
|
21
|
+
import { spawn as spawn$1 } from 'node:child_process';
|
|
24
22
|
|
|
25
23
|
var name = "flockbay";
|
|
26
|
-
var version = "0.10.
|
|
24
|
+
var version = "0.10.17";
|
|
27
25
|
var description = "Flockbay CLI (local agent + daemon)";
|
|
28
26
|
var author = "Eduardo Orellana";
|
|
29
27
|
var license = "UNLICENSED";
|
|
@@ -174,6 +172,30 @@ var packageJson = {
|
|
|
174
172
|
packageManager: packageManager
|
|
175
173
|
};
|
|
176
174
|
|
|
175
|
+
function parseProfileFromProcessArgs() {
|
|
176
|
+
const args = process.argv.slice(2);
|
|
177
|
+
for (let i = 0; i < args.length; i++) {
|
|
178
|
+
const a = String(args[i] || "");
|
|
179
|
+
if (a === "--profile" || a === "-profile" || a === "-p") {
|
|
180
|
+
const v = String(args[i + 1] || "").trim();
|
|
181
|
+
if (v && !v.startsWith("-")) return v;
|
|
182
|
+
}
|
|
183
|
+
if (a.startsWith("--profile=")) {
|
|
184
|
+
const v = a.slice("--profile=".length).trim();
|
|
185
|
+
if (v) return v;
|
|
186
|
+
}
|
|
187
|
+
if (a.startsWith("-profile=")) {
|
|
188
|
+
const v = a.slice("-profile=".length).trim();
|
|
189
|
+
if (v) return v;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return "";
|
|
193
|
+
}
|
|
194
|
+
function sanitizeProfileName(input) {
|
|
195
|
+
const raw = String(input).trim();
|
|
196
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
197
|
+
return cleaned || "default";
|
|
198
|
+
}
|
|
177
199
|
function normalizeServerUrlForNode(url) {
|
|
178
200
|
try {
|
|
179
201
|
const u = new URL(url);
|
|
@@ -187,6 +209,7 @@ class Configuration {
|
|
|
187
209
|
serverUrl;
|
|
188
210
|
webappUrl;
|
|
189
211
|
isDaemonProcess;
|
|
212
|
+
profile;
|
|
190
213
|
// Directories and paths (from persistence)
|
|
191
214
|
flockbayHomeDir;
|
|
192
215
|
logsDir;
|
|
@@ -200,12 +223,15 @@ class Configuration {
|
|
|
200
223
|
constructor() {
|
|
201
224
|
const args = process.argv.slice(2);
|
|
202
225
|
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
226
|
+
const profileFromArgs = parseProfileFromProcessArgs();
|
|
227
|
+
const profileFromEnv = process.env.FLOCKBAY_PROFILE;
|
|
228
|
+
this.profile = sanitizeProfileName(profileFromEnv || profileFromArgs || "default");
|
|
203
229
|
const homeOverride = process.env.FLOCKBAY_HOME_DIR;
|
|
204
230
|
if (homeOverride) {
|
|
205
231
|
const expandedPath = homeOverride.replace(/^~/, homedir());
|
|
206
|
-
this.flockbayHomeDir = expandedPath;
|
|
232
|
+
this.flockbayHomeDir = join(expandedPath, "profiles", this.profile);
|
|
207
233
|
} else {
|
|
208
|
-
this.flockbayHomeDir = join(homedir(), ".flockbay");
|
|
234
|
+
this.flockbayHomeDir = join(homedir(), ".flockbay", "profiles", this.profile);
|
|
209
235
|
}
|
|
210
236
|
this.logsDir = join(this.flockbayHomeDir, "logs");
|
|
211
237
|
this.settingsFile = join(this.flockbayHomeDir, "settings.json");
|
|
@@ -239,83 +265,6 @@ class Configuration {
|
|
|
239
265
|
}
|
|
240
266
|
const configuration = new Configuration();
|
|
241
267
|
|
|
242
|
-
function encodeBase64(buffer, variant = "base64") {
|
|
243
|
-
if (variant === "base64url") {
|
|
244
|
-
return encodeBase64Url(buffer);
|
|
245
|
-
}
|
|
246
|
-
return Buffer.from(buffer).toString("base64");
|
|
247
|
-
}
|
|
248
|
-
function encodeBase64Url(buffer) {
|
|
249
|
-
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
250
|
-
}
|
|
251
|
-
function decodeBase64(base64, variant = "base64") {
|
|
252
|
-
if (variant === "base64url") {
|
|
253
|
-
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
254
|
-
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
255
|
-
}
|
|
256
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
257
|
-
}
|
|
258
|
-
function getRandomBytes(size) {
|
|
259
|
-
return new Uint8Array(randomBytes(size));
|
|
260
|
-
}
|
|
261
|
-
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
262
|
-
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
263
|
-
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
264
|
-
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
265
|
-
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
|
|
266
|
-
result.set(ephemeralKeyPair.publicKey, 0);
|
|
267
|
-
result.set(nonce, ephemeralKeyPair.publicKey.length);
|
|
268
|
-
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
function encryptWithDataKey(data, dataKey) {
|
|
272
|
-
const nonce = getRandomBytes(12);
|
|
273
|
-
const cipher = createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
274
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
275
|
-
const encrypted = Buffer.concat([
|
|
276
|
-
cipher.update(plaintext),
|
|
277
|
-
cipher.final()
|
|
278
|
-
]);
|
|
279
|
-
const authTag = cipher.getAuthTag();
|
|
280
|
-
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
281
|
-
bundle.set([0], 0);
|
|
282
|
-
bundle.set(nonce, 1);
|
|
283
|
-
bundle.set(new Uint8Array(encrypted), 13);
|
|
284
|
-
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
285
|
-
return bundle;
|
|
286
|
-
}
|
|
287
|
-
function decryptWithDataKey(bundle, dataKey) {
|
|
288
|
-
if (bundle.length < 1) {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
if (bundle[0] !== 0) {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
if (bundle.length < 12 + 16 + 1) {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
const nonce = bundle.slice(1, 13);
|
|
298
|
-
const authTag = bundle.slice(bundle.length - 16);
|
|
299
|
-
const ciphertext = bundle.slice(13, bundle.length - 16);
|
|
300
|
-
try {
|
|
301
|
-
const decipher = createDecipheriv("aes-256-gcm", dataKey, nonce);
|
|
302
|
-
decipher.setAuthTag(authTag);
|
|
303
|
-
const decrypted = Buffer.concat([
|
|
304
|
-
decipher.update(ciphertext),
|
|
305
|
-
decipher.final()
|
|
306
|
-
]);
|
|
307
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
308
|
-
} catch (error) {
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function encrypt(key, data) {
|
|
313
|
-
return encryptWithDataKey(data, key);
|
|
314
|
-
}
|
|
315
|
-
function decrypt(key, data) {
|
|
316
|
-
return decryptWithDataKey(data, key);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
268
|
const defaultSettings = {
|
|
320
269
|
onboardingCompleted: false
|
|
321
270
|
};
|
|
@@ -381,40 +330,30 @@ async function updateSettings(updater) {
|
|
|
381
330
|
});
|
|
382
331
|
}
|
|
383
332
|
}
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
machineKey: z.string().base64()
|
|
389
|
-
})
|
|
333
|
+
const workspaceAuthSchema = z.object({
|
|
334
|
+
machineToken: z.string().min(1),
|
|
335
|
+
orgId: z.string().min(1),
|
|
336
|
+
createdAtMs: z.number().optional()
|
|
390
337
|
});
|
|
391
338
|
async function readCredentials() {
|
|
392
|
-
if (!existsSync(configuration.privateKeyFile))
|
|
393
|
-
return null;
|
|
394
|
-
}
|
|
339
|
+
if (!existsSync(configuration.privateKeyFile)) return null;
|
|
395
340
|
try {
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
return {
|
|
399
|
-
token: credentials.token,
|
|
400
|
-
encryption: {
|
|
401
|
-
type: "dataKey",
|
|
402
|
-
publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
|
|
403
|
-
machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
|
|
404
|
-
}
|
|
405
|
-
};
|
|
341
|
+
const raw = await readFile(configuration.privateKeyFile, "utf8");
|
|
342
|
+
const parsed = workspaceAuthSchema.parse(JSON.parse(raw));
|
|
343
|
+
return { machineToken: parsed.machineToken, orgId: parsed.orgId, createdAtMs: parsed.createdAtMs };
|
|
406
344
|
} catch {
|
|
407
345
|
return null;
|
|
408
346
|
}
|
|
409
347
|
}
|
|
410
|
-
async function
|
|
348
|
+
async function writeCredentials(auth) {
|
|
411
349
|
if (!existsSync(configuration.flockbayHomeDir)) {
|
|
412
350
|
await mkdir(configuration.flockbayHomeDir, { recursive: true });
|
|
413
351
|
}
|
|
414
|
-
await writeFile(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
352
|
+
await writeFile(
|
|
353
|
+
configuration.privateKeyFile,
|
|
354
|
+
JSON.stringify({ machineToken: auth.machineToken, orgId: auth.orgId, createdAtMs: auth.createdAtMs ?? Date.now() }, null, 2),
|
|
355
|
+
"utf8"
|
|
356
|
+
);
|
|
418
357
|
}
|
|
419
358
|
async function clearCredentials() {
|
|
420
359
|
if (existsSync(configuration.privateKeyFile)) {
|
|
@@ -720,222 +659,13 @@ async function getLatestDaemonLog() {
|
|
|
720
659
|
return latest || null;
|
|
721
660
|
}
|
|
722
661
|
|
|
723
|
-
const SessionMessageContentSchema = z$1.object({
|
|
724
|
-
c: z$1.string(),
|
|
725
|
-
// Base64 encoded encrypted content
|
|
726
|
-
t: z$1.literal("encrypted")
|
|
727
|
-
});
|
|
728
|
-
const UpdateBodySchema = z$1.object({
|
|
729
|
-
message: z$1.object({
|
|
730
|
-
id: z$1.string(),
|
|
731
|
-
seq: z$1.number(),
|
|
732
|
-
content: SessionMessageContentSchema
|
|
733
|
-
}),
|
|
734
|
-
sid: z$1.string(),
|
|
735
|
-
// Session ID
|
|
736
|
-
t: z$1.literal("new-message")
|
|
737
|
-
});
|
|
738
|
-
const UpdateSessionBodySchema = z$1.object({
|
|
739
|
-
t: z$1.literal("update-session"),
|
|
740
|
-
sid: z$1.string(),
|
|
741
|
-
metadata: z$1.object({
|
|
742
|
-
version: z$1.number(),
|
|
743
|
-
value: z$1.string()
|
|
744
|
-
}).nullish(),
|
|
745
|
-
agentState: z$1.object({
|
|
746
|
-
version: z$1.number(),
|
|
747
|
-
value: z$1.string()
|
|
748
|
-
}).nullish()
|
|
749
|
-
});
|
|
750
|
-
const UpdateMachineBodySchema = z$1.object({
|
|
751
|
-
t: z$1.literal("update-machine"),
|
|
752
|
-
machineId: z$1.string(),
|
|
753
|
-
metadata: z$1.object({
|
|
754
|
-
version: z$1.number(),
|
|
755
|
-
value: z$1.string()
|
|
756
|
-
}).nullish(),
|
|
757
|
-
daemonState: z$1.object({
|
|
758
|
-
version: z$1.number(),
|
|
759
|
-
value: z$1.string()
|
|
760
|
-
}).nullish()
|
|
761
|
-
});
|
|
762
|
-
z$1.object({
|
|
763
|
-
id: z$1.string(),
|
|
764
|
-
seq: z$1.number(),
|
|
765
|
-
body: z$1.union([
|
|
766
|
-
UpdateBodySchema,
|
|
767
|
-
UpdateSessionBodySchema,
|
|
768
|
-
UpdateMachineBodySchema
|
|
769
|
-
]),
|
|
770
|
-
createdAt: z$1.number()
|
|
771
|
-
});
|
|
772
|
-
z$1.object({
|
|
773
|
-
host: z$1.string(),
|
|
774
|
-
platform: z$1.string(),
|
|
775
|
-
flockbayCliVersion: z$1.string(),
|
|
776
|
-
homeDir: z$1.string(),
|
|
777
|
-
flockbayHomeDir: z$1.string(),
|
|
778
|
-
flockbayLibDir: z$1.string(),
|
|
779
|
-
// Dev-only: bypass Unreal project + SDK gating (so sessions can run in arbitrary folders).
|
|
780
|
-
flockbayDevBypassUeGates: z$1.boolean().optional()
|
|
781
|
-
});
|
|
782
|
-
z$1.object({
|
|
783
|
-
status: z$1.union([
|
|
784
|
-
z$1.enum(["running", "shutting-down"]),
|
|
785
|
-
z$1.string()
|
|
786
|
-
// Forward compatibility
|
|
787
|
-
]),
|
|
788
|
-
pid: z$1.number().optional(),
|
|
789
|
-
httpPort: z$1.number().optional(),
|
|
790
|
-
startedAt: z$1.number().optional(),
|
|
791
|
-
shutdownRequestedAt: z$1.number().optional(),
|
|
792
|
-
shutdownSource: z$1.union([
|
|
793
|
-
z$1.enum(["mobile-app", "cli", "os-signal", "unknown"]),
|
|
794
|
-
z$1.string()
|
|
795
|
-
// Forward compatibility
|
|
796
|
-
]).optional()
|
|
797
|
-
});
|
|
798
|
-
z$1.object({
|
|
799
|
-
content: SessionMessageContentSchema,
|
|
800
|
-
createdAt: z$1.number(),
|
|
801
|
-
id: z$1.string(),
|
|
802
|
-
seq: z$1.number(),
|
|
803
|
-
updatedAt: z$1.number()
|
|
804
|
-
});
|
|
805
|
-
const MessageMetaSchema = z$1.object({
|
|
806
|
-
sentFrom: z$1.string().optional(),
|
|
807
|
-
// Source identifier
|
|
808
|
-
permissionMode: z$1.string().optional(),
|
|
809
|
-
// Permission mode for this message
|
|
810
|
-
model: z$1.string().nullable().optional(),
|
|
811
|
-
// Model name for this message (null = reset)
|
|
812
|
-
customSystemPrompt: z$1.string().nullable().optional(),
|
|
813
|
-
// Custom system prompt for this message (null = reset)
|
|
814
|
-
appendSystemPrompt: z$1.string().nullable().optional(),
|
|
815
|
-
// Append to system prompt for this message (null = reset)
|
|
816
|
-
allowedTools: z$1.array(z$1.string()).nullable().optional(),
|
|
817
|
-
// Allowed tools for this message (null = reset)
|
|
818
|
-
disallowedTools: z$1.array(z$1.string()).nullable().optional(),
|
|
819
|
-
// Disallowed tools for this message (null = reset)
|
|
820
|
-
// Optional text to show in UI instead of the raw prompt (mobile/web feature).
|
|
821
|
-
displayText: z$1.string().optional(),
|
|
822
|
-
// Optional multimodal attachments (base64-embedded; used for vision-capable agents).
|
|
823
|
-
attachments: z$1.object({
|
|
824
|
-
images: z$1.array(
|
|
825
|
-
z$1.object({
|
|
826
|
-
mimeType: z$1.string(),
|
|
827
|
-
base64: z$1.string(),
|
|
828
|
-
name: z$1.string().optional(),
|
|
829
|
-
width: z$1.number().optional(),
|
|
830
|
-
height: z$1.number().optional()
|
|
831
|
-
})
|
|
832
|
-
).optional()
|
|
833
|
-
}).optional()
|
|
834
|
-
});
|
|
835
|
-
z$1.object({
|
|
836
|
-
session: z$1.object({
|
|
837
|
-
id: z$1.string(),
|
|
838
|
-
tag: z$1.string(),
|
|
839
|
-
seq: z$1.number(),
|
|
840
|
-
createdAt: z$1.number(),
|
|
841
|
-
updatedAt: z$1.number(),
|
|
842
|
-
metadata: z$1.string(),
|
|
843
|
-
metadataVersion: z$1.number(),
|
|
844
|
-
agentState: z$1.string().nullable(),
|
|
845
|
-
agentStateVersion: z$1.number()
|
|
846
|
-
})
|
|
847
|
-
});
|
|
848
|
-
const UserMessageSchema = z$1.object({
|
|
849
|
-
role: z$1.literal("user"),
|
|
850
|
-
content: z$1.object({
|
|
851
|
-
type: z$1.literal("text"),
|
|
852
|
-
text: z$1.string()
|
|
853
|
-
}),
|
|
854
|
-
localKey: z$1.string().optional(),
|
|
855
|
-
// Mobile messages include this
|
|
856
|
-
meta: MessageMetaSchema.optional()
|
|
857
|
-
});
|
|
858
|
-
const AgentMessageSchema = z$1.object({
|
|
859
|
-
role: z$1.literal("agent"),
|
|
860
|
-
content: z$1.object({
|
|
861
|
-
type: z$1.literal("output"),
|
|
862
|
-
data: z$1.any()
|
|
863
|
-
}),
|
|
864
|
-
meta: MessageMetaSchema.optional()
|
|
865
|
-
});
|
|
866
|
-
z$1.union([UserMessageSchema, AgentMessageSchema]);
|
|
867
|
-
|
|
868
|
-
async function delay(ms) {
|
|
869
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
870
|
-
}
|
|
871
|
-
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
872
|
-
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
|
|
873
|
-
return Math.round(Math.random() * maxDelayRet);
|
|
874
|
-
}
|
|
875
|
-
function createBackoff(opts) {
|
|
876
|
-
return async (callback) => {
|
|
877
|
-
let currentFailureCount = 0;
|
|
878
|
-
const minDelay = 250;
|
|
879
|
-
const maxDelay = 1e3;
|
|
880
|
-
const maxFailureCount = 50;
|
|
881
|
-
while (true) {
|
|
882
|
-
try {
|
|
883
|
-
return await callback();
|
|
884
|
-
} catch (e) {
|
|
885
|
-
if (currentFailureCount < maxFailureCount) {
|
|
886
|
-
currentFailureCount++;
|
|
887
|
-
}
|
|
888
|
-
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
889
|
-
await delay(waitForRequest);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
let backoff = createBackoff();
|
|
895
|
-
|
|
896
|
-
class AsyncLock {
|
|
897
|
-
permits = 1;
|
|
898
|
-
promiseResolverQueue = [];
|
|
899
|
-
async inLock(func) {
|
|
900
|
-
try {
|
|
901
|
-
await this.lock();
|
|
902
|
-
return await func();
|
|
903
|
-
} finally {
|
|
904
|
-
this.unlock();
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
async lock() {
|
|
908
|
-
if (this.permits > 0) {
|
|
909
|
-
this.permits = this.permits - 1;
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
|
|
913
|
-
}
|
|
914
|
-
unlock() {
|
|
915
|
-
this.permits += 1;
|
|
916
|
-
if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
|
|
917
|
-
throw new Error("this.permits should never be > 0 when there is someone waiting.");
|
|
918
|
-
} else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
|
|
919
|
-
this.permits -= 1;
|
|
920
|
-
const nextResolver = this.promiseResolverQueue.shift();
|
|
921
|
-
if (nextResolver) {
|
|
922
|
-
setTimeout(() => {
|
|
923
|
-
nextResolver(true);
|
|
924
|
-
}, 0);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
662
|
class RpcHandlerManager {
|
|
931
663
|
handlers = /* @__PURE__ */ new Map();
|
|
932
664
|
scopePrefix;
|
|
933
|
-
encryptionKey;
|
|
934
665
|
logger;
|
|
935
666
|
socket = null;
|
|
936
667
|
constructor(config) {
|
|
937
668
|
this.scopePrefix = config.scopePrefix;
|
|
938
|
-
this.encryptionKey = config.encryptionKey;
|
|
939
669
|
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
|
|
940
670
|
}
|
|
941
671
|
/**
|
|
@@ -960,20 +690,24 @@ class RpcHandlerManager {
|
|
|
960
690
|
const handler = this.handlers.get(request.method);
|
|
961
691
|
if (!handler) {
|
|
962
692
|
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
693
|
+
return { error: "Method not found" };
|
|
694
|
+
}
|
|
695
|
+
const rawParams = request.params;
|
|
696
|
+
const parsedParams = (() => {
|
|
697
|
+
if (typeof rawParams !== "string") return rawParams;
|
|
698
|
+
const s = rawParams.trim();
|
|
699
|
+
if (!s) return rawParams;
|
|
700
|
+
if (!(s.startsWith("{") || s.startsWith("["))) return rawParams;
|
|
701
|
+
try {
|
|
702
|
+
return JSON.parse(s);
|
|
703
|
+
} catch {
|
|
704
|
+
return rawParams;
|
|
705
|
+
}
|
|
706
|
+
})();
|
|
707
|
+
return await handler(parsedParams);
|
|
971
708
|
} catch (error) {
|
|
972
709
|
this.logger("[RPC] [ERROR] Error handling request", { error });
|
|
973
|
-
|
|
974
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
975
|
-
};
|
|
976
|
-
return encodeBase64(encrypt(this.encryptionKey, errorResponse));
|
|
710
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
977
711
|
}
|
|
978
712
|
}
|
|
979
713
|
onSocketConnect(socket) {
|
|
@@ -1179,6 +913,19 @@ async function sendUnrealMcpTcpCommand(options) {
|
|
|
1179
913
|
const paramName = missingParamMatch[1];
|
|
1180
914
|
hints.push(`Tip: include the required parameter \`${paramName}\` in the request params (UnrealMCP does not infer defaults).`);
|
|
1181
915
|
}
|
|
916
|
+
const details = response?.details;
|
|
917
|
+
if (details && typeof details === "object") {
|
|
918
|
+
const kind = typeof details.kind === "string" ? details.kind : null;
|
|
919
|
+
const missing = Array.isArray(details.missing) ? details.missing.filter((x) => typeof x === "string") : [];
|
|
920
|
+
if (kind === "missing_params" && missing.length > 0) {
|
|
921
|
+
hints.push(`Missing params: ${missing.map((m) => `\`${m}\``).join(", ")}`);
|
|
922
|
+
}
|
|
923
|
+
const exampleCall = details.exampleCall;
|
|
924
|
+
if (exampleCall && typeof exampleCall === "object") {
|
|
925
|
+
hints.push(`Example call:
|
|
926
|
+
${JSON.stringify(exampleCall, null, 2)}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
1182
929
|
const hint = hints.length > 0 ? `
|
|
1183
930
|
|
|
1184
931
|
${hints.join("\n\n")}` : "";
|
|
@@ -1457,7 +1204,7 @@ async function apiJsonGet(params) {
|
|
|
1457
1204
|
const res = await fetch(url, {
|
|
1458
1205
|
method: "GET",
|
|
1459
1206
|
headers: {
|
|
1460
|
-
Authorization: `
|
|
1207
|
+
Authorization: `Machine ${params.token}`,
|
|
1461
1208
|
Accept: "application/json"
|
|
1462
1209
|
}
|
|
1463
1210
|
});
|
|
@@ -1473,7 +1220,7 @@ async function apiJsonPost(params) {
|
|
|
1473
1220
|
const res = await fetch(url, {
|
|
1474
1221
|
method: "POST",
|
|
1475
1222
|
headers: {
|
|
1476
|
-
Authorization: `
|
|
1223
|
+
Authorization: `Machine ${params.token}`,
|
|
1477
1224
|
"Content-Type": "application/json",
|
|
1478
1225
|
Accept: "application/json"
|
|
1479
1226
|
},
|
|
@@ -1838,8 +1585,20 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory, coordinatio
|
|
|
1838
1585
|
"play_in_editor_windowed",
|
|
1839
1586
|
"stop_play_in_editor",
|
|
1840
1587
|
"take_screenshot",
|
|
1588
|
+
"map_check",
|
|
1589
|
+
"compile_blueprints_all",
|
|
1590
|
+
"get_editor_context",
|
|
1591
|
+
"get_player_context",
|
|
1592
|
+
"raycast_from_camera",
|
|
1593
|
+
"raycast_down",
|
|
1594
|
+
"get_actor_transform",
|
|
1595
|
+
"get_actor_bounds",
|
|
1841
1596
|
"spawn_actor",
|
|
1842
|
-
"set_actor_transform"
|
|
1597
|
+
"set_actor_transform",
|
|
1598
|
+
"search_assets",
|
|
1599
|
+
"get_asset_info",
|
|
1600
|
+
"list_asset_packs",
|
|
1601
|
+
"place_asset"
|
|
1843
1602
|
];
|
|
1844
1603
|
const expected = await readExpectedUnrealMcpUplugin().catch((err) => {
|
|
1845
1604
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3101,48 +2860,38 @@ class ApiSessionClient extends EventEmitter {
|
|
|
3101
2860
|
agentState;
|
|
3102
2861
|
agentStateVersion;
|
|
3103
2862
|
socket;
|
|
3104
|
-
pendingMessages = [];
|
|
3105
|
-
pendingMessageCallback = null;
|
|
3106
|
-
pendingOutboundMessages = [];
|
|
3107
2863
|
rpcHandlerManager;
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
coordinationLeaseGuard;
|
|
3112
|
-
coordinationLedgerReadAt = 0;
|
|
3113
|
-
docsIndexReadAt = 0;
|
|
2864
|
+
coordinationLeaseGuard = new CoordinationLeaseGuard();
|
|
2865
|
+
coordinationLedgerLastReadAtMs = null;
|
|
2866
|
+
outboundQueue = [];
|
|
3114
2867
|
constructor(token, session) {
|
|
3115
2868
|
super();
|
|
3116
2869
|
this.token = token;
|
|
3117
2870
|
this.sessionId = session.id;
|
|
3118
|
-
this.metadata = session.metadata;
|
|
3119
|
-
this.metadataVersion = session.metadataVersion;
|
|
3120
|
-
this.agentState = session.agentState;
|
|
3121
|
-
this.agentStateVersion = session.agentStateVersion;
|
|
3122
|
-
this.encryptionKey = session.encryptionKey;
|
|
3123
|
-
this.coordinationLeaseGuard = new CoordinationLeaseGuard();
|
|
2871
|
+
this.metadata = session.metadata ?? null;
|
|
2872
|
+
this.metadataVersion = Number(session.metadataVersion || 0);
|
|
2873
|
+
this.agentState = session.agentState ?? null;
|
|
2874
|
+
this.agentStateVersion = Number(session.agentStateVersion || 0);
|
|
3124
2875
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
3125
2876
|
scopePrefix: this.sessionId,
|
|
3126
|
-
encryptionKey: this.encryptionKey,
|
|
3127
2877
|
logger: (msg, data) => logger.debug(msg, data)
|
|
3128
2878
|
});
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
workspaceProjectId: this.metadata?.workspaceProjectId ? String(this.metadata.workspaceProjectId) : null,
|
|
3137
|
-
leaseGuard: this.coordinationLeaseGuard
|
|
3138
|
-
}, { bashEnabled: false });
|
|
3139
|
-
this.startCoordinationAutopilot();
|
|
3140
|
-
this.socket = io(configuration.serverUrl, {
|
|
3141
|
-
auth: {
|
|
2879
|
+
const cwd = String(this.metadata?.path || process.cwd()).trim() || process.cwd();
|
|
2880
|
+
const machineId = String(this.metadata?.machineId || "").trim();
|
|
2881
|
+
registerCommonHandlers(
|
|
2882
|
+
this.rpcHandlerManager,
|
|
2883
|
+
cwd,
|
|
2884
|
+
{
|
|
2885
|
+
serverUrl: configuration.serverUrl,
|
|
3142
2886
|
token: this.token,
|
|
3143
|
-
|
|
3144
|
-
|
|
2887
|
+
sessionId: this.sessionId,
|
|
2888
|
+
machineId,
|
|
2889
|
+
projectRootPath: String(this.metadata?.projectRootPath || cwd)
|
|
3145
2890
|
},
|
|
2891
|
+
{ bashEnabled: false }
|
|
2892
|
+
);
|
|
2893
|
+
this.socket = io(configuration.serverUrl, {
|
|
2894
|
+
auth: { token: this.token, clientType: "session-scoped", sessionId: this.sessionId, machineId },
|
|
3146
2895
|
path: "/v1/updates",
|
|
3147
2896
|
reconnection: true,
|
|
3148
2897
|
reconnectionAttempts: Infinity,
|
|
@@ -3153,386 +2902,196 @@ class ApiSessionClient extends EventEmitter {
|
|
|
3153
2902
|
autoConnect: false
|
|
3154
2903
|
});
|
|
3155
2904
|
this.socket.on("connect", () => {
|
|
3156
|
-
logger.debug("
|
|
2905
|
+
logger.debug("[session] socket connected");
|
|
3157
2906
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
3158
|
-
this.
|
|
2907
|
+
this.flush();
|
|
2908
|
+
this.emit("connect");
|
|
3159
2909
|
});
|
|
3160
|
-
this.socket.on("
|
|
3161
|
-
|
|
2910
|
+
this.socket.on("connect_error", (error) => {
|
|
2911
|
+
const message = error instanceof Error ? error.message : String(error?.message || error || "connect_error");
|
|
2912
|
+
logger.debug("[session] socket connect_error", { message });
|
|
3162
2913
|
});
|
|
3163
2914
|
this.socket.on("disconnect", (reason) => {
|
|
3164
|
-
logger.debug("[
|
|
2915
|
+
logger.debug("[session] socket disconnected", reason);
|
|
3165
2916
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
2917
|
+
this.emit("disconnect", reason);
|
|
3166
2918
|
});
|
|
3167
|
-
this.socket.on("
|
|
3168
|
-
|
|
3169
|
-
this.rpcHandlerManager.onSocketDisconnect();
|
|
2919
|
+
this.socket.on("rpc-request", async (data, callback) => {
|
|
2920
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
3170
2921
|
});
|
|
3171
2922
|
this.socket.on("update", (data) => {
|
|
3172
|
-
|
|
3173
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
|
|
3174
|
-
if (!data.body) {
|
|
3175
|
-
logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
|
|
3176
|
-
return;
|
|
3177
|
-
}
|
|
3178
|
-
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
3179
|
-
const body = decrypt(this.encryptionKey, decodeBase64(data.body.message.content.c));
|
|
3180
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
3181
|
-
const userResult = UserMessageSchema.safeParse(body);
|
|
3182
|
-
if (userResult.success) {
|
|
3183
|
-
if (this.pendingMessageCallback) {
|
|
3184
|
-
this.pendingMessageCallback(userResult.data);
|
|
3185
|
-
} else {
|
|
3186
|
-
this.pendingMessages.push(userResult.data);
|
|
3187
|
-
}
|
|
3188
|
-
} else {
|
|
3189
|
-
this.emit("message", body);
|
|
3190
|
-
}
|
|
3191
|
-
} else if (data.body.t === "update-session") {
|
|
3192
|
-
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
|
|
3193
|
-
this.metadata = decrypt(this.encryptionKey, decodeBase64(data.body.metadata.value));
|
|
3194
|
-
this.metadataVersion = data.body.metadata.version;
|
|
3195
|
-
}
|
|
3196
|
-
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
|
|
3197
|
-
this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, decodeBase64(data.body.agentState.value)) : null;
|
|
3198
|
-
this.agentStateVersion = data.body.agentState.version;
|
|
3199
|
-
}
|
|
3200
|
-
} else if (data.body.t === "update-machine") {
|
|
3201
|
-
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
3202
|
-
} else {
|
|
3203
|
-
this.emit("message", data.body);
|
|
3204
|
-
}
|
|
3205
|
-
} catch (error) {
|
|
3206
|
-
logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
|
|
3207
|
-
}
|
|
2923
|
+
this.emit("update", data);
|
|
3208
2924
|
});
|
|
3209
|
-
this.socket.on("error", (error) => {
|
|
3210
|
-
logger.debug("[API] Socket error:", error);
|
|
3211
|
-
});
|
|
3212
|
-
this.socket.connect();
|
|
3213
|
-
}
|
|
3214
|
-
/**
|
|
3215
|
-
* Returns the session client's bearer token for calling authenticated HTTP endpoints.
|
|
3216
|
-
* Intended for internal integrations (e.g. uploading artifacts for this session).
|
|
3217
|
-
*/
|
|
3218
|
-
getAuthToken() {
|
|
3219
|
-
return this.token;
|
|
3220
2925
|
}
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
* (i.e. ledger tools are available and claim enforcement makes sense).
|
|
3224
|
-
*/
|
|
3225
|
-
hasCoordinationContext() {
|
|
3226
|
-
const meta = this.metadata;
|
|
3227
|
-
if (!meta) return false;
|
|
3228
|
-
const projectId = String(meta.workspaceProjectId || "").trim();
|
|
3229
|
-
const workItemId = String(meta.workItemId || "").trim();
|
|
3230
|
-
return Boolean(projectId && workItemId);
|
|
2926
|
+
connect() {
|
|
2927
|
+
this.socket.connect();
|
|
3231
2928
|
}
|
|
3232
|
-
|
|
3233
|
-
this.
|
|
2929
|
+
async connectAndWait(timeoutMs = 15e3) {
|
|
2930
|
+
if (this.socket.connected) return;
|
|
2931
|
+
await new Promise((resolve, reject) => {
|
|
2932
|
+
let timeoutHandle = null;
|
|
2933
|
+
const cleanup = () => {
|
|
2934
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2935
|
+
this.socket.off("connect", onConnect);
|
|
2936
|
+
this.socket.off("connect_error", onError);
|
|
2937
|
+
this.socket.off("disconnect", onDisconnect);
|
|
2938
|
+
};
|
|
2939
|
+
const onConnect = () => {
|
|
2940
|
+
cleanup();
|
|
2941
|
+
resolve();
|
|
2942
|
+
};
|
|
2943
|
+
const onError = (err) => {
|
|
2944
|
+
cleanup();
|
|
2945
|
+
const message = err instanceof Error ? err.message : String(err?.message || err || "connect_error");
|
|
2946
|
+
reject(new Error(message));
|
|
2947
|
+
};
|
|
2948
|
+
const onDisconnect = (reason) => {
|
|
2949
|
+
cleanup();
|
|
2950
|
+
reject(new Error(`disconnected:${String(reason || "unknown")}`));
|
|
2951
|
+
};
|
|
2952
|
+
timeoutHandle = setTimeout(() => {
|
|
2953
|
+
cleanup();
|
|
2954
|
+
reject(new Error("connect_timeout"));
|
|
2955
|
+
}, timeoutMs);
|
|
2956
|
+
this.socket.once("connect", onConnect);
|
|
2957
|
+
this.socket.once("connect_error", onError);
|
|
2958
|
+
this.socket.once("disconnect", onDisconnect);
|
|
2959
|
+
this.connect();
|
|
2960
|
+
});
|
|
3234
2961
|
}
|
|
3235
|
-
|
|
3236
|
-
|
|
2962
|
+
disconnect() {
|
|
2963
|
+
this.socket.disconnect();
|
|
3237
2964
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
if (!Number.isFinite(last) || last <= 0) return false;
|
|
3241
|
-
return Date.now() - last <= ms;
|
|
2965
|
+
close() {
|
|
2966
|
+
this.disconnect();
|
|
3242
2967
|
}
|
|
3243
|
-
|
|
3244
|
-
this.
|
|
2968
|
+
getAuthToken() {
|
|
2969
|
+
return this.token;
|
|
3245
2970
|
}
|
|
3246
|
-
|
|
3247
|
-
|
|
2971
|
+
async listMessages() {
|
|
2972
|
+
const baseUrl = configuration.serverUrl.replace(/\/+$/, "");
|
|
2973
|
+
const url = `${baseUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/messages`;
|
|
2974
|
+
const res = await axios.get(url, {
|
|
2975
|
+
headers: { Authorization: `Machine ${this.token}`, "Content-Type": "application/json" },
|
|
2976
|
+
timeout: 6e4
|
|
2977
|
+
});
|
|
2978
|
+
return Array.isArray(res.data?.messages) ? res.data.messages : [];
|
|
3248
2979
|
}
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
if (!Number.isFinite(last) || last <= 0) return false;
|
|
3252
|
-
return Date.now() - last <= ms;
|
|
2980
|
+
markCoordinationLedgerRead(timeMs = Date.now()) {
|
|
2981
|
+
this.coordinationLedgerLastReadAtMs = timeMs;
|
|
3253
2982
|
}
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
if (!meta) return;
|
|
3257
|
-
const projectId = String(meta.workspaceProjectId || "").trim();
|
|
3258
|
-
const workItemId = String(meta.workItemId || "").trim();
|
|
3259
|
-
const machineId = String(meta.machineId || "").trim();
|
|
3260
|
-
const projectRootPath = String(meta.projectRootPath || meta.path || "").trim();
|
|
3261
|
-
const cwd = String(meta.path || "").trim();
|
|
3262
|
-
if (!projectId || !workItemId || !machineId || !projectRootPath || !cwd) return;
|
|
3263
|
-
String(process.env.FLOCKBAY_COORDINATION_AUTO_SHIP_ON_COMMIT || "").trim() === "1";
|
|
3264
|
-
const serverUrl = configuration.serverUrl.replace(/\/+$/, "");
|
|
3265
|
-
const token = this.token;
|
|
3266
|
-
const sessionId = this.sessionId;
|
|
3267
|
-
const postJson = async (path, body) => {
|
|
3268
|
-
const res = await fetch(`${serverUrl}${path}`, {
|
|
3269
|
-
method: "POST",
|
|
3270
|
-
headers: {
|
|
3271
|
-
Authorization: `Bearer ${token}`,
|
|
3272
|
-
"Content-Type": "application/json"
|
|
3273
|
-
},
|
|
3274
|
-
body: JSON.stringify(body ?? {})
|
|
3275
|
-
});
|
|
3276
|
-
const data = await res.json().catch(() => null);
|
|
3277
|
-
if (!res.ok) {
|
|
3278
|
-
const msg = typeof data?.error === "string" ? data.error : `Request failed (${res.status})`;
|
|
3279
|
-
throw new Error(msg);
|
|
3280
|
-
}
|
|
3281
|
-
return data;
|
|
3282
|
-
};
|
|
3283
|
-
void postJson(
|
|
3284
|
-
"/v1/coordination/work-items/update",
|
|
3285
|
-
{ projectId, workItemId, sessionId }
|
|
3286
|
-
).catch((err) => logger.debug("[coordination-autopilot] Failed to attach session to work item:", err));
|
|
3287
|
-
logger.debug("[coordination] Enabled", { projectId, workItemId, cwd });
|
|
3288
|
-
return;
|
|
2983
|
+
getCoordinationLedgerLastReadAtMs() {
|
|
2984
|
+
return this.coordinationLedgerLastReadAtMs;
|
|
3289
2985
|
}
|
|
3290
|
-
|
|
2986
|
+
keepAlive(thinking, mode) {
|
|
3291
2987
|
if (!this.socket.connected) return;
|
|
3292
|
-
|
|
3293
|
-
const batch = this.pendingOutboundMessages;
|
|
3294
|
-
this.pendingOutboundMessages = [];
|
|
3295
|
-
logger.debug("[API] Flushing queued outbound messages", { count: batch.length });
|
|
3296
|
-
for (const item of batch) {
|
|
3297
|
-
this.socket.emit("message", { sid: item.sid, message: item.message });
|
|
3298
|
-
}
|
|
3299
|
-
}
|
|
3300
|
-
emitMessageOrQueue(args) {
|
|
3301
|
-
if (this.socket.connected) {
|
|
3302
|
-
this.socket.emit("message", { sid: args.sid, message: args.message });
|
|
3303
|
-
return;
|
|
3304
|
-
}
|
|
3305
|
-
const MAX_QUEUED = 200;
|
|
3306
|
-
if (this.pendingOutboundMessages.length >= MAX_QUEUED) {
|
|
3307
|
-
const head = this.pendingOutboundMessages[0];
|
|
3308
|
-
throw new Error(
|
|
3309
|
-
`Socket not connected; outbound queue full (${MAX_QUEUED}). Oldest=${head?.kind ?? "unknown"} queuedAt=${head?.queuedAt ?? "unknown"}`
|
|
3310
|
-
);
|
|
3311
|
-
}
|
|
3312
|
-
this.pendingOutboundMessages.push({ ...args, queuedAt: Date.now() });
|
|
3313
|
-
logger.debug("[API] Socket not connected; queued outbound message", {
|
|
3314
|
-
kind: args.kind,
|
|
3315
|
-
queued: this.pendingOutboundMessages.length
|
|
3316
|
-
});
|
|
2988
|
+
this.socket.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking, mode });
|
|
3317
2989
|
}
|
|
3318
|
-
|
|
3319
|
-
this.
|
|
3320
|
-
while (this.pendingMessages.length > 0) {
|
|
3321
|
-
callback(this.pendingMessages.shift());
|
|
3322
|
-
}
|
|
2990
|
+
sendSessionDeath() {
|
|
2991
|
+
this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
|
|
3323
2992
|
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
if (body.type === "user" && typeof body.message.content === "string" && body.isSidechain !== true && body.isMeta !== true) {
|
|
3331
|
-
content = {
|
|
3332
|
-
role: "user",
|
|
3333
|
-
content: {
|
|
3334
|
-
type: "text",
|
|
3335
|
-
text: body.message.content
|
|
3336
|
-
},
|
|
3337
|
-
meta: {
|
|
3338
|
-
sentFrom: "cli"
|
|
3339
|
-
}
|
|
3340
|
-
};
|
|
3341
|
-
} else {
|
|
3342
|
-
content = {
|
|
3343
|
-
role: "agent",
|
|
3344
|
-
content: {
|
|
3345
|
-
type: "output",
|
|
3346
|
-
data: body
|
|
3347
|
-
// This wraps the entire Claude message
|
|
3348
|
-
},
|
|
3349
|
-
meta: {
|
|
3350
|
-
sentFrom: "cli"
|
|
3351
|
-
}
|
|
3352
|
-
};
|
|
3353
|
-
}
|
|
3354
|
-
logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
|
|
3355
|
-
const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
|
|
3356
|
-
this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: "claude" });
|
|
3357
|
-
if (body.type === "assistant" && body.message?.usage) {
|
|
3358
|
-
try {
|
|
3359
|
-
this.sendUsageData(body.message.usage);
|
|
3360
|
-
} catch (error) {
|
|
3361
|
-
logger.debug("[SOCKET] Failed to send usage data:", error);
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
|
|
3365
|
-
this.updateMetadata((metadata) => ({
|
|
3366
|
-
...metadata,
|
|
3367
|
-
summary: {
|
|
3368
|
-
text: body.summary,
|
|
3369
|
-
updatedAt: Date.now()
|
|
3370
|
-
}
|
|
3371
|
-
}));
|
|
3372
|
-
}
|
|
2993
|
+
sendSessionEvent(event) {
|
|
2994
|
+
this.emitMessageOrQueue({
|
|
2995
|
+
role: "agent",
|
|
2996
|
+
content: { type: "event", id: randomUUID$1(), data: event },
|
|
2997
|
+
meta: { sentFrom: "cli" }
|
|
2998
|
+
});
|
|
3373
2999
|
}
|
|
3374
3000
|
sendCodexMessage(body) {
|
|
3375
|
-
|
|
3376
|
-
role: "agent",
|
|
3377
|
-
content: {
|
|
3378
|
-
type: "codex",
|
|
3379
|
-
data: body
|
|
3380
|
-
// This wraps the entire Claude message
|
|
3381
|
-
},
|
|
3382
|
-
meta: {
|
|
3383
|
-
sentFrom: "cli"
|
|
3384
|
-
}
|
|
3385
|
-
};
|
|
3386
|
-
const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
|
|
3387
|
-
this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `codex:${String(body?.type ?? "unknown")}` });
|
|
3001
|
+
this.emitMessageOrQueue({ role: "agent", content: { type: "codex", data: body }, meta: { sentFrom: "cli" } });
|
|
3388
3002
|
}
|
|
3389
|
-
/**
|
|
3390
|
-
* Send a generic agent message to the session.
|
|
3391
|
-
* Works for any agent type (Gemini, Codex, Claude, etc.)
|
|
3392
|
-
*
|
|
3393
|
-
* @param agentType - The type of agent sending the message (e.g., 'gemini', 'codex', 'claude')
|
|
3394
|
-
* @param body - The message payload
|
|
3395
|
-
*/
|
|
3396
3003
|
sendAgentMessage(agentType, body) {
|
|
3397
|
-
|
|
3398
|
-
role: "agent",
|
|
3399
|
-
content: {
|
|
3400
|
-
type: agentType,
|
|
3401
|
-
data: body
|
|
3402
|
-
},
|
|
3403
|
-
meta: {
|
|
3404
|
-
sentFrom: "cli"
|
|
3405
|
-
}
|
|
3406
|
-
};
|
|
3407
|
-
logger.debug(`[SOCKET] Sending ${agentType} message:`, { type: body.type, hasMessage: !!body.message });
|
|
3408
|
-
const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
|
|
3409
|
-
this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `${agentType}:${String(body?.type ?? "unknown")}` });
|
|
3004
|
+
this.emitMessageOrQueue({ role: "agent", content: { type: agentType, data: body }, meta: { sentFrom: "cli" } });
|
|
3410
3005
|
}
|
|
3411
|
-
|
|
3412
|
-
|
|
3006
|
+
sendClaudeSessionMessage(body) {
|
|
3007
|
+
this.emitMessageOrQueue({
|
|
3413
3008
|
role: "agent",
|
|
3414
|
-
content: {
|
|
3415
|
-
|
|
3416
|
-
type: "event",
|
|
3417
|
-
data: event
|
|
3418
|
-
}
|
|
3419
|
-
};
|
|
3420
|
-
const encrypted = encodeBase64(encrypt(this.encryptionKey, content));
|
|
3421
|
-
this.emitMessageOrQueue({ sid: this.sessionId, message: encrypted, kind: `event:${event.type}` });
|
|
3422
|
-
}
|
|
3423
|
-
/**
|
|
3424
|
-
* Send a ping message to keep the connection alive
|
|
3425
|
-
*/
|
|
3426
|
-
keepAlive(thinking, mode) {
|
|
3427
|
-
if (process.env.DEBUG) {
|
|
3428
|
-
logger.debug(`[API] Sending keep alive message: ${thinking}`);
|
|
3429
|
-
}
|
|
3430
|
-
this.socket.volatile.emit("session-alive", {
|
|
3431
|
-
sid: this.sessionId,
|
|
3432
|
-
time: Date.now(),
|
|
3433
|
-
thinking,
|
|
3434
|
-
mode
|
|
3009
|
+
content: { type: "output", data: body },
|
|
3010
|
+
meta: { sentFrom: "cli" }
|
|
3435
3011
|
});
|
|
3436
3012
|
}
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3013
|
+
emitMessageOrQueue(content) {
|
|
3014
|
+
const payload = { sid: this.sessionId, content };
|
|
3015
|
+
if (this.socket.connected) {
|
|
3016
|
+
this.socket.emit("message", payload);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
this.outboundQueue.push(payload);
|
|
3442
3020
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
total: 0,
|
|
3462
|
-
input: 0,
|
|
3463
|
-
output: 0
|
|
3464
|
-
}
|
|
3021
|
+
flush() {
|
|
3022
|
+
if (!this.socket.connected) return;
|
|
3023
|
+
while (this.outboundQueue.length > 0) {
|
|
3024
|
+
const next = this.outboundQueue.shift();
|
|
3025
|
+
if (!next) continue;
|
|
3026
|
+
this.socket.emit("message", next);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
onUserMessage(handler) {
|
|
3030
|
+
const listener = (u) => {
|
|
3031
|
+
if (u?.body?.t !== "new-message") return;
|
|
3032
|
+
const body = u.body;
|
|
3033
|
+
const raw = body?.message ?? null;
|
|
3034
|
+
if (!raw || typeof raw !== "object") return;
|
|
3035
|
+
const record = raw?.content;
|
|
3036
|
+
if (!record || typeof record !== "object") return;
|
|
3037
|
+
if (record.role !== "user") return;
|
|
3038
|
+
handler(record);
|
|
3465
3039
|
};
|
|
3466
|
-
|
|
3467
|
-
this.
|
|
3040
|
+
this.on("update", listener);
|
|
3041
|
+
return () => this.off("update", listener);
|
|
3468
3042
|
}
|
|
3469
|
-
/**
|
|
3470
|
-
* Update session metadata
|
|
3471
|
-
* @param handler - Handler function that returns the updated metadata
|
|
3472
|
-
*/
|
|
3473
3043
|
updateMetadata(handler) {
|
|
3474
|
-
this.
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
if (answer.version > this.metadataVersion) {
|
|
3483
|
-
this.metadataVersion = answer.version;
|
|
3484
|
-
this.metadata = decrypt(this.encryptionKey, decodeBase64(answer.metadata));
|
|
3485
|
-
}
|
|
3486
|
-
throw new Error("Metadata version mismatch");
|
|
3487
|
-
} else if (answer.result === "error") ;
|
|
3488
|
-
});
|
|
3489
|
-
});
|
|
3044
|
+
const current = this.metadata || {};
|
|
3045
|
+
const next = handler(current);
|
|
3046
|
+
void this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: next }).then((answer) => {
|
|
3047
|
+
if (answer?.result === "success") {
|
|
3048
|
+
this.metadata = next;
|
|
3049
|
+
this.metadataVersion += 1;
|
|
3050
|
+
}
|
|
3051
|
+
}).catch(() => null);
|
|
3490
3052
|
}
|
|
3491
|
-
/**
|
|
3492
|
-
* Update session agent state
|
|
3493
|
-
* @param handler - Handler function that returns the updated agent state
|
|
3494
|
-
*/
|
|
3495
3053
|
updateAgentState(handler) {
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
logger.debug("Agent state updated", this.agentState);
|
|
3505
|
-
} else if (answer.result === "version-mismatch") {
|
|
3506
|
-
if (answer.version > this.agentStateVersion) {
|
|
3507
|
-
this.agentStateVersion = answer.version;
|
|
3508
|
-
this.agentState = answer.agentState ? decrypt(this.encryptionKey, decodeBase64(answer.agentState)) : null;
|
|
3509
|
-
}
|
|
3510
|
-
throw new Error("Agent state version mismatch");
|
|
3511
|
-
} else if (answer.result === "error") ;
|
|
3512
|
-
});
|
|
3513
|
-
});
|
|
3054
|
+
const current = this.agentState || {};
|
|
3055
|
+
const next = handler(current);
|
|
3056
|
+
void this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: next }).then((answer) => {
|
|
3057
|
+
if (answer?.result === "success") {
|
|
3058
|
+
this.agentState = next;
|
|
3059
|
+
this.agentStateVersion += 1;
|
|
3060
|
+
}
|
|
3061
|
+
}).catch(() => null);
|
|
3514
3062
|
}
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
async function delay(ms) {
|
|
3066
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3067
|
+
}
|
|
3068
|
+
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
3069
|
+
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
|
|
3070
|
+
return Math.round(Math.random() * maxDelayRet);
|
|
3071
|
+
}
|
|
3072
|
+
function createBackoff(opts) {
|
|
3073
|
+
return async (callback) => {
|
|
3074
|
+
let currentFailureCount = 0;
|
|
3075
|
+
const minDelay = opts && opts.minDelay !== void 0 ? opts.minDelay : 250;
|
|
3076
|
+
const maxDelay = opts && opts.maxDelay !== void 0 ? opts.maxDelay : 1e3;
|
|
3077
|
+
const maxFailureCount = opts && opts.maxFailureCount !== void 0 ? opts.maxFailureCount : 50;
|
|
3078
|
+
while (true) {
|
|
3079
|
+
try {
|
|
3080
|
+
return await callback();
|
|
3081
|
+
} catch (e) {
|
|
3082
|
+
if (currentFailureCount < maxFailureCount) {
|
|
3083
|
+
currentFailureCount++;
|
|
3084
|
+
}
|
|
3085
|
+
if (opts && opts.onError) {
|
|
3086
|
+
opts.onError(e, currentFailureCount);
|
|
3087
|
+
}
|
|
3088
|
+
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
3089
|
+
await delay(waitForRequest);
|
|
3090
|
+
}
|
|
3521
3091
|
}
|
|
3522
|
-
|
|
3523
|
-
this.socket.emit("ping", () => {
|
|
3524
|
-
resolve();
|
|
3525
|
-
});
|
|
3526
|
-
setTimeout(() => {
|
|
3527
|
-
resolve();
|
|
3528
|
-
}, 1e4);
|
|
3529
|
-
});
|
|
3530
|
-
}
|
|
3531
|
-
async close() {
|
|
3532
|
-
logger.debug("[API] socket.close() called");
|
|
3533
|
-
this.socket.close();
|
|
3534
|
-
}
|
|
3092
|
+
};
|
|
3535
3093
|
}
|
|
3094
|
+
let backoff = createBackoff();
|
|
3536
3095
|
|
|
3537
3096
|
function looksLikeEngineRoot(engineRoot) {
|
|
3538
3097
|
if (!engineRoot) return false;
|
|
@@ -3705,6 +3264,11 @@ async function buildAndInstallUnrealMcpPlugin(options) {
|
|
|
3705
3264
|
`-Plugin=${pluginUpluginPath}`,
|
|
3706
3265
|
`-Package=${packageDir}`,
|
|
3707
3266
|
"-Rocket",
|
|
3267
|
+
// UAT enforces a global single-instance mutex. On macOS/Linux, the named mutex can
|
|
3268
|
+
// appear "already created" even when no process is actually holding it, which makes
|
|
3269
|
+
// repeated installs fail with: "A conflicting instance of AutomationTool is already running."
|
|
3270
|
+
// `-WaitForUATMutex` makes UAT acquire the mutex if it's free (or wait if another build is in flight).
|
|
3271
|
+
"-WaitForUATMutex",
|
|
3708
3272
|
`-TargetPlatforms=${targetPlatform()}`
|
|
3709
3273
|
];
|
|
3710
3274
|
const logStream = fs__default.createWriteStream(buildLogPath, { flags: "a" });
|
|
@@ -3777,7 +3341,6 @@ class ApiMachineClient {
|
|
|
3777
3341
|
this.machine = machine;
|
|
3778
3342
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
3779
3343
|
scopePrefix: this.machine.id,
|
|
3780
|
-
encryptionKey: this.machine.encryptionKey,
|
|
3781
3344
|
logger: (msg, data) => logger.debug(msg, data)
|
|
3782
3345
|
});
|
|
3783
3346
|
const rootDir = this.machine?.metadata?.homeDir || os.homedir() || process.cwd();
|
|
@@ -3818,6 +3381,24 @@ class ApiMachineClient {
|
|
|
3818
3381
|
socket;
|
|
3819
3382
|
keepAliveInterval = null;
|
|
3820
3383
|
rpcHandlerManager;
|
|
3384
|
+
connected = false;
|
|
3385
|
+
lastConnectError = null;
|
|
3386
|
+
lastDisconnectReason = null;
|
|
3387
|
+
lastHttpUpsertError = null;
|
|
3388
|
+
lastHttpUpsertStatus = null;
|
|
3389
|
+
lastHttpUpsertAt = null;
|
|
3390
|
+
upsertBackoff = createBackoff({
|
|
3391
|
+
minDelay: 1e3,
|
|
3392
|
+
maxDelay: 3e4,
|
|
3393
|
+
maxFailureCount: 200,
|
|
3394
|
+
onError: (e, failures) => {
|
|
3395
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3396
|
+
this.lastHttpUpsertError = msg;
|
|
3397
|
+
if (process.env.DEBUG) {
|
|
3398
|
+
logger.debug(`[API MACHINE] Machine upsert retry (${failures}): ${msg}`);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
});
|
|
3821
3402
|
setRPCHandlers({
|
|
3822
3403
|
spawnSession,
|
|
3823
3404
|
stopSession,
|
|
@@ -3864,74 +3445,86 @@ class ApiMachineClient {
|
|
|
3864
3445
|
});
|
|
3865
3446
|
}
|
|
3866
3447
|
/**
|
|
3867
|
-
*
|
|
3868
|
-
* Currently unused, changes from the mobile client are more likely
|
|
3869
|
-
* for example to set a custom name.
|
|
3448
|
+
* Upsert machine record (metadata + daemon state) via HTTP (workspace-native V1).
|
|
3870
3449
|
*/
|
|
3871
|
-
async
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
}
|
|
3888
|
-
throw new Error("Metadata version mismatch");
|
|
3889
|
-
}
|
|
3450
|
+
async upsertMachineHttp(params) {
|
|
3451
|
+
const attemptAt = Date.now();
|
|
3452
|
+
const endpoint = configuration.serverUrl.replace(/\/+$/, "");
|
|
3453
|
+
const timeoutMs = Math.max(1e3, Math.min(12e4, Number.parseInt(process.env.FLOCKBAY_MACHINE_UPSERT_TIMEOUT_MS || "30000", 10) || 3e4));
|
|
3454
|
+
const res = await fetch(`${endpoint}/v1/machines`, {
|
|
3455
|
+
method: "POST",
|
|
3456
|
+
headers: {
|
|
3457
|
+
Authorization: `Machine ${this.token}`,
|
|
3458
|
+
"Content-Type": "application/json"
|
|
3459
|
+
},
|
|
3460
|
+
body: JSON.stringify({
|
|
3461
|
+
id: this.machine.id,
|
|
3462
|
+
...params.metadata !== void 0 ? { metadata: params.metadata } : {},
|
|
3463
|
+
...params.daemonState !== void 0 ? { daemonState: params.daemonState } : {}
|
|
3464
|
+
}),
|
|
3465
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3890
3466
|
});
|
|
3467
|
+
if (!res.ok) {
|
|
3468
|
+
const detail = await res.text().catch(() => "");
|
|
3469
|
+
const msg = `machine_upsert_failed:${res.status}${detail ? `:${detail.slice(0, 2e3)}` : ""}`;
|
|
3470
|
+
this.lastHttpUpsertStatus = res.status;
|
|
3471
|
+
this.lastHttpUpsertError = msg;
|
|
3472
|
+
throw new Error(msg);
|
|
3473
|
+
}
|
|
3474
|
+
const data = await res.json().catch(() => null);
|
|
3475
|
+
const machine = data?.machine && typeof data.machine === "object" ? data.machine : null;
|
|
3476
|
+
if (machine) {
|
|
3477
|
+
this.machine.metadata = machine.metadata ?? null;
|
|
3478
|
+
this.machine.daemonState = machine.daemonState ?? null;
|
|
3479
|
+
this.machine.seq = Number(machine.seq || this.machine.seq || 0);
|
|
3480
|
+
}
|
|
3481
|
+
this.lastHttpUpsertStatus = res.status;
|
|
3482
|
+
this.lastHttpUpsertError = null;
|
|
3483
|
+
this.lastHttpUpsertAt = attemptAt;
|
|
3891
3484
|
}
|
|
3892
3485
|
/**
|
|
3893
|
-
* Update daemon state (runtime info)
|
|
3894
|
-
* Simplified without lock - relies on backoff for retry
|
|
3486
|
+
* Update daemon state (runtime info) via HTTP upsert.
|
|
3895
3487
|
*/
|
|
3896
3488
|
async updateDaemonState(handler) {
|
|
3897
|
-
await
|
|
3489
|
+
await this.upsertBackoff(async () => {
|
|
3898
3490
|
const updated = handler(this.machine.daemonState);
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
daemonState: encodeBase64(encrypt(this.machine.encryptionKey, updated)),
|
|
3902
|
-
expectedVersion: this.machine.daemonStateVersion
|
|
3903
|
-
});
|
|
3904
|
-
if (answer.result === "success") {
|
|
3905
|
-
this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(answer.daemonState));
|
|
3906
|
-
this.machine.daemonStateVersion = answer.version;
|
|
3907
|
-
logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
3908
|
-
} else if (answer.result === "version-mismatch") {
|
|
3909
|
-
if (answer.version > this.machine.daemonStateVersion) {
|
|
3910
|
-
this.machine.daemonStateVersion = answer.version;
|
|
3911
|
-
this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(answer.daemonState));
|
|
3912
|
-
}
|
|
3913
|
-
throw new Error("Daemon state version mismatch");
|
|
3914
|
-
}
|
|
3491
|
+
await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
|
|
3492
|
+
logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
3915
3493
|
});
|
|
3916
3494
|
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Best-effort single attempt (no retries). Useful during shutdown.
|
|
3497
|
+
*/
|
|
3498
|
+
async updateDaemonStateOnce(handler) {
|
|
3499
|
+
try {
|
|
3500
|
+
const updated = handler(this.machine.daemonState);
|
|
3501
|
+
await this.upsertMachineHttp({ daemonState: updated, metadata: this.machine.metadata });
|
|
3502
|
+
return true;
|
|
3503
|
+
} catch (e) {
|
|
3504
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3505
|
+
logger.debug("[API MACHINE] Daemon state update failed (best-effort):", msg);
|
|
3506
|
+
return false;
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
getStatusSnapshot() {
|
|
3510
|
+
return {
|
|
3511
|
+
connected: this.connected,
|
|
3512
|
+
lastConnectError: this.lastConnectError,
|
|
3513
|
+
lastDisconnectReason: this.lastDisconnectReason,
|
|
3514
|
+
lastHttpUpsertError: this.lastHttpUpsertError,
|
|
3515
|
+
lastHttpUpsertStatus: this.lastHttpUpsertStatus,
|
|
3516
|
+
lastHttpUpsertAt: this.lastHttpUpsertAt
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3917
3519
|
connect() {
|
|
3918
3520
|
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
3919
3521
|
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
3920
|
-
const machineMetadataEncrypted = encodeBase64(encrypt(this.machine.encryptionKey, this.machine.metadata));
|
|
3921
|
-
const daemonStateEncrypted = this.machine.daemonState ? encodeBase64(encrypt(this.machine.encryptionKey, this.machine.daemonState)) : "";
|
|
3922
|
-
const dataEncryptionKey = String(this.machine.dataEncryptionKey || "").trim();
|
|
3923
|
-
if (!dataEncryptionKey) {
|
|
3924
|
-
throw new Error("Missing machine dataEncryptionKey (DEK)");
|
|
3925
|
-
}
|
|
3926
3522
|
this.socket = io(serverUrl, {
|
|
3927
3523
|
transports: ["websocket"],
|
|
3928
3524
|
auth: {
|
|
3929
3525
|
token: this.token,
|
|
3930
3526
|
clientType: "machine-scoped",
|
|
3931
|
-
machineId: this.machine.id
|
|
3932
|
-
dataEncryptionKey,
|
|
3933
|
-
machineMetadata: machineMetadataEncrypted,
|
|
3934
|
-
daemonState: daemonStateEncrypted
|
|
3527
|
+
machineId: this.machine.id
|
|
3935
3528
|
},
|
|
3936
3529
|
path: "/v1/updates",
|
|
3937
3530
|
reconnection: true,
|
|
@@ -3940,6 +3533,9 @@ class ApiMachineClient {
|
|
|
3940
3533
|
});
|
|
3941
3534
|
this.socket.on("connect", () => {
|
|
3942
3535
|
logger.debug("[API MACHINE] Connected to server");
|
|
3536
|
+
this.connected = true;
|
|
3537
|
+
this.lastConnectError = null;
|
|
3538
|
+
this.lastDisconnectReason = null;
|
|
3943
3539
|
this.updateDaemonState((state) => ({
|
|
3944
3540
|
...state,
|
|
3945
3541
|
status: "running",
|
|
@@ -3950,8 +3546,10 @@ class ApiMachineClient {
|
|
|
3950
3546
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
3951
3547
|
this.startKeepAlive();
|
|
3952
3548
|
});
|
|
3953
|
-
this.socket.on("disconnect", () => {
|
|
3954
|
-
logger.debug("[API MACHINE] Disconnected from server");
|
|
3549
|
+
this.socket.on("disconnect", (reason) => {
|
|
3550
|
+
logger.debug("[API MACHINE] Disconnected from server", reason);
|
|
3551
|
+
this.connected = false;
|
|
3552
|
+
this.lastDisconnectReason = typeof reason === "string" ? reason : "disconnected";
|
|
3955
3553
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
3956
3554
|
this.stopKeepAlive();
|
|
3957
3555
|
});
|
|
@@ -3960,24 +3558,19 @@ class ApiMachineClient {
|
|
|
3960
3558
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
3961
3559
|
});
|
|
3962
3560
|
this.socket.on("update", (data) => {
|
|
3963
|
-
if (data
|
|
3561
|
+
if (data?.body?.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
3964
3562
|
const update = data.body;
|
|
3965
|
-
if (update.
|
|
3966
|
-
|
|
3967
|
-
this.machine.
|
|
3968
|
-
this.machine.
|
|
3563
|
+
if (update.machine) {
|
|
3564
|
+
this.machine.metadata = update.machine.metadata ?? null;
|
|
3565
|
+
this.machine.daemonState = update.machine.daemonState ?? null;
|
|
3566
|
+
this.machine.seq = Number(update.machine.seq || this.machine.seq || 0);
|
|
3969
3567
|
}
|
|
3970
|
-
if (update.daemonState) {
|
|
3971
|
-
logger.debug("[API MACHINE] Received external daemon state update");
|
|
3972
|
-
this.machine.daemonState = decrypt(this.machine.encryptionKey, decodeBase64(update.daemonState.value));
|
|
3973
|
-
this.machine.daemonStateVersion = update.daemonState.version;
|
|
3974
|
-
}
|
|
3975
|
-
} else {
|
|
3976
|
-
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
3977
3568
|
}
|
|
3978
3569
|
});
|
|
3979
3570
|
this.socket.on("connect_error", (error) => {
|
|
3980
3571
|
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
3572
|
+
this.connected = false;
|
|
3573
|
+
this.lastConnectError = String(error?.message || "connect_error");
|
|
3981
3574
|
});
|
|
3982
3575
|
this.socket.io.on("error", (error) => {
|
|
3983
3576
|
logger.debug("[API MACHINE] Socket error:", error);
|
|
@@ -4014,389 +3607,123 @@ class ApiMachineClient {
|
|
|
4014
3607
|
}
|
|
4015
3608
|
}
|
|
4016
3609
|
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
} catch (error) {
|
|
4046
|
-
logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
|
|
4047
|
-
throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4048
|
-
}
|
|
4049
|
-
}
|
|
4050
|
-
/**
|
|
4051
|
-
* Send push notification via Expo Push API with retry
|
|
4052
|
-
* @param messages - Array of push messages to send
|
|
4053
|
-
*/
|
|
4054
|
-
async sendPushNotifications(messages) {
|
|
4055
|
-
logger.debug(`Sending ${messages.length} push notifications`);
|
|
4056
|
-
const validMessages = messages.filter((message) => {
|
|
4057
|
-
if (Array.isArray(message.to)) {
|
|
4058
|
-
return message.to.every((token) => Expo.isExpoPushToken(token));
|
|
4059
|
-
}
|
|
4060
|
-
return Expo.isExpoPushToken(message.to);
|
|
4061
|
-
});
|
|
4062
|
-
if (validMessages.length === 0) {
|
|
4063
|
-
logger.debug("No valid Expo push tokens found");
|
|
4064
|
-
return;
|
|
4065
|
-
}
|
|
4066
|
-
const chunks = this.expo.chunkPushNotifications(validMessages);
|
|
4067
|
-
for (const chunk of chunks) {
|
|
4068
|
-
const startTime = Date.now();
|
|
4069
|
-
const timeout = 3e5;
|
|
4070
|
-
let attempt = 0;
|
|
4071
|
-
while (true) {
|
|
4072
|
-
try {
|
|
4073
|
-
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
|
|
4074
|
-
const errors = ticketChunk.filter((ticket) => ticket.status === "error");
|
|
4075
|
-
if (errors.length > 0) {
|
|
4076
|
-
const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
|
|
4077
|
-
logger.debug("[PUSH] Some notifications failed:", errorDetails);
|
|
4078
|
-
}
|
|
4079
|
-
if (errors.length === ticketChunk.length) {
|
|
4080
|
-
throw new Error("All push notifications in chunk failed");
|
|
4081
|
-
}
|
|
4082
|
-
break;
|
|
4083
|
-
} catch (error) {
|
|
4084
|
-
const elapsed = Date.now() - startTime;
|
|
4085
|
-
if (elapsed >= timeout) {
|
|
4086
|
-
logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
|
|
4087
|
-
break;
|
|
4088
|
-
}
|
|
4089
|
-
attempt++;
|
|
4090
|
-
const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
4091
|
-
const remainingTime = timeout - elapsed;
|
|
4092
|
-
const waitTime = Math.min(delay, remainingTime);
|
|
4093
|
-
if (waitTime > 0) {
|
|
4094
|
-
logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
|
|
4095
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
|
-
}
|
|
4099
|
-
}
|
|
4100
|
-
logger.debug(`Push notifications sent successfully`);
|
|
4101
|
-
}
|
|
4102
|
-
/**
|
|
4103
|
-
* Send a push notification to all registered devices for the user
|
|
4104
|
-
* @param title - Notification title
|
|
4105
|
-
* @param body - Notification body
|
|
4106
|
-
* @param data - Additional data to send with the notification
|
|
4107
|
-
*/
|
|
4108
|
-
sendToAllDevices(title, body, data) {
|
|
4109
|
-
logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
|
|
4110
|
-
(async () => {
|
|
4111
|
-
try {
|
|
4112
|
-
logger.debug("[PUSH] Fetching push tokens...");
|
|
4113
|
-
const tokens = await this.fetchPushTokens();
|
|
4114
|
-
logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
|
|
4115
|
-
tokens.forEach((token, index) => {
|
|
4116
|
-
logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
|
|
4117
|
-
});
|
|
4118
|
-
if (tokens.length === 0) {
|
|
4119
|
-
logger.debug("No push tokens found for user");
|
|
4120
|
-
return;
|
|
4121
|
-
}
|
|
4122
|
-
const messages = tokens.map((token, index) => {
|
|
4123
|
-
logger.debug(`[PUSH] Creating message ${index + 1} for token`);
|
|
4124
|
-
return {
|
|
4125
|
-
to: token.token,
|
|
4126
|
-
title,
|
|
4127
|
-
body,
|
|
4128
|
-
data,
|
|
4129
|
-
sound: "default",
|
|
4130
|
-
priority: "high"
|
|
4131
|
-
};
|
|
4132
|
-
});
|
|
4133
|
-
logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
|
|
4134
|
-
await this.sendPushNotifications(messages);
|
|
4135
|
-
logger.debug("[PUSH] Push notifications sent successfully");
|
|
4136
|
-
} catch (error) {
|
|
4137
|
-
logger.debug("[PUSH] Error sending to all devices:", error);
|
|
4138
|
-
}
|
|
4139
|
-
})();
|
|
4140
|
-
}
|
|
3610
|
+
function machineAuthHeaders(machineToken) {
|
|
3611
|
+
return { Authorization: `Machine ${machineToken}`, "Content-Type": "application/json" };
|
|
3612
|
+
}
|
|
3613
|
+
function normalizeSession(raw) {
|
|
3614
|
+
return {
|
|
3615
|
+
id: String(raw?.id || ""),
|
|
3616
|
+
seq: Number(raw?.seq || 0),
|
|
3617
|
+
active: Boolean(raw?.active),
|
|
3618
|
+
activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
|
|
3619
|
+
createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
|
|
3620
|
+
updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
|
|
3621
|
+
metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
|
|
3622
|
+
metadataVersion: Number(raw?.metadataVersion || 0),
|
|
3623
|
+
agentState: raw?.agentState && typeof raw.agentState === "object" ? raw.agentState : null,
|
|
3624
|
+
agentStateVersion: Number(raw?.agentStateVersion || 0)
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
function normalizeMachine(raw) {
|
|
3628
|
+
return {
|
|
3629
|
+
id: String(raw?.id || ""),
|
|
3630
|
+
seq: Number(raw?.seq || 0),
|
|
3631
|
+
active: Boolean(raw?.active),
|
|
3632
|
+
activeAt: typeof raw?.activeAt === "number" ? raw.activeAt : null,
|
|
3633
|
+
createdAt: typeof raw?.createdAt === "number" ? raw.createdAt : null,
|
|
3634
|
+
updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : null,
|
|
3635
|
+
metadata: raw?.metadata && typeof raw.metadata === "object" ? raw.metadata : null,
|
|
3636
|
+
daemonState: raw?.daemonState && typeof raw.daemonState === "object" ? raw.daemonState : null
|
|
3637
|
+
};
|
|
4141
3638
|
}
|
|
4142
|
-
|
|
4143
3639
|
class ApiClient {
|
|
4144
|
-
|
|
4145
|
-
|
|
3640
|
+
constructor(auth) {
|
|
3641
|
+
this.auth = auth;
|
|
4146
3642
|
}
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
3643
|
+
static async create(auth) {
|
|
3644
|
+
return new ApiClient(auth);
|
|
3645
|
+
}
|
|
3646
|
+
baseUrl() {
|
|
3647
|
+
return configuration.serverUrl.replace(/\/+$/, "");
|
|
3648
|
+
}
|
|
3649
|
+
async createSession(opts) {
|
|
3650
|
+
const response = await axios.post(
|
|
3651
|
+
`${this.baseUrl()}/v1/sessions`,
|
|
3652
|
+
{ metadata: opts.metadata ?? {}, agentState: opts.state ?? null },
|
|
3653
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3654
|
+
);
|
|
3655
|
+
return normalizeSession(response.data?.session);
|
|
4152
3656
|
}
|
|
4153
|
-
/**
|
|
4154
|
-
* Create a new session or load existing one with the given tag
|
|
4155
|
-
*/
|
|
4156
3657
|
async getOrCreateSession(opts) {
|
|
4157
|
-
const
|
|
4158
|
-
const
|
|
4159
|
-
const
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
},
|
|
4171
|
-
{
|
|
4172
|
-
headers: {
|
|
4173
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4174
|
-
"Content-Type": "application/json"
|
|
4175
|
-
},
|
|
4176
|
-
timeout: 6e4
|
|
4177
|
-
// 1 minute timeout for very bad network connections
|
|
4178
|
-
}
|
|
4179
|
-
);
|
|
4180
|
-
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
4181
|
-
let raw = response.data.session;
|
|
4182
|
-
let session = {
|
|
4183
|
-
id: raw.id,
|
|
4184
|
-
seq: raw.seq,
|
|
4185
|
-
metadata: decrypt(encryptionKey, decodeBase64(raw.metadata)),
|
|
4186
|
-
metadataVersion: raw.metadataVersion,
|
|
4187
|
-
agentState: raw.agentState ? decrypt(encryptionKey, decodeBase64(raw.agentState)) : null,
|
|
4188
|
-
agentStateVersion: raw.agentStateVersion,
|
|
4189
|
-
encryptionKey
|
|
4190
|
-
};
|
|
4191
|
-
return session;
|
|
4192
|
-
} catch (error) {
|
|
4193
|
-
logger.debug("[API] [ERROR] Failed to get or create session:", error);
|
|
4194
|
-
throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4195
|
-
}
|
|
3658
|
+
const tag = String(opts?.tag || "").trim();
|
|
3659
|
+
const metadata = opts?.metadata && typeof opts.metadata === "object" ? opts.metadata : {};
|
|
3660
|
+
const merged = tag ? { ...metadata, tag } : metadata;
|
|
3661
|
+
return this.createSession({ metadata: merged, state: opts.state ?? null });
|
|
3662
|
+
}
|
|
3663
|
+
async getSessionById(sessionId) {
|
|
3664
|
+
const id = String(sessionId || "").trim();
|
|
3665
|
+
if (!id) throw new Error("Session id is required");
|
|
3666
|
+
const response = await axios.get(
|
|
3667
|
+
`${this.baseUrl()}/v1/sessions/${encodeURIComponent(id)}`,
|
|
3668
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3669
|
+
);
|
|
3670
|
+
return normalizeSession(response.data?.session);
|
|
4196
3671
|
}
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
*/
|
|
4201
|
-
async getOrCreateMachine(opts) {
|
|
4202
|
-
const encryptionKey = this.credential.encryption.machineKey;
|
|
4203
|
-
const encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
|
|
4204
|
-
const dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
|
|
4205
|
-
dataEncryptionKey.set([0], 0);
|
|
4206
|
-
dataEncryptionKey.set(encryptedDataKey, 1);
|
|
3672
|
+
async upsertMachine(opts) {
|
|
3673
|
+
const machineId = String(opts.machineId || "").trim();
|
|
3674
|
+
if (!machineId) throw new Error("Machine id is required");
|
|
4207
3675
|
const response = await axios.post(
|
|
4208
|
-
`${
|
|
4209
|
-
{
|
|
4210
|
-
|
|
4211
|
-
metadata: encodeBase64(encrypt(encryptionKey, opts.metadata)),
|
|
4212
|
-
daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, opts.daemonState)) : void 0,
|
|
4213
|
-
dataEncryptionKey: encodeBase64(dataEncryptionKey)
|
|
4214
|
-
},
|
|
4215
|
-
{
|
|
4216
|
-
headers: {
|
|
4217
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4218
|
-
"Content-Type": "application/json"
|
|
4219
|
-
},
|
|
4220
|
-
timeout: 6e4
|
|
4221
|
-
// 1 minute timeout for very bad network connections
|
|
4222
|
-
}
|
|
3676
|
+
`${this.baseUrl()}/v1/machines`,
|
|
3677
|
+
{ id: machineId, metadata: opts.metadata ?? {}, daemonState: opts.daemonState ?? null },
|
|
3678
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
4223
3679
|
);
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
}
|
|
4229
|
-
const raw = response.data.machine;
|
|
4230
|
-
logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
|
|
4231
|
-
const machine = {
|
|
4232
|
-
id: raw.id,
|
|
4233
|
-
dataEncryptionKey: String(raw.dataEncryptionKey || ""),
|
|
4234
|
-
encryptionKey,
|
|
4235
|
-
metadata: raw.metadata ? decrypt(encryptionKey, decodeBase64(raw.metadata)) : null,
|
|
4236
|
-
metadataVersion: raw.metadataVersion || 0,
|
|
4237
|
-
daemonState: raw.daemonState ? decrypt(encryptionKey, decodeBase64(raw.daemonState)) : null,
|
|
4238
|
-
daemonStateVersion: raw.daemonStateVersion || 0
|
|
4239
|
-
};
|
|
4240
|
-
return machine;
|
|
3680
|
+
return normalizeMachine(response.data?.machine);
|
|
3681
|
+
}
|
|
3682
|
+
async getOrCreateMachine(opts) {
|
|
3683
|
+
return this.upsertMachine(opts);
|
|
4241
3684
|
}
|
|
4242
|
-
/**
|
|
4243
|
-
* Fetch an existing machine by id.
|
|
4244
|
-
* Useful when the daemon needs the latest server-side machine metadata (e.g. dev toggles).
|
|
4245
|
-
*/
|
|
4246
3685
|
async getMachine(machineId) {
|
|
4247
3686
|
const id = String(machineId || "").trim();
|
|
4248
|
-
if (!id)
|
|
4249
|
-
throw new Error("Machine id is required");
|
|
4250
|
-
}
|
|
4251
|
-
const encryptionKey = this.credential.encryption.machineKey;
|
|
3687
|
+
if (!id) throw new Error("Machine id is required");
|
|
4252
3688
|
const response = await axios.get(
|
|
4253
|
-
`${
|
|
4254
|
-
{
|
|
4255
|
-
headers: {
|
|
4256
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4257
|
-
"Content-Type": "application/json"
|
|
4258
|
-
},
|
|
4259
|
-
timeout: 6e4
|
|
4260
|
-
}
|
|
3689
|
+
`${this.baseUrl()}/v1/machines/${encodeURIComponent(id)}`,
|
|
3690
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
4261
3691
|
);
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
const
|
|
4266
|
-
if (!
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
metadata: raw.metadata ? decrypt(encryptionKey, decodeBase64(raw.metadata)) : null,
|
|
4274
|
-
metadataVersion: raw.metadataVersion || 0,
|
|
4275
|
-
daemonState: raw.daemonState ? decrypt(encryptionKey, decodeBase64(raw.daemonState)) : null,
|
|
4276
|
-
daemonStateVersion: raw.daemonStateVersion || 0
|
|
4277
|
-
};
|
|
4278
|
-
return machine;
|
|
4279
|
-
}
|
|
4280
|
-
sessionSyncClient(session) {
|
|
4281
|
-
return new ApiSessionClient(this.credential.token, session);
|
|
3692
|
+
return normalizeMachine(response.data?.machine);
|
|
3693
|
+
}
|
|
3694
|
+
async registerVendorToken(vendor, token) {
|
|
3695
|
+
const v = String(vendor || "").trim().toLowerCase();
|
|
3696
|
+
if (!v) throw new Error("Vendor is required");
|
|
3697
|
+
await axios.post(
|
|
3698
|
+
`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`,
|
|
3699
|
+
{ token },
|
|
3700
|
+
{ headers: machineAuthHeaders(this.auth.machineToken), timeout: 6e4 }
|
|
3701
|
+
);
|
|
3702
|
+
return { ok: true };
|
|
4282
3703
|
}
|
|
4283
|
-
|
|
4284
|
-
|
|
3704
|
+
async getVendorToken(vendor) {
|
|
3705
|
+
const v = String(vendor || "").trim().toLowerCase();
|
|
3706
|
+
if (!v) throw new Error("Vendor is required");
|
|
3707
|
+
const response = await axios.get(`${this.baseUrl()}/v1/vendor-tokens/${encodeURIComponent(v)}`, {
|
|
3708
|
+
headers: machineAuthHeaders(this.auth.machineToken),
|
|
3709
|
+
timeout: 6e4,
|
|
3710
|
+
validateStatus: (s) => s >= 200 && s < 300 || s === 404
|
|
3711
|
+
});
|
|
3712
|
+
if (response.status === 404) return null;
|
|
3713
|
+
return response.data?.token ?? null;
|
|
4285
3714
|
}
|
|
4286
3715
|
push() {
|
|
4287
|
-
return
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
* Register a vendor API token with the server
|
|
4291
|
-
* The token is sent as a JSON string - server handles encryption
|
|
4292
|
-
*/
|
|
4293
|
-
async registerVendorToken(vendor, apiKey) {
|
|
4294
|
-
try {
|
|
4295
|
-
const response = await axios.post(
|
|
4296
|
-
`${configuration.serverUrl}/v1/connect/${vendor}/register`,
|
|
4297
|
-
{
|
|
4298
|
-
token: JSON.stringify(apiKey)
|
|
4299
|
-
},
|
|
4300
|
-
{
|
|
4301
|
-
headers: {
|
|
4302
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4303
|
-
"Content-Type": "application/json"
|
|
4304
|
-
},
|
|
4305
|
-
timeout: 5e3
|
|
4306
|
-
}
|
|
4307
|
-
);
|
|
4308
|
-
if (response.status !== 200 && response.status !== 201) {
|
|
4309
|
-
throw new Error(`Server returned status ${response.status}`);
|
|
3716
|
+
return {
|
|
3717
|
+
sendToAllDevices: async () => {
|
|
3718
|
+
return;
|
|
4310
3719
|
}
|
|
4311
|
-
|
|
4312
|
-
} catch (error) {
|
|
4313
|
-
logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
|
|
4314
|
-
throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4315
|
-
}
|
|
3720
|
+
};
|
|
4316
3721
|
}
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
try {
|
|
4323
|
-
const response = await axios.get(
|
|
4324
|
-
`${configuration.serverUrl}/v1/connect/${vendor}/token`,
|
|
4325
|
-
{
|
|
4326
|
-
headers: {
|
|
4327
|
-
"Authorization": `Bearer ${this.credential.token}`,
|
|
4328
|
-
"Content-Type": "application/json"
|
|
4329
|
-
},
|
|
4330
|
-
timeout: 5e3
|
|
4331
|
-
}
|
|
4332
|
-
);
|
|
4333
|
-
if (response.status === 404) {
|
|
4334
|
-
logger.debug(`[API] No vendor token found for ${vendor}`);
|
|
4335
|
-
return null;
|
|
4336
|
-
}
|
|
4337
|
-
if (response.status !== 200) {
|
|
4338
|
-
throw new Error(`Server returned status ${response.status}`);
|
|
4339
|
-
}
|
|
4340
|
-
logger.debug(`[API] Raw vendor token response:`, {
|
|
4341
|
-
status: response.status,
|
|
4342
|
-
dataKeys: Object.keys(response.data || {}),
|
|
4343
|
-
hasToken: "token" in (response.data || {}),
|
|
4344
|
-
tokenType: typeof response.data?.token,
|
|
4345
|
-
present: response.data?.present
|
|
4346
|
-
});
|
|
4347
|
-
const present = typeof response.data?.present === "boolean" ? response.data.present : null;
|
|
4348
|
-
const tokenField = response.data?.token;
|
|
4349
|
-
if (present === true && (tokenField === null || tokenField === void 0 || tokenField === "")) {
|
|
4350
|
-
logger.debug(`[API] Vendor token for ${vendor} is present but redacted by the server (FLOCKBAY_REDACT_CONNECTED_TOKENS=1).`);
|
|
4351
|
-
return null;
|
|
4352
|
-
}
|
|
4353
|
-
let tokenData = null;
|
|
4354
|
-
if (response.data?.token) {
|
|
4355
|
-
if (typeof response.data.token === "string") {
|
|
4356
|
-
try {
|
|
4357
|
-
tokenData = JSON.parse(response.data.token);
|
|
4358
|
-
} catch (parseError) {
|
|
4359
|
-
logger.debug(`[API] Failed to parse token as JSON, using as string:`, parseError);
|
|
4360
|
-
tokenData = response.data.token;
|
|
4361
|
-
}
|
|
4362
|
-
} else if (response.data.token !== null) {
|
|
4363
|
-
tokenData = response.data.token;
|
|
4364
|
-
} else {
|
|
4365
|
-
logger.debug(`[API] Token is null for ${vendor}, treating as not found`);
|
|
4366
|
-
return null;
|
|
4367
|
-
}
|
|
4368
|
-
} else if (response.data && typeof response.data === "object") {
|
|
4369
|
-
if (response.data.token === null && response.data.present === false) {
|
|
4370
|
-
logger.debug(`[API] Response contains present=false and null token for ${vendor}, treating as not found`);
|
|
4371
|
-
return null;
|
|
4372
|
-
}
|
|
4373
|
-
if (response.data.token === null && Object.keys(response.data).length === 1) {
|
|
4374
|
-
logger.debug(`[API] Response contains only null token for ${vendor}, treating as not found`);
|
|
4375
|
-
return null;
|
|
4376
|
-
}
|
|
4377
|
-
tokenData = response.data;
|
|
4378
|
-
}
|
|
4379
|
-
if (tokenData === null || tokenData && typeof tokenData === "object" && tokenData.token === null && Object.keys(tokenData).length === 1) {
|
|
4380
|
-
logger.debug(`[API] Token data is null for ${vendor}`);
|
|
4381
|
-
return null;
|
|
4382
|
-
}
|
|
4383
|
-
if (tokenData && typeof tokenData === "object" && "token" in tokenData && "present" in tokenData) {
|
|
4384
|
-
logger.debug(`[API] Received token metadata object for ${vendor}; returning parsed token field only.`);
|
|
4385
|
-
return tokenData.token ? tokenData.token : null;
|
|
4386
|
-
}
|
|
4387
|
-
logger.debug(`[API] Vendor token for ${vendor} retrieved successfully`, {
|
|
4388
|
-
tokenDataType: typeof tokenData,
|
|
4389
|
-
tokenDataKeys: tokenData && typeof tokenData === "object" ? Object.keys(tokenData) : "not an object"
|
|
4390
|
-
});
|
|
4391
|
-
return tokenData;
|
|
4392
|
-
} catch (error) {
|
|
4393
|
-
if (error.response?.status === 404) {
|
|
4394
|
-
logger.debug(`[API] No vendor token found for ${vendor}`);
|
|
4395
|
-
return null;
|
|
4396
|
-
}
|
|
4397
|
-
logger.debug(`[API] [ERROR] Failed to get vendor token:`, error);
|
|
4398
|
-
return null;
|
|
4399
|
-
}
|
|
3722
|
+
sessionSyncClient(session) {
|
|
3723
|
+
return new ApiSessionClient(this.auth.machineToken, session);
|
|
3724
|
+
}
|
|
3725
|
+
machineSyncClient(machine) {
|
|
3726
|
+
return new ApiMachineClient(this.auth.machineToken, machine);
|
|
4400
3727
|
}
|
|
4401
3728
|
}
|
|
4402
3729
|
|
|
@@ -4447,4 +3774,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
|
|
|
4447
3774
|
}).passthrough()
|
|
4448
3775
|
]);
|
|
4449
3776
|
|
|
4450
|
-
export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e,
|
|
3777
|
+
export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, readDaemonState as f, clearDaemonState as g, readCredentials as h, unrealMcpPythonDir as i, acquireDaemonLock as j, writeDaemonState as k, logger as l, ApiMachineClient as m, releaseDaemonLock as n, clearCredentials as o, projectPath as p, clearMachineId as q, readSettings as r, sendUnrealMcpTcpCommand as s, installUnrealMcpPluginToEngine as t, updateSettings as u, getLatestDaemonLog as v, writeCredentials as w };
|