@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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {