@beeos-ai/cli 1.0.16 → 1.0.17

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
@@ -1546,22 +1546,36 @@ var init_device = __esm({
1546
1546
  const lines = result.stdout.split("\n").slice(1);
1547
1547
  for (const line of lines) {
1548
1548
  const parts = line.trim().split(/\s+/);
1549
- if (parts.length >= 2 && parts[1] === "device") {
1550
- devices.push({ serial: parts[0] });
1551
- }
1549
+ if (parts.length < 2)
1550
+ continue;
1551
+ const serial = parts[0];
1552
+ const rawStatus = parts[1];
1553
+ let status2;
1554
+ if (rawStatus === "device")
1555
+ status2 = "device";
1556
+ else if (rawStatus === "unauthorized")
1557
+ status2 = "unauthorized";
1558
+ else
1559
+ status2 = "offline";
1560
+ devices.push({ serial, status: status2, rawStatus });
1552
1561
  }
1553
1562
  return devices;
1554
1563
  },
1555
1564
  async detectSingleDevice() {
1556
- const devices = await this.listAdbDevices();
1557
- if (devices.length === 0) {
1565
+ const all = await this.listAdbDevices();
1566
+ const ready = all.filter((d) => d.status === "device");
1567
+ if (ready.length === 0) {
1568
+ if (all.length > 0) {
1569
+ const issues = all.map((d) => `${d.serial}=${d.rawStatus}`).join(", ");
1570
+ throw new Error(`No ADB devices ready (only: ${issues}). Accept the USB prompt on the phone or check the cable.`);
1571
+ }
1558
1572
  throw new Error("No ADB devices found. Connect a device and try again.");
1559
1573
  }
1560
- if (devices.length > 1) {
1561
- const serials = devices.map((d) => d.serial).join(", ");
1574
+ if (ready.length > 1) {
1575
+ const serials = ready.map((d) => d.serial).join(", ");
1562
1576
  throw new Error(`Multiple ADB devices found: ${serials}. Use --serial to specify one.`);
1563
1577
  }
1564
- return devices[0].serial;
1578
+ return ready[0].serial;
1565
1579
  },
1566
1580
  async spawnForDevice(serial, agentBin, bridgeUrl, httpEnabled, httpPort, agentGatewayUrl) {
1567
1581
  const p = getPlatformAdapter();
@@ -3347,13 +3361,13 @@ function inferInitChoices(state) {
3347
3361
  if (state.hasIdentity && state.binding) {
3348
3362
  return {
3349
3363
  defaultDecision: "upgrade",
3350
- options: ["upgrade", "rebind-new-key", "skip"]
3364
+ options: ["upgrade", "rebind", "reset-all", "skip"]
3351
3365
  };
3352
3366
  }
3353
3367
  if (state.hasIdentity) {
3354
3368
  return {
3355
3369
  defaultDecision: "upgrade",
3356
- options: ["upgrade", "rebind-new-key", "skip"]
3370
+ options: ["upgrade", "reset-all", "skip"]
3357
3371
  };
3358
3372
  }
3359
3373
  return { defaultDecision: "fresh", options: ["fresh", "skip"] };
@@ -3364,8 +3378,10 @@ function initDecisionLabel(decision) {
3364
3378
  return "Install + bind";
3365
3379
  case "upgrade":
3366
3380
  return "Upgrade CLI & agents (keep binding & key)";
3367
- case "rebind-new-key":
3368
- return "Re-bind (rotate Ed25519 key)";
3381
+ case "rebind":
3382
+ return "Re-bind (keep key, refresh binding \u2014 same instance)";
3383
+ case "reset-all":
3384
+ return "Reset everything (NEW key, NEW instance \u2014 for compromised key)";
3369
3385
  case "skip":
3370
3386
  return "Skip (do nothing)";
3371
3387
  }
@@ -5109,6 +5125,20 @@ var init_fallback_banner = __esm({
5109
5125
  }
5110
5126
  });
5111
5127
 
5128
+ // src/json-envelope.ts
5129
+ function jsonOk(data) {
5130
+ return { ok: true, data };
5131
+ }
5132
+ function emitJsonEnvelope(env) {
5133
+ console.log(JSON.stringify(env, null, 2));
5134
+ }
5135
+ var init_json_envelope = __esm({
5136
+ "src/json-envelope.ts"() {
5137
+ "use strict";
5138
+ init_dist();
5139
+ }
5140
+ });
5141
+
5112
5142
  // src/commands/device/fleet-notify.ts
5113
5143
  async function notifyFleetReloadBestEffort(baseUrl = FLEET_STATUS_BASE_URL) {
5114
5144
  try {
@@ -5245,6 +5275,207 @@ var init_upgrade2 = __esm({
5245
5275
  }
5246
5276
  });
5247
5277
 
5278
+ // src/lib/instance-picker.ts
5279
+ import readline from "readline";
5280
+ function resolveIO(ctx) {
5281
+ return {
5282
+ input: ctx.input ?? process.stdin,
5283
+ output: ctx.output ?? process.stdout
5284
+ };
5285
+ }
5286
+ function isTTY(ctx, io) {
5287
+ if (ctx.isTTY !== void 0) return ctx.isTTY;
5288
+ return Boolean(io.input.isTTY);
5289
+ }
5290
+ function ask(io, question) {
5291
+ const rl = readline.createInterface({ input: io.input, output: io.output });
5292
+ return new Promise((resolve) => {
5293
+ rl.question(question, (answer) => {
5294
+ rl.close();
5295
+ resolve(answer);
5296
+ });
5297
+ });
5298
+ }
5299
+ function statusBlurb(status2) {
5300
+ switch (status2) {
5301
+ case "device":
5302
+ return "device";
5303
+ case "unauthorized":
5304
+ return "unauthorized \u2014 accept the USB prompt on the phone";
5305
+ case "offline":
5306
+ return "offline \u2014 check the USB cable / `adb kill-server`";
5307
+ }
5308
+ }
5309
+ async function pickInstanceInteractive(candidates, ctx) {
5310
+ const io = resolveIO(ctx);
5311
+ if (!isTTY(ctx, io)) {
5312
+ return { kind: "skip" };
5313
+ }
5314
+ const frameworks = candidates.filter(
5315
+ (c) => c.kind === "framework"
5316
+ );
5317
+ const devices = candidates.filter(
5318
+ (c) => c.kind === "device"
5319
+ );
5320
+ if (ctx.caller === "init") {
5321
+ if (frameworks.length === 1 && devices.length === 0) {
5322
+ return { kind: "framework", id: frameworks[0].id };
5323
+ }
5324
+ return renderMixedMenu(frameworks, devices, io);
5325
+ }
5326
+ if (devices.length === 0) {
5327
+ if (ctx.caller === "discover") {
5328
+ io.output.write("No ADB devices found. Connect a device and try again.\n");
5329
+ }
5330
+ return { kind: "skip" };
5331
+ }
5332
+ if (devices.length === 1) {
5333
+ return askYesNoSingleDevice(devices[0], io);
5334
+ }
5335
+ return renderDeviceMenu(devices, io);
5336
+ }
5337
+ async function renderMixedMenu(frameworks, devices, io) {
5338
+ const items = [];
5339
+ let idx = 1;
5340
+ for (const fw of frameworks) {
5341
+ items.push({
5342
+ idx: idx++,
5343
+ kind: "framework",
5344
+ id: fw.id,
5345
+ label: fw.label,
5346
+ isDefault: fw.isDefault === true
5347
+ });
5348
+ }
5349
+ for (const d of devices) {
5350
+ items.push({
5351
+ idx: idx++,
5352
+ kind: "device",
5353
+ serial: d.serial,
5354
+ status: d.status,
5355
+ selectable: d.status === "device"
5356
+ });
5357
+ }
5358
+ const defaultItem = items.find((i) => i.kind === "framework" && i.isDefault) ?? items[0];
5359
+ io.output.write("\n");
5360
+ io.output.write("What would you like to bind on this machine?\n");
5361
+ io.output.write("\n");
5362
+ if (frameworks.length > 0) {
5363
+ io.output.write(" Local agent:\n");
5364
+ for (const it of items) {
5365
+ if (it.kind !== "framework") continue;
5366
+ const def = it.isDefault ? " (default)" : "";
5367
+ io.output.write(` [${it.idx}] ${it.label}${def}
5368
+ `);
5369
+ }
5370
+ io.output.write("\n");
5371
+ }
5372
+ if (devices.length > 0) {
5373
+ io.output.write(` ADB devices (${devices.length} found):
5374
+ `);
5375
+ for (const it of items) {
5376
+ if (it.kind !== "device") continue;
5377
+ const tag = statusBlurb(it.status);
5378
+ io.output.write(` [${it.idx}] ${it.serial.padEnd(20)} ${tag}
5379
+ `);
5380
+ }
5381
+ io.output.write("\n");
5382
+ }
5383
+ io.output.write(" [s] Skip\n");
5384
+ io.output.write("\n");
5385
+ return promptForChoice(items, defaultItem.idx, io);
5386
+ }
5387
+ async function renderDeviceMenu(devices, io) {
5388
+ const items = devices.map((d, i) => ({
5389
+ idx: i + 1,
5390
+ kind: "device",
5391
+ serial: d.serial,
5392
+ status: d.status,
5393
+ selectable: d.status === "device"
5394
+ }));
5395
+ io.output.write("\n");
5396
+ io.output.write(`Detected ${devices.length} ADB devices:
5397
+ `);
5398
+ for (const it of items) {
5399
+ io.output.write(` [${it.idx}] ${it.serial.padEnd(20)} ${statusBlurb(it.status)}
5400
+ `);
5401
+ }
5402
+ io.output.write("\n");
5403
+ return promptForChoice(
5404
+ items,
5405
+ /* defaultIdx */
5406
+ -1,
5407
+ io
5408
+ );
5409
+ }
5410
+ async function askYesNoSingleDevice(device, io) {
5411
+ if (device.status !== "device") {
5412
+ io.output.write(
5413
+ `Found 1 ADB device (serial=${device.serial}) but its status is ${statusBlurb(device.status)}.
5414
+ Skipping. Fix the device state and re-run.
5415
+ `
5416
+ );
5417
+ return { kind: "skip" };
5418
+ }
5419
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
5420
+ const answer = (await ask(io, `Found 1 ADB device (serial=${device.serial}). Attach now? [Y/n]: `)).trim().toLowerCase();
5421
+ if (answer === "" || answer === "y" || answer === "yes") {
5422
+ return { kind: "device", serial: device.serial };
5423
+ }
5424
+ if (answer === "n" || answer === "no" || answer === "s" || answer === "skip") {
5425
+ return { kind: "skip" };
5426
+ }
5427
+ io.output.write(` invalid input '${answer}'. Please answer Y or n.
5428
+ `);
5429
+ }
5430
+ io.output.write(" too many invalid inputs \u2014 skipping.\n");
5431
+ return { kind: "skip" };
5432
+ }
5433
+ async function promptForChoice(items, defaultIdx, io) {
5434
+ const range = `1-${items.length}`;
5435
+ const defaultHint = defaultIdx > 0 ? `default ${defaultIdx}` : "default skip";
5436
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
5437
+ const raw = await ask(io, `Choose [${range} / s] (${defaultHint}): `);
5438
+ const trimmed = raw.trim().toLowerCase();
5439
+ if (trimmed === "" && defaultIdx > 0) {
5440
+ return itemToSelection(items[defaultIdx - 1], io);
5441
+ }
5442
+ if (trimmed === "" || trimmed === "s" || trimmed === "skip") {
5443
+ return { kind: "skip" };
5444
+ }
5445
+ const n = Number.parseInt(trimmed, 10);
5446
+ if (!Number.isFinite(n) || n < 1 || n > items.length) {
5447
+ io.output.write(` invalid input '${raw.trim()}'. Please pick a number in ${range} or 's'.
5448
+ `);
5449
+ continue;
5450
+ }
5451
+ const picked = items[n - 1];
5452
+ if (picked.kind === "device" && !picked.selectable) {
5453
+ io.output.write(
5454
+ ` device ${picked.serial} is ${statusBlurb(picked.status)} and cannot be attached yet.
5455
+ Fix the device state on the phone and re-run.
5456
+ `
5457
+ );
5458
+ continue;
5459
+ }
5460
+ return itemToSelection(picked, io);
5461
+ }
5462
+ io.output.write(" too many invalid inputs \u2014 skipping.\n");
5463
+ return { kind: "skip" };
5464
+ }
5465
+ function itemToSelection(item, _io) {
5466
+ if (item.kind === "framework") {
5467
+ return { kind: "framework", id: item.id };
5468
+ }
5469
+ return { kind: "device", serial: item.serial };
5470
+ }
5471
+ var MAX_RETRIES;
5472
+ var init_instance_picker = __esm({
5473
+ "src/lib/instance-picker.ts"() {
5474
+ "use strict";
5475
+ MAX_RETRIES = 3;
5476
+ }
5477
+ });
5478
+
5248
5479
  // src/commands/device/state.ts
5249
5480
  import lockfile from "proper-lockfile";
5250
5481
  function deviceAgentTargetId(serial) {
@@ -5310,7 +5541,7 @@ var init_state2 = __esm({
5310
5541
 
5311
5542
  // src/commands/device/attach.ts
5312
5543
  import { spawn as spawn2 } from "child_process";
5313
- import readline from "readline";
5544
+ import readline2 from "readline";
5314
5545
  function resolvePerDeviceAgentGatewayUrl(cfg, override) {
5315
5546
  if (override === void 0) return resolveAgentGatewayUrl(cfg);
5316
5547
  const trimmed = override.trim();
@@ -5342,6 +5573,24 @@ function resolvePerDeviceAgentGatewayUrl(cfg, override) {
5342
5573
  }
5343
5574
  return trimmed;
5344
5575
  }
5576
+ async function pickAttachSerial() {
5577
+ const devices = await deviceRuntime.listAdbDevices();
5578
+ const ready = devices.filter((d) => d.status === "device");
5579
+ if (ready.length <= 1) {
5580
+ return deviceRuntime.detectSingleDevice();
5581
+ }
5582
+ if (!process.stdin.isTTY) {
5583
+ return deviceRuntime.detectSingleDevice();
5584
+ }
5585
+ const candidates = devices.map((d) => ({
5586
+ kind: "device",
5587
+ serial: d.serial,
5588
+ status: d.status
5589
+ }));
5590
+ const sel = await pickInstanceInteractive(candidates, { caller: "attach" });
5591
+ if (sel.kind === "device") return sel.serial;
5592
+ return deviceRuntime.detectSingleDevice();
5593
+ }
5345
5594
  async function attach(options) {
5346
5595
  const cfg = await loadOrCreateConfig();
5347
5596
  const reporter = new CliReporter();
@@ -5353,7 +5602,7 @@ async function attach(options) {
5353
5602
  if (options.all) {
5354
5603
  return attachAll(cfg, reporter, withVideo, options);
5355
5604
  }
5356
- const serial = options.serial ?? await deviceRuntime.detectSingleDevice();
5605
+ const serial = options.serial ?? await pickAttachSerial();
5357
5606
  const name = options.name ?? serial;
5358
5607
  await deviceRuntime.ensureAgent(reporter);
5359
5608
  reporter.stop();
@@ -5468,11 +5717,26 @@ async function attach(options) {
5468
5717
  await maybeNotifyFleetWithHint(cfg);
5469
5718
  }
5470
5719
  async function attachAll(cfg, reporter, withVideo, options) {
5471
- const devices = await deviceRuntime.listAdbDevices();
5720
+ const all = await deviceRuntime.listAdbDevices();
5721
+ const devices = all.filter((d) => d.status === "device");
5722
+ const skipped = all.filter((d) => d.status !== "device");
5472
5723
  if (devices.length === 0) {
5473
- console.log("No ADB devices detected");
5724
+ if (skipped.length > 0) {
5725
+ console.log("No ADB devices ready to attach. Issues:");
5726
+ for (const d of skipped) {
5727
+ console.log(` ${d.serial}: ${d.rawStatus}`);
5728
+ }
5729
+ } else {
5730
+ console.log("No ADB devices detected");
5731
+ }
5474
5732
  return;
5475
5733
  }
5734
+ if (skipped.length > 0) {
5735
+ console.log(`Skipping ${skipped.length} device(s) not in 'device' state:`);
5736
+ for (const d of skipped) {
5737
+ console.log(` ${d.serial}: ${d.rawStatus}`);
5738
+ }
5739
+ }
5476
5740
  console.log(`Discovered ${devices.length} device(s) via adb.`);
5477
5741
  await deviceRuntime.ensureAgent(reporter);
5478
5742
  reporter.stop();
@@ -5712,7 +5976,7 @@ async function maybeNotifyFleetWithHint(cfg) {
5712
5976
  console.log("it, the device is recorded in ~/.beeos/devices.json but");
5713
5977
  console.log("no process is supervising the per-device agent.");
5714
5978
  console.log("");
5715
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5979
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
5716
5980
  const answer = await new Promise((resolve) => {
5717
5981
  rl.question("Enable fleet now (registers a launchd service)? [Y/n]: ", (a) => {
5718
5982
  rl.close();
@@ -5886,7 +6150,7 @@ Android SDK Platform-Tools (adb) is licensed under the Android SDK License Agree
5886
6150
  Set BEEOS_ACCEPT_ADB_LICENSE=1 in your environment to skip this prompt next time.
5887
6151
  `
5888
6152
  );
5889
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6153
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
5890
6154
  const ans = await new Promise((resolve) => {
5891
6155
  rl.question("Agree to the Android SDK license and download platform-tools? [Y/n]: ", (a) => {
5892
6156
  rl.close();
@@ -5900,6 +6164,7 @@ var init_attach = __esm({
5900
6164
  "use strict";
5901
6165
  init_dist();
5902
6166
  init_progress2();
6167
+ init_instance_picker();
5903
6168
  init_fallback_banner();
5904
6169
  init_fleet_notify();
5905
6170
  init_state2();
@@ -6136,11 +6401,65 @@ var init_refresh_config = __esm({
6136
6401
  }
6137
6402
  });
6138
6403
 
6404
+ // src/commands/device/discover.ts
6405
+ async function run6(options = {}) {
6406
+ const reporter = new CliReporter();
6407
+ await ensureAdb(reporter, { autoInstall: false });
6408
+ reporter.stop();
6409
+ let devices;
6410
+ try {
6411
+ devices = await deviceRuntime.listAdbDevices();
6412
+ } catch (e) {
6413
+ if (options.json) {
6414
+ emitJsonEnvelope(jsonOk({ devices: [], selected: null, action: "skip" }));
6415
+ return;
6416
+ }
6417
+ console.log("ADB is not available \u2014 install Android platform-tools or run `beeos device attach` once to auto-install.");
6418
+ console.log(` (${e instanceof Error ? e.message : String(e)})`);
6419
+ return;
6420
+ }
6421
+ if (options.json) {
6422
+ emitJsonEnvelope(
6423
+ jsonOk({
6424
+ devices: devices.map((d) => ({
6425
+ serial: d.serial,
6426
+ status: d.status,
6427
+ rawStatus: d.rawStatus
6428
+ })),
6429
+ selected: null,
6430
+ action: "list"
6431
+ })
6432
+ );
6433
+ return;
6434
+ }
6435
+ const candidates = devices.map((d) => ({
6436
+ kind: "device",
6437
+ serial: d.serial,
6438
+ status: d.status
6439
+ }));
6440
+ const sel = await pickInstanceInteractive(candidates, { caller: "discover" });
6441
+ if (sel.kind === "skip") return;
6442
+ if (sel.kind === "device") {
6443
+ await attach({ serial: sel.serial });
6444
+ }
6445
+ }
6446
+ var init_discover = __esm({
6447
+ "src/commands/device/discover.ts"() {
6448
+ "use strict";
6449
+ init_dist();
6450
+ init_progress2();
6451
+ init_json_envelope();
6452
+ init_instance_picker();
6453
+ init_attach();
6454
+ }
6455
+ });
6456
+
6139
6457
  // src/commands/device/index.ts
6140
6458
  var device_exports = {};
6141
6459
  __export(device_exports, {
6142
6460
  attach: () => attach,
6143
6461
  detach: () => detach,
6462
+ discover: () => run6,
6144
6463
  exec: () => exec,
6145
6464
  list: () => list,
6146
6465
  nextHttpPort: () => nextHttpPort,
@@ -6156,6 +6475,7 @@ var init_device2 = __esm({
6156
6475
  init_exec();
6157
6476
  init_upgrade2();
6158
6477
  init_refresh_config();
6478
+ init_discover();
6159
6479
  init_state2();
6160
6480
  }
6161
6481
  });
@@ -6379,18 +6699,8 @@ var NodePlatformAdapter = class {
6379
6699
  init_dist();
6380
6700
  init_progress2();
6381
6701
  init_fallback_banner();
6702
+ init_json_envelope();
6382
6703
  import os6 from "os";
6383
-
6384
- // src/json-envelope.ts
6385
- init_dist();
6386
- function jsonOk(data) {
6387
- return { ok: true, data };
6388
- }
6389
- function emitJsonEnvelope(env) {
6390
- console.log(JSON.stringify(env, null, 2));
6391
- }
6392
-
6393
- // src/commands/start.ts
6394
6704
  async function run(agentFramework, options) {
6395
6705
  const p = getPlatformAdapter();
6396
6706
  await ensureDirs();
@@ -6581,6 +6891,7 @@ async function run2(agentFramework) {
6581
6891
 
6582
6892
  // src/commands/status.ts
6583
6893
  init_dist();
6894
+ init_json_envelope();
6584
6895
  async function run3(options = {}) {
6585
6896
  const home = beeoHome();
6586
6897
  const cfg = await loadOrCreateConfig();
@@ -6646,6 +6957,7 @@ function formatService(s) {
6646
6957
 
6647
6958
  // src/commands/update.ts
6648
6959
  init_dist();
6960
+ init_json_envelope();
6649
6961
  init_upgrade2();
6650
6962
  async function run4(agentFramework, options = {}) {
6651
6963
  if (agentFramework === "device") {
@@ -6686,6 +6998,7 @@ async function run4(agentFramework, options = {}) {
6686
6998
 
6687
6999
  // src/commands/bind-status.ts
6688
7000
  init_dist();
7001
+ init_json_envelope();
6689
7002
  async function run5(bindId, options) {
6690
7003
  const cfg = await loadOrCreateConfig();
6691
7004
  const resp = await getBindStatus(cfg.platform.api_url, bindId);
@@ -6749,14 +7062,15 @@ init_device2();
6749
7062
 
6750
7063
  // src/commands/init.ts
6751
7064
  init_dist();
6752
- import readline2 from "readline";
7065
+ import readline3 from "readline";
6753
7066
 
6754
7067
  // src/commands/doctor.ts
6755
7068
  init_dist();
7069
+ init_json_envelope();
6756
7070
  import fs9 from "fs/promises";
6757
7071
  import path9 from "path";
6758
7072
  var LOG_SIZE_WARN_BYTES = 50 * 1024 * 1024;
6759
- async function run6(options) {
7073
+ async function run7(options) {
6760
7074
  const state = await detectExistingInstall();
6761
7075
  const cfg = await loadOrCreateConfig();
6762
7076
  const p = getPlatformAdapter();
@@ -7062,9 +7376,11 @@ function formatAgentGatewayStatus(h) {
7062
7376
  }
7063
7377
 
7064
7378
  // src/commands/init.ts
7379
+ init_attach();
7065
7380
  init_progress2();
7066
7381
  init_fallback_banner();
7067
- async function run7(options) {
7382
+ init_instance_picker();
7383
+ async function run8(options) {
7068
7384
  await ensureDirs();
7069
7385
  if (options.device) {
7070
7386
  const { attach: attach2 } = await Promise.resolve().then(() => (init_device2(), device_exports));
@@ -7073,7 +7389,7 @@ async function run7(options) {
7073
7389
  }
7074
7390
  const state = await detectExistingInstall();
7075
7391
  if (options.json) {
7076
- await run6({ json: true });
7392
+ await run7({ json: true });
7077
7393
  } else {
7078
7394
  printBanner();
7079
7395
  for (const line of summarizeExistingInstall(state)) {
@@ -7099,10 +7415,45 @@ async function run7(options) {
7099
7415
  }
7100
7416
  console.log("Ensuring OpenClaw is running...\n");
7101
7417
  }
7102
- if (decision === "rebind-new-key") {
7418
+ if (decision === "rebind") {
7419
+ await removeBindingInfo();
7420
+ }
7421
+ if (decision === "reset-all") {
7103
7422
  await rotateIdentity();
7104
7423
  await removeBindingInfo();
7105
7424
  }
7425
+ if (decision === "fresh" && !options.framework && !options.yes && !options.json && process.stdin.isTTY) {
7426
+ const sel = await pickFreshInstance(options);
7427
+ if (sel.kind === "skip") {
7428
+ console.log("Skipped. Re-run `beeos init` or `beeos device attach` whenever ready.");
7429
+ return;
7430
+ }
7431
+ if (sel.kind === "device") {
7432
+ await attach({ serial: sel.serial });
7433
+ return;
7434
+ }
7435
+ const descriptor2 = frameworkById(sel.id);
7436
+ if (!descriptor2 || descriptor2.status !== "available") {
7437
+ throw new BeeosError({
7438
+ code: "framework_unavailable",
7439
+ message: `Agent framework '${sel.id}' is not available.`,
7440
+ hint: "Pick a different framework or reinstall the CLI.",
7441
+ details: { requested: sel.id, available: availableFrameworks().map((f) => f.id) }
7442
+ });
7443
+ }
7444
+ await run(descriptor2.id, {
7445
+ force: true,
7446
+ json: options.json,
7447
+ browser: options.browser !== false
7448
+ });
7449
+ if (!options.json && !options.skipServicePrompt) {
7450
+ await maybePromptServiceInstall();
7451
+ }
7452
+ if (!options.json) {
7453
+ printNextSteps();
7454
+ }
7455
+ return;
7456
+ }
7106
7457
  const frameworkId = await decideFramework(state, decision, options);
7107
7458
  const descriptor = frameworkById(frameworkId);
7108
7459
  if (!descriptor || descriptor.status !== "available") {
@@ -7201,6 +7552,32 @@ async function decideFramework(state, decision, options) {
7201
7552
  }
7202
7553
  return picked.id;
7203
7554
  }
7555
+ async function safeListAdbDevices() {
7556
+ const adbPath = await findAdb().catch(() => null);
7557
+ if (!adbPath) return [];
7558
+ try {
7559
+ return await deviceRuntime.listAdbDevices();
7560
+ } catch {
7561
+ return [];
7562
+ }
7563
+ }
7564
+ async function pickFreshInstance(_opts) {
7565
+ const frameworks = availableFrameworks().map((f) => ({
7566
+ kind: "framework",
7567
+ id: f.id,
7568
+ label: f.displayName,
7569
+ description: f.description,
7570
+ isDefault: f.id === defaultFrameworkId()
7571
+ }));
7572
+ const devices = (await safeListAdbDevices()).map((d) => ({
7573
+ kind: "device",
7574
+ serial: d.serial,
7575
+ status: d.status
7576
+ }));
7577
+ return pickInstanceInteractive([...frameworks, ...devices], {
7578
+ caller: "init"
7579
+ });
7580
+ }
7204
7581
  async function maybePromptServiceInstall() {
7205
7582
  try {
7206
7583
  const mgr = await getServiceManager();
@@ -7252,7 +7629,7 @@ function printNextSteps() {
7252
7629
  console.log("");
7253
7630
  }
7254
7631
  function prompt(question) {
7255
- const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
7632
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
7256
7633
  return new Promise((resolve) => {
7257
7634
  rl.question(question, (answer) => {
7258
7635
  rl.close();
@@ -7328,6 +7705,7 @@ async function pickInstalledPackages() {
7328
7705
 
7329
7706
  // src/commands/service.ts
7330
7707
  init_dist();
7708
+ init_json_envelope();
7331
7709
  async function install(options) {
7332
7710
  const mgr = await getServiceManager();
7333
7711
  const check = await mgr.selfCheck();
@@ -7412,7 +7790,8 @@ async function status(options) {
7412
7790
 
7413
7791
  // src/commands/migrate.ts
7414
7792
  init_dist();
7415
- async function run8(options) {
7793
+ init_json_envelope();
7794
+ async function run9(options) {
7416
7795
  await ensureDirs();
7417
7796
  const mgr = await getServiceManager();
7418
7797
  const mig = await migrateLegacySupervisor(mgr);
@@ -7449,6 +7828,7 @@ async function run8(options) {
7449
7828
 
7450
7829
  // src/commands/logs.ts
7451
7830
  init_dist();
7831
+ init_json_envelope();
7452
7832
  function resolveLogTarget(input, services) {
7453
7833
  const exact = services.find((s) => s.id === input);
7454
7834
  if (exact) {
@@ -7470,7 +7850,7 @@ function resolveLogTarget(input, services) {
7470
7850
  matchedFrom: "fallback"
7471
7851
  };
7472
7852
  }
7473
- async function run9(idArg, options) {
7853
+ async function run10(idArg, options) {
7474
7854
  const mgr = await getServiceManager();
7475
7855
  let services = [];
7476
7856
  try {
@@ -7544,12 +7924,12 @@ setPlatformAdapter(new NodePlatformAdapter());
7544
7924
  var program = new Command();
7545
7925
  var cliVersion = getCliVersion(import.meta.url, resolveActiveDistTag());
7546
7926
  program.name("beeos").version(cliVersion.display).description("BeeOS \u2014 run AI agents from your desktop");
7547
- program.command("init").description("One-shot install + bind flow (default entry from the curl installer)").option("--framework <name>", "Agent framework to install (default: openclaw)").option("--yes", "Non-interactive \u2014 accept the default action", false).option("--json", "Output machine-readable JSON (implies --yes)", false).option("--no-browser", "Don't auto-open a browser for bind confirmation").option("--headless", "Never open a browser (use terminal QR only)", false).option("--skip-service-prompt", "Skip the system-service install prompt", false).option("--device", "Jump straight into `beeos device attach` instead", false).action((opts) => run7(opts));
7548
- program.command("doctor").description("Inspect local BeeOS state (install, binding, services, warnings)").option("--json", "Output machine-readable JSON", false).action((opts) => run6(opts));
7549
- program.command("migrate").description("Migrate legacy Node supervisor state to OS-native services (one-shot)").option("--json", "Output machine-readable JSON", false).action((opts) => run8(opts));
7927
+ program.command("init").description("One-shot install + bind flow (default entry from the curl installer)").option("--framework <name>", "Agent framework to install (default: openclaw)").option("--yes", "Non-interactive \u2014 accept the default action", false).option("--json", "Output machine-readable JSON (implies --yes)", false).option("--no-browser", "Don't auto-open a browser for bind confirmation").option("--headless", "Never open a browser (use terminal QR only)", false).option("--skip-service-prompt", "Skip the system-service install prompt", false).option("--device", "Jump straight into `beeos device attach` instead", false).action((opts) => run8(opts));
7928
+ program.command("doctor").description("Inspect local BeeOS state (install, binding, services, warnings)").option("--json", "Output machine-readable JSON", false).action((opts) => run7(opts));
7929
+ program.command("migrate").description("Migrate legacy Node supervisor state to OS-native services (one-shot)").option("--json", "Output machine-readable JSON", false).action((opts) => run9(opts));
7550
7930
  program.command("logs").description(
7551
7931
  "Tail logs for a BeeOS service. Pass an ADB serial or service id; omit to list every registered service's log file path."
7552
- ).argument("[id]", "Service id (e.g. `device-agent-<serial>`) or raw ADB serial").option("-f, --follow", "Stream new log lines (Ctrl-C to exit)", false).option("-n, --lines <n>", "Initial trailing lines to print (default 50)", (v) => Number(v)).option("--json", "Output machine-readable JSON (resolves the path; does not stream)", false).action((id, opts) => run9(id, opts));
7932
+ ).argument("[id]", "Service id (e.g. `device-agent-<serial>`) or raw ADB serial").option("-f, --follow", "Stream new log lines (Ctrl-C to exit)", false).option("-n, --lines <n>", "Initial trailing lines to print (default 50)", (v) => Number(v)).option("--json", "Output machine-readable JSON (resolves the path; does not stream)", false).action((id, opts) => run10(id, opts));
7553
7933
  var serviceCmd = program.command("service").description("Inspect and manage the OS-native service manager (launchd / systemd --user / Task Scheduler)");
7554
7934
  serviceCmd.command("install").description("Verify the OS service manager is available (per-target services install automatically)").option("--json", "Output machine-readable JSON", false).action((opts) => install(opts));
7555
7935
  serviceCmd.command("uninstall").description("Uninstall every BeeOS-managed OS service").option("--json", "Output machine-readable JSON", false).action((opts) => uninstall(opts));
@@ -7577,6 +7957,9 @@ deviceCmd.command("upgrade").description("Upgrade the device-agent (and scrcpy/v
7577
7957
  deviceCmd.command("refresh-config").description(
7578
7958
  "Re-fetch this device's VLM/ARouter config from Agent Gateway and rewrite ~/.beeos/<instance_id>.config.json (use after dashboard changes)."
7579
7959
  ).option("--serial <serial>", "ADB device serial number").option("--all", "Refresh every attached device", false).action(refreshConfig);
7960
+ deviceCmd.command("discover").description(
7961
+ "List ADB devices and pick one to attach (interactive). Use this when you've plugged in a phone after `beeos init` and want to attach it without remembering --serial."
7962
+ ).option("--json", "Output machine-readable JSON envelope ({ok,data})", false).action((opts) => run6(opts));
7580
7963
  program.parseAsync(process.argv).catch((err) => {
7581
7964
  const wantsJson = process.argv.includes("--json");
7582
7965
  if (isBeeosError(err)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beeos-ai/cli",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "description": "BeeOS CLI — run AI agents from your desktop",
6
6
  "bin": {
@@ -7,7 +7,7 @@
7
7
  "",
8
8
  " 1. web/packages/cli/scripts/install.sh (bash, pre-Node)",
9
9
  " 2. web/packages/cli/scripts/install.ps1 (PowerShell, pre-Node)",
10
- " 3. web/packages/cli/src/commands/init.ts (TS, post-Node)",
10
+ " 3. web/packages/core/src/detect.ts (TS, post-Node, used by init.ts)",
11
11
  "",
12
12
  "Pre-Node sites can't import TypeScript constants, so we anchor the",
13
13
  "wording with this manifest plus a vitest grep lint",
@@ -15,7 +15,23 @@
15
15
  "If you change a label in any of the three files, update the",
16
16
  "matching `keywords` here so the lint stays representative; if you",
17
17
  "add a new action altogether, add a new entry below + render it",
18
- "in all three sites."
18
+ "in all three sites.",
19
+ "",
20
+ "## Note on the 1.0.17 rebind split (P2 of the install-link review)",
21
+ "",
22
+ "detect.ts now exposes TWO rebind variants in addition to the three",
23
+ "actions enforced here:",
24
+ "",
25
+ " - 'rebind' — soft rebind, keeps Ed25519 key, refreshes binding;",
26
+ " same instance_id (platform Bind handler returns",
27
+ " the existing instance for a known pubkey).",
28
+ " - 'reset-all' — rotates the key AND wipes binding; NEW instance.",
29
+ "",
30
+ "These are NOT enforced across all 3 call sites because the",
31
+ "pre-Node bash/PowerShell installers have only [Reinstall][Re-run][Skip]",
32
+ "buttons — the rebind variants live exclusively inside `beeos init`'s",
33
+ "interactive menu. The 'rerun' keyword 'Re-bind' below incidentally",
34
+ "covers detect.ts's soft-rebind label, which keeps this lint passing."
19
35
  ],
20
36
  "actions": {
21
37
  "upgrade": {
@@ -23,7 +39,7 @@
23
39
  "keywords": ["Reinstall", "Upgrade"]
24
40
  },
25
41
  "rerun": {
26
- "$comment": "Reuse the existing install (no npm i -g). install.sh/install.ps1 say 'Re-run beeos init', init.ts shows the 'Re-bind' wording for the same conceptual action.",
42
+ "$comment": "Reuse the existing install (no npm i -g). install.sh/install.ps1 say 'Re-run beeos init'. detect.ts's soft-rebind label includes 'Re-bind' which is intentionally captured here too — see the 1.0.17 split note in $comment above.",
27
43
  "keywords": ["Re-run", "Re-bind", "Reusing"]
28
44
  },
29
45
  "skip": {
@@ -36,5 +52,18 @@
36
52
  "packages/cli/scripts/install.sh",
37
53
  "packages/cli/scripts/install.ps1",
38
54
  "packages/core/src/detect.ts"
39
- ]
55
+ ],
56
+ "$advisory_comment": "Strings below are advisory — not enforced by the lint, but checked by `existing-install-actions.test.ts` to make sure detect.ts's two rebind variants stay distinguishable from each other.",
57
+ "advisory": {
58
+ "rebind_soft": {
59
+ "$comment": "1.0.17+ : detect.ts must include this exact phrase so users see the soft-rebind keeps the same instance_id.",
60
+ "must_appear_in": ["packages/core/src/detect.ts"],
61
+ "keywords": ["keep key, refresh binding", "same instance"]
62
+ },
63
+ "rebind_destructive": {
64
+ "$comment": "1.0.17+ : detect.ts must include this phrase so the destructive rebind is unmistakable.",
65
+ "must_appear_in": ["packages/core/src/detect.ts"],
66
+ "keywords": ["NEW key", "NEW instance"]
67
+ }
68
+ }
40
69
  }
@@ -23,18 +23,24 @@
23
23
 
24
24
  # ── Parse-time checks ────────────────────────────────────────
25
25
 
26
- $scriptPath = Join-Path $PSScriptRoot "install.ps1"
26
+ # Pester 5 runs file-level code in the discovery phase but does NOT
27
+ # propagate vars defined there into It blocks at run phase. Compute
28
+ # the path inside each test (or via BeforeAll) so $scriptPath is
29
+ # always non-null when the assertions execute.
30
+ BeforeAll {
31
+ $script:ScriptPath = Join-Path $PSScriptRoot "install.ps1"
32
+ }
27
33
 
28
34
  Describe "install.ps1 parse-time integrity" {
29
35
  It "exists at the expected path" {
30
- Test-Path $scriptPath | Should -Be $true
36
+ Test-Path $script:ScriptPath | Should -Be $true
31
37
  }
32
38
 
33
39
  It "parses without syntax errors (AST round-trip)" {
34
40
  $tokens = $null
35
41
  $errors = $null
36
42
  [System.Management.Automation.Language.Parser]::ParseFile(
37
- $scriptPath,
43
+ $script:ScriptPath,
38
44
  [ref]$tokens,
39
45
  [ref]$errors
40
46
  ) | Out-Null
@@ -45,7 +51,7 @@ Describe "install.ps1 parse-time integrity" {
45
51
  $tokens = $null
46
52
  $errors = $null
47
53
  $ast = [System.Management.Automation.Language.Parser]::ParseFile(
48
- $scriptPath,
54
+ $script:ScriptPath,
49
55
  [ref]$tokens,
50
56
  [ref]$errors
51
57
  )
@@ -55,6 +61,9 @@ Describe "install.ps1 parse-time integrity" {
55
61
  $node -is [System.Management.Automation.Language.FunctionDefinitionAst]
56
62
  }, $true) | ForEach-Object { $_.Name }
57
63
 
64
+ # CLI >= 1.0.16: Install-DeviceAgentSuite has been removed —
65
+ # the device suite installs lazily on the first
66
+ # `beeos device attach` (see core/device-setup.ts).
58
67
  $expected = @(
59
68
  "Write-BeeInfo",
60
69
  "Write-BeeOk",
@@ -66,7 +75,6 @@ Describe "install.ps1 parse-time integrity" {
66
75
  "Show-InstallHints",
67
76
  "Send-Telemetry",
68
77
  "Read-ExistingInstallAction",
69
- "Install-DeviceAgentSuite",
70
78
  "Invoke-BeeosCli",
71
79
  # P2-8: PSCore + RunAs auto-elevation helpers.
72
80
  "Test-IsAdministrator",
@@ -83,7 +91,7 @@ Describe "install.ps1 parse-time integrity" {
83
91
 
84
92
  Describe "install.ps1 platform guard (P2-8)" {
85
93
  It "contains the non-Windows early-exit branch" {
86
- $content = Get-Content -Raw -Path $scriptPath
94
+ $content = Get-Content -Raw -Path $script:ScriptPath
87
95
  # Look for the marker comment AND the exit branch so a
88
96
  # superficial rename of the variable still trips the test.
89
97
  $content | Should -Match "This installer is Windows-only"
@@ -91,12 +99,12 @@ Describe "install.ps1 platform guard (P2-8)" {
91
99
  }
92
100
 
93
101
  It "contains a UAC auto-elevation branch" {
94
- $content = Get-Content -Raw -Path $scriptPath
102
+ $content = Get-Content -Raw -Path $script:ScriptPath
95
103
  $content | Should -Match "Start-Process .* -Verb RunAs"
96
104
  }
97
105
 
98
106
  It "honours BEEOS_NO_AUTO_ELEVATE=1 as an opt-out" {
99
- $content = Get-Content -Raw -Path $scriptPath
107
+ $content = Get-Content -Raw -Path $script:ScriptPath
100
108
  $content | Should -Match "BEEOS_NO_AUTO_ELEVATE"
101
109
  }
102
110
  }
@@ -106,13 +114,16 @@ Describe "install.ps1 stdin / TTY handover (P1-J)" {
106
114
  # prompts read from the exhausted pipe and silently take defaults.
107
115
  # Mirrors install.sh's `</dev/tty` reattachment.
108
116
  It "detects [Console]::IsInputRedirected" {
109
- $content = Get-Content -Raw -Path $scriptPath
117
+ $content = Get-Content -Raw -Path $script:ScriptPath
110
118
  $content | Should -Match "\[Console\]::IsInputRedirected"
111
119
  }
112
120
 
113
121
  It "uses Start-Process with -NoNewWindow when stdin is redirected" {
114
- $content = Get-Content -Raw -Path $scriptPath
115
- $content | Should -Match "Start-Process .* -NoNewWindow"
122
+ $content = Get-Content -Raw -Path $script:ScriptPath
123
+ # The Start-Process invocation in install.ps1 uses backtick line
124
+ # continuation, so `-NoNewWindow` lives on a separate line from
125
+ # `Start-Process`. `(?s)` enables dotall so `.` matches newlines.
126
+ $content | Should -Match "(?s)Start-Process .*?-NoNewWindow"
116
127
  }
117
128
  }
118
129
 
@@ -121,25 +132,29 @@ Describe "install.ps1 stderr log file (P1-I)" {
121
132
  # MUST land in a forensic file instead of `2>$null`. The lint
122
133
  # below is structural — assert the variable + the redirect appear.
123
134
  It "declares a `$BeeosInstallLog` variable" {
124
- $content = Get-Content -Raw -Path $scriptPath
135
+ $content = Get-Content -Raw -Path $script:ScriptPath
125
136
  $content | Should -Match "BeeosInstallLog"
126
137
  }
127
138
 
128
139
  It "redirects winget stderr to the install log" {
129
- $content = Get-Content -Raw -Path $scriptPath
140
+ $content = Get-Content -Raw -Path $script:ScriptPath
130
141
  # `2>>$BeeosInstallLog` is the canonical append-redirect syntax;
131
142
  # `2>$null` here would be a regression to the silent
132
143
  # behaviour the review called out.
133
- $content | Should -Match "winget install [^`r`n]+2>>\$BeeosInstallLog"
144
+ # Use a single-quoted string so `$BeeosInstallLog` is NOT
145
+ # subject to PowerShell variable interpolation; the .NET regex
146
+ # engine still treats `\r` / `\n` as CR / LF and `\$` as a
147
+ # literal `$`.
148
+ $content | Should -Match 'winget install [^\r\n]+2>>\$BeeosInstallLog'
134
149
  }
135
150
 
136
151
  It "redirects choco stderr to the install log" {
137
- $content = Get-Content -Raw -Path $scriptPath
138
- $content | Should -Match "choco install [^`r`n]+2>>\$BeeosInstallLog"
152
+ $content = Get-Content -Raw -Path $script:ScriptPath
153
+ $content | Should -Match 'choco install [^\r\n]+2>>\$BeeosInstallLog'
139
154
  }
140
155
 
141
156
  It "Show-InstallHints surfaces the log path on failure" {
142
- $content = Get-Content -Raw -Path $scriptPath
157
+ $content = Get-Content -Raw -Path $script:ScriptPath
143
158
  $content | Should -Match "Installer log:"
144
159
  }
145
160
  }
@@ -164,6 +179,6 @@ Describe "install.ps1 dry-run hook" {
164
179
  # Dot-source so we exercise the param-block + helper definitions
165
180
  # without invoking the main install body. The dry-run hook
166
181
  # `return`s before any state-changing call.
167
- { . $scriptPath } | Should -Not -Throw
182
+ { . $script:ScriptPath } | Should -Not -Throw
168
183
  }
169
184
  }
@@ -23,16 +23,24 @@
23
23
  bootstrap requests).
24
24
  $env:BEEOS_DASHBOARD_URL Dashboard base (OAuth + bind redirect).
25
25
  $env:BEEOS_DEVICE_AGENT_VERSION Pin @beeos-ai/device-agent semver.
26
- $env:BEEOS_MCP_SERVER_VERSION Pin @beeos-ai/device-mcp-server semver.
26
+ NOT consumed by this script
27
+ (CLI >= 1.0.16 lazy-installs the
28
+ device suite on the first
29
+ `beeos device attach`); export it
30
+ in the shell that runs `beeos
31
+ device attach` instead.
32
+ $env:BEEOS_MCP_SERVER_VERSION Same, for @beeos-ai/device-mcp-server.
27
33
  $env:BEEOS_USE_NPX = "1" Power-user escape hatch: use `npx`
28
34
  for a throwaway install. `beeos`
29
35
  will NOT persist on PATH after
30
36
  the script exits. Default
31
37
  behaviour (since CLI 1.0.11) is
32
38
  always `npm install -g` so the
33
- CLI plus the device-agent suite
34
- are both globally available for
35
- follow-up commands.
39
+ CLI is globally available for
40
+ follow-up commands. The device
41
+ suite is NOT eagerly installed
42
+ (CLI >= 1.0.16); see
43
+ web/packages/core/src/device-setup.ts.
36
44
 
37
45
  .EXAMPLE
38
46
  # One-liner (PowerShell 5+):
@@ -47,12 +55,10 @@
47
55
  .\install.ps1 -Device
48
56
 
49
57
  .EXAMPLE
50
- # Bare-metal one-shot staging install (CLI >= 1.0.11):
58
+ # Bare-metal one-shot staging install (CLI >= 1.0.16):
51
59
  $env:BEEOS_API_URL = "https://public-api-staging.beeos.ai"
52
60
  $env:BEEOS_AGENT_GATEWAY_URL = "https://agent-gw-staging.beeos.ai"
53
61
  $env:BEEOS_DASHBOARD_URL = "https://beeos-staging-web.vercel.app"
54
- $env:BEEOS_DEVICE_AGENT_VERSION = "0.4.2"
55
- $env:BEEOS_MCP_SERVER_VERSION = "0.2.3"
56
62
  irm https://beeos.ai/install.ps1 | iex
57
63
  #>
58
64
  param(
@@ -102,18 +108,11 @@ if (-not $script:IsWindowsHost) {
102
108
 
103
109
  $CliPackage = if ($Version -eq "latest") { "@beeos-ai/cli@latest" } else { "@beeos-ai/cli@$Version" }
104
110
 
105
- # Sibling packages installed alongside the CLI so that `beeos device attach`
106
- # works on a clean machine without further `npm i -g` steps (a.k.a. the
107
- # "fleet ready" install strategy see agents/device-agent/ARCHITECTURE.md
108
- # for the 0.2.0+ sibling-pair topology between device-agent and
109
- # device-mcp-server). Versions are independent of `-Version` (which only
110
- # pins the CLI) — both packages follow their own semver. Override via
111
- # $env:BEEOS_DEVICE_AGENT_VERSION / $env:BEEOS_MCP_SERVER_VERSION when you
112
- # need to pin a specific build (e.g. for a staging smoke test).
113
- $DeviceAgentVersion = if ($env:BEEOS_DEVICE_AGENT_VERSION) { $env:BEEOS_DEVICE_AGENT_VERSION } else { "latest" }
114
- $McpServerVersion = if ($env:BEEOS_MCP_SERVER_VERSION) { $env:BEEOS_MCP_SERVER_VERSION } else { "latest" }
115
- $DeviceAgentPackage = "@beeos-ai/device-agent@$DeviceAgentVersion"
116
- $McpServerPackage = "@beeos-ai/device-mcp-server@$McpServerVersion"
111
+ # `@beeos-ai/device-agent` + `@beeos-ai/device-mcp-server` are NOT
112
+ # installed here. CLI >= 1.0.16 installs them lazily on the first
113
+ # `beeos device attach` via `ensureDeviceAgent` in
114
+ # `web/packages/core/src/device-setup.ts`. OpenClaw-only users never
115
+ # download the device suite at all.
117
116
 
118
117
  # Telemetry endpoint (override with $env:BEEOS_API_URL for staging).
119
118
  if (-not $env:BEEOS_API_URL -or $env:BEEOS_API_URL.Length -eq 0) {
@@ -479,39 +478,6 @@ function Read-ExistingInstallAction {
479
478
  }
480
479
  }
481
480
 
482
- # ── Device-agent sibling suite installer ─────────────────────
483
- #
484
- # Installs `@beeos-ai/device-agent` + `@beeos-ai/device-mcp-server` globally
485
- # so the very first `beeos device attach` on a clean machine does not need
486
- # to fetch them separately. `@beeos-ai/device-common` is pulled
487
- # transitively as a `dependencies` entry of both — no separate install
488
- # needed.
489
- #
490
- # Failure here is NEVER fatal: the CLI itself was already installed
491
- # successfully, and `beeos device attach` will prompt to install missing
492
- # pieces via ensureDeviceAgent on first use. We just emit a WARN with the
493
- # manual recovery command.
494
- function Install-DeviceAgentSuite {
495
- $npmPath = Get-Command npm -ErrorAction SilentlyContinue
496
- if (-not $npmPath) {
497
- Write-BeeWarn "npm not available — skipping device-agent suite install."
498
- Write-BeeWarn "Manual fix: install Node + run ``npm i -g $DeviceAgentPackage $McpServerPackage``."
499
- return
500
- }
501
-
502
- Write-BeeInfo "Installing device-agent suite ($DeviceAgentPackage, $McpServerPackage)..."
503
- & npm install -g $DeviceAgentPackage $McpServerPackage
504
- if ($LASTEXITCODE -eq 0) {
505
- Write-BeeOk "device-agent suite installed."
506
- } else {
507
- Write-BeeWarn "Failed to install device-agent suite — ``beeos device attach`` will prompt for it on first use."
508
- Write-BeeWarn "Manual fix: npm i -g $DeviceAgentPackage $McpServerPackage"
509
- # Reset $LASTEXITCODE so subsequent `if ($LASTEXITCODE -ne 0)` checks
510
- # in callers don't trip on this non-fatal failure.
511
- $global:LASTEXITCODE = 0
512
- }
513
- }
514
-
515
481
  # ── Run CLI ──────────────────────────────────────────────────
516
482
 
517
483
  function Invoke-BeeosCli {
@@ -547,9 +513,7 @@ function Invoke-BeeosCli {
547
513
  if ($LASTEXITCODE -ne 0) {
548
514
  # P0-A of the install-link review: fail-after-bootstrap signal.
549
515
  # The `install.bootstrap.cli_installed` event is only emitted
550
- # AFTER npm + the device-agent suite report success, so npm
551
- # failures are recorded as a distinct outcome (not as silent
552
- # post-success).
516
+ # AFTER npm reports success.
553
517
  Send-Telemetry -Event "install.bootstrap.npm_fail" -ErrorCode "npm_install_cli_failed" -Success $false
554
518
  Write-BeeError "npm install -g $CliPackage failed."
555
519
  Write-BeeError ""
@@ -559,7 +523,10 @@ function Invoke-BeeosCli {
559
523
  Write-BeeError " - EACCES / permission error: run PowerShell as Administrator"
560
524
  exit 1
561
525
  }
562
- Install-DeviceAgentSuite
526
+ # Device-agent suite is intentionally NOT installed here; `beeos
527
+ # device attach` will auto-install it on first use via
528
+ # `ensureDeviceAgent` (see `web/packages/core/src/device-setup.ts`).
529
+ # OpenClaw-only users never download those ~20MB.
563
530
  Send-Telemetry -Event "install.bootstrap.cli_installed"
564
531
 
565
532
  # P1-J of the install-link review: when the user invokes the
@@ -662,7 +629,10 @@ if ($beeosCmd) {
662
629
  Write-BeeError " - EACCES / permission error: run PowerShell as Administrator"
663
630
  exit 1
664
631
  }
665
- Install-DeviceAgentSuite
632
+ # Device suite is NOT upgraded here. `beeos device upgrade`
633
+ # is the canonical refresh path (it also restarts the
634
+ # running fleet); the install-script upgrade prompt only
635
+ # refreshes the CLI itself.
666
636
  }
667
637
  "rerun" {
668
638
  Write-BeeInfo "Reusing existing install."