@livedesk/client 0.1.22 → 0.1.24
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/README.md +7 -7
- package/bin/livedesk-client-node.js +11 -11
- package/bin/livedesk-client.js +72 -14
- package/fast/linux-x64/mindexec-remote-fast.dll +0 -0
- package/fast/linux-x64/mindexec-remote-fast.pdb +0 -0
- package/fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/fast/osx-arm64/mindexec-remote-fast.pdb +0 -0
- package/fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/fast/osx-x64/mindexec-remote-fast.pdb +0 -0
- package/fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/fast/win-x64/mindexec-remote-fast.pdb +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,14 +7,14 @@ npx @livedesk/client
|
|
|
7
7
|
npx @livedesk/client 3
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
The default flow opens Google sign-in, waits for the active LiveDesk
|
|
11
|
-
published by the same account, and connects locally. If the
|
|
10
|
+
The default flow opens Google sign-in, waits for the active LiveDesk Hub
|
|
11
|
+
published by the same account, and connects locally. If the Hub is not
|
|
12
12
|
ready yet, the client keeps checking every 5 seconds instead of exiting. Omit the
|
|
13
13
|
number for first-available placement, or pass `1` to `999` to pin this machine
|
|
14
14
|
to a screen wall slot.
|
|
15
15
|
|
|
16
|
-
On the
|
|
17
|
-
dashboard keeps its local
|
|
16
|
+
On the Hub computer, open the LiveDesk dashboard and sign in first. The
|
|
17
|
+
dashboard keeps its local Hub address and private pair token refreshed in the
|
|
18
18
|
LiveDesk Supabase registry for the signed-in account.
|
|
19
19
|
|
|
20
20
|
Supabase Auth must allow the CLI callback URL:
|
|
@@ -27,11 +27,11 @@ If you use a custom port, run `npx @livedesk/client --auth-port 5200` and add
|
|
|
27
27
|
the matching callback URL to Supabase Auth redirect URLs.
|
|
28
28
|
|
|
29
29
|
The client registers the device, sends status heartbeats, can return
|
|
30
|
-
|
|
30
|
+
Hub-requested thumbnails, can stream a focused view-only live screen, and
|
|
31
31
|
can receive safe task-only instructions. The C# RemoteFast engine also supports
|
|
32
32
|
keyboard/mouse control, file transfer, and Windows system-audio loopback
|
|
33
|
-
playback. When file transfer is enabled by the
|
|
34
|
-
to `Desktop/LiveDeskFiles` unless the
|
|
33
|
+
playback. When file transfer is enabled by the Hub, received files are saved
|
|
34
|
+
to `Desktop/LiveDeskFiles` unless the Hub sets another destination folder.
|
|
35
35
|
|
|
36
36
|
By default, the launcher uses the packaged C# RemoteFast engine when supported.
|
|
37
37
|
It falls back to the Node engine for AI assist or when a compatible RemoteFast
|
|
@@ -6,7 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import { promises as fs, statfsSync } from 'fs';
|
|
8
8
|
|
|
9
|
-
const AGENT_VERSION = '0.1.
|
|
9
|
+
const AGENT_VERSION = '0.1.24-livedesk.1';
|
|
10
10
|
const DEFAULT_MANAGER = '127.0.0.1:5197';
|
|
11
11
|
const DEFAULT_HEARTBEAT_MS = 5000;
|
|
12
12
|
const DEFAULT_RECONNECT_MS = 5000;
|
|
@@ -27,8 +27,8 @@ Usage:
|
|
|
27
27
|
npx @livedesk/client connect --manager 127.0.0.1:5197 --pair <token>
|
|
28
28
|
|
|
29
29
|
Options:
|
|
30
|
-
--manager <host:port>
|
|
31
|
-
--pair <token>
|
|
30
|
+
--manager <host:port> LiveDesk Hub address. Default: ${DEFAULT_MANAGER}
|
|
31
|
+
--pair <token> LiveDesk Hub pairing token. Can also use LIVEDESK_CLIENT_PAIR_TOKEN.
|
|
32
32
|
--slot <number> Screen wall slot number for this computer.
|
|
33
33
|
--name <name> Friendly device name. Default: OS hostname.
|
|
34
34
|
--heartbeat <ms> Status heartbeat interval. Default: ${DEFAULT_HEARTBEAT_MS}
|
|
@@ -45,8 +45,8 @@ Options:
|
|
|
45
45
|
--ai-model <model> OpenAI model for AI assist. Default: ${DEFAULT_AI_MODEL}
|
|
46
46
|
--fake-ai Use deterministic AI assist responses for smoke tests.
|
|
47
47
|
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
48
|
-
--exit-on-disconnect Exit after
|
|
49
|
-
--exit-on-invalid-pair Exit when the
|
|
48
|
+
--exit-on-disconnect Exit after Hub disconnect. Useful for tests.
|
|
49
|
+
--exit-on-invalid-pair Exit when the Hub rejects the pair token.
|
|
50
50
|
--once Connect, send one status packet, and exit after welcome.
|
|
51
51
|
--version Show the agent version.
|
|
52
52
|
--help Show this help.
|
|
@@ -587,14 +587,14 @@ function buildAiPrompt(options, payload, instruction) {
|
|
|
587
587
|
const title = String(payload?.title || 'Remote AI task').trim();
|
|
588
588
|
return [
|
|
589
589
|
'You are the LiveDesk client AI assistant running on a controlled remote computer.',
|
|
590
|
-
'Complete the
|
|
590
|
+
'Complete the LiveDesk Hub task as a text-only assistant.',
|
|
591
591
|
'Do not claim you used shell commands, file writes, browser automation, keyboard input, or mouse input.',
|
|
592
592
|
'If the task requires external side effects, explain what would be needed and stop.',
|
|
593
593
|
'',
|
|
594
594
|
`Device: ${options.name} (${os.hostname()}, ${os.platform()} ${os.release()}, ${os.arch()})`,
|
|
595
595
|
`Task title: ${title}`,
|
|
596
596
|
'',
|
|
597
|
-
'
|
|
597
|
+
'Hub instruction:',
|
|
598
598
|
instruction
|
|
599
599
|
].join('\n');
|
|
600
600
|
}
|
|
@@ -1082,7 +1082,7 @@ function connectOnce(options, deviceId) {
|
|
|
1082
1082
|
|
|
1083
1083
|
const message = JSON.parse(line);
|
|
1084
1084
|
if (message.type === 'welcome') {
|
|
1085
|
-
console.log(`Connected to
|
|
1085
|
+
console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
|
|
1086
1086
|
writeJsonLine(socket, {
|
|
1087
1087
|
type: 'status',
|
|
1088
1088
|
status: getStatus(options)
|
|
@@ -1113,7 +1113,7 @@ function connectOnce(options, deviceId) {
|
|
|
1113
1113
|
finish();
|
|
1114
1114
|
return;
|
|
1115
1115
|
} else if (message.type === 'error') {
|
|
1116
|
-
const remoteError = new Error(message.error || '
|
|
1116
|
+
const remoteError = new Error(message.error || 'LiveDesk Hub rejected the connection.');
|
|
1117
1117
|
if (message.error === 'invalid-pair-token' && options.exitOnInvalidPair) {
|
|
1118
1118
|
remoteError.exitCode = EXIT_INVALID_PAIR_TOKEN;
|
|
1119
1119
|
}
|
|
@@ -1130,7 +1130,7 @@ function connectOnce(options, deviceId) {
|
|
|
1130
1130
|
|
|
1131
1131
|
async function connectWithRetry(options) {
|
|
1132
1132
|
if (!options.pair) {
|
|
1133
|
-
throw new Error('Missing --pair token.
|
|
1133
|
+
throw new Error('Missing --pair token. Sign in with Google or get it from LiveDesk Hub status.');
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
1136
|
const deviceId = await getDeviceId(options.deviceId);
|
|
@@ -1158,7 +1158,7 @@ async function connectWithRetry(options) {
|
|
|
1158
1158
|
throw err;
|
|
1159
1159
|
}
|
|
1160
1160
|
const delayMs = DEFAULT_RECONNECT_MS;
|
|
1161
|
-
console.error(`
|
|
1161
|
+
console.error(`LiveDesk Hub connection failed: ${err?.message || err}. Retrying in ${delayMs}ms.`);
|
|
1162
1162
|
await wait(delayMs);
|
|
1163
1163
|
}
|
|
1164
1164
|
}
|
package/bin/livedesk-client.js
CHANGED
|
@@ -17,6 +17,7 @@ const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
|
|
|
17
17
|
const DEFAULT_AUTH_CALLBACK_PORT = 5198;
|
|
18
18
|
const DISCOVERY_RETRY_MS = 5000;
|
|
19
19
|
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
20
|
+
const SESSION_REFRESH_SKEW_SECONDS = 60;
|
|
20
21
|
const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co';
|
|
21
22
|
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
22
23
|
const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
|
|
@@ -32,9 +33,9 @@ Usage:
|
|
|
32
33
|
npx @livedesk/client 3
|
|
33
34
|
|
|
34
35
|
Default flow:
|
|
35
|
-
Opens Google sign-in, waits for the signed-in
|
|
36
|
+
Opens Google sign-in, waits for the signed-in LiveDesk Hub, and connects locally.
|
|
36
37
|
Omit the number for first-available placement, or pass 1-999 to pin a slot.
|
|
37
|
-
If no
|
|
38
|
+
If no LiveDesk Hub is active yet, the client keeps checking every 5 seconds.
|
|
38
39
|
|
|
39
40
|
Options:
|
|
40
41
|
--slot <number> Screen wall slot number for this computer.
|
|
@@ -53,7 +54,7 @@ Options:
|
|
|
53
54
|
--files-dir <path> Default folder for received files.
|
|
54
55
|
--tasks Enable safe remote task inbox.
|
|
55
56
|
--no-tasks Disable remote task dispatch capability.
|
|
56
|
-
--login Sign in with Google and auto-discover
|
|
57
|
+
--login Sign in with Google and auto-discover the Hub.
|
|
57
58
|
--logout Forget saved Google session before connecting.
|
|
58
59
|
--auth-port <port> Google sign-in callback port. Default: 5198.
|
|
59
60
|
--version Show the agent version.
|
|
@@ -278,7 +279,7 @@ async function createSupabaseClient() {
|
|
|
278
279
|
const { createClient } = await import('@supabase/supabase-js');
|
|
279
280
|
return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
|
|
280
281
|
auth: {
|
|
281
|
-
autoRefreshToken:
|
|
282
|
+
autoRefreshToken: false,
|
|
282
283
|
persistSession: true,
|
|
283
284
|
detectSessionInUrl: false,
|
|
284
285
|
flowType: 'pkce',
|
|
@@ -288,6 +289,62 @@ async function createSupabaseClient() {
|
|
|
288
289
|
});
|
|
289
290
|
}
|
|
290
291
|
|
|
292
|
+
function getNestedErrorCode(error) {
|
|
293
|
+
let cursor = error;
|
|
294
|
+
for (let i = 0; i < 5 && cursor; i++) {
|
|
295
|
+
if (typeof cursor.code === 'string' && cursor.code.trim()) {
|
|
296
|
+
return cursor.code.trim();
|
|
297
|
+
}
|
|
298
|
+
cursor = cursor.cause;
|
|
299
|
+
}
|
|
300
|
+
return '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function getNestedErrorMessage(error) {
|
|
304
|
+
const messages = [];
|
|
305
|
+
let cursor = error;
|
|
306
|
+
for (let i = 0; i < 5 && cursor; i++) {
|
|
307
|
+
if (typeof cursor.message === 'string' && cursor.message.trim()) {
|
|
308
|
+
messages.push(cursor.message.trim());
|
|
309
|
+
}
|
|
310
|
+
cursor = cursor.cause;
|
|
311
|
+
}
|
|
312
|
+
return messages.join(' ');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isTransientNetworkError(error) {
|
|
316
|
+
const code = getNestedErrorCode(error).toUpperCase();
|
|
317
|
+
if (['ENOTFOUND', 'EAI_AGAIN', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH'].includes(code)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return /fetch failed|network|dns|socket|connection|timeout|getaddrinfo/i.test(getNestedErrorMessage(error));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function formatDiscoveryError(error) {
|
|
324
|
+
if (isTransientNetworkError(error)) {
|
|
325
|
+
const code = getNestedErrorCode(error);
|
|
326
|
+
return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
|
|
327
|
+
}
|
|
328
|
+
return error instanceof Error ? error.message : String(error);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function refreshSessionIfNeeded(supabase) {
|
|
332
|
+
const { data: existing } = await supabase.auth.getSession();
|
|
333
|
+
const session = existing?.session;
|
|
334
|
+
if (!session?.access_token) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const expiresAt = Number(session.expires_at || 0);
|
|
338
|
+
if (expiresAt <= 0 || expiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS) {
|
|
339
|
+
return session;
|
|
340
|
+
}
|
|
341
|
+
const { data, error } = await supabase.auth.refreshSession(session);
|
|
342
|
+
if (error) {
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
return data?.session || session;
|
|
346
|
+
}
|
|
347
|
+
|
|
291
348
|
function openBrowser(url) {
|
|
292
349
|
if (os.platform() === 'win32') {
|
|
293
350
|
spawn('rundll32.exe', ['url.dll,FileProtocolHandler', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
@@ -533,6 +590,7 @@ function normalizeEndpointCandidates(target) {
|
|
|
533
590
|
}
|
|
534
591
|
|
|
535
592
|
async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
593
|
+
await refreshSessionIfNeeded(supabase);
|
|
536
594
|
const { data, error } = await supabase
|
|
537
595
|
.from('livedesk_remote_host_targets')
|
|
538
596
|
.select('endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
@@ -542,21 +600,21 @@ async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
|
542
600
|
throw error;
|
|
543
601
|
}
|
|
544
602
|
if (!data?.active) {
|
|
545
|
-
throw new Error('No active LiveDesk
|
|
603
|
+
throw new Error('No active LiveDesk Hub was found for this Google account. Start LiveDesk Hub on the screen wall computer and sign in there first.');
|
|
546
604
|
}
|
|
547
605
|
if (data.expires_at && Date.parse(data.expires_at) <= Date.now()) {
|
|
548
|
-
throw new Error('The LiveDesk
|
|
606
|
+
throw new Error('The LiveDesk Hub record is expired. Open the Hub screen again while signed in.');
|
|
549
607
|
}
|
|
550
608
|
if (!data.pair_token) {
|
|
551
|
-
throw new Error('The LiveDesk
|
|
609
|
+
throw new Error('The LiveDesk Hub record is missing its private connection token.');
|
|
552
610
|
}
|
|
553
611
|
const endpointCandidates = normalizeEndpointCandidates(data);
|
|
554
612
|
if (endpointCandidates.length === 0) {
|
|
555
|
-
throw new Error('The LiveDesk
|
|
613
|
+
throw new Error('The LiveDesk Hub record does not contain a reachable local address.');
|
|
556
614
|
}
|
|
557
615
|
const manager = await chooseReachableEndpoint(endpointCandidates, { requireReachable: options.requireReachable === true });
|
|
558
616
|
if (!manager) {
|
|
559
|
-
throw new Error(`No reachable LiveDesk
|
|
617
|
+
throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
|
|
560
618
|
}
|
|
561
619
|
return {
|
|
562
620
|
manager,
|
|
@@ -570,16 +628,16 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
570
628
|
const intervalSeconds = Math.max(1, Math.round(intervalMs / 1000));
|
|
571
629
|
let attempts = 0;
|
|
572
630
|
let lastMessage = '';
|
|
573
|
-
console.log(`Waiting for a LiveDesk
|
|
631
|
+
console.log(`Waiting for a LiveDesk Hub. This client will keep trying every ${intervalSeconds}s.`);
|
|
574
632
|
while (true) {
|
|
575
633
|
attempts += 1;
|
|
576
634
|
try {
|
|
577
635
|
return await resolveManagerFromSupabase(supabase, { requireReachable: true });
|
|
578
636
|
} catch (err) {
|
|
579
|
-
const message =
|
|
637
|
+
const message = formatDiscoveryError(err);
|
|
580
638
|
if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
|
|
581
639
|
const suffix = attempts === 1 ? '' : ` attempt ${attempts}`;
|
|
582
|
-
console.log(`Still waiting for LiveDesk
|
|
640
|
+
console.log(`Still waiting for LiveDesk Hub${suffix}: ${message}`);
|
|
583
641
|
lastMessage = message;
|
|
584
642
|
}
|
|
585
643
|
await sleep(intervalMs);
|
|
@@ -607,7 +665,7 @@ async function prepareLoginConnection(parsed) {
|
|
|
607
665
|
const resolved = await waitForManagerFromSupabase(supabase);
|
|
608
666
|
manager = resolved.manager;
|
|
609
667
|
pair = resolved.pair;
|
|
610
|
-
console.log(`Found LiveDesk
|
|
668
|
+
console.log(`Found LiveDesk Hub at ${manager}.`);
|
|
611
669
|
}
|
|
612
670
|
|
|
613
671
|
const slot = normalizeSlotNumber(parsed.slot);
|
|
@@ -876,7 +934,7 @@ async function main() {
|
|
|
876
934
|
return;
|
|
877
935
|
}
|
|
878
936
|
if (prepared.rediscoverOnInvalidPair && result?.code === EXIT_INVALID_PAIR_TOKEN) {
|
|
879
|
-
console.error(`LiveDesk
|
|
937
|
+
console.error(`LiveDesk Hub pair token changed. Refreshing Hub discovery in ${DISCOVERY_RETRY_MS}ms.`);
|
|
880
938
|
await sleep(DISCOVERY_RETRY_MS);
|
|
881
939
|
continue;
|
|
882
940
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|