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