@jian-ai/remote-control 0.8.2 → 0.8.3

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
@@ -29,6 +29,16 @@ npx @jian-ai/remote-control@latest update
29
29
  Reads `~/.config/jian-remote` (no token, machine id, or gateway flags), installs a compatible
30
30
  build, and restarts the worker service.
31
31
 
32
+ ## Version and service control
33
+
34
+ ```sh
35
+ jian-remote version # also --version / -V
36
+ jian-remote stop | start | restart
37
+ ```
38
+
39
+ `start` / `restart` stop any previous worker before launching one process (LaunchAgent,
40
+ systemd --user, or Task Scheduler).
41
+
32
42
  ## Configure
33
43
 
34
44
  Change the stored gateway URL and/or worker token without a full reinstall (restarts the
@@ -41,7 +51,9 @@ jian-remote configure --gateway https://gateway.example --token WORKER_TOKEN
41
51
 
42
52
  ## Compatibility
43
53
 
44
- Worker and gateway must share the same major.minor. See [docs/remote.md](../../docs/remote.md).
54
+ Connected workers self-update to the gateway’s release (gateway `/update` script, npm
55
+ fallback) including major.minor skew, then restart the OS service. Manual `install` /
56
+ `update` still gate on `/health`. See [docs/remote.md](../../docs/remote.md).
45
57
 
46
58
  Requires Node.js 24+. Computer Use works on macOS, Linux (X11 + `xdotool`/`ffmpeg`), and
47
59
  Windows. Published builds include a prebuilt macOS `computer-helper` (no Xcode on the worker
@@ -49,6 +61,6 @@ Mac) plus Linux/Windows helper scripts. From source on a Mac, run `macos/build-h
49
61
  let the worker compile with `swiftc` when Command Line Tools are installed.
50
62
 
51
63
  On macOS the LaunchAgent uses `ProcessType=Interactive` and an idle `caffeinate` so the worker
52
- keeps polling while the screen is locked (App Nap must not freeze timers). Computer Use still
53
- needs an unlocked session; a locked Mac stays **online** with `uiState: locked`. See
54
- [docs/remote.md](../../docs/remote.md).
64
+ keeps polling while the screen is locked (App Nap must not freeze timers). A locked Mac stays
65
+ **online** (`uiState: locked` is advisory): the worker wakes the display and attempts
66
+ Computer Use; it does not soft-refuse. See [docs/remote.md](../../docs/remote.md).
@@ -20,8 +20,11 @@ const HELPER_NAME = 'computer-helper';
20
20
  const UI_STATE_CACHE_MS = 5_000;
21
21
  export const MAC_UI_STATES = Object.freeze(['unlocked', 'locked', 'display_asleep', 'unknown']);
22
22
 
23
- const LOCKED_COMPUTER_MESSAGE =
24
- 'Mac is locked — unlock the screen to use Computer Use. Shell and file tools may still work while the worker is online.';
23
+ const LOCKED_SCREENSHOT_NOTE =
24
+ 'Note: Mac session reports locked — image may be the lock screen (or black if the display stayed asleep). Desktop apps behind loginwindow are often not visible.';
25
+
26
+ const LOCKED_ACTION_HINT =
27
+ ' Mac session reports locked; if this failed, unlock or keep the display awake (post-login lock ≠ FileVault loginwindow).';
25
28
 
26
29
  let cachedUiState = { at: 0, state: 'unknown' };
27
30
  let workerIdleCaffeinate = null;
@@ -386,39 +389,48 @@ export class MacComputerSession {
386
389
  };
387
390
  }
388
391
 
389
- const uiBlocked = assertComputerUiAllowed();
390
- if (uiBlocked) {
391
- return {
392
- exitCode: 1,
393
- stdout: '',
394
- stderr: uiBlocked,
395
- timedOut: false,
396
- computer: { ok: false, error: uiBlocked },
397
- };
398
- }
392
+ // Wake display when locked/asleep, then always attempt — never soft-block on uiState.
393
+ const uiPrep = prepareComputerUi();
399
394
 
400
395
  if (action === 'screenshot') {
401
- await this.ensure();
402
- const shot = capturePrimaryScreenshotBase64();
403
- if (!this.geometry) throw new Error('Display geometry unavailable.');
404
- this.shotGeometry = {
405
- ...this.geometry,
406
- widthPx: shot.widthPx,
407
- heightPx: shot.heightPx,
408
- };
409
- this.bumpIdle();
410
- return {
411
- exitCode: 0,
412
- stdout: shot.base64,
413
- stderr: '',
414
- timedOut: false,
415
- computer: {
416
- ok: true,
396
+ try {
397
+ await this.ensure();
398
+ const shot = capturePrimaryScreenshotBase64();
399
+ if (!this.geometry) throw new Error('Display geometry unavailable.');
400
+ this.shotGeometry = {
401
+ ...this.geometry,
417
402
  widthPx: shot.widthPx,
418
403
  heightPx: shot.heightPx,
419
- scale: scaleFromGeometry(this.shotGeometry),
420
- },
421
- };
404
+ };
405
+ this.bumpIdle();
406
+ const stderr = uiPrep.sessionLocked ? LOCKED_SCREENSHOT_NOTE : '';
407
+ return {
408
+ exitCode: 0,
409
+ stdout: shot.base64,
410
+ stderr,
411
+ timedOut: false,
412
+ computer: {
413
+ ok: true,
414
+ widthPx: shot.widthPx,
415
+ heightPx: shot.heightPx,
416
+ scale: scaleFromGeometry(this.shotGeometry),
417
+ },
418
+ };
419
+ } catch (error) {
420
+ const message = error instanceof Error ? error.message : String(error);
421
+ const hint = uiPrep.sessionLocked ? LOCKED_ACTION_HINT : '';
422
+ const tcc = /accessib|not permitted|not authorized|Screen Recording/i.test(message)
423
+ ? ' Jian Remote needs Screen Recording (and Accessibility for input) in System Settings.'
424
+ : '';
425
+ const full = `${message}${tcc}${hint}`.trim();
426
+ return {
427
+ exitCode: 1,
428
+ stdout: '',
429
+ stderr: full,
430
+ timedOut: false,
431
+ computer: { ok: false, error: full },
432
+ };
433
+ }
422
434
  }
423
435
 
424
436
  await this.ensure();
@@ -479,12 +491,14 @@ export class MacComputerSession {
479
491
  const tcc = /accessib|not permitted|not authorized/i.test(message)
480
492
  ? ' Jian Remote needs Accessibility (and Screen Recording for screenshots) in System Settings.'
481
493
  : '';
494
+ const hint = uiPrep.sessionLocked ? LOCKED_ACTION_HINT : '';
495
+ const full = `${message}${tcc}${hint}`.trim();
482
496
  return {
483
497
  exitCode: 1,
484
498
  stdout: '',
485
- stderr: `${message}${tcc}`,
499
+ stderr: full,
486
500
  timedOut: false,
487
- computer: { ok: false, error: `${message}${tcc}`.trim() },
501
+ computer: { ok: false, error: full },
488
502
  };
489
503
  }
490
504
 
@@ -661,15 +675,14 @@ export function wakeMacDisplay(seconds = 3) {
661
675
  }
662
676
 
663
677
  /**
664
- * Before GUI actions: wake a sleeping display; refuse clearly when the session is locked.
665
- * Returns null when Computer Use may proceed, or an English error string.
678
+ * Wake the display when locked or asleep (lock often coincides with display sleep).
679
+ * Never soft-blocks: callers always attempt the OS API next.
666
680
  */
667
- export function assertComputerUiAllowed() {
681
+ export function prepareComputerUi() {
668
682
  let state = probeMacUiState({ force: true });
669
- if (state === 'display_asleep' || state === 'unknown') {
683
+ if (state === 'display_asleep' || state === 'locked' || state === 'unknown') {
670
684
  wakeMacDisplay(3);
671
685
  state = probeMacUiState({ force: true });
672
686
  }
673
- if (state === 'locked') return LOCKED_COMPUTER_MESSAGE;
674
- return null;
687
+ return { sessionLocked: state === 'locked', state };
675
688
  }