@livedesk/client 0.1.4 → 0.1.6
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 +10 -0
- package/bin/livedesk-client-node.js +117 -0
- package/bin/livedesk-client.js +36 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,15 @@ On the manager computer, open the LiveDesk dashboard and sign in first. The
|
|
|
16
16
|
dashboard keeps its local hub address and private pair token refreshed in the
|
|
17
17
|
LiveDesk Supabase registry for the signed-in account.
|
|
18
18
|
|
|
19
|
+
Supabase Auth must allow the CLI callback URL:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
http://127.0.0.1:5198/callback
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If you use a custom port, run `npx @livedesk/client --auth-port 5200` and add
|
|
26
|
+
the matching callback URL to Supabase Auth redirect URLs.
|
|
27
|
+
|
|
19
28
|
For manual LAN fallback, start `@livedesk/hub` with an externally reachable
|
|
20
29
|
RemoteHub host and pass the address/token explicitly:
|
|
21
30
|
|
|
@@ -43,5 +52,6 @@ npx @livedesk/client --no-thumbnail
|
|
|
43
52
|
npx @livedesk/client --no-live
|
|
44
53
|
npx @livedesk/client --engine node
|
|
45
54
|
npx @livedesk/client --engine fast --trace-frames
|
|
55
|
+
npx @livedesk/client --auth-port 5200
|
|
46
56
|
npx @livedesk/client --logout
|
|
47
57
|
```
|
|
@@ -14,6 +14,8 @@ const DEFAULT_LIVE_FPS = 20;
|
|
|
14
14
|
const MAX_LIVE_FPS = 24;
|
|
15
15
|
const MAX_FRAME_BASE64_CHARS = 3 * 1024 * 1024;
|
|
16
16
|
const MAX_AI_OUTPUT_CHARS = 6000;
|
|
17
|
+
const MAX_FILE_TRANSFER_FILES = 24;
|
|
18
|
+
const MAX_FILE_TRANSFER_BYTES = 24 * 1024 * 1024;
|
|
17
19
|
|
|
18
20
|
function printHelp() {
|
|
19
21
|
console.log(`
|
|
@@ -35,6 +37,7 @@ Options:
|
|
|
35
37
|
--no-live Disable focused live screen streaming.
|
|
36
38
|
--tasks Enable safe remote task inbox. Default on.
|
|
37
39
|
--no-tasks Disable remote task dispatch capability.
|
|
40
|
+
--files-dir <path> Default folder for received files. Default: ~/Desktop/LiveDeskFiles.
|
|
38
41
|
--ai Enable OpenAI-backed remote AI assist tasks.
|
|
39
42
|
--no-ai Disable OpenAI-backed remote AI assist tasks.
|
|
40
43
|
--ai-model <model> OpenAI model for AI assist. Default: ${DEFAULT_AI_MODEL}
|
|
@@ -68,6 +71,7 @@ function parseArgs(argv) {
|
|
|
68
71
|
thumbnailEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_THUMBNAIL ?? process.env.MINDEXEC_REMOTE_THUMBNAIL),
|
|
69
72
|
liveEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_LIVE ?? process.env.MINDEXEC_REMOTE_LIVE),
|
|
70
73
|
taskEnabled: !isFalsy(process.env.LIVEDESK_CLIENT_TASKS ?? process.env.MINDEXEC_REMOTE_TASKS),
|
|
74
|
+
filesDir: process.env.LIVEDESK_CLIENT_FILES_DIR || process.env.MINDEXEC_REMOTE_FILES_DIR || '',
|
|
71
75
|
aiEnabled: isTruthy(process.env.LIVEDESK_CLIENT_AI || process.env.MINDEXEC_REMOTE_AI),
|
|
72
76
|
aiModel: process.env.LIVEDESK_CLIENT_AI_MODEL || process.env.MINDEXEC_REMOTE_AI_MODEL || process.env.OPENAI_MODEL || DEFAULT_AI_MODEL,
|
|
73
77
|
openAiApiKey: process.env.OPENAI_API_KEY || '',
|
|
@@ -123,6 +127,9 @@ function parseArgs(argv) {
|
|
|
123
127
|
case '--no-tasks':
|
|
124
128
|
result.taskEnabled = false;
|
|
125
129
|
break;
|
|
130
|
+
case '--files-dir':
|
|
131
|
+
result.filesDir = args[++index] || result.filesDir;
|
|
132
|
+
break;
|
|
126
133
|
case '--ai':
|
|
127
134
|
result.aiEnabled = true;
|
|
128
135
|
result.taskEnabled = true;
|
|
@@ -255,6 +262,95 @@ function wait(ms) {
|
|
|
255
262
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
256
263
|
}
|
|
257
264
|
|
|
265
|
+
function getDefaultFilesDir() {
|
|
266
|
+
return path.join(os.homedir(), 'Desktop', 'LiveDeskFiles');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeDirectoryPath(value, fallback = getDefaultFilesDir()) {
|
|
270
|
+
const text = String(value || '').replace(/\0/g, '').trim();
|
|
271
|
+
if (!text) {
|
|
272
|
+
return fallback;
|
|
273
|
+
}
|
|
274
|
+
if (path.isAbsolute(text)) {
|
|
275
|
+
return path.resolve(text);
|
|
276
|
+
}
|
|
277
|
+
return path.resolve(fallback, text);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function sanitizePathSegment(value, fallback = 'file') {
|
|
281
|
+
const text = String(value || '')
|
|
282
|
+
.replace(/\0/g, '')
|
|
283
|
+
.replace(/[<>:"|?*\x00-\x1f]/g, '_')
|
|
284
|
+
.replace(/[\\/]+/g, '_')
|
|
285
|
+
.trim();
|
|
286
|
+
const normalized = text && text !== '.' && text !== '..' ? text : fallback;
|
|
287
|
+
return normalized.slice(0, 160);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function sanitizeRelativeFilePath(value, fallbackName = 'file') {
|
|
291
|
+
const parts = String(value || '')
|
|
292
|
+
.replace(/\0/g, '')
|
|
293
|
+
.split(/[\\/]+/)
|
|
294
|
+
.map(part => sanitizePathSegment(part, ''))
|
|
295
|
+
.filter(Boolean)
|
|
296
|
+
.filter(part => part !== '.' && part !== '..');
|
|
297
|
+
if (parts.length === 0) {
|
|
298
|
+
return sanitizePathSegment(fallbackName, 'file');
|
|
299
|
+
}
|
|
300
|
+
return path.join(...parts.slice(-8));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function handleFileTransferCommand(options, payload = {}) {
|
|
304
|
+
const files = Array.isArray(payload.files) ? payload.files.slice(0, MAX_FILE_TRANSFER_FILES) : [];
|
|
305
|
+
if (files.length === 0) {
|
|
306
|
+
throw new Error('No files were included in the transfer.');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const baseDir = normalizeDirectoryPath(payload.remoteDirectory || options.filesDir);
|
|
310
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
311
|
+
|
|
312
|
+
let totalBytes = 0;
|
|
313
|
+
const saved = [];
|
|
314
|
+
for (const file of files) {
|
|
315
|
+
const name = sanitizePathSegment(file?.name, 'file');
|
|
316
|
+
const relativePath = sanitizeRelativeFilePath(file?.relativePath || name, name);
|
|
317
|
+
const targetPath = path.resolve(baseDir, relativePath);
|
|
318
|
+
const relativeFromBase = path.relative(baseDir, targetPath);
|
|
319
|
+
if (relativeFromBase.startsWith('..') || path.isAbsolute(relativeFromBase)) {
|
|
320
|
+
throw new Error(`Unsafe file path: ${relativePath}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const dataBase64 = String(file?.dataBase64 || '').replace(/^data:[^,]*,/i, '').trim();
|
|
324
|
+
if (!dataBase64) {
|
|
325
|
+
throw new Error(`Missing file data: ${name}`);
|
|
326
|
+
}
|
|
327
|
+
const buffer = Buffer.from(dataBase64, 'base64');
|
|
328
|
+
totalBytes += buffer.length;
|
|
329
|
+
if (totalBytes > MAX_FILE_TRANSFER_BYTES) {
|
|
330
|
+
throw new Error('File transfer exceeded the local size limit.');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
334
|
+
await fs.writeFile(targetPath, buffer);
|
|
335
|
+
saved.push({
|
|
336
|
+
name,
|
|
337
|
+
relativePath,
|
|
338
|
+
path: targetPath,
|
|
339
|
+
bytes: buffer.length
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
kind: 'file.transfer',
|
|
345
|
+
transferId: String(payload.transferId || '').slice(0, 128),
|
|
346
|
+
status: 'completed',
|
|
347
|
+
directory: baseDir,
|
|
348
|
+
files: saved,
|
|
349
|
+
totalBytes,
|
|
350
|
+
completedAt: new Date().toISOString()
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
258
354
|
function clampNumber(value, min, max, fallback) {
|
|
259
355
|
const number = Number(value);
|
|
260
356
|
if (!Number.isFinite(number)) {
|
|
@@ -747,6 +843,24 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
747
843
|
return;
|
|
748
844
|
}
|
|
749
845
|
|
|
846
|
+
if (command === 'file.transfer') {
|
|
847
|
+
try {
|
|
848
|
+
const result = await handleFileTransferCommand(options, message.payload || {});
|
|
849
|
+
writeJsonLine(socket, {
|
|
850
|
+
type: 'command.result',
|
|
851
|
+
commandId: message.commandId,
|
|
852
|
+
result
|
|
853
|
+
});
|
|
854
|
+
} catch (error) {
|
|
855
|
+
writeJsonLine(socket, {
|
|
856
|
+
type: 'command.result',
|
|
857
|
+
commandId: message.commandId,
|
|
858
|
+
error: error?.message || String(error)
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
750
864
|
if (command === 'input.control') {
|
|
751
865
|
writeJsonLine(socket, {
|
|
752
866
|
type: 'command.result',
|
|
@@ -822,6 +936,9 @@ function connectOnce(options, deviceId) {
|
|
|
822
936
|
thumbnail: options.thumbnailEnabled,
|
|
823
937
|
liveStream: options.liveEnabled,
|
|
824
938
|
control: false,
|
|
939
|
+
fileTransfer: true,
|
|
940
|
+
remoteFiles: true,
|
|
941
|
+
fileTransferMaxBytes: MAX_FILE_TRANSFER_BYTES,
|
|
825
942
|
computerAgent: options.taskEnabled,
|
|
826
943
|
taskDispatch: options.taskEnabled,
|
|
827
944
|
aiAssist: options.taskEnabled && options.aiEnabled && (options.fakeAi || !!options.openAiApiKey),
|
package/bin/livedesk-client.js
CHANGED
|
@@ -13,6 +13,8 @@ const packageRoot = resolve(__dirname, '..');
|
|
|
13
13
|
const nodeAgentPath = join(__dirname, 'livedesk-client-node.js');
|
|
14
14
|
const FAST_PREFLIGHT_TIMEOUT_MS = 5000;
|
|
15
15
|
const DEFAULT_MANAGER = '127.0.0.1:5197';
|
|
16
|
+
const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
|
|
17
|
+
const DEFAULT_AUTH_CALLBACK_PORT = 5198;
|
|
16
18
|
const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co';
|
|
17
19
|
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
18
20
|
const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
|
|
@@ -47,6 +49,7 @@ Options:
|
|
|
47
49
|
--no-tasks Disable remote task dispatch capability.
|
|
48
50
|
--login Sign in with Google and auto-discover manager.
|
|
49
51
|
--logout Forget saved Google session before connecting.
|
|
52
|
+
--auth-port <port> Google sign-in callback port. Default: 5198.
|
|
50
53
|
--version Show the agent version.
|
|
51
54
|
--help Show this help.
|
|
52
55
|
|
|
@@ -80,6 +83,7 @@ function parseLauncherArgs(argv) {
|
|
|
80
83
|
let manager = process.env.LIVEDESK_CLIENT_MANAGER || process.env.MINDEXEC_REMOTE_MANAGER || '';
|
|
81
84
|
let pair = process.env.LIVEDESK_CLIENT_PAIR_TOKEN || process.env.MINDEXEC_REMOTE_PAIR_TOKEN || '';
|
|
82
85
|
let slot = process.env.LIVEDESK_CLIENT_SLOT || process.env.MINDEXEC_REMOTE_SLOT || '';
|
|
86
|
+
let authPort = normalizePort(process.env.LIVEDESK_CLIENT_AUTH_PORT) || DEFAULT_AUTH_CALLBACK_PORT;
|
|
83
87
|
let command = 'connect';
|
|
84
88
|
let nodeOnlyFeature = false;
|
|
85
89
|
let fakeThumbnail = false;
|
|
@@ -129,6 +133,11 @@ function parseLauncherArgs(argv) {
|
|
|
129
133
|
logout = true;
|
|
130
134
|
continue;
|
|
131
135
|
}
|
|
136
|
+
if (arg === '--auth-port') {
|
|
137
|
+
authPort = normalizePort(argv[index + 1]) || authPort;
|
|
138
|
+
index += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
132
141
|
if (arg === '--manager') {
|
|
133
142
|
manager = argv[index + 1] || manager;
|
|
134
143
|
forwarded.push(arg, argv[index + 1]);
|
|
@@ -178,11 +187,20 @@ function parseLauncherArgs(argv) {
|
|
|
178
187
|
manager,
|
|
179
188
|
pair,
|
|
180
189
|
slot,
|
|
190
|
+
authPort,
|
|
181
191
|
nodeOnlyFeature,
|
|
182
192
|
fakeThumbnail
|
|
183
193
|
};
|
|
184
194
|
}
|
|
185
195
|
|
|
196
|
+
function normalizePort(value) {
|
|
197
|
+
const port = Number(String(value || '').trim());
|
|
198
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
return port;
|
|
202
|
+
}
|
|
203
|
+
|
|
186
204
|
function normalizeSlotNumber(value) {
|
|
187
205
|
const number = Number(String(value || '').trim());
|
|
188
206
|
if (!Number.isInteger(number) || number < 1 || number > 999) {
|
|
@@ -260,7 +278,9 @@ function openBrowser(url) {
|
|
|
260
278
|
spawn(command, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
261
279
|
}
|
|
262
280
|
|
|
263
|
-
async function startOAuthCallbackServer() {
|
|
281
|
+
async function startOAuthCallbackServer(options = {}) {
|
|
282
|
+
const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
|
|
283
|
+
const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
|
|
264
284
|
let settleCode;
|
|
265
285
|
let rejectCode;
|
|
266
286
|
const waitForCode = new Promise((resolve, reject) => {
|
|
@@ -288,25 +308,32 @@ async function startOAuthCallbackServer() {
|
|
|
288
308
|
settleCode(code);
|
|
289
309
|
server.close();
|
|
290
310
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
311
|
+
try {
|
|
312
|
+
await new Promise((resolve, reject) => {
|
|
313
|
+
server.once('error', reject);
|
|
314
|
+
server.listen(port, host, resolve);
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err?.code === 'EADDRINUSE') {
|
|
318
|
+
throw new Error(`LiveDesk Google sign-in callback port ${port} is already in use. Close the app using it or run with --auth-port <port> and add that callback URL in Supabase Auth redirect URLs.`);
|
|
319
|
+
}
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
295
322
|
return {
|
|
296
323
|
get redirectTo() {
|
|
297
|
-
return `http
|
|
324
|
+
return `http://${host}:${server.address().port}/callback`;
|
|
298
325
|
},
|
|
299
326
|
waitForCode
|
|
300
327
|
};
|
|
301
328
|
}
|
|
302
329
|
|
|
303
|
-
async function signInWithGoogle(supabase) {
|
|
330
|
+
async function signInWithGoogle(supabase, options = {}) {
|
|
304
331
|
const { data: existing } = await supabase.auth.getSession();
|
|
305
332
|
if (existing?.session?.access_token) {
|
|
306
333
|
return existing.session;
|
|
307
334
|
}
|
|
308
335
|
|
|
309
|
-
const callback = await startOAuthCallbackServer();
|
|
336
|
+
const callback = await startOAuthCallbackServer({ authPort: options.authPort });
|
|
310
337
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
311
338
|
provider: 'google',
|
|
312
339
|
options: {
|
|
@@ -428,7 +455,7 @@ async function prepareLoginConnection(parsed) {
|
|
|
428
455
|
|
|
429
456
|
if (shouldLogin) {
|
|
430
457
|
const supabase = await createSupabaseClient();
|
|
431
|
-
const session = await signInWithGoogle(supabase);
|
|
458
|
+
const session = await signInWithGoogle(supabase, { authPort: parsed.authPort });
|
|
432
459
|
const email = session.user?.email ? ` as ${session.user.email}` : '';
|
|
433
460
|
console.log(`Signed in to LiveDesk${email}.`);
|
|
434
461
|
const resolved = await resolveManagerFromSupabase(supabase);
|