@beeos-ai/cli 1.0.24 → 1.1.1

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/dist/index.js CHANGED
@@ -1245,6 +1245,18 @@ var init_upgrade = __esm({
1245
1245
  });
1246
1246
 
1247
1247
  // ../core/dist/device-setup.js
1248
+ function inferHostBackend() {
1249
+ switch (process.platform) {
1250
+ case "darwin":
1251
+ return "macos";
1252
+ case "linux":
1253
+ return "linux";
1254
+ case "win32":
1255
+ return "windows";
1256
+ default:
1257
+ return null;
1258
+ }
1259
+ }
1248
1260
  function agentBinCommandAndArgs(bin) {
1249
1261
  if (bin.type === "executable") {
1250
1262
  return { cmd: bin.path, args: [] };
@@ -1746,6 +1758,8 @@ function cargoDistTarget() {
1746
1758
  case "win32":
1747
1759
  if (arch === "x64")
1748
1760
  return "x86_64-pc-windows-msvc";
1761
+ if (arch === "arm64")
1762
+ return "aarch64-pc-windows-msvc";
1749
1763
  return null;
1750
1764
  default:
1751
1765
  return null;
@@ -2335,6 +2349,9 @@ function hasStartupDiagnostics(driver) {
2335
2349
  function hasPreflightConflictCheck(driver) {
2336
2350
  return typeof driver.preflightConflictCheck === "function";
2337
2351
  }
2352
+ function hasDesktopCapability(driver) {
2353
+ return typeof driver.desktopBridgeSpec === "function";
2354
+ }
2338
2355
  var init_driver = __esm({
2339
2356
  "../core/dist/agent/driver.js"() {
2340
2357
  "use strict";
@@ -2721,6 +2738,35 @@ var init_wait_for_healthcheck = __esm({
2721
2738
  }
2722
2739
  });
2723
2740
 
2741
+ // ../core/dist/services/target-spec.js
2742
+ function buildVncBridgeTargetSpec(binary, opts) {
2743
+ return {
2744
+ id: `vnc-bridge-${opts.serial}`,
2745
+ kind: "vnc-bridge",
2746
+ command: binary,
2747
+ args: [],
2748
+ env: vncBridgeRuntime.buildEnv(opts),
2749
+ restart: "on-failure",
2750
+ label: `vnc-bridge (${opts.serial})`
2751
+ };
2752
+ }
2753
+ function buildOpenclawDesktopVncBridgeSpec(binary, opts) {
2754
+ return buildVncBridgeTargetSpec(binary, {
2755
+ ...opts,
2756
+ serial: OPENCLAW_VNC_BRIDGE_SERIAL
2757
+ });
2758
+ }
2759
+ var OPENCLAW_VNC_BRIDGE_SERIAL;
2760
+ var init_target_spec = __esm({
2761
+ "../core/dist/services/target-spec.js"() {
2762
+ "use strict";
2763
+ init_scrcpy_bridge();
2764
+ init_vnc_bridge();
2765
+ init_spawn_env2();
2766
+ OPENCLAW_VNC_BRIDGE_SERIAL = "openclaw";
2767
+ }
2768
+ });
2769
+
2724
2770
  // ../core/dist/openclaw/config.js
2725
2771
  import { sha256 as sha2562 } from "@noble/hashes/sha256";
2726
2772
  import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
@@ -3153,6 +3199,8 @@ var init_driver2 = __esm({
3153
3199
  init_platform_adapter();
3154
3200
  init_paths();
3155
3201
  init_detector();
3202
+ init_vnc_bridge();
3203
+ init_target_spec();
3156
3204
  init_constants();
3157
3205
  init_config();
3158
3206
  init_detect_local();
@@ -3251,6 +3299,56 @@ var init_driver2 = __esm({
3251
3299
  return { state: "unknown", reason: `unhandled probe state` };
3252
3300
  }
3253
3301
  }
3302
+ /**
3303
+ * Multi-OS refactor Phase 0: previously `start.ts` knew the
3304
+ * vnc-bridge wiring directly (binary path lookup, env-var shape,
3305
+ * `bridgePrivateKeyFile`/`bridgePublicKeyFile` reusing OpenClaw's
3306
+ * keypair). That coupling broke the "framework axis ⊥ desktop
3307
+ * axis" promise — adding e.g. Hermes-with-desktop required
3308
+ * copying the same boilerplate in `start.ts` for each new
3309
+ * framework. The driver now owns the bridge wiring; `start.ts`
3310
+ * just calls `desktopBridgeSpec()` and folds the outcome into
3311
+ * its existing `launchWarnings` stream.
3312
+ *
3313
+ * Soft-failure contract: any path that today produced a
3314
+ * `launchWarnings` entry in `tryAttachOpenclawDesktopBridge`
3315
+ * (binary unavailable, download failed, install threw) returns
3316
+ * `{ spec: null, warnings: [...] }` here instead of throwing.
3317
+ * The caller MUST NOT abort the bind on a `null` spec — desktop
3318
+ * streaming is bonus on top of an already-bound OpenClaw.
3319
+ */
3320
+ async desktopBridgeSpec(endpoint, ctx, reporter) {
3321
+ let binary;
3322
+ try {
3323
+ binary = await vncBridgeRuntime.ensureInstalled(reporter);
3324
+ } catch (e) {
3325
+ const reason = e instanceof Error ? e.message : String(e);
3326
+ return {
3327
+ spec: null,
3328
+ warnings: [
3329
+ `vnc-bridge install failed: ${reason}; OpenClaw is bound but desktop streaming is unavailable.`
3330
+ ]
3331
+ };
3332
+ }
3333
+ if (!binary) {
3334
+ return {
3335
+ spec: null,
3336
+ warnings: [
3337
+ `vnc-bridge binary unavailable (download or PATH lookup failed); OpenClaw is bound but desktop streaming is unavailable. Set BEEOS_VNC_BRIDGE_BIN to override.`
3338
+ ]
3339
+ };
3340
+ }
3341
+ const spec = buildOpenclawDesktopVncBridgeSpec(binary, {
3342
+ deviceId: ctx.instanceId,
3343
+ vncHost: endpoint.host,
3344
+ vncPort: endpoint.port,
3345
+ ...ctx.vncPassword ? { vncPassword: ctx.vncPassword } : {},
3346
+ agentGatewayUrl: ctx.agentGatewayUrl,
3347
+ bridgePrivateKeyFile: ctx.keyFile,
3348
+ bridgePublicKeyFile: ctx.keyFile
3349
+ });
3350
+ return { spec, warnings: [] };
3351
+ }
3254
3352
  };
3255
3353
  openClawDriver = new OpenclawDriver();
3256
3354
  }
@@ -3273,10 +3371,6 @@ var init_agent_status = __esm({
3273
3371
  });
3274
3372
 
3275
3373
  // ../core/dist/openclaw/desktop-detect.js
3276
- function printMacosDesktopHint() {
3277
- for (const line of MACOS_DESKTOP_HINT_LINES)
3278
- console.log(line);
3279
- }
3280
3374
  function readEnv(name) {
3281
3375
  return globalThis.process?.env?.[name];
3282
3376
  }
@@ -3344,6 +3438,75 @@ var init_desktop_detect = __esm({
3344
3438
  }
3345
3439
  });
3346
3440
 
3441
+ // ../core/dist/desktop/platforms/common.js
3442
+ import { createCipheriv } from "crypto";
3443
+ async function readVncPasswordFromBeeoHome() {
3444
+ const env = process.env.BEEOS_VNC_PASSWORD?.trim();
3445
+ if (env)
3446
+ return env;
3447
+ const p = getPlatformAdapter();
3448
+ const file = p.joinPath(beeoHome(), "vnc.password");
3449
+ try {
3450
+ const raw = (await p.readFile(file)).trim();
3451
+ return raw || void 0;
3452
+ } catch {
3453
+ return void 0;
3454
+ }
3455
+ }
3456
+ function generateLegacyVncPassword() {
3457
+ const out = new Array(VNC_PASSWORD_LENGTH);
3458
+ const buf = new Uint8Array(VNC_PASSWORD_LENGTH);
3459
+ globalThis.crypto.getRandomValues(buf);
3460
+ for (let i = 0; i < VNC_PASSWORD_LENGTH; i++) {
3461
+ out[i] = VNC_PASSWORD_CHARSET[buf[i] % VNC_PASSWORD_CHARSET.length];
3462
+ }
3463
+ return out.join("");
3464
+ }
3465
+ function isShellSafeVncPassword(p) {
3466
+ return /^[a-zA-Z0-9]{8}$/.test(p);
3467
+ }
3468
+ function encodeVncPasswordBytes(plaintext) {
3469
+ const padded = Buffer.alloc(8);
3470
+ Buffer.from(plaintext, "utf8").copy(padded, 0, 0, Math.min(plaintext.length, 8));
3471
+ const key = Buffer.alloc(8);
3472
+ for (let i = 0; i < 8; i++) {
3473
+ key[i] = reverseBits(padded[i]);
3474
+ }
3475
+ const challenge = Buffer.from([
3476
+ 23,
3477
+ 82,
3478
+ 107,
3479
+ 6,
3480
+ 35,
3481
+ 78,
3482
+ 88,
3483
+ 7
3484
+ ]);
3485
+ const tripleKey = Buffer.concat([key, key, key]);
3486
+ const cipher = createCipheriv("des-ede3-ecb", tripleKey, null);
3487
+ cipher.setAutoPadding(false);
3488
+ return Buffer.concat([cipher.update(challenge), cipher.final()]);
3489
+ }
3490
+ function reverseBits(b) {
3491
+ let r = 0;
3492
+ let v = b & 255;
3493
+ for (let i = 0; i < 8; i++) {
3494
+ r = r << 1 | v & 1;
3495
+ v >>= 1;
3496
+ }
3497
+ return r & 255;
3498
+ }
3499
+ var VNC_PASSWORD_LENGTH, VNC_PASSWORD_CHARSET;
3500
+ var init_common = __esm({
3501
+ "../core/dist/desktop/platforms/common.js"() {
3502
+ "use strict";
3503
+ init_platform_adapter();
3504
+ init_paths();
3505
+ VNC_PASSWORD_LENGTH = 8;
3506
+ VNC_PASSWORD_CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
3507
+ }
3508
+ });
3509
+
3347
3510
  // ../core/dist/openclaw/macos-desktop-cold-start.js
3348
3511
  async function detectColdStartState() {
3349
3512
  const p = getPlatformAdapter();
@@ -3456,15 +3619,6 @@ async function ensureBeeoHome(p) {
3456
3619
  await p.mkdir(home);
3457
3620
  }
3458
3621
  }
3459
- function generateLegacyVncPassword() {
3460
- const out = new Array(VNC_PASSWORD_LENGTH);
3461
- const buf = new Uint8Array(VNC_PASSWORD_LENGTH);
3462
- globalThis.crypto.getRandomValues(buf);
3463
- for (let i = 0; i < VNC_PASSWORD_LENGTH; i++) {
3464
- out[i] = VNC_PASSWORD_CHARSET[buf[i] % VNC_PASSWORD_CHARSET.length];
3465
- }
3466
- return out.join("");
3467
- }
3468
3622
  function encodeMacosVncPassword(password) {
3469
3623
  const pwBytes = Buffer.alloc(APPLE_VNC_FIXED_KEY.length);
3470
3624
  Buffer.from(password, "utf8").copy(pwBytes);
@@ -3540,15 +3694,14 @@ async function probeArdAllLocalUsers() {
3540
3694
  function sleep3(ms) {
3541
3695
  return new Promise((resolve) => setTimeout(resolve, ms));
3542
3696
  }
3543
- var APPLE_VNC_FIXED_KEY, VNC_PASSWORD_LENGTH, VNC_PASSWORD_CHARSET, ARD_PLIST_PATH, VNC_SETTINGS_PATH, PORT_5900_PROBE_TIMEOUT_MS, VERIFY_5900_TIMEOUT_MS, VERIFY_5900_POLL_INTERVAL_MS, OSASCRIPT_TIMEOUT_MS;
3697
+ var APPLE_VNC_FIXED_KEY, ARD_PLIST_PATH, VNC_SETTINGS_PATH, PORT_5900_PROBE_TIMEOUT_MS, VERIFY_5900_TIMEOUT_MS, VERIFY_5900_POLL_INTERVAL_MS, OSASCRIPT_TIMEOUT_MS;
3544
3698
  var init_macos_desktop_cold_start = __esm({
3545
3699
  "../core/dist/openclaw/macos-desktop-cold-start.js"() {
3546
3700
  "use strict";
3547
3701
  init_platform_adapter();
3548
3702
  init_paths();
3703
+ init_common();
3549
3704
  APPLE_VNC_FIXED_KEY = Buffer.from("1734516E8BA8C5E2FF1C39567390ADCA", "hex");
3550
- VNC_PASSWORD_LENGTH = 8;
3551
- VNC_PASSWORD_CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
3552
3705
  ARD_PLIST_PATH = "/Library/Preferences/com.apple.RemoteManagement.plist";
3553
3706
  VNC_SETTINGS_PATH = "/Library/Preferences/com.apple.VNCSettings.txt";
3554
3707
  PORT_5900_PROBE_TIMEOUT_MS = 200;
@@ -3558,6 +3711,655 @@ var init_macos_desktop_cold_start = __esm({
3558
3711
  }
3559
3712
  });
3560
3713
 
3714
+ // ../core/dist/desktop/pipeline.js
3715
+ var DesktopPipeline;
3716
+ var init_pipeline = __esm({
3717
+ "../core/dist/desktop/pipeline.js"() {
3718
+ "use strict";
3719
+ DesktopPipeline = class {
3720
+ platform;
3721
+ constructor(platform) {
3722
+ this.platform = platform;
3723
+ }
3724
+ /** Stable platform tag — proxied from the underlying adapter. */
3725
+ get id() {
3726
+ return this.platform.id;
3727
+ }
3728
+ coldStart(opts) {
3729
+ return this.platform.coldStart(opts);
3730
+ }
3731
+ probe(opts) {
3732
+ return this.platform.probe(opts);
3733
+ }
3734
+ readPasswordOptional() {
3735
+ return this.platform.readPasswordOptional();
3736
+ }
3737
+ postSetupHint(endpoint) {
3738
+ return this.platform.postSetupHint(endpoint);
3739
+ }
3740
+ };
3741
+ }
3742
+ });
3743
+
3744
+ // ../core/dist/desktop/platforms/macos.js
3745
+ var MacosDesktopPlatform, macosDesktopPlatform;
3746
+ var init_macos = __esm({
3747
+ "../core/dist/desktop/platforms/macos.js"() {
3748
+ "use strict";
3749
+ init_desktop_detect();
3750
+ init_macos_desktop_cold_start();
3751
+ init_common();
3752
+ MacosDesktopPlatform = class {
3753
+ id = "macos";
3754
+ coldStart(opts) {
3755
+ return macosDesktopColdStart(opts);
3756
+ }
3757
+ probe(opts) {
3758
+ return probeLocalVnc(opts);
3759
+ }
3760
+ readPasswordOptional() {
3761
+ return readVncPasswordFromBeeoHome();
3762
+ }
3763
+ postSetupHint(endpoint) {
3764
+ if (endpoint.kind !== "macos")
3765
+ return [];
3766
+ return MACOS_DESKTOP_HINT_LINES;
3767
+ }
3768
+ };
3769
+ macosDesktopPlatform = new MacosDesktopPlatform();
3770
+ }
3771
+ });
3772
+
3773
+ // ../core/dist/desktop/platforms/linux.js
3774
+ async function ensureBeeoHome2() {
3775
+ const p = getPlatformAdapter();
3776
+ const home = beeoHome();
3777
+ if (!await p.exists(home)) {
3778
+ await p.mkdir(home);
3779
+ }
3780
+ }
3781
+ async function detectLinuxColdStartState() {
3782
+ const p = getPlatformAdapter();
3783
+ const port5901Listening = await p.tcpProbe("127.0.0.1", LINUX_VNC_PORT, PORT_PROBE_TIMEOUT_MS).catch(() => false);
3784
+ const beeoFile = p.joinPath(beeoHome(), "vnc.password");
3785
+ let beeoPasswordFileExists = false;
3786
+ try {
3787
+ const raw = await p.readFile(beeoFile);
3788
+ beeoPasswordFileExists = raw.trim().length > 0;
3789
+ } catch {
3790
+ beeoPasswordFileExists = false;
3791
+ }
3792
+ const systemFile = p.joinPath(p.homeDir(), ".vnc", "passwd");
3793
+ let systemPasswordFileExists = false;
3794
+ try {
3795
+ const raw = await p.readFile(systemFile);
3796
+ systemPasswordFileExists = raw.length > 0;
3797
+ } catch {
3798
+ systemPasswordFileExists = false;
3799
+ }
3800
+ const passwdToolPath = await firstOnPath(VNCPASSWD_CANDIDATES);
3801
+ const vncServerPath = await firstOnPath(VNCSERVER_CANDIDATES);
3802
+ return {
3803
+ port5901Listening,
3804
+ beeoPasswordFileExists,
3805
+ systemPasswordFileExists,
3806
+ passwdToolPath,
3807
+ vncServerPath
3808
+ };
3809
+ }
3810
+ async function firstOnPath(candidates) {
3811
+ const p = getPlatformAdapter();
3812
+ for (const name of candidates) {
3813
+ try {
3814
+ const result = await p.exec("which", [name]);
3815
+ if (result.code === 0 && result.stdout.trim()) {
3816
+ return result.stdout.trim().split("\n")[0].trim();
3817
+ }
3818
+ } catch {
3819
+ }
3820
+ }
3821
+ return null;
3822
+ }
3823
+ async function writeSystemVncPasswd(toolPath, password) {
3824
+ if (!isShellSafeVncPassword(password)) {
3825
+ return {
3826
+ ok: false,
3827
+ detail: "internal: refusing to pipe shell-unsafe password into tigervncpasswd"
3828
+ };
3829
+ }
3830
+ const p = getPlatformAdapter();
3831
+ const home = p.homeDir();
3832
+ const vncDir = p.joinPath(home, ".vnc");
3833
+ const target = p.joinPath(vncDir, "passwd");
3834
+ if (!await p.exists(vncDir)) {
3835
+ try {
3836
+ await p.mkdir(vncDir);
3837
+ } catch (e) {
3838
+ return { ok: false, detail: `mkdir ${vncDir}: ${describeError3(e)}` };
3839
+ }
3840
+ }
3841
+ const shellCmd = `printf '%s' '${password}' | '${toolPath}' -f > '${target}' && chmod 0600 '${target}'`;
3842
+ try {
3843
+ const result = await p.exec("sh", ["-c", shellCmd], {
3844
+ timeout: VNCPASSWD_TIMEOUT_MS
3845
+ });
3846
+ if (result.code !== 0) {
3847
+ return {
3848
+ ok: false,
3849
+ detail: result.stderr.trim() || `exit ${result.code}`
3850
+ };
3851
+ }
3852
+ return { ok: true };
3853
+ } catch (e) {
3854
+ return { ok: false, detail: describeError3(e) };
3855
+ }
3856
+ }
3857
+ async function startVncServer(vncServerPath) {
3858
+ const p = getPlatformAdapter();
3859
+ try {
3860
+ const result = await p.exec(vncServerPath, [
3861
+ LINUX_VNC_DISPLAY,
3862
+ "-localhost",
3863
+ "yes",
3864
+ "-SecurityTypes",
3865
+ "VncAuth"
3866
+ ], { timeout: VNCSERVER_TIMEOUT_MS });
3867
+ if (result.code !== 0) {
3868
+ return {
3869
+ ok: false,
3870
+ detail: (result.stderr || result.stdout).trim() || `exit ${result.code}`
3871
+ };
3872
+ }
3873
+ return { ok: true };
3874
+ } catch (e) {
3875
+ return { ok: false, detail: describeError3(e) };
3876
+ }
3877
+ }
3878
+ async function waitForPort(port, timeoutMs) {
3879
+ const p = getPlatformAdapter();
3880
+ const deadline = Date.now() + timeoutMs;
3881
+ while (Date.now() < deadline) {
3882
+ if (await p.tcpProbe("127.0.0.1", port, PORT_PROBE_TIMEOUT_MS).catch(() => false)) {
3883
+ return true;
3884
+ }
3885
+ await sleep4(VERIFY_POLL_INTERVAL_MS);
3886
+ }
3887
+ return false;
3888
+ }
3889
+ function sleep4(ms) {
3890
+ return new Promise((resolve) => setTimeout(resolve, ms));
3891
+ }
3892
+ function describeError3(e) {
3893
+ if (e instanceof Error) {
3894
+ const msg = e.message.split(/\r?\n/, 1)[0]?.trim();
3895
+ return msg && msg.length > 0 ? msg : e.name;
3896
+ }
3897
+ return String(e);
3898
+ }
3899
+ var LINUX_VNC_PORT, LINUX_VNC_DISPLAY, PORT_PROBE_TIMEOUT_MS, VERIFY_TIMEOUT_MS, VERIFY_POLL_INTERVAL_MS, VNCSERVER_TIMEOUT_MS, VNCPASSWD_TIMEOUT_MS, VNCPASSWD_CANDIDATES, VNCSERVER_CANDIDATES, LinuxDesktopPlatform, linuxDesktopPlatform;
3900
+ var init_linux = __esm({
3901
+ "../core/dist/desktop/platforms/linux.js"() {
3902
+ "use strict";
3903
+ init_desktop_detect();
3904
+ init_platform_adapter();
3905
+ init_paths();
3906
+ init_common();
3907
+ LINUX_VNC_PORT = 5901;
3908
+ LINUX_VNC_DISPLAY = ":1";
3909
+ PORT_PROBE_TIMEOUT_MS = 200;
3910
+ VERIFY_TIMEOUT_MS = 8e3;
3911
+ VERIFY_POLL_INTERVAL_MS = 250;
3912
+ VNCSERVER_TIMEOUT_MS = 3e4;
3913
+ VNCPASSWD_TIMEOUT_MS = 5e3;
3914
+ VNCPASSWD_CANDIDATES = [
3915
+ "tigervncpasswd",
3916
+ "vncpasswd"
3917
+ ];
3918
+ VNCSERVER_CANDIDATES = ["vncserver"];
3919
+ LinuxDesktopPlatform = class {
3920
+ id = "linux";
3921
+ async coldStart(opts) {
3922
+ if (opts.noDesktop)
3923
+ return { status: "skipped_opt_out" };
3924
+ const log = opts.log ?? ((m) => console.log(m));
3925
+ const p = getPlatformAdapter();
3926
+ const state = await detectLinuxColdStartState();
3927
+ if (state.passwdToolPath === null) {
3928
+ return { status: "skipped_no_vnc_server" };
3929
+ }
3930
+ const beeoPasswordFile = p.joinPath(beeoHome(), "vnc.password");
3931
+ if (state.port5901Listening && state.beeoPasswordFileExists && state.systemPasswordFileExists) {
3932
+ let existing;
3933
+ try {
3934
+ existing = (await p.readFile(beeoPasswordFile)).trim() || void 0;
3935
+ } catch {
3936
+ existing = void 0;
3937
+ }
3938
+ return { status: "already_configured", vncPassword: existing };
3939
+ }
3940
+ if (!opts.prompt && !opts.yes) {
3941
+ return { status: "skipped_no_tty" };
3942
+ }
3943
+ if (!opts.yes) {
3944
+ log("");
3945
+ log("BeeOS desktop streaming needs a local VNC server with a known password.");
3946
+ log("This will:");
3947
+ log(" - Generate an 8-char VNC viewer password");
3948
+ log(" - Write it to ~/.vnc/passwd (chmod 600) using `tigervncpasswd -f`");
3949
+ log(" - Mirror the same plaintext into ~/.beeos/vnc.password");
3950
+ log(` - Start \`vncserver ${LINUX_VNC_DISPLAY} -localhost yes\` if it isn't already running`);
3951
+ log("If you already use ~/.vnc/passwd for other tools the existing");
3952
+ log("password will be REPLACED. No sudo, no package install \u2014 only");
3953
+ log("files inside your home directory are modified.");
3954
+ log("");
3955
+ const answer = (await opts.prompt("Configure VNC password and start vncserver for desktop streaming? [Y/n]: ")).trim();
3956
+ if (answer !== "" && !/^y(es)?$/i.test(answer)) {
3957
+ return { status: "user_declined" };
3958
+ }
3959
+ }
3960
+ const newPassword = generateLegacyVncPassword();
3961
+ if (!isShellSafeVncPassword(newPassword)) {
3962
+ return {
3963
+ status: "script_failed",
3964
+ warnings: [
3965
+ "linux cold-start: generated VNC password contained shell-unsafe characters; refusing to invoke `tigervncpasswd -f` via shell pipe. Re-run `beeos init`."
3966
+ ]
3967
+ };
3968
+ }
3969
+ try {
3970
+ await ensureBeeoHome2();
3971
+ await p.writeFile(beeoPasswordFile, newPassword);
3972
+ await p.chmod(beeoPasswordFile, 384);
3973
+ } catch (e) {
3974
+ return {
3975
+ status: "script_failed",
3976
+ warnings: [
3977
+ `linux cold-start: failed to write ${beeoPasswordFile}: ${describeError3(e)}. Desktop streaming will not work this run.`
3978
+ ]
3979
+ };
3980
+ }
3981
+ const passwdResult = await writeSystemVncPasswd(state.passwdToolPath, newPassword);
3982
+ if (!passwdResult.ok) {
3983
+ await p.rm(beeoPasswordFile).catch(() => void 0);
3984
+ return {
3985
+ status: "script_failed",
3986
+ warnings: [
3987
+ `linux cold-start: \`${state.passwdToolPath} -f\` failed: ${passwdResult.detail}. Desktop streaming will not work this run.`
3988
+ ]
3989
+ };
3990
+ }
3991
+ if (!state.port5901Listening) {
3992
+ if (state.vncServerPath === null) {
3993
+ return {
3994
+ status: "verify_timeout",
3995
+ warnings: [
3996
+ `linux cold-start: password files written but \`vncserver\` is not on PATH \u2014 start it manually with \`vncserver ${LINUX_VNC_DISPLAY} -localhost yes -SecurityTypes VncAuth\` and re-run \`beeos init\`.`
3997
+ ]
3998
+ };
3999
+ }
4000
+ const startResult = await startVncServer(state.vncServerPath);
4001
+ if (!startResult.ok) {
4002
+ return {
4003
+ status: "script_failed",
4004
+ warnings: [
4005
+ `linux cold-start: \`vncserver ${LINUX_VNC_DISPLAY}\` failed: ${startResult.detail}. Check ~/.vnc/<host>:1.log for startup errors.`
4006
+ ]
4007
+ };
4008
+ }
4009
+ }
4010
+ const up = await waitForPort(LINUX_VNC_PORT, VERIFY_TIMEOUT_MS);
4011
+ if (!up) {
4012
+ return {
4013
+ status: "verify_timeout",
4014
+ warnings: [
4015
+ `linux cold-start ran but :${LINUX_VNC_PORT} never came up within 8s. Check \`~/.vnc/$HOSTNAME${LINUX_VNC_DISPLAY}.log\` or run \`beeos doctor\`.`
4016
+ ]
4017
+ };
4018
+ }
4019
+ log(`vncserver ${LINUX_VNC_DISPLAY} is listening on :${LINUX_VNC_PORT} (loopback only).`);
4020
+ return { status: "enabled_now", vncPassword: newPassword };
4021
+ }
4022
+ probe(opts) {
4023
+ return probeLocalVnc(opts);
4024
+ }
4025
+ readPasswordOptional() {
4026
+ return readVncPasswordFromBeeoHome();
4027
+ }
4028
+ postSetupHint(_endpoint) {
4029
+ return [
4030
+ "Linux desktop streaming notes:",
4031
+ " \u2022 vncserver is listening on 127.0.0.1:5901 only \u2014 exposing it externally requires",
4032
+ " explicit firewall + auth setup (out of scope for the BeeOS install).",
4033
+ " \u2022 Headless setups need an X session: edit ~/.vnc/xstartup if the desktop comes up blank."
4034
+ ];
4035
+ }
4036
+ };
4037
+ linuxDesktopPlatform = new LinuxDesktopPlatform();
4038
+ }
4039
+ });
4040
+
4041
+ // ../core/dist/desktop/platforms/windows.js
4042
+ async function ensureBeeoHome3() {
4043
+ const p = getPlatformAdapter();
4044
+ const home = beeoHome();
4045
+ if (!await p.exists(home)) {
4046
+ await p.mkdir(home);
4047
+ }
4048
+ }
4049
+ async function detectWindowsColdStartState() {
4050
+ const p = getPlatformAdapter();
4051
+ const port5900Listening = await p.tcpProbe("127.0.0.1", WIN_VNC_PORT, PORT_PROBE_TIMEOUT_MS2).catch(() => false);
4052
+ const beeoFile = p.joinPath(beeoHome(), "vnc.password");
4053
+ let beeoPasswordFileExists = false;
4054
+ try {
4055
+ const raw = await p.readFile(beeoFile);
4056
+ beeoPasswordFileExists = raw.trim().length > 0;
4057
+ } catch {
4058
+ beeoPasswordFileExists = false;
4059
+ }
4060
+ const [tvnserverServiceExists, tvnserverRegistryExists] = await Promise.all([
4061
+ detectViaPowerShell("(Get-Service tvnserver -ErrorAction SilentlyContinue) -ne $null"),
4062
+ detectViaPowerShell(`Test-Path "${TVNSERVER_REG_PATH}"`)
4063
+ ]);
4064
+ return {
4065
+ port5900Listening,
4066
+ beeoPasswordFileExists,
4067
+ tvnserverServiceExists,
4068
+ tvnserverRegistryExists
4069
+ };
4070
+ }
4071
+ async function detectViaPowerShell(expression) {
4072
+ const p = getPlatformAdapter();
4073
+ try {
4074
+ const result = await p.exec("powershell", ["-NoProfile", "-Command", expression], { timeout: 5e3 });
4075
+ if (result.code !== 0)
4076
+ return false;
4077
+ return result.stdout.trim().toLowerCase() === "true";
4078
+ } catch {
4079
+ return false;
4080
+ }
4081
+ }
4082
+ async function runElevatedTvnserverConfig(password) {
4083
+ if (!isShellSafeVncPassword(password)) {
4084
+ return { ok: false, detail: "internal: refusing to embed shell-unsafe password" };
4085
+ }
4086
+ const p = getPlatformAdapter();
4087
+ const encoded = encodeVncPasswordBytes(password);
4088
+ const byteLiteral = Array.from(encoded).map((b) => `0x${b.toString(16).padStart(2, "0")}`).join(",");
4089
+ const innerScript = [
4090
+ `$bytes = [byte[]](${byteLiteral})`,
4091
+ `Set-ItemProperty -Path '${TVNSERVER_REG_PATH}' -Name 'Password' -Value $bytes -Type Binary`,
4092
+ `Set-ItemProperty -Path '${TVNSERVER_REG_PATH}' -Name 'AllowLoopback' -Value 1 -Type DWord`,
4093
+ `Set-ItemProperty -Path '${TVNSERVER_REG_PATH}' -Name 'LoopbackOnly' -Value 1 -Type DWord`,
4094
+ `Set-ItemProperty -Path '${TVNSERVER_REG_PATH}' -Name 'LocalInputPriority' -Value 1 -Type DWord`,
4095
+ `Restart-Service -Name 'tvnserver' -Force`,
4096
+ `exit 0`
4097
+ ].join("; ");
4098
+ const outerScript = `try { $p = Start-Process powershell -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-Command',"${innerScript.replace(/"/g, '`"')}" -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode} catch { if ($_.Exception.Message -match 'cancel|cancelled') { exit 1223 } else { Write-Error $_; exit 1 }}`;
4099
+ try {
4100
+ const result = await p.exec("powershell", ["-NoProfile", "-Command", outerScript], { timeout: POWERSHELL_TIMEOUT_MS });
4101
+ if (result.code === 0)
4102
+ return { ok: true };
4103
+ if (result.code === 1223) {
4104
+ return { ok: false, canceled: true, detail: "UAC denied" };
4105
+ }
4106
+ return {
4107
+ ok: false,
4108
+ detail: result.stderr.trim() || `exit ${result.code}`
4109
+ };
4110
+ } catch (e) {
4111
+ return { ok: false, detail: describeError4(e) };
4112
+ }
4113
+ }
4114
+ async function waitForPort2(port, timeoutMs) {
4115
+ const p = getPlatformAdapter();
4116
+ const deadline = Date.now() + timeoutMs;
4117
+ while (Date.now() < deadline) {
4118
+ if (await p.tcpProbe("127.0.0.1", port, PORT_PROBE_TIMEOUT_MS2).catch(() => false)) {
4119
+ return true;
4120
+ }
4121
+ await sleep5(VERIFY_POLL_INTERVAL_MS2);
4122
+ }
4123
+ return false;
4124
+ }
4125
+ function sleep5(ms) {
4126
+ return new Promise((resolve) => setTimeout(resolve, ms));
4127
+ }
4128
+ function describeError4(e) {
4129
+ if (e instanceof Error) {
4130
+ const msg = e.message.split(/\r?\n/, 1)[0]?.trim();
4131
+ return msg && msg.length > 0 ? msg : e.name;
4132
+ }
4133
+ return String(e);
4134
+ }
4135
+ var WIN_VNC_PORT, PORT_PROBE_TIMEOUT_MS2, VERIFY_TIMEOUT_MS2, VERIFY_POLL_INTERVAL_MS2, POWERSHELL_TIMEOUT_MS, TVNSERVER_REG_PATH, WINGET_PACKAGE_HINT, WINDOWS_VNC_INSTALL_HINT_LINES, WindowsDesktopPlatform, windowsDesktopPlatform;
4136
+ var init_windows = __esm({
4137
+ "../core/dist/desktop/platforms/windows.js"() {
4138
+ "use strict";
4139
+ init_platform_adapter();
4140
+ init_paths();
4141
+ init_common();
4142
+ WIN_VNC_PORT = 5900;
4143
+ PORT_PROBE_TIMEOUT_MS2 = 200;
4144
+ VERIFY_TIMEOUT_MS2 = 8e3;
4145
+ VERIFY_POLL_INTERVAL_MS2 = 250;
4146
+ POWERSHELL_TIMEOUT_MS = 6e4;
4147
+ TVNSERVER_REG_PATH = "HKLM:\\SOFTWARE\\TightVNC\\Server";
4148
+ WINGET_PACKAGE_HINT = "winget install GlavSoft.TightVNC --accept-package-agreements --accept-source-agreements --silent";
4149
+ WINDOWS_VNC_INSTALL_HINT_LINES = [
4150
+ "Tip: BeeOS desktop streaming on Windows uses TightVNC. To install:",
4151
+ ` ${WINGET_PACKAGE_HINT}`,
4152
+ "Then re-run `beeos init`. Set BEEOS_NO_DESKTOP=1 to suppress this hint."
4153
+ ];
4154
+ WindowsDesktopPlatform = class {
4155
+ id = "windows";
4156
+ async coldStart(opts) {
4157
+ if (opts.noDesktop)
4158
+ return { status: "skipped_opt_out" };
4159
+ const log = opts.log ?? ((m) => console.log(m));
4160
+ const p = getPlatformAdapter();
4161
+ const state = await detectWindowsColdStartState();
4162
+ if (!state.tvnserverServiceExists && !state.tvnserverRegistryExists) {
4163
+ return {
4164
+ status: "skipped_no_vnc_server",
4165
+ warnings: [...WINDOWS_VNC_INSTALL_HINT_LINES]
4166
+ };
4167
+ }
4168
+ const beeoPasswordFile = p.joinPath(beeoHome(), "vnc.password");
4169
+ if (state.port5900Listening && state.beeoPasswordFileExists) {
4170
+ let existing;
4171
+ try {
4172
+ existing = (await p.readFile(beeoPasswordFile)).trim() || void 0;
4173
+ } catch {
4174
+ existing = void 0;
4175
+ }
4176
+ return { status: "already_configured", vncPassword: existing };
4177
+ }
4178
+ if (!opts.prompt && !opts.yes) {
4179
+ return { status: "skipped_no_tty" };
4180
+ }
4181
+ if (!opts.yes) {
4182
+ log("");
4183
+ log("BeeOS desktop streaming needs TightVNC configured for loopback access.");
4184
+ log("This will:");
4185
+ log(" - Generate an 8-char VNC viewer password");
4186
+ log(" - Write it to HKLM\\SOFTWARE\\TightVNC\\Server (requires UAC)");
4187
+ log(" - Restrict TightVNC to 127.0.0.1:5900 (loopback only)");
4188
+ log(" - Restart the tvnserver service");
4189
+ log("Windows will pop ONE UAC dialog. The CLI never sees your password \u2014");
4190
+ log("only the elevated PowerShell child does, and only inside its own process.");
4191
+ log("");
4192
+ const answer = (await opts.prompt("Configure TightVNC for desktop streaming? [Y/n]: ")).trim();
4193
+ if (answer !== "" && !/^y(es)?$/i.test(answer)) {
4194
+ return { status: "user_declined" };
4195
+ }
4196
+ }
4197
+ const newPassword = generateLegacyVncPassword();
4198
+ if (!isShellSafeVncPassword(newPassword)) {
4199
+ return {
4200
+ status: "script_failed",
4201
+ warnings: [
4202
+ "windows cold-start: generated VNC password contained shell-unsafe characters; refusing to embed in PowerShell payload. Re-run `beeos init`."
4203
+ ]
4204
+ };
4205
+ }
4206
+ try {
4207
+ await ensureBeeoHome3();
4208
+ await p.writeFile(beeoPasswordFile, newPassword);
4209
+ await p.chmod(beeoPasswordFile, 384).catch(() => void 0);
4210
+ } catch (e) {
4211
+ return {
4212
+ status: "script_failed",
4213
+ warnings: [
4214
+ `windows cold-start: failed to write ${beeoPasswordFile}: ${describeError4(e)}. Desktop streaming will not work this run.`
4215
+ ]
4216
+ };
4217
+ }
4218
+ log("Requesting Windows administrator privileges (UAC dialog) ...");
4219
+ const elevated = await runElevatedTvnserverConfig(newPassword);
4220
+ if (!elevated.ok) {
4221
+ await p.rm(beeoPasswordFile).catch(() => void 0);
4222
+ const canceled = elevated.canceled === true;
4223
+ return {
4224
+ status: canceled ? "user_canceled_dialog" : "script_failed",
4225
+ warnings: [
4226
+ canceled ? "windows cold-start: UAC dialog was canceled \u2014 desktop streaming not enabled." : `windows cold-start: tvnserver registry write failed: ${elevated.detail}. Desktop streaming will not work this run.`
4227
+ ]
4228
+ };
4229
+ }
4230
+ const up = await waitForPort2(WIN_VNC_PORT, VERIFY_TIMEOUT_MS2);
4231
+ if (!up) {
4232
+ return {
4233
+ status: "verify_timeout",
4234
+ warnings: [
4235
+ `windows cold-start ran but :${WIN_VNC_PORT} never came up within 8s. Open Services.msc and check \`tvnserver\`, or run \`beeos doctor\`.`
4236
+ ]
4237
+ };
4238
+ }
4239
+ log(`tvnserver is listening on 127.0.0.1:${WIN_VNC_PORT}.`);
4240
+ return { status: "enabled_now", vncPassword: newPassword };
4241
+ }
4242
+ async probe(opts) {
4243
+ if (process.env.BEEOS_NO_DESKTOP === "1")
4244
+ return null;
4245
+ const p = getPlatformAdapter();
4246
+ if (p.platform() !== "win32")
4247
+ return null;
4248
+ const ok = await p.tcpProbe("127.0.0.1", WIN_VNC_PORT, PORT_PROBE_TIMEOUT_MS2).catch(() => false);
4249
+ if (!ok) {
4250
+ if (opts?.ttyHints) {
4251
+ for (const line of WINDOWS_VNC_INSTALL_HINT_LINES)
4252
+ console.log(line);
4253
+ }
4254
+ return null;
4255
+ }
4256
+ return {
4257
+ kind: "linux",
4258
+ // VncEndpoint.kind is a coarse RFB-family tag; "linux"
4259
+ // is fine because TightVNC speaks the same RFB scheme-2 surface
4260
+ // as TigerVNC. We deliberately pin osType="windows-desktop" so
4261
+ // the backend `isDesktopInstance` matcher sees the right host
4262
+ // tag without having to extend the union.
4263
+ host: "127.0.0.1",
4264
+ port: WIN_VNC_PORT,
4265
+ osType: "windows-desktop"
4266
+ };
4267
+ }
4268
+ readPasswordOptional() {
4269
+ return readVncPasswordFromBeeoHome();
4270
+ }
4271
+ postSetupHint(_endpoint) {
4272
+ return [
4273
+ "Windows desktop streaming notes:",
4274
+ " \u2022 TightVNC is configured to accept loopback connections only \u2014 exposing it externally",
4275
+ " requires extra Windows Firewall + auth rules outside BeeOS.",
4276
+ " \u2022 If the dashboard's desktop view stays blank, open Services.msc and verify",
4277
+ " that `tvnserver` is in the Running state."
4278
+ ];
4279
+ }
4280
+ };
4281
+ windowsDesktopPlatform = new WindowsDesktopPlatform();
4282
+ }
4283
+ });
4284
+
4285
+ // ../core/dist/desktop/hints.js
4286
+ var LINUX_DESKTOP_DOCTOR_HINT_LINES, WINDOWS_DESKTOP_DOCTOR_HINT_LINES;
4287
+ var init_hints = __esm({
4288
+ "../core/dist/desktop/hints.js"() {
4289
+ "use strict";
4290
+ LINUX_DESKTOP_DOCTOR_HINT_LINES = [
4291
+ "vnc-bridge-openclaw is failing on Linux. Likely causes:",
4292
+ " \u2022 `vncserver :1` is not running on 127.0.0.1:5901 \u2014 start it with",
4293
+ " `vncserver :1 -localhost yes -SecurityTypes VncAuth` and re-run",
4294
+ " `beeos doctor`.",
4295
+ " \u2022 ~/.vnc/passwd drifted out of sync with ~/.beeos/vnc.password \u2014",
4296
+ " re-run `beeos init` to regenerate both files in lockstep.",
4297
+ " \u2022 Headless box: ~/.vnc/xstartup boots into a black session \u2014 install",
4298
+ " a desktop environment (`xfce4`, `gnome-session`, \u2026) and add it to",
4299
+ " the xstartup line."
4300
+ ];
4301
+ WINDOWS_DESKTOP_DOCTOR_HINT_LINES = [
4302
+ "vnc-bridge-openclaw is failing on Windows. Likely causes:",
4303
+ " \u2022 `tvnserver` service is stopped \u2014 open Services.msc, find",
4304
+ " `TightVNC Server`, click Start. Or re-run `beeos init` to",
4305
+ " re-apply the cold-start configuration.",
4306
+ " \u2022 Someone changed the TightVNC password outside BeeOS \u2014",
4307
+ " `HKLM\\SOFTWARE\\TightVNC\\Server\\Password` no longer matches",
4308
+ " %USERPROFILE%\\.beeos\\vnc.password. Re-run `beeos init` to",
4309
+ " regenerate both ends in one shot (UAC dialog will appear).",
4310
+ " \u2022 Windows Firewall is blocking the loopback bridge \u2014 TightVNC",
4311
+ " is configured for AllowLoopback=1 + LoopbackOnly=1, but a",
4312
+ " firewall rule that blocks 127.0.0.1:5900 will still break it."
4313
+ ];
4314
+ }
4315
+ });
4316
+
4317
+ // ../core/dist/desktop/index.js
4318
+ function inferDesktopPlatformId(plat) {
4319
+ switch (plat) {
4320
+ case "darwin":
4321
+ return "macos";
4322
+ case "linux":
4323
+ return "linux";
4324
+ case "win32":
4325
+ return "windows";
4326
+ default:
4327
+ return null;
4328
+ }
4329
+ }
4330
+ function getDesktopPlatform() {
4331
+ const plat = getPlatformAdapter().platform();
4332
+ const id = inferDesktopPlatformId(plat);
4333
+ switch (id) {
4334
+ case "macos":
4335
+ return macosDesktopPlatform;
4336
+ case "linux":
4337
+ return linuxDesktopPlatform;
4338
+ case "windows":
4339
+ return windowsDesktopPlatform;
4340
+ default:
4341
+ return linuxDesktopPlatform;
4342
+ }
4343
+ }
4344
+ function getDesktopPipeline() {
4345
+ if (process.env.BEEOS_NO_DESKTOP === "1")
4346
+ return null;
4347
+ return new DesktopPipeline(getDesktopPlatform());
4348
+ }
4349
+ var init_desktop = __esm({
4350
+ "../core/dist/desktop/index.js"() {
4351
+ "use strict";
4352
+ init_platform_adapter();
4353
+ init_pipeline();
4354
+ init_macos();
4355
+ init_linux();
4356
+ init_windows();
4357
+ init_pipeline();
4358
+ init_common();
4359
+ init_hints();
4360
+ }
4361
+ });
4362
+
3561
4363
  // ../core/dist/detect.js
3562
4364
  async function detectExistingInstall() {
3563
4365
  const p = getPlatformAdapter();
@@ -3788,35 +4590,6 @@ var init_registry = __esm({
3788
4590
  }
3789
4591
  });
3790
4592
 
3791
- // ../core/dist/services/target-spec.js
3792
- function buildVncBridgeTargetSpec(binary, opts) {
3793
- return {
3794
- id: `vnc-bridge-${opts.serial}`,
3795
- kind: "vnc-bridge",
3796
- command: binary,
3797
- args: [],
3798
- env: vncBridgeRuntime.buildEnv(opts),
3799
- restart: "on-failure",
3800
- label: `vnc-bridge (${opts.serial})`
3801
- };
3802
- }
3803
- function buildOpenclawDesktopVncBridgeSpec(binary, opts) {
3804
- return buildVncBridgeTargetSpec(binary, {
3805
- ...opts,
3806
- serial: OPENCLAW_VNC_BRIDGE_SERIAL
3807
- });
3808
- }
3809
- var OPENCLAW_VNC_BRIDGE_SERIAL;
3810
- var init_target_spec = __esm({
3811
- "../core/dist/services/target-spec.js"() {
3812
- "use strict";
3813
- init_scrcpy_bridge();
3814
- init_vnc_bridge();
3815
- init_spawn_env2();
3816
- OPENCLAW_VNC_BRIDGE_SERIAL = "openclaw";
3817
- }
3818
- });
3819
-
3820
4593
  // ../core/dist/services/ids.js
3821
4594
  import path from "path";
3822
4595
  function safeId(id) {
@@ -5414,6 +6187,7 @@ var init_dist = __esm({
5414
6187
  init_agent_status();
5415
6188
  init_desktop_detect();
5416
6189
  init_macos_desktop_cold_start();
6190
+ init_desktop();
5417
6191
  init_detect();
5418
6192
  init_registry();
5419
6193
  init_target_spec();
@@ -5936,6 +6710,20 @@ function resolvePerDeviceAgentGatewayUrl(cfg, override) {
5936
6710
  }
5937
6711
  return trimmed;
5938
6712
  }
6713
+ function resolveBackendForAttach(options, hostBackend) {
6714
+ if (options.self === true) {
6715
+ if (hostBackend === null || hostBackend === "self" || hostBackend === "adb") {
6716
+ throw new BeeosError({
6717
+ code: "self_attach_unsupported_os",
6718
+ message: "`beeos device attach --self` is not supported on this host yet.",
6719
+ hint: "Self-attach maps the host's native backend (macOS/Linux/Windows). Run on a supported OS, or attach an ADB device with `--serial` instead."
6720
+ });
6721
+ }
6722
+ return hostBackend;
6723
+ }
6724
+ if (options.vncHost) return void 0;
6725
+ return "adb";
6726
+ }
5939
6727
  async function pickAttachSerial() {
5940
6728
  const devices = await deviceRuntime.listAdbDevices();
5941
6729
  const ready = devices.filter((d) => d.status === "device");
@@ -6009,7 +6797,10 @@ async function attach(options) {
6009
6797
  });
6010
6798
  const fp = fingerprintFromB64(pubkeyB64);
6011
6799
  const boundAt = Math.floor(Date.now() / 1e3);
6012
- const backend = options.vncHost ? void 0 : "adb";
6800
+ const backend = resolveBackendForAttach(
6801
+ options,
6802
+ inferHostBackend()
6803
+ );
6013
6804
  state.devices.push({
6014
6805
  serial,
6015
6806
  name,
@@ -6017,12 +6808,6 @@ async function attach(options) {
6017
6808
  key_file: keyFile,
6018
6809
  http_port: httpPort,
6019
6810
  video_mode: bridgeInfo.mode,
6020
- // For now `beeos device attach` only binds ADB devices (the
6021
- // `--vnc-host` shape spawns a vnc-bridge but does NOT spawn a
6022
- // device-mcp-server, so `backend` only ever drives the ADB
6023
- // path through fleet → mcp). When self-attach for
6024
- // macOS/Linux/Windows lands, this field will be set
6025
- // accordingly (`macos`/`linux`/`windows`).
6026
6811
  backend,
6027
6812
  vnc_host: options.vncHost,
6028
6813
  vnc_port: options.vncPort,
@@ -6262,7 +7047,7 @@ async function runAttachStage2(params) {
6262
7047
  key_file: keyFile,
6263
7048
  http_port: httpPort,
6264
7049
  video_mode: bridgeInfo.mode,
6265
- backend: options.vncHost ? void 0 : "adb",
7050
+ backend: resolveBackendForAttach(options, inferHostBackend()),
6266
7051
  vnc_host: options.vncHost,
6267
7052
  vnc_port: options.vncPort,
6268
7053
  // P0-D: see attach() above — every new entry self-pins to the
@@ -6371,7 +7156,7 @@ function shouldOfferFleetEnable(inputs = {
6371
7156
  stdoutIsTTY: process.stdout.isTTY,
6372
7157
  env: process.env
6373
7158
  }) {
6374
- if (inputs.platform !== "darwin") return false;
7159
+ if (inputs.platform !== "darwin" && inputs.platform !== "linux") return false;
6375
7160
  if (inputs.env.BEEOS_NO_FLEET_AUTOSTART === "1") return false;
6376
7161
  if (!inputs.stdinIsTTY) return false;
6377
7162
  if (!inputs.stdoutIsTTY) return false;
@@ -7112,23 +7897,24 @@ async function run(agentFramework, options) {
7112
7897
  const launchWarnings = [...launchOutcome.warnings];
7113
7898
  reporter.stop();
7114
7899
  const ttyHints = !options.json && process.stdin.isTTY === true;
7900
+ const desktopCapable = hasDesktopCapability(driver);
7901
+ const desktopPipeline = desktopCapable ? getDesktopPipeline() : null;
7115
7902
  let coldStart = null;
7116
- if (agentFramework === OPENCLAW_ID) {
7903
+ if (desktopPipeline) {
7117
7904
  const interactive = !options.json && process.stdin.isTTY === true && process.stdout.isTTY === true;
7118
- coldStart = await macosDesktopColdStart({
7905
+ coldStart = await desktopPipeline.coldStart({
7119
7906
  prompt: interactive ? promptYesNo : void 0,
7120
7907
  log: options.json ? () => void 0 : (m) => console.log(m),
7121
- yes: options.force === true,
7122
- noDesktop: process.env.BEEOS_NO_DESKTOP === "1"
7908
+ yes: options.force === true
7123
7909
  });
7124
7910
  if (coldStart.warnings && coldStart.warnings.length > 0) {
7125
7911
  launchWarnings.push(...coldStart.warnings);
7126
7912
  }
7127
7913
  }
7128
- const vncEndpoint = agentFramework === OPENCLAW_ID ? await probeLocalVnc({ ttyHints }) : null;
7914
+ const vncEndpoint = desktopPipeline ? await desktopPipeline.probe({ ttyHints }) : null;
7129
7915
  const hostname = buildHostname();
7130
7916
  const cachedBinding = await loadBindingInfo();
7131
- const bindVncPassword = vncEndpoint ? await readVncPasswordOptional() : void 0;
7917
+ const bindVncPassword = vncEndpoint && desktopPipeline ? await desktopPipeline.readPasswordOptional() : void 0;
7132
7918
  const outcome = await bindAgent({
7133
7919
  apiUrl: cfg.platform.api_url,
7134
7920
  dashboardBaseUrl: cfg.platform.dashboard_base_url,
@@ -7242,18 +8028,36 @@ async function run(agentFramework, options) {
7242
8028
  });
7243
8029
  }
7244
8030
  }
7245
- if (vncEndpoint && !isOffline) {
7246
- if (vncEndpoint.kind === "macos" && ttyHints) {
7247
- printMacosDesktopHint();
8031
+ if (vncEndpoint && !isOffline && desktopPipeline && hasDesktopCapability(driver)) {
8032
+ const platformHints = desktopPipeline.postSetupHint(vncEndpoint);
8033
+ if (platformHints.length > 0 && ttyHints) {
8034
+ for (const line of platformHints) console.log(line);
7248
8035
  }
7249
- const desktopWarning = await tryAttachOpenclawDesktopBridge({
8036
+ const desktopReporter = new CliReporter();
8037
+ const bridgeOutcome = await driver.desktopBridgeSpec(
7250
8038
  vncEndpoint,
7251
- instanceId: boundInstanceId,
7252
- keyFile,
7253
- agentGatewayUrl,
7254
- mgr
7255
- });
7256
- if (desktopWarning) launchWarnings.push(desktopWarning);
8039
+ {
8040
+ instanceId: boundInstanceId,
8041
+ keyFile,
8042
+ agentGatewayUrl,
8043
+ ...bindVncPassword ? { vncPassword: bindVncPassword } : {}
8044
+ },
8045
+ desktopReporter
8046
+ );
8047
+ desktopReporter.stop();
8048
+ if (bridgeOutcome.warnings.length > 0) {
8049
+ launchWarnings.push(...bridgeOutcome.warnings);
8050
+ }
8051
+ if (bridgeOutcome.spec) {
8052
+ try {
8053
+ await mgr.install(bridgeOutcome.spec);
8054
+ } catch (e) {
8055
+ const reason = e instanceof Error ? e.message : String(e);
8056
+ launchWarnings.push(
8057
+ `vnc-bridge service install failed: ${reason}; ${driver.displayName} is bound but desktop streaming is unavailable. Re-run \`beeos start ${agentFramework} --force\` to retry.`
8058
+ );
8059
+ }
8060
+ }
7257
8061
  }
7258
8062
  const gatewayPid = status2.pid ?? void 0;
7259
8063
  emit(options.json, {
@@ -7267,50 +8071,6 @@ async function run(agentFramework, options) {
7267
8071
  }, isOffline ? `Agent running (offline, cached instance: ${boundInstanceId})` : `Agent bound to instance: ${boundInstanceId}`);
7268
8072
  if (!options.json) printLaunchWarnings(launchWarnings);
7269
8073
  }
7270
- async function tryAttachOpenclawDesktopBridge(params) {
7271
- const reporter = new CliReporter();
7272
- let binary = null;
7273
- try {
7274
- binary = await vncBridgeRuntime.ensureInstalled(reporter);
7275
- } catch (e) {
7276
- reporter.stop();
7277
- const reason = e instanceof Error ? e.message : String(e);
7278
- return `vnc-bridge install failed: ${reason}; OpenClaw is bound but desktop streaming is unavailable.`;
7279
- }
7280
- reporter.stop();
7281
- if (!binary) {
7282
- return `vnc-bridge binary unavailable (download or PATH lookup failed); OpenClaw is bound but desktop streaming is unavailable. Set BEEOS_VNC_BRIDGE_BIN to override.`;
7283
- }
7284
- const vncPassword = await readVncPasswordOptional();
7285
- const spec = buildOpenclawDesktopVncBridgeSpec(binary, {
7286
- deviceId: params.instanceId,
7287
- vncHost: params.vncEndpoint.host,
7288
- vncPort: params.vncEndpoint.port,
7289
- ...vncPassword ? { vncPassword } : {},
7290
- agentGatewayUrl: params.agentGatewayUrl,
7291
- bridgePrivateKeyFile: params.keyFile,
7292
- bridgePublicKeyFile: params.keyFile
7293
- });
7294
- try {
7295
- await params.mgr.install(spec);
7296
- } catch (e) {
7297
- const reason = e instanceof Error ? e.message : String(e);
7298
- return `vnc-bridge service install failed: ${reason}; OpenClaw is bound but desktop streaming is unavailable. Re-run \`beeos start openclaw --force\` to retry.`;
7299
- }
7300
- return null;
7301
- }
7302
- async function readVncPasswordOptional() {
7303
- const env = process.env.BEEOS_VNC_PASSWORD?.trim();
7304
- if (env) return env;
7305
- const p = getPlatformAdapter();
7306
- const file = p.joinPath(beeoHome(), "vnc.password");
7307
- try {
7308
- const raw = (await p.readFile(file)).trim();
7309
- return raw || void 0;
7310
- } catch {
7311
- return void 0;
7312
- }
7313
- }
7314
8074
  async function promptYesNo(question) {
7315
8075
  return new Promise((resolve) => {
7316
8076
  const rl = readline.createInterface({
@@ -7794,6 +8554,9 @@ function printNextSteps() {
7794
8554
  console.log("Next steps:");
7795
8555
  console.log(" beeos status # show running agents");
7796
8556
  console.log(" beeos device attach # bind a physical Android device");
8557
+ if (inferDesktopPlatformId(process.platform) === "macos" || inferDesktopPlatformId(process.platform) === "linux") {
8558
+ console.log(" beeos device attach --self # (experimental) attach this Mac/Linux desktop directly");
8559
+ }
7797
8560
  console.log(" beeos doctor # health check & diagnostics");
7798
8561
  console.log("");
7799
8562
  }
@@ -7952,10 +8715,10 @@ async function run8(options) {
7952
8715
  );
7953
8716
  }
7954
8717
  if (s.id === `vnc-bridge-${OPENCLAW_VNC_BRIDGE_SERIAL}`) {
7955
- if (process.platform === "darwin") {
7956
- for (const line of MACOS_DESKTOP_DOCTOR_HINT_LINES) {
7957
- hints.push(line);
7958
- }
8718
+ const desktopId = inferDesktopPlatformId(process.platform);
8719
+ const platformHints = desktopId === "macos" ? MACOS_DESKTOP_DOCTOR_HINT_LINES : desktopId === "linux" ? LINUX_DESKTOP_DOCTOR_HINT_LINES : desktopId === "windows" ? WINDOWS_DESKTOP_DOCTOR_HINT_LINES : null;
8720
+ if (platformHints) {
8721
+ for (const line of platformHints) hints.push(line);
7959
8722
  } else {
7960
8723
  hints.push(
7961
8724
  `vnc-bridge-${OPENCLAW_VNC_BRIDGE_SERIAL} is failing \u2014 see ${s.logFile} for the underlying error. Common causes: VNC server moved port, BEEOS_VNC_PASSWORD missing/incorrect, network blocking outbound MQTT.`
@@ -8443,6 +9206,9 @@ deviceCmd.command("attach").description("Attach an ADB-connected device as an AI
8443
9206
  "--vnc-host <host>",
8444
9207
  "Use vnc-bridge against a VNC server at this host (desktop/Linux/macOS). Implies video mode = vnc instead of scrcpy."
8445
9208
  ).option("--vnc-port <port>", "VNC TCP port (default 5900)", (v) => Number(v)).option("--vnc-password <password>", "VNC password (forwarded via env)").option(
9209
+ "--self",
9210
+ "(experimental) Tag the persisted DeviceEntry with the host's native backend (macOS/Linux/Windows) instead of `adb`. Forward-compatible with the upcoming self-attach lifecycle (Phase 4 of the multi-OS install-link refactor); today only the persisted backend tag changes."
9211
+ ).option(
8446
9212
  "--agent-gateway-url <url>",
8447
9213
  "Override the Agent Gateway URL for THIS device only (advanced; multi-region staging). Persists to the device entry so the fleet supervisor reuses it on every restart. Falls back to the global config when omitted."
8448
9214
  ).action(attach);