@livedesk/client 0.1.5 → 0.1.7
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 +3 -12
- package/bin/livedesk-client-node.js +117 -0
- package/bin/livedesk-client.js +0 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,20 +25,11 @@ http://127.0.0.1:5198/callback
|
|
|
25
25
|
If you use a custom port, run `npx @livedesk/client --auth-port 5200` and add
|
|
26
26
|
the matching callback URL to Supabase Auth redirect URLs.
|
|
27
27
|
|
|
28
|
-
For manual LAN fallback, start `@livedesk/hub` with an externally reachable
|
|
29
|
-
RemoteHub host and pass the address/token explicitly:
|
|
30
|
-
|
|
31
|
-
```powershell
|
|
32
|
-
$env:REMOTE_HUB_HOST="0.0.0.0"
|
|
33
|
-
$env:REMOTE_HUB_PORT="5197"
|
|
34
|
-
npm run dev:hub
|
|
35
|
-
|
|
36
|
-
npx @livedesk/client --no-login --manager 192.168.0.10:5197 --pair <strong-token> --slot 2
|
|
37
|
-
```
|
|
38
|
-
|
|
39
28
|
The client registers the device, sends status heartbeats, can return
|
|
40
29
|
manager-requested thumbnails, can stream a focused view-only live screen, and
|
|
41
|
-
can receive safe task-only instructions.
|
|
30
|
+
can receive safe task-only instructions. When file transfer is enabled by the
|
|
31
|
+
manager, received files are saved to `Desktop/LiveDeskFiles` unless the manager
|
|
32
|
+
sets another destination folder.
|
|
42
33
|
|
|
43
34
|
By default, the launcher uses the packaged C# RemoteFast engine when supported.
|
|
44
35
|
It falls back to the Node engine for AI assist or when a compatible .NET runtime
|
|
@@ -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
|
@@ -53,9 +53,6 @@ Options:
|
|
|
53
53
|
--version Show the agent version.
|
|
54
54
|
--help Show this help.
|
|
55
55
|
|
|
56
|
-
Manual fallback:
|
|
57
|
-
npx @livedesk/client --no-login --manager 192.168.0.10:5197 --pair <token> --slot 2
|
|
58
|
-
|
|
59
56
|
Auto uses C# RemoteFast when supported and falls back to Node for AI assist or
|
|
60
57
|
when a packaged RemoteFast runtime is unavailable.
|
|
61
58
|
`.trimStart());
|