@livedesk/client 0.1.203 → 0.1.205
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/bin/livedesk-client-node.js +66 -187
- package/bin/livedesk-client.js +383 -136
- package/package.json +5 -6
|
@@ -19,15 +19,13 @@ const AGENT_VERSION = (() => {
|
|
|
19
19
|
const PRODUCT_VERSION = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || AGENT_VERSION).trim() || AGENT_VERSION;
|
|
20
20
|
const DEFAULT_MANAGER = '127.0.0.1:5197';
|
|
21
21
|
const DEFAULT_HEARTBEAT_MS = 5000;
|
|
22
|
-
const DEFAULT_RECONNECT_MS = 5000;
|
|
23
|
-
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
24
|
-
const EXIT_CLIENT_UPDATE = 42;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const MAX_AI_OUTPUT_CHARS = 6000;
|
|
30
|
-
const MAX_FILE_TRANSFER_FILES = 24;
|
|
22
|
+
const DEFAULT_RECONNECT_MS = 5000;
|
|
23
|
+
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
24
|
+
const EXIT_CLIENT_UPDATE = 42;
|
|
25
|
+
const DEFAULT_LIVE_FPS = 30;
|
|
26
|
+
const MAX_LIVE_FPS = 30;
|
|
27
|
+
const MAX_FRAME_BASE64_CHARS = 3 * 1024 * 1024;
|
|
28
|
+
const MAX_FILE_TRANSFER_FILES = 24;
|
|
31
29
|
const MAX_FILE_TRANSFER_BYTES = 24 * 1024 * 1024;
|
|
32
30
|
const MAX_AGENT_OUTPUT_CHARS = 32000;
|
|
33
31
|
const launchedProcessRegistry = new Map();
|
|
@@ -281,14 +279,10 @@ Options:
|
|
|
281
279
|
--no-thumbnail Disable thumbnail capture capability.
|
|
282
280
|
--live Enable focused live screen streaming. Default on.
|
|
283
281
|
--no-live Disable focused live screen streaming.
|
|
284
|
-
--tasks Enable safe remote task inbox. Default on.
|
|
285
|
-
--no-tasks Disable remote task dispatch capability.
|
|
286
|
-
--files-dir <path> Default folder for received files. Default: ~/Desktop/LiveDeskFiles.
|
|
287
|
-
--
|
|
288
|
-
--no-ai Disable OpenAI-backed remote AI assist tasks.
|
|
289
|
-
--ai-model <model> OpenAI model for AI assist. Default: ${DEFAULT_AI_MODEL}
|
|
290
|
-
--fake-ai Use deterministic AI assist responses for smoke tests.
|
|
291
|
-
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
282
|
+
--tasks Enable safe remote task inbox. Default on.
|
|
283
|
+
--no-tasks Disable remote task dispatch capability.
|
|
284
|
+
--files-dir <path> Default folder for received files. Default: ~/Desktop/LiveDeskFiles.
|
|
285
|
+
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
292
286
|
--exit-on-disconnect Exit after Hub disconnect. Useful for tests.
|
|
293
287
|
--exit-on-invalid-pair Exit when the Hub rejects the pair token.
|
|
294
288
|
--once Connect, send one status packet, and exit after welcome.
|
|
@@ -328,14 +322,10 @@ function parseArgs(argv) {
|
|
|
328
322
|
heartbeatMs: DEFAULT_HEARTBEAT_MS,
|
|
329
323
|
deviceId: '',
|
|
330
324
|
thumbnailEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_THUMBNAIL ?? process.env.MINDEXEC_REMOTE_THUMBNAIL),
|
|
331
|
-
liveEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_LIVE ?? process.env.MINDEXEC_REMOTE_LIVE),
|
|
332
|
-
taskEnabled: !isFalsy(process.env.LIVEDESK_CLIENT_TASKS ?? process.env.MINDEXEC_REMOTE_TASKS),
|
|
333
|
-
filesDir: process.env.LIVEDESK_CLIENT_FILES_DIR || process.env.MINDEXEC_REMOTE_FILES_DIR || '',
|
|
334
|
-
|
|
335
|
-
aiModel: process.env.LIVEDESK_CLIENT_AI_MODEL || process.env.MINDEXEC_REMOTE_AI_MODEL || process.env.OPENAI_MODEL || DEFAULT_AI_MODEL,
|
|
336
|
-
openAiApiKey: process.env.OPENAI_API_KEY || '',
|
|
337
|
-
fakeAi: isTruthy(process.env.LIVEDESK_CLIENT_FAKE_AI || process.env.MINDEXEC_REMOTE_FAKE_AI),
|
|
338
|
-
fakeThumbnail: isTruthy(process.env.LIVEDESK_CLIENT_FAKE_THUMBNAIL || process.env.MINDEXEC_REMOTE_FAKE_THUMBNAIL),
|
|
325
|
+
liveEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_LIVE ?? process.env.MINDEXEC_REMOTE_LIVE),
|
|
326
|
+
taskEnabled: !isFalsy(process.env.LIVEDESK_CLIENT_TASKS ?? process.env.MINDEXEC_REMOTE_TASKS),
|
|
327
|
+
filesDir: process.env.LIVEDESK_CLIENT_FILES_DIR || process.env.MINDEXEC_REMOTE_FILES_DIR || '',
|
|
328
|
+
fakeThumbnail: isTruthy(process.env.LIVEDESK_CLIENT_FAKE_THUMBNAIL || process.env.MINDEXEC_REMOTE_FAKE_THUMBNAIL),
|
|
339
329
|
exitOnDisconnect: false,
|
|
340
330
|
exitOnInvalidPair: false,
|
|
341
331
|
once: false,
|
|
@@ -387,26 +377,11 @@ function parseArgs(argv) {
|
|
|
387
377
|
case '--no-tasks':
|
|
388
378
|
result.taskEnabled = false;
|
|
389
379
|
break;
|
|
390
|
-
case '--files-dir':
|
|
391
|
-
result.filesDir = args[++index] || result.filesDir;
|
|
392
|
-
break;
|
|
393
|
-
case '--
|
|
394
|
-
result.
|
|
395
|
-
result.taskEnabled = true;
|
|
396
|
-
break;
|
|
397
|
-
case '--no-ai':
|
|
398
|
-
result.aiEnabled = false;
|
|
399
|
-
break;
|
|
400
|
-
case '--ai-model':
|
|
401
|
-
result.aiModel = args[++index] || result.aiModel;
|
|
402
|
-
break;
|
|
403
|
-
case '--fake-ai':
|
|
404
|
-
result.fakeAi = true;
|
|
405
|
-
result.aiEnabled = true;
|
|
406
|
-
result.taskEnabled = true;
|
|
407
|
-
break;
|
|
408
|
-
case '--fake-thumbnail':
|
|
409
|
-
result.fakeThumbnail = true;
|
|
380
|
+
case '--files-dir':
|
|
381
|
+
result.filesDir = args[++index] || result.filesDir;
|
|
382
|
+
break;
|
|
383
|
+
case '--fake-thumbnail':
|
|
384
|
+
result.fakeThumbnail = true;
|
|
410
385
|
result.thumbnailEnabled = true;
|
|
411
386
|
break;
|
|
412
387
|
case '--exit-on-disconnect':
|
|
@@ -586,7 +561,7 @@ function getStatus(options = {}) {
|
|
|
586
561
|
usedMemRatio,
|
|
587
562
|
platform: os.platform(),
|
|
588
563
|
release: os.release(),
|
|
589
|
-
role: options.
|
|
564
|
+
role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
|
|
590
565
|
cpu: {
|
|
591
566
|
cores: cpuCores,
|
|
592
567
|
model: cpus[0]?.model || '',
|
|
@@ -609,9 +584,9 @@ function getStatus(options = {}) {
|
|
|
609
584
|
gpu: acceleratorStatus.gpu,
|
|
610
585
|
npu: acceleratorStatus.npu,
|
|
611
586
|
network: getNetworkStatus(),
|
|
612
|
-
workload: {
|
|
613
|
-
role: options.
|
|
614
|
-
status: 'idle',
|
|
587
|
+
workload: {
|
|
588
|
+
role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
|
|
589
|
+
status: 'idle',
|
|
615
590
|
title: 'idle'
|
|
616
591
|
},
|
|
617
592
|
timestamp: new Date().toISOString()
|
|
@@ -941,95 +916,17 @@ async function captureThumbnailFrame(options, payload, frameSeq) {
|
|
|
941
916
|
return await captureScreenFrame(options, payload, frameSeq, 'thumbnail');
|
|
942
917
|
}
|
|
943
918
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
'If the task requires external side effects, explain what would be needed and stop.',
|
|
956
|
-
'',
|
|
957
|
-
`Device: ${options.name} (${os.hostname()}, ${os.platform()} ${os.release()}, ${os.arch()})`,
|
|
958
|
-
`Task title: ${title}`,
|
|
959
|
-
'',
|
|
960
|
-
'Hub instruction:',
|
|
961
|
-
instruction
|
|
962
|
-
].join('\n');
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function extractResponseText(response) {
|
|
966
|
-
if (typeof response?.output_text === 'string' && response.output_text.trim()) {
|
|
967
|
-
return response.output_text.trim();
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const parts = [];
|
|
971
|
-
if (Array.isArray(response?.output)) {
|
|
972
|
-
for (const item of response.output) {
|
|
973
|
-
if (!Array.isArray(item?.content)) {
|
|
974
|
-
continue;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
for (const content of item.content) {
|
|
978
|
-
if (typeof content?.text === 'string') {
|
|
979
|
-
parts.push(content.text);
|
|
980
|
-
}
|
|
981
|
-
if (typeof content?.output_text === 'string') {
|
|
982
|
-
parts.push(content.output_text);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return parts.join('\n').trim();
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
async function runAiAssistTask(options, payload, instruction) {
|
|
992
|
-
if (!options.aiEnabled) {
|
|
993
|
-
throw new Error('AI assist capability is disabled. Start the agent with --ai to enable it.');
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (options.fakeAi) {
|
|
997
|
-
return {
|
|
998
|
-
text: [
|
|
999
|
-
`Fake AI assist completed on ${options.name}.`,
|
|
1000
|
-
'',
|
|
1001
|
-
`Instruction: ${instruction.slice(0, 500)}`,
|
|
1002
|
-
'',
|
|
1003
|
-
'No shell, file, input, browser, or persistent side effects were performed.'
|
|
1004
|
-
].join('\n'),
|
|
1005
|
-
model: 'fake-ai',
|
|
1006
|
-
responseId: `fake-ai-${String(payload?.taskId || crypto.randomUUID()).slice(0, 96)}`
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
if (!options.openAiApiKey) {
|
|
1011
|
-
throw new Error('OPENAI_API_KEY is required for --ai remote tasks.');
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
const openaiModule = await import('openai');
|
|
1015
|
-
const OpenAI = openaiModule.default || openaiModule.OpenAI;
|
|
1016
|
-
const client = new OpenAI({ apiKey: options.openAiApiKey });
|
|
1017
|
-
const model = String(payload?.model || options.aiModel || DEFAULT_AI_MODEL).trim() || DEFAULT_AI_MODEL;
|
|
1018
|
-
const response = await client.responses.create({
|
|
1019
|
-
model,
|
|
1020
|
-
input: buildAiPrompt(options, payload, instruction)
|
|
1021
|
-
});
|
|
1022
|
-
const text = extractResponseText(response);
|
|
1023
|
-
if (!text) {
|
|
1024
|
-
throw new Error('AI assist returned no text output.');
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
return {
|
|
1028
|
-
text: text.slice(0, MAX_AI_OUTPUT_CHARS),
|
|
1029
|
-
model,
|
|
1030
|
-
responseId: response?.id || ''
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
919
|
+
const RETIRED_CLIENT_AGENT_TASK_ERROR = 'agent-ai-assist-retired';
|
|
920
|
+
|
|
921
|
+
function getRetiredClientAgentTaskError(payload = {}) {
|
|
922
|
+
const source = payload && typeof payload === 'object' ? payload : {};
|
|
923
|
+
const approvalLevel = String(source.approvalLevel || '').trim().toLowerCase();
|
|
924
|
+
const hasModelSelection = ['model', 'aiModel', 'aiProvider', 'provider']
|
|
925
|
+
.some(key => Object.prototype.hasOwnProperty.call(source, key));
|
|
926
|
+
return approvalLevel === 'ai-assist' || hasModelSelection
|
|
927
|
+
? RETIRED_CLIENT_AGENT_TASK_ERROR
|
|
928
|
+
: '';
|
|
929
|
+
}
|
|
1033
930
|
|
|
1034
931
|
function clampInteger(value, min, max, fallback) {
|
|
1035
932
|
const number = Number(value);
|
|
@@ -1846,10 +1743,25 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1846
1743
|
error: 'remote task capability is disabled'
|
|
1847
1744
|
});
|
|
1848
1745
|
return;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
const payload = message.payload || {};
|
|
1852
|
-
const
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const payload = message.payload || {};
|
|
1749
|
+
const retiredTaskError = getRetiredClientAgentTaskError(payload);
|
|
1750
|
+
if (retiredTaskError) {
|
|
1751
|
+
writeJsonLine(socket, {
|
|
1752
|
+
type: 'command.result',
|
|
1753
|
+
commandId: message.commandId,
|
|
1754
|
+
error: retiredTaskError,
|
|
1755
|
+
result: {
|
|
1756
|
+
ok: false,
|
|
1757
|
+
status: 'failed',
|
|
1758
|
+
error: retiredTaskError,
|
|
1759
|
+
completedAt: new Date().toISOString()
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const instruction = String(payload.instruction || '').replace(/\0/g, '').trim().slice(0, 4000);
|
|
1853
1765
|
if (!instruction) {
|
|
1854
1766
|
writeJsonLine(socket, {
|
|
1855
1767
|
type: 'command.result',
|
|
@@ -1859,43 +1771,13 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1859
1771
|
return;
|
|
1860
1772
|
}
|
|
1861
1773
|
|
|
1862
|
-
const
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
.
|
|
1866
|
-
.
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
try {
|
|
1870
|
-
const aiResult = await runAiAssistTask(options, payload, instruction);
|
|
1871
|
-
writeJsonLine(socket, {
|
|
1872
|
-
type: 'command.result',
|
|
1873
|
-
commandId: message.commandId,
|
|
1874
|
-
result: {
|
|
1875
|
-
kind: 'agent.task',
|
|
1876
|
-
mode: 'ai-assist',
|
|
1877
|
-
taskId: String(payload.taskId || '').slice(0, 128),
|
|
1878
|
-
title,
|
|
1879
|
-
status: 'completed',
|
|
1880
|
-
summary: aiResult.text,
|
|
1881
|
-
sideEffects: 'none',
|
|
1882
|
-
model: aiResult.model,
|
|
1883
|
-
responseId: aiResult.responseId,
|
|
1884
|
-
instructionPreview: instruction.slice(0, 320),
|
|
1885
|
-
completedAt: new Date().toISOString()
|
|
1886
|
-
}
|
|
1887
|
-
});
|
|
1888
|
-
} catch (error) {
|
|
1889
|
-
writeJsonLine(socket, {
|
|
1890
|
-
type: 'command.result',
|
|
1891
|
-
commandId: message.commandId,
|
|
1892
|
-
error: error?.message || String(error)
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
writeJsonLine(socket, {
|
|
1774
|
+
const completedAt = new Date().toISOString();
|
|
1775
|
+
const title = String(payload.title || instruction.split(/\r?\n/)[0] || 'Remote task')
|
|
1776
|
+
.replace(/[\r\n\t]/g, ' ')
|
|
1777
|
+
.trim()
|
|
1778
|
+
.slice(0, 120);
|
|
1779
|
+
|
|
1780
|
+
writeJsonLine(socket, {
|
|
1899
1781
|
type: 'command.result',
|
|
1900
1782
|
commandId: message.commandId,
|
|
1901
1783
|
result: {
|
|
@@ -2140,14 +2022,11 @@ function connectOnce(options, deviceId) {
|
|
|
2140
2022
|
taskDispatch: options.taskEnabled,
|
|
2141
2023
|
clientUpdate: true,
|
|
2142
2024
|
productVersion: PRODUCT_VERSION,
|
|
2143
|
-
agentApproval: options.taskEnabled,
|
|
2144
|
-
agentAudit: options.taskEnabled,
|
|
2145
|
-
agentTools: [...NODE_AGENT_OPERATIONS],
|
|
2146
|
-
elevation: typeof process.getuid === 'function' ? process.getuid() === 0 : false,
|
|
2147
|
-
|
|
2148
|
-
aiModel: options.aiModel,
|
|
2149
|
-
aiProvider: options.fakeAi ? 'fake' : (options.openAiApiKey ? 'openai' : ''),
|
|
2150
|
-
externalEffects: options.taskEnabled
|
|
2025
|
+
agentApproval: options.taskEnabled,
|
|
2026
|
+
agentAudit: options.taskEnabled,
|
|
2027
|
+
agentTools: [...NODE_AGENT_OPERATIONS],
|
|
2028
|
+
elevation: typeof process.getuid === 'function' ? process.getuid() === 0 : false,
|
|
2029
|
+
externalEffects: options.taskEnabled
|
|
2151
2030
|
}
|
|
2152
2031
|
});
|
|
2153
2032
|
});
|
package/bin/livedesk-client.js
CHANGED
|
@@ -39,6 +39,9 @@ const DEFAULT_RELAY_PORT = 5199;
|
|
|
39
39
|
const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
|
|
40
40
|
const DEFAULT_AUTH_CALLBACK_PORT = 5179;
|
|
41
41
|
const ENDPOINT_PROBE_TIMEOUT_MS = 900;
|
|
42
|
+
const FRESH_REGISTRY_TIMEOUT_MS = 4000;
|
|
43
|
+
const FRESH_REGISTRY_TIMEOUT_MIN_MS = 25;
|
|
44
|
+
const FRESH_REGISTRY_TIMEOUT_MAX_MS = 10000;
|
|
42
45
|
// Hub-online delivery is an optimization, not a correctness dependency. A
|
|
43
46
|
// browser, proxy, or suspended network can miss the wake event, so registry
|
|
44
47
|
// polling must stay interactive instead of backing off to multi-minute waits.
|
|
@@ -49,6 +52,7 @@ const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
|
49
52
|
const EXIT_CLIENT_UPDATE = 42;
|
|
50
53
|
const SESSION_REFRESH_SKEW_SECONDS = 60;
|
|
51
54
|
const HUB_TARGET_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
55
|
+
const HUB_TARGET_CACHE_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
52
56
|
const WINDOWS_STARTUP_SCRIPT_NAME = 'LiveDesk Desktop.vbs';
|
|
53
57
|
const WINDOWS_STARTUP_LEGACY_SCRIPT_NAME = 'LiveDesk Client.vbs';
|
|
54
58
|
const WINDOWS_STARTUP_LEGACY_CMD_NAME = 'LiveDesk Client.cmd';
|
|
@@ -67,6 +71,11 @@ const FAST_PREFLIGHT_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'fast-preflight
|
|
|
67
71
|
const CLIENT_SLOT_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'device-slot.json');
|
|
68
72
|
const CLIENT_UPDATE_STATE_PATH = process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH
|
|
69
73
|
|| join(UNIFIED_CLIENT_STATE_DIR, 'client-update.json');
|
|
74
|
+
// Migration-only compatibility: releases before 0.1.204 could persist these
|
|
75
|
+
// Client-side model options in startup/update commands. Consume them without
|
|
76
|
+
// forwarding so an upgrade starts normally, but never restores that feature.
|
|
77
|
+
const RETIRED_CLIENT_MODEL_SINGLE_OPTIONS = new Set(['--ai', '--no-ai', '--fake-ai']);
|
|
78
|
+
const RETIRED_CLIENT_MODEL_VALUE_OPTION = '--ai-model';
|
|
70
79
|
let activeAgentProcess = null;
|
|
71
80
|
let roleRestartRequest = null;
|
|
72
81
|
let agentRestartRequest = null;
|
|
@@ -301,8 +310,8 @@ Options:
|
|
|
301
310
|
--version Show the agent version.
|
|
302
311
|
--help Show this help.
|
|
303
312
|
|
|
304
|
-
Auto uses C# RemoteFast when supported and falls back to Node
|
|
305
|
-
|
|
313
|
+
Auto uses C# RemoteFast when supported and falls back to Node only when a
|
|
314
|
+
packaged RemoteFast runtime is unavailable.
|
|
306
315
|
Enable Windows auto-start from the connection page when this client signs in.
|
|
307
316
|
`.trimStart());
|
|
308
317
|
}
|
|
@@ -394,15 +403,30 @@ export function parseLauncherArgs(argv) {
|
|
|
394
403
|
let slot = process.env.LIVEDESK_CLIENT_SLOT || process.env.MINDEXEC_REMOTE_SLOT || '';
|
|
395
404
|
let authPort = normalizePort(process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_CLIENT_AUTH_PORT) || DEFAULT_AUTH_CALLBACK_PORT;
|
|
396
405
|
let command = 'connect';
|
|
397
|
-
let
|
|
398
|
-
let fakeThumbnail = false;
|
|
406
|
+
let fakeThumbnail = false;
|
|
399
407
|
let startupRun = isTruthy(process.env.LIVEDESK_ROLE_TRANSITION);
|
|
400
|
-
let openBrowserOnStart = !isTruthy(process.env.LIVEDESK_SKIP_BROWSER_OPEN);
|
|
401
|
-
let updateWaitPid = 0;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
408
|
+
let openBrowserOnStart = !isTruthy(process.env.LIVEDESK_SKIP_BROWSER_OPEN);
|
|
409
|
+
let updateWaitPid = 0;
|
|
410
|
+
let retiredModelOptionsDiscarded = 0;
|
|
411
|
+
|
|
412
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
413
|
+
const arg = argv[index];
|
|
414
|
+
if (RETIRED_CLIENT_MODEL_SINGLE_OPTIONS.has(arg)) {
|
|
415
|
+
retiredModelOptionsDiscarded += 1;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (arg === RETIRED_CLIENT_MODEL_VALUE_OPTION) {
|
|
419
|
+
retiredModelOptionsDiscarded += 1;
|
|
420
|
+
if (index + 1 < argv.length && !String(argv[index + 1] || '').startsWith('-')) {
|
|
421
|
+
index += 1;
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (String(arg || '').startsWith(`${RETIRED_CLIENT_MODEL_VALUE_OPTION}=`)) {
|
|
426
|
+
retiredModelOptionsDiscarded += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (arg && !arg.startsWith('-')) {
|
|
406
430
|
const lowerArg = arg.toLowerCase();
|
|
407
431
|
const shortcutSlot = normalizeSlotNumber(arg);
|
|
408
432
|
if (lowerArg === 'connect') {
|
|
@@ -512,11 +536,8 @@ export function parseLauncherArgs(argv) {
|
|
|
512
536
|
if (arg === '--version') {
|
|
513
537
|
version = true;
|
|
514
538
|
}
|
|
515
|
-
if (arg === '--
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
if (arg === '--fake-thumbnail') {
|
|
519
|
-
fakeThumbnail = true;
|
|
539
|
+
if (arg === '--fake-thumbnail') {
|
|
540
|
+
fakeThumbnail = true;
|
|
520
541
|
}
|
|
521
542
|
forwarded.push(arg);
|
|
522
543
|
}
|
|
@@ -539,13 +560,13 @@ export function parseLauncherArgs(argv) {
|
|
|
539
560
|
deviceId,
|
|
540
561
|
slot,
|
|
541
562
|
authPort,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
};
|
|
548
|
-
}
|
|
563
|
+
fakeThumbnail,
|
|
564
|
+
startupRun,
|
|
565
|
+
openBrowserOnStart,
|
|
566
|
+
updateWaitPid,
|
|
567
|
+
retiredModelOptionsDiscarded
|
|
568
|
+
};
|
|
569
|
+
}
|
|
549
570
|
|
|
550
571
|
function normalizePort(value) {
|
|
551
572
|
const port = Number(String(value || '').trim());
|
|
@@ -854,13 +875,15 @@ const CLIENT_UPDATE_SINGLE_FLAGS = new Set([
|
|
|
854
875
|
'--no-open',
|
|
855
876
|
'--exit-on-disconnect',
|
|
856
877
|
'--exit-on-invalid-pair',
|
|
857
|
-
'--once'
|
|
878
|
+
'--once',
|
|
879
|
+
...RETIRED_CLIENT_MODEL_SINGLE_OPTIONS
|
|
858
880
|
]);
|
|
859
881
|
const CLIENT_UPDATE_VALUE_FLAGS = new Set([
|
|
860
882
|
'--update-wait-pid',
|
|
861
883
|
// Pairing credentials stay in the inherited private environment and must
|
|
862
884
|
// never be copied onto a replacement process command line.
|
|
863
|
-
'--pair'
|
|
885
|
+
'--pair',
|
|
886
|
+
RETIRED_CLIENT_MODEL_VALUE_OPTION
|
|
864
887
|
]);
|
|
865
888
|
|
|
866
889
|
export function buildClientUpdateRestartArgs(prepared = {}) {
|
|
@@ -874,6 +897,9 @@ export function buildClientUpdateRestartArgs(prepared = {}) {
|
|
|
874
897
|
if (arg.startsWith('--pair=')) {
|
|
875
898
|
continue;
|
|
876
899
|
}
|
|
900
|
+
if (arg.startsWith(`${RETIRED_CLIENT_MODEL_VALUE_OPTION}=`)) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
877
903
|
if (CLIENT_UPDATE_VALUE_FLAGS.has(arg)) {
|
|
878
904
|
index += 1;
|
|
879
905
|
continue;
|
|
@@ -945,7 +971,9 @@ export function normalizeCachedHubTarget(value, expectedOwnerKey, now = Date.now
|
|
|
945
971
|
return null;
|
|
946
972
|
}
|
|
947
973
|
const updatedAt = Date.parse(String(cached.updatedAt || ''));
|
|
948
|
-
if (!Number.isFinite(updatedAt)
|
|
974
|
+
if (!Number.isFinite(updatedAt)
|
|
975
|
+
|| updatedAt > now + HUB_TARGET_CACHE_FUTURE_SKEW_MS
|
|
976
|
+
|| now - updatedAt > HUB_TARGET_CACHE_MAX_AGE_MS) {
|
|
949
977
|
return null;
|
|
950
978
|
}
|
|
951
979
|
const pair = String(cached.pair || '').trim();
|
|
@@ -1109,13 +1137,63 @@ function isTransientNetworkError(error) {
|
|
|
1109
1137
|
return /fetch failed|network|dns|socket|connection|timeout|getaddrinfo/i.test(getNestedErrorMessage(error));
|
|
1110
1138
|
}
|
|
1111
1139
|
|
|
1112
|
-
function formatDiscoveryError(error) {
|
|
1113
|
-
if (isTransientNetworkError(error)) {
|
|
1114
|
-
const code = getNestedErrorCode(error);
|
|
1115
|
-
return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
|
|
1116
|
-
}
|
|
1117
|
-
return error instanceof Error ? error.message : String(error);
|
|
1118
|
-
}
|
|
1140
|
+
function formatDiscoveryError(error) {
|
|
1141
|
+
if (isTransientNetworkError(error)) {
|
|
1142
|
+
const code = getNestedErrorCode(error);
|
|
1143
|
+
return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
|
|
1144
|
+
}
|
|
1145
|
+
return error instanceof Error ? error.message : String(error);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function createHubDiscoveryError(code, message, cause = null) {
|
|
1149
|
+
const error = new Error(message);
|
|
1150
|
+
error.code = String(code || '').trim();
|
|
1151
|
+
if (cause) {
|
|
1152
|
+
error.cause = cause;
|
|
1153
|
+
}
|
|
1154
|
+
return error;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export function canUseCachedRelayAfterRegistryError(error) {
|
|
1158
|
+
const code = getNestedErrorCode(error).toLowerCase();
|
|
1159
|
+
if ([
|
|
1160
|
+
'hub-registry-missing',
|
|
1161
|
+
'hub-registry-expired',
|
|
1162
|
+
'hub-registry-timeout',
|
|
1163
|
+
'pin-not-found',
|
|
1164
|
+
'pin-expired',
|
|
1165
|
+
'resolver-unavailable'
|
|
1166
|
+
].includes(code)) {
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
// Semantic registry failures must never be reclassified as network errors
|
|
1170
|
+
// merely because their message mentions a "connection token".
|
|
1171
|
+
if (code.startsWith('hub-registry-')
|
|
1172
|
+
|| code.startsWith('pin-registry-')
|
|
1173
|
+
|| ['invalid-pin', 'rate-limited', 'request-too-large'].includes(code)) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
return isTransientNetworkError(error);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async function normalizePinResolverInvocationError(error) {
|
|
1180
|
+
if (isTransientNetworkError(error)) {
|
|
1181
|
+
return error;
|
|
1182
|
+
}
|
|
1183
|
+
const response = error?.context;
|
|
1184
|
+
const status = Number(response?.status || 0);
|
|
1185
|
+
let reason = '';
|
|
1186
|
+
try {
|
|
1187
|
+
const payload = await response?.clone?.().json?.();
|
|
1188
|
+
reason = String(payload?.reason || '').trim().toLowerCase();
|
|
1189
|
+
} catch {
|
|
1190
|
+
// HTTP status still gives a conservative semantic classification below.
|
|
1191
|
+
}
|
|
1192
|
+
if (!reason && status === 404) reason = 'pin-not-found';
|
|
1193
|
+
if (!reason && status === 503) reason = 'resolver-unavailable';
|
|
1194
|
+
if (!reason) return error;
|
|
1195
|
+
return createHubDiscoveryError(reason, reason, error);
|
|
1196
|
+
}
|
|
1119
1197
|
|
|
1120
1198
|
async function recoverRotatedSession(supabase, previousSession) {
|
|
1121
1199
|
for (const delayMs of [100, 250, 500]) {
|
|
@@ -2618,54 +2696,72 @@ function readRequestBody(req, maxBytes = 4096) {
|
|
|
2618
2696
|
});
|
|
2619
2697
|
}
|
|
2620
2698
|
|
|
2621
|
-
async function resolveManagerFromPin(supabase, pin, options = {}) {
|
|
2699
|
+
export async function resolveManagerFromPin(supabase, pin, options = {}) {
|
|
2622
2700
|
const normalizedPin = normalizePairingPin(pin);
|
|
2623
2701
|
if (!normalizedPin) {
|
|
2624
2702
|
throw new Error('Enter a 6-digit LiveDesk PIN.');
|
|
2625
2703
|
}
|
|
2626
2704
|
const cacheOwnerKey = hubCacheOwnerForPin(normalizedPin);
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2705
|
+
const resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
|
|
2706
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2707
|
+
relayEndpoint: options.relayEndpoint,
|
|
2708
|
+
probeEndpoint: options.probeEndpoint,
|
|
2709
|
+
resolveFreshTarget: async () => {
|
|
2710
|
+
const { data, error } = await supabase.functions.invoke('livedesk-resolve-remote-host', {
|
|
2711
|
+
body: { pin: normalizedPin }
|
|
2712
|
+
});
|
|
2713
|
+
if (error) {
|
|
2714
|
+
throw await normalizePinResolverInvocationError(error);
|
|
2715
|
+
}
|
|
2716
|
+
const result = Array.isArray(data) ? data[0] : data;
|
|
2717
|
+
if (!result?.ok) {
|
|
2718
|
+
const reason = String(result?.reason || 'pin-not-found').trim().toLowerCase();
|
|
2719
|
+
throw createHubDiscoveryError(reason, reason);
|
|
2720
|
+
}
|
|
2721
|
+
if (!result.pair_token) {
|
|
2722
|
+
throw createHubDiscoveryError(
|
|
2723
|
+
'pin-registry-pair-token-missing',
|
|
2724
|
+
'The PIN matched a Hub record without a private connection token.'
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
const endpointCandidates = normalizeEndpointCandidates(result);
|
|
2728
|
+
if (endpointCandidates.length === 0) {
|
|
2729
|
+
throw createHubDiscoveryError(
|
|
2730
|
+
'pin-registry-endpoints-missing',
|
|
2731
|
+
'The PIN matched a Hub record without a reachable address.'
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
const target = await chooseManagerConnectionTarget(endpointCandidates, {
|
|
2735
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2736
|
+
probeEndpoint: options.probeEndpoint
|
|
2737
|
+
});
|
|
2738
|
+
if (!target) {
|
|
2739
|
+
throw createHubDiscoveryError(
|
|
2740
|
+
'pin-direct-unreachable',
|
|
2741
|
+
`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
manager: target.manager,
|
|
2746
|
+
pair: result.pair_token,
|
|
2747
|
+
hubDeviceId: String(result.node_id || '').trim(),
|
|
2748
|
+
endpointCandidates,
|
|
2749
|
+
directReachable: target.directReachable,
|
|
2750
|
+
connectionTransport: target.connectionTransport,
|
|
2751
|
+
discoverySource: 'supabase'
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2652
2754
|
});
|
|
2653
|
-
if (
|
|
2654
|
-
|
|
2755
|
+
if (resolved.discoverySource === 'supabase') {
|
|
2756
|
+
writeCachedHubTarget(cacheOwnerKey, resolved);
|
|
2655
2757
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
endpointCandidates,
|
|
2662
|
-
directReachable: target.directReachable,
|
|
2663
|
-
connectionTransport: target.connectionTransport,
|
|
2664
|
-
discoverySource: 'supabase'
|
|
2665
|
-
};
|
|
2666
|
-
writeCachedHubTarget(cacheOwnerKey, resolved);
|
|
2758
|
+
console.log(resolved.connectionTransport === 'relay-fallback' && resolved.discoverySource === 'cache'
|
|
2759
|
+
? 'Fresh PIN registry lookup was unavailable; using the saved PIN-bound identity for the bounded encrypted relay path.'
|
|
2760
|
+
: resolved.discoverySource === 'cache'
|
|
2761
|
+
? `Fresh PIN registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`
|
|
2762
|
+
: `Found current PIN-bound LiveDesk Hub at ${resolved.manager}.`);
|
|
2667
2763
|
return resolved;
|
|
2668
|
-
}
|
|
2764
|
+
}
|
|
2669
2765
|
|
|
2670
2766
|
async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
|
|
2671
2767
|
const configuredIntervalMs = Number(options.intervalMs || 0);
|
|
@@ -2680,7 +2776,8 @@ async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
|
|
|
2680
2776
|
attempts += 1;
|
|
2681
2777
|
try {
|
|
2682
2778
|
return await resolveManagerFromPin(supabase, pin, {
|
|
2683
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
2779
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2780
|
+
relayEndpoint: options.relayEndpoint
|
|
2684
2781
|
});
|
|
2685
2782
|
} catch (err) {
|
|
2686
2783
|
if (shouldStop()) {
|
|
@@ -2834,9 +2931,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
2834
2931
|
}
|
|
2835
2932
|
dashboardState.message = 'Saved LiveDesk PIN found. Waiting for the Hub to become reachable.';
|
|
2836
2933
|
try {
|
|
2837
|
-
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
2838
|
-
|
|
2839
|
-
|
|
2934
|
+
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
2935
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2936
|
+
relayEndpoint: options.relayEndpoint,
|
|
2937
|
+
shouldStop: () => completed || dashboardState.loggedOut
|
|
2938
|
+
});
|
|
2840
2939
|
if (!resolved) {
|
|
2841
2940
|
return;
|
|
2842
2941
|
}
|
|
@@ -3066,7 +3165,8 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3066
3165
|
pin = normalizePairingPin(pin);
|
|
3067
3166
|
try {
|
|
3068
3167
|
const resolved = await resolveManagerFromPin(supabase, pin, {
|
|
3069
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3168
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3169
|
+
relayEndpoint: options.relayEndpoint
|
|
3070
3170
|
});
|
|
3071
3171
|
writeSavedPin(pin);
|
|
3072
3172
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
@@ -3229,7 +3329,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3229
3329
|
resolvePin: pin => {
|
|
3230
3330
|
if (!supabase) throw new Error('supabase-session-required');
|
|
3231
3331
|
return resolveManagerFromPin(supabase, pin, {
|
|
3232
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3332
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3333
|
+
relayEndpoint: options.relayEndpoint
|
|
3233
3334
|
});
|
|
3234
3335
|
},
|
|
3235
3336
|
changeRole: async (role, snapshot) => {
|
|
@@ -3328,7 +3429,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3328
3429
|
startupArgs: options.startupArgs,
|
|
3329
3430
|
savedSession: options.savedSession,
|
|
3330
3431
|
savedPin: options.savedPin,
|
|
3331
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3432
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3433
|
+
relayEndpoint: options.relayEndpoint
|
|
3332
3434
|
});
|
|
3333
3435
|
if (options.openBrowser !== false) {
|
|
3334
3436
|
console.log('Opening LiveDesk connection page...');
|
|
@@ -3506,10 +3608,16 @@ export async function chooseManagerConnectionTarget(candidates, options = {}) {
|
|
|
3506
3608
|
}
|
|
3507
3609
|
|
|
3508
3610
|
export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
3509
|
-
const cached = options
|
|
3611
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3612
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3613
|
+
: readCachedHubTarget(ownerKey);
|
|
3510
3614
|
if (!cached) return null;
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3615
|
+
const target = await chooseManagerConnectionTarget(cached.endpointCandidates, {
|
|
3616
|
+
allowRelayFallback: false,
|
|
3617
|
+
probeEndpoint: options.probeEndpoint
|
|
3618
|
+
});
|
|
3619
|
+
if (!target) return null;
|
|
3620
|
+
const manager = target.manager;
|
|
3513
3621
|
const resolved = {
|
|
3514
3622
|
manager,
|
|
3515
3623
|
pair: cached.pair,
|
|
@@ -3518,6 +3626,8 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
|
3518
3626
|
manager,
|
|
3519
3627
|
...cached.endpointCandidates.filter(endpoint => endpoint !== manager)
|
|
3520
3628
|
],
|
|
3629
|
+
directReachable: target.directReachable,
|
|
3630
|
+
connectionTransport: target.connectionTransport,
|
|
3521
3631
|
discoverySource: 'cache'
|
|
3522
3632
|
};
|
|
3523
3633
|
if (options.persist !== false) {
|
|
@@ -3526,6 +3636,86 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
|
3526
3636
|
return resolved;
|
|
3527
3637
|
}
|
|
3528
3638
|
|
|
3639
|
+
export function resolveManagerFromCachedRelayFallback(ownerKey, options = {}) {
|
|
3640
|
+
if (options.allowRelayFallback !== true) return null;
|
|
3641
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3642
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3643
|
+
: readCachedHubTarget(ownerKey);
|
|
3644
|
+
if (!cached) return null;
|
|
3645
|
+
const normalizedOwner = String(cached.ownerKey || '');
|
|
3646
|
+
const accountIdentityComplete = normalizedOwner.startsWith('user:')
|
|
3647
|
+
&& normalizedOwner.length > 'user:'.length
|
|
3648
|
+
&& Boolean(cached.hubDeviceId);
|
|
3649
|
+
// The public PIN resolver intentionally does not expose node_id. Its
|
|
3650
|
+
// previous signed pair/endpoints remain bound to the exact saved six-digit
|
|
3651
|
+
// PIN owner in the private, finite-lifetime cache.
|
|
3652
|
+
const pinIdentityComplete = /^pin:[0-9]{6}$/.test(normalizedOwner);
|
|
3653
|
+
const relayEndpoint = String(options.relayEndpoint || '').trim().replace(/^tcp:\/\//i, '');
|
|
3654
|
+
if ((!accountIdentityComplete && !pinIdentityComplete) || !parseManagerEndpoint(relayEndpoint)) {
|
|
3655
|
+
return null;
|
|
3656
|
+
}
|
|
3657
|
+
return {
|
|
3658
|
+
manager: cached.endpointCandidates[0],
|
|
3659
|
+
pair: cached.pair,
|
|
3660
|
+
hubDeviceId: cached.hubDeviceId,
|
|
3661
|
+
endpointCandidates: [...cached.endpointCandidates],
|
|
3662
|
+
directReachable: false,
|
|
3663
|
+
connectionTransport: 'relay-fallback',
|
|
3664
|
+
discoverySource: 'cache'
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
export async function resolveManagerWithCachedRecovery(ownerKey, options = {}) {
|
|
3669
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3670
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3671
|
+
: readCachedHubTarget(ownerKey);
|
|
3672
|
+
try {
|
|
3673
|
+
if (typeof options.resolveFreshTarget !== 'function') {
|
|
3674
|
+
throw createHubDiscoveryError('fresh-registry-resolver-required', 'Fresh Hub registry resolver is required.');
|
|
3675
|
+
}
|
|
3676
|
+
const configuredFreshTimeoutMs = Number(options.freshTimeoutMs);
|
|
3677
|
+
const freshTimeoutMs = Number.isFinite(configuredFreshTimeoutMs)
|
|
3678
|
+
? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configuredFreshTimeoutMs))
|
|
3679
|
+
: FRESH_REGISTRY_TIMEOUT_MS;
|
|
3680
|
+
let timeoutHandle = null;
|
|
3681
|
+
const fresh = await Promise.race([
|
|
3682
|
+
Promise.resolve().then(() => options.resolveFreshTarget()),
|
|
3683
|
+
new Promise((_, reject) => {
|
|
3684
|
+
timeoutHandle = setTimeout(() => reject(createHubDiscoveryError(
|
|
3685
|
+
'hub-registry-timeout',
|
|
3686
|
+
`Fresh LiveDesk Hub registry lookup exceeded ${freshTimeoutMs}ms.`
|
|
3687
|
+
)), freshTimeoutMs);
|
|
3688
|
+
})
|
|
3689
|
+
]).finally(() => clearTimeout(timeoutHandle));
|
|
3690
|
+
if (!fresh?.manager || !fresh?.pair) {
|
|
3691
|
+
throw createHubDiscoveryError('fresh-registry-target-invalid', 'Fresh Hub registry target is incomplete.');
|
|
3692
|
+
}
|
|
3693
|
+
return {
|
|
3694
|
+
...fresh,
|
|
3695
|
+
discoverySource: fresh.discoverySource || 'supabase'
|
|
3696
|
+
};
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
if (canUseCachedRelayAfterRegistryError(error)) {
|
|
3699
|
+
const direct = await resolveManagerFromCachedTarget(ownerKey, {
|
|
3700
|
+
cachedTarget: cached,
|
|
3701
|
+
probeEndpoint: options.probeEndpoint,
|
|
3702
|
+
// A TCP listener is not pair-token authentication. Only a
|
|
3703
|
+
// fresh signed registry result may extend cache trust.
|
|
3704
|
+
persist: false
|
|
3705
|
+
});
|
|
3706
|
+
if (direct) return direct;
|
|
3707
|
+
const relay = resolveManagerFromCachedRelayFallback(ownerKey, {
|
|
3708
|
+
cachedTarget: cached,
|
|
3709
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3710
|
+
relayEndpoint: options.relayEndpoint,
|
|
3711
|
+
now: options.now
|
|
3712
|
+
});
|
|
3713
|
+
if (relay) return relay;
|
|
3714
|
+
}
|
|
3715
|
+
throw error;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3529
3719
|
export function getDiscoveryRetryDelay(attempt, configuredIntervalMs = 0, random = Math.random) {
|
|
3530
3720
|
const configured = Number(configuredIntervalMs);
|
|
3531
3721
|
if (Number.isFinite(configured) && configured > 0) {
|
|
@@ -3577,34 +3767,50 @@ function normalizeEndpointCandidates(target) {
|
|
|
3577
3767
|
return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
|
|
3578
3768
|
}
|
|
3579
3769
|
|
|
3580
|
-
async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
3770
|
+
export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
3581
3771
|
await refreshSessionIfNeeded(supabase);
|
|
3582
3772
|
const { data, error } = await supabase
|
|
3583
3773
|
.from('livedesk_remote_host_targets')
|
|
3584
3774
|
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
3585
3775
|
.eq('product_key', 'livedesk')
|
|
3586
3776
|
.maybeSingle();
|
|
3587
|
-
if (error) {
|
|
3588
|
-
throw error;
|
|
3589
|
-
}
|
|
3590
|
-
if (!data?.active) {
|
|
3591
|
-
throw
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
}
|
|
3596
|
-
if (
|
|
3597
|
-
throw
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3777
|
+
if (error) {
|
|
3778
|
+
throw error;
|
|
3779
|
+
}
|
|
3780
|
+
if (!data?.active) {
|
|
3781
|
+
throw createHubDiscoveryError(
|
|
3782
|
+
'hub-registry-missing',
|
|
3783
|
+
'No active LiveDesk Hub was found for this Google account. Start LiveDesk Hub on the screen wall computer and sign in there first.'
|
|
3784
|
+
);
|
|
3785
|
+
}
|
|
3786
|
+
if (data.expires_at && Date.parse(data.expires_at) <= Date.now()) {
|
|
3787
|
+
throw createHubDiscoveryError(
|
|
3788
|
+
'hub-registry-expired',
|
|
3789
|
+
'The LiveDesk Hub record is expired. Open the Hub screen again while signed in.'
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
if (!data.pair_token) {
|
|
3793
|
+
throw createHubDiscoveryError(
|
|
3794
|
+
'hub-registry-pair-token-missing',
|
|
3795
|
+
'The LiveDesk Hub record is missing its private connection token.'
|
|
3796
|
+
);
|
|
3797
|
+
}
|
|
3798
|
+
const endpointCandidates = normalizeEndpointCandidates(data);
|
|
3799
|
+
if (endpointCandidates.length === 0) {
|
|
3800
|
+
throw createHubDiscoveryError(
|
|
3801
|
+
'hub-registry-endpoints-missing',
|
|
3802
|
+
'The LiveDesk Hub record does not contain a reachable local address.'
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3603
3805
|
const target = await chooseManagerConnectionTarget(endpointCandidates, {
|
|
3604
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3806
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3807
|
+
probeEndpoint: options.probeEndpoint
|
|
3605
3808
|
});
|
|
3606
3809
|
if (!target) {
|
|
3607
|
-
throw
|
|
3810
|
+
throw createHubDiscoveryError(
|
|
3811
|
+
'hub-direct-unreachable',
|
|
3812
|
+
`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
|
|
3813
|
+
);
|
|
3608
3814
|
}
|
|
3609
3815
|
return {
|
|
3610
3816
|
manager: target.manager,
|
|
@@ -3647,6 +3853,7 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3647
3853
|
let attempts = 0;
|
|
3648
3854
|
let lastMessage = '';
|
|
3649
3855
|
let wakeListener = null;
|
|
3856
|
+
let initialError = options.initialError || null;
|
|
3650
3857
|
console.log('Waiting for a LiveDesk Hub. LiveDesk is listening for Hub-online events with adaptive registry retries as a fallback.');
|
|
3651
3858
|
try {
|
|
3652
3859
|
while (true) {
|
|
@@ -3655,6 +3862,11 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3655
3862
|
}
|
|
3656
3863
|
attempts += 1;
|
|
3657
3864
|
try {
|
|
3865
|
+
if (initialError) {
|
|
3866
|
+
const error = initialError;
|
|
3867
|
+
initialError = null;
|
|
3868
|
+
throw error;
|
|
3869
|
+
}
|
|
3658
3870
|
return await resolveManagerFromSupabase(supabase, {
|
|
3659
3871
|
allowRelayFallback: options.allowRelayFallback === true
|
|
3660
3872
|
});
|
|
@@ -3730,7 +3942,8 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3730
3942
|
choice = { type: 'google', session: savedSession };
|
|
3731
3943
|
} else if ((parsed.startupRun || existingConnectionPage) && savedPin) {
|
|
3732
3944
|
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
3733
|
-
allowRelayFallback
|
|
3945
|
+
allowRelayFallback,
|
|
3946
|
+
relayEndpoint: parsed.relay
|
|
3734
3947
|
});
|
|
3735
3948
|
choice = { type: 'pin', ...resolved };
|
|
3736
3949
|
} else if (existingConnectionPage && previousChoice) {
|
|
@@ -3747,8 +3960,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3747
3960
|
savedSession,
|
|
3748
3961
|
savedPin,
|
|
3749
3962
|
allowRelayFallback,
|
|
3963
|
+
relayEndpoint: parsed.relay,
|
|
3750
3964
|
openBrowser: parsed.openBrowserOnStart
|
|
3751
|
-
});
|
|
3965
|
+
});
|
|
3752
3966
|
}
|
|
3753
3967
|
connectionPage = existingConnectionPage || choice.connectionPage || null;
|
|
3754
3968
|
if (choice.type === 'pin') {
|
|
@@ -3770,26 +3984,39 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3770
3984
|
: 'Connected by LiveDesk PIN.');
|
|
3771
3985
|
} else {
|
|
3772
3986
|
const cacheOwnerKey = hubCacheOwnerForSession(choice.session);
|
|
3773
|
-
let resolved = await resolveManagerFromCachedTarget(cacheOwnerKey);
|
|
3774
3987
|
let session = choice.session;
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3988
|
+
let initialDiscoveryError = null;
|
|
3989
|
+
let resolved = null;
|
|
3990
|
+
try {
|
|
3991
|
+
resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
|
|
3992
|
+
allowRelayFallback,
|
|
3993
|
+
relayEndpoint: parsed.relay,
|
|
3994
|
+
resolveFreshTarget: async () => {
|
|
3995
|
+
// The signed registry is authoritative for Hub replacement
|
|
3996
|
+
// and pair-token rotation. Cache direct/relay recovery is
|
|
3997
|
+
// considered only after this one current lookup reports
|
|
3998
|
+
// an explicitly recoverable availability failure.
|
|
3999
|
+
try {
|
|
4000
|
+
session = await activateSupabaseSession(supabase, choice.session);
|
|
4001
|
+
} catch (error) {
|
|
4002
|
+
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
4003
|
+
session = readSavedSessionFromFile() || choice.session;
|
|
4004
|
+
console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
|
|
4005
|
+
}
|
|
4006
|
+
return await resolveManagerFromSupabase(supabase, {
|
|
4007
|
+
allowRelayFallback
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
} catch (error) {
|
|
4012
|
+
initialDiscoveryError = error;
|
|
4013
|
+
}
|
|
4014
|
+
discoverySource = resolved?.discoverySource || 'supabase';
|
|
4015
|
+
if (resolved?.discoverySource === 'cache') {
|
|
3779
4016
|
writeSavedSessionToFile(session);
|
|
3780
|
-
console.log(
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
// Activate the accepted browser session only when registry
|
|
3784
|
-
// discovery is needed; a reachable account-bound cache can
|
|
3785
|
-
// start the Agent without a Supabase round trip.
|
|
3786
|
-
try {
|
|
3787
|
-
session = await activateSupabaseSession(supabase, choice.session);
|
|
3788
|
-
} catch (error) {
|
|
3789
|
-
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
3790
|
-
session = readSavedSessionFromFile() || choice.session;
|
|
3791
|
-
console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
|
|
3792
|
-
}
|
|
4017
|
+
console.log(resolved.connectionTransport === 'relay-fallback'
|
|
4018
|
+
? `Fresh Hub registry lookup was unavailable; using saved account-bound Hub identity ${resolved.hubDeviceId} for the bounded encrypted relay path.`
|
|
4019
|
+
: `Fresh Hub registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`);
|
|
3793
4020
|
}
|
|
3794
4021
|
choice.session = session;
|
|
3795
4022
|
const email = session?.user?.email ? ` as ${session.user.email}` : '';
|
|
@@ -3797,18 +4024,20 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3797
4024
|
if (!resolved) {
|
|
3798
4025
|
resolved = await waitForManagerFromSupabase(supabase, {
|
|
3799
4026
|
allowRelayFallback,
|
|
4027
|
+
initialError: initialDiscoveryError,
|
|
3800
4028
|
shouldStop: () => Boolean(roleRestartRequest?.role)
|
|
3801
4029
|
});
|
|
4030
|
+
discoverySource = 'supabase';
|
|
3802
4031
|
}
|
|
3803
4032
|
if (!resolved && roleRestartRequest?.role) {
|
|
3804
|
-
return {
|
|
3805
|
-
...parsed,
|
|
3806
|
-
manager,
|
|
3807
|
-
pair,
|
|
3808
|
-
slot: normalizeSlotNumber(parsed.slot),
|
|
3809
|
-
connectionPage,
|
|
3810
|
-
forwarded,
|
|
3811
|
-
rediscoverOnDisconnect: shouldLogin,
|
|
4033
|
+
return {
|
|
4034
|
+
...parsed,
|
|
4035
|
+
manager,
|
|
4036
|
+
pair,
|
|
4037
|
+
slot: normalizeSlotNumber(parsed.slot),
|
|
4038
|
+
connectionPage,
|
|
4039
|
+
forwarded,
|
|
4040
|
+
rediscoverOnDisconnect: shouldLogin,
|
|
3812
4041
|
rediscoverOnInvalidPair: shouldLogin
|
|
3813
4042
|
};
|
|
3814
4043
|
}
|
|
@@ -4233,6 +4462,18 @@ export function buildFastArgs(args, fakeThumbnail, transport = 'auto', relay = '
|
|
|
4233
4462
|
const forwarded = [];
|
|
4234
4463
|
for (let index = 0; index < args.length; index += 1) {
|
|
4235
4464
|
const arg = args[index];
|
|
4465
|
+
if (RETIRED_CLIENT_MODEL_SINGLE_OPTIONS.has(arg)) {
|
|
4466
|
+
continue;
|
|
4467
|
+
}
|
|
4468
|
+
if (arg === RETIRED_CLIENT_MODEL_VALUE_OPTION) {
|
|
4469
|
+
if (index + 1 < args.length && !String(args[index + 1] || '').startsWith('-')) {
|
|
4470
|
+
index += 1;
|
|
4471
|
+
}
|
|
4472
|
+
continue;
|
|
4473
|
+
}
|
|
4474
|
+
if (String(arg || '').startsWith(`${RETIRED_CLIENT_MODEL_VALUE_OPTION}=`)) {
|
|
4475
|
+
continue;
|
|
4476
|
+
}
|
|
4236
4477
|
if (arg === '--transport' || arg === '--relay') {
|
|
4237
4478
|
index += 1;
|
|
4238
4479
|
continue;
|
|
@@ -4241,8 +4482,8 @@ export function buildFastArgs(args, fakeThumbnail, transport = 'auto', relay = '
|
|
|
4241
4482
|
forwarded.push('--fake-frame');
|
|
4242
4483
|
continue;
|
|
4243
4484
|
}
|
|
4244
|
-
if (arg === '--tasks' || arg === '--no-tasks'
|
|
4245
|
-
continue;
|
|
4485
|
+
if (arg === '--tasks' || arg === '--no-tasks') {
|
|
4486
|
+
continue;
|
|
4246
4487
|
}
|
|
4247
4488
|
forwarded.push(arg);
|
|
4248
4489
|
}
|
|
@@ -4446,15 +4687,15 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
|
|
|
4446
4687
|
});
|
|
4447
4688
|
}
|
|
4448
4689
|
|
|
4449
|
-
function shouldUseFast(parsed) {
|
|
4450
|
-
if (parsed.engine === 'node') {
|
|
4451
|
-
return false;
|
|
4690
|
+
function shouldUseFast(parsed) {
|
|
4691
|
+
if (parsed.engine === 'node') {
|
|
4692
|
+
return false;
|
|
4452
4693
|
}
|
|
4453
4694
|
if (parsed.engine === 'fast') {
|
|
4454
4695
|
return true;
|
|
4455
4696
|
}
|
|
4456
|
-
return
|
|
4457
|
-
}
|
|
4697
|
+
return true;
|
|
4698
|
+
}
|
|
4458
4699
|
|
|
4459
4700
|
function shouldTryFast(parsed, runtime) {
|
|
4460
4701
|
if (parsed.engine === 'fast') {
|
|
@@ -4480,6 +4721,12 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
|
|
|
4480
4721
|
console.warn('[LiveDesk] @livedesk/client is deprecated. Use the unified livedesk package or LiveDesk Desktop.');
|
|
4481
4722
|
}
|
|
4482
4723
|
const parsed = parseLauncherArgs(argv);
|
|
4724
|
+
if (parsed.retiredModelOptionsDiscarded > 0) {
|
|
4725
|
+
console.warn(
|
|
4726
|
+
'[LiveDesk Client] Ignored retired Client model-runtime options. '
|
|
4727
|
+
+ 'Approved AI computer commands are handled by the Hub Codex Agent.'
|
|
4728
|
+
);
|
|
4729
|
+
}
|
|
4483
4730
|
if (parsed.updateWaitPid) {
|
|
4484
4731
|
await waitForProcessExit(parsed.updateWaitPid);
|
|
4485
4732
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.205",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,14 +37,13 @@
|
|
|
37
37
|
"@livedesk/runtime-core": "0.1.0",
|
|
38
38
|
"@supabase/supabase-js": "^2.110.0",
|
|
39
39
|
"node-screenshots": "^0.2.8",
|
|
40
|
-
"openai": "^6.42.0",
|
|
41
40
|
"ws": "^8.18.3"
|
|
42
41
|
},
|
|
43
42
|
"optionalDependencies": {
|
|
44
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
46
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
47
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
43
|
+
"@livedesk/fast-linux-x64": "0.1.412",
|
|
44
|
+
"@livedesk/fast-osx-arm64": "0.1.412",
|
|
45
|
+
"@livedesk/fast-osx-x64": "0.1.412",
|
|
46
|
+
"@livedesk/fast-win-x64": "0.1.412"
|
|
48
47
|
},
|
|
49
48
|
"publishConfig": {
|
|
50
49
|
"access": "public"
|