@livedesk/client 0.1.5 → 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/bin/livedesk-client-node.js +117 -0
- package/package.json +1 -1
|
@@ -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),
|