@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 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),
@@ -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
- await new Promise((resolve, reject) => {
292
- server.once('error', reject);
293
- server.listen(0, '127.0.0.1', resolve);
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://127.0.0.1:${server.address().port}/callback`;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {