@beeos-ai/cli 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -127,14 +127,14 @@ function projectSpawnEnv(cfg) {
127
127
  }
128
128
  async function saveSpawnEnvSnapshot(cfg) {
129
129
  const p = getPlatformAdapter();
130
- const path10 = spawnEnvJsonPath();
130
+ const path12 = spawnEnvJsonPath();
131
131
  const snapshot = projectSpawnEnv(cfg);
132
132
  try {
133
- await p.mkdir(p.dirname(path10));
134
- await p.writeFile(path10, JSON.stringify(snapshot, null, 2) + "\n");
133
+ await p.mkdir(p.dirname(path12));
134
+ await p.writeFile(path12, JSON.stringify(snapshot, null, 2) + "\n");
135
135
  } catch (err) {
136
136
  const message = err instanceof Error ? err.message : String(err);
137
- console.error(`! warning: could not write ${path10}: ${message}
137
+ console.error(`! warning: could not write ${path12}: ${message}
138
138
  device-agent fleet will fall back to parsing config.toml directly.`);
139
139
  }
140
140
  }
@@ -208,17 +208,17 @@ function defaultState() {
208
208
  }
209
209
  async function loadState() {
210
210
  const p = getPlatformAdapter();
211
- const path10 = statePath();
212
- if (await p.exists(path10)) {
211
+ const path12 = statePath();
212
+ if (await p.exists(path12)) {
213
213
  let raw;
214
214
  try {
215
- raw = await p.readFile(path10);
215
+ raw = await p.readFile(path12);
216
216
  } catch (e) {
217
217
  throw new BeeosError({
218
218
  code: "state_corrupted",
219
- message: `failed to read ${path10}: ${describeError(e)}`,
219
+ message: `failed to read ${path12}: ${describeError(e)}`,
220
220
  hint: "Inspect the file or move it aside and re-run `beeos doctor` to regenerate from legacy state.",
221
- details: { path: path10 },
221
+ details: { path: path12 },
222
222
  cause: e
223
223
  });
224
224
  }
@@ -228,23 +228,23 @@ async function loadState() {
228
228
  } catch (e) {
229
229
  throw new BeeosError({
230
230
  code: "state_corrupted",
231
- message: `${path10} is not valid JSON: ${describeError(e)}`,
231
+ message: `${path12} is not valid JSON: ${describeError(e)}`,
232
232
  hint: "Move the file aside (e.g. `mv ~/.beeos/state.json ~/.beeos/state.json.broken`) and re-run `beeos doctor` to migrate from the legacy state files.",
233
- details: { path: path10 }
233
+ details: { path: path12 }
234
234
  });
235
235
  }
236
- return validateState(parsed, path10);
236
+ return validateState(parsed, path12);
237
237
  }
238
238
  return migrateLegacy();
239
239
  }
240
240
  async function saveState(state) {
241
241
  const p = getPlatformAdapter();
242
- const path10 = statePath();
243
- await p.mkdir(p.dirname(path10));
244
- await p.writeFile(path10, JSON.stringify(state, null, 2) + "\n");
242
+ const path12 = statePath();
243
+ await p.mkdir(p.dirname(path12));
244
+ await p.writeFile(path12, JSON.stringify(state, null, 2) + "\n");
245
245
  if (p.platform() !== "win32") {
246
246
  try {
247
- await p.chmod(path10, 384);
247
+ await p.chmod(path12, 384);
248
248
  } catch {
249
249
  }
250
250
  }
@@ -319,11 +319,11 @@ async function migrateLegacy() {
319
319
  }
320
320
  async function tryReadBinding() {
321
321
  const p = getPlatformAdapter();
322
- const path10 = bindingPath();
323
- if (!await p.exists(path10))
322
+ const path12 = bindingPath();
323
+ if (!await p.exists(path12))
324
324
  return null;
325
325
  try {
326
- const raw = await p.readFile(path10);
326
+ const raw = await p.readFile(path12);
327
327
  return JSON.parse(raw);
328
328
  } catch {
329
329
  return null;
@@ -331,11 +331,11 @@ async function tryReadBinding() {
331
331
  }
332
332
  async function tryReadDevices() {
333
333
  const p = getPlatformAdapter();
334
- const path10 = legacyDevicesPath();
335
- if (!await p.exists(path10))
334
+ const path12 = legacyDevicesPath();
335
+ if (!await p.exists(path12))
336
336
  return [];
337
337
  try {
338
- const raw = await p.readFile(path10);
338
+ const raw = await p.readFile(path12);
339
339
  const parsed = JSON.parse(raw);
340
340
  return Array.isArray(parsed?.devices) ? parsed.devices : [];
341
341
  } catch {
@@ -344,11 +344,11 @@ async function tryReadDevices() {
344
344
  }
345
345
  async function tryReadSpawnEnvIntoPlatform(state) {
346
346
  const p = getPlatformAdapter();
347
- const path10 = spawnEnvJsonPath();
348
- if (!await p.exists(path10))
347
+ const path12 = spawnEnvJsonPath();
348
+ if (!await p.exists(path12))
349
349
  return;
350
350
  try {
351
- const raw = await p.readFile(path10);
351
+ const raw = await p.readFile(path12);
352
352
  const parsed = JSON.parse(raw);
353
353
  if (!parsed || typeof parsed !== "object")
354
354
  return;
@@ -364,7 +364,7 @@ async function tryReadSpawnEnvIntoPlatform(state) {
364
364
  async function tryFoldVlmConfigs(state) {
365
365
  const p = getPlatformAdapter();
366
366
  const root = beeoHome();
367
- let entries = [];
367
+ let entries;
368
368
  try {
369
369
  entries = await p.readdir(root);
370
370
  } catch {
@@ -391,13 +391,13 @@ async function tryFoldVlmConfigs(state) {
391
391
  }
392
392
  }
393
393
  }
394
- function validateState(parsed, path10) {
394
+ function validateState(parsed, path12) {
395
395
  if (!parsed || typeof parsed !== "object") {
396
396
  throw new BeeosError({
397
397
  code: "state_corrupted",
398
- message: `${path10} did not deserialise to a JSON object.`,
398
+ message: `${path12} did not deserialise to a JSON object.`,
399
399
  hint: "Move the file aside and re-run `beeos doctor` to regenerate.",
400
- details: { path: path10 }
400
+ details: { path: path12 }
401
401
  });
402
402
  }
403
403
  const obj = parsed;
@@ -405,26 +405,26 @@ function validateState(parsed, path10) {
405
405
  if (version !== 2) {
406
406
  throw new BeeosError({
407
407
  code: "state_version_mismatch",
408
- message: `${path10} declares version=${JSON.stringify(version)}; this CLI only understands version=2.`,
408
+ message: `${path12} declares version=${JSON.stringify(version)}; this CLI only understands version=2.`,
409
409
  hint: "Either upgrade the CLI (`beeos --version` to see what you have) or move the file aside and re-bind your agents with the older CLI's flow.",
410
- details: { path: path10, observed: version, expected: 2 }
410
+ details: { path: path12, observed: version, expected: 2 }
411
411
  });
412
412
  }
413
413
  const platform = obj.platform;
414
414
  if (!platform || typeof platform !== "object") {
415
415
  throw new BeeosError({
416
416
  code: "state_corrupted",
417
- message: `${path10} is missing a \`platform\` object.`,
417
+ message: `${path12} is missing a \`platform\` object.`,
418
418
  hint: "Move the file aside and re-run `beeos doctor`.",
419
- details: { path: path10 }
419
+ details: { path: path12 }
420
420
  });
421
421
  }
422
422
  if (!Array.isArray(obj.agents)) {
423
423
  throw new BeeosError({
424
424
  code: "state_corrupted",
425
- message: `${path10} \`agents\` is not an array.`,
425
+ message: `${path12} \`agents\` is not an array.`,
426
426
  hint: "Move the file aside and re-run `beeos doctor`.",
427
- details: { path: path10 }
427
+ details: { path: path12 }
428
428
  });
429
429
  }
430
430
  return obj;
@@ -448,9 +448,9 @@ var init_state = __esm({
448
448
  import * as TOML from "smol-toml";
449
449
  async function loadOrCreateConfig() {
450
450
  const p = getPlatformAdapter();
451
- const path10 = configPath();
452
- if (await p.exists(path10)) {
453
- const raw = await p.readFile(path10);
451
+ const path12 = configPath();
452
+ if (await p.exists(path12)) {
453
+ const raw = await p.readFile(path12);
454
454
  const parsed = TOML.parse(raw);
455
455
  const cfg2 = mergeWithDefaults(parsed);
456
456
  await saveSpawnEnvSnapshot(cfg2);
@@ -478,8 +478,8 @@ function applyEnvOverrides(cfg) {
478
478
  }
479
479
  async function saveConfig(cfg) {
480
480
  const p = getPlatformAdapter();
481
- const path10 = configPath();
482
- await p.mkdir(p.dirname(path10));
481
+ const path12 = configPath();
482
+ await p.mkdir(p.dirname(path12));
483
483
  const platformBlock = {
484
484
  api_url: cfg.platform.api_url,
485
485
  agent_gateway_url: cfg.platform.agent_gateway_url,
@@ -496,7 +496,7 @@ async function saveConfig(cfg) {
496
496
  }
497
497
  };
498
498
  const raw = TOML.stringify(serializable);
499
- await p.writeFile(path10, raw);
499
+ await p.writeFile(path12, raw);
500
500
  await saveSpawnEnvSnapshot(cfg);
501
501
  await refreshStatePlatformBestEffort(cfg);
502
502
  }
@@ -570,22 +570,22 @@ var init_toml = __esm({
570
570
  // ../core/dist/config/binding.js
571
571
  async function loadBindingInfo() {
572
572
  const p = getPlatformAdapter();
573
- const path10 = bindingPath();
574
- if (!await p.exists(path10))
573
+ const path12 = bindingPath();
574
+ if (!await p.exists(path12))
575
575
  return null;
576
- const raw = await p.readFile(path10);
576
+ const raw = await p.readFile(path12);
577
577
  return JSON.parse(raw);
578
578
  }
579
579
  async function saveBindingInfo(info) {
580
580
  const p = getPlatformAdapter();
581
- const path10 = bindingPath();
582
- await p.writeFile(path10, JSON.stringify(info, null, 2));
581
+ const path12 = bindingPath();
582
+ await p.writeFile(path12, JSON.stringify(info, null, 2));
583
583
  }
584
584
  async function removeBindingInfo() {
585
585
  const p = getPlatformAdapter();
586
- const path10 = bindingPath();
587
- if (await p.exists(path10)) {
588
- await p.rm(path10);
586
+ const path12 = bindingPath();
587
+ if (await p.exists(path12)) {
588
+ await p.rm(path12);
589
589
  }
590
590
  }
591
591
  var init_binding = __esm({
@@ -599,17 +599,17 @@ var init_binding = __esm({
599
599
  // ../core/dist/config/gateway-token.js
600
600
  async function loadOrCreateGatewayToken() {
601
601
  const p = getPlatformAdapter();
602
- const path10 = p.joinPath(beeoHome(), "gateway_token");
603
- if (await p.exists(path10)) {
604
- const token2 = (await p.readFile(path10)).trim();
602
+ const path12 = p.joinPath(beeoHome(), "gateway_token");
603
+ if (await p.exists(path12)) {
604
+ const token2 = (await p.readFile(path12)).trim();
605
605
  if (token2)
606
606
  return token2;
607
607
  }
608
608
  const token = crypto.randomUUID();
609
- await p.mkdir(p.dirname(path10));
610
- await p.writeFile(path10, token);
609
+ await p.mkdir(p.dirname(path12));
610
+ await p.writeFile(path12, token);
611
611
  if (p.platform() !== "win32") {
612
- await p.chmod(path10, 384);
612
+ await p.chmod(path12, 384);
613
613
  }
614
614
  return token;
615
615
  }
@@ -650,9 +650,9 @@ function fingerprintFromB64(pubkeyB64) {
650
650
  }
651
651
  async function loadOrCreateIdentity() {
652
652
  const p = getPlatformAdapter();
653
- const path10 = keypairPath();
654
- if (await p.exists(path10)) {
655
- return loadFromFile(path10);
653
+ const path12 = keypairPath();
654
+ if (await p.exists(path12)) {
655
+ return loadFromFile(path12);
656
656
  }
657
657
  const id = generate();
658
658
  await save(id);
@@ -684,10 +684,10 @@ async function readPubkeyFromKeyFile(keyPath) {
684
684
  async function loadIdentityFromKeyFile(keyPath) {
685
685
  return loadFromFile(keyPath);
686
686
  }
687
- async function signRequest(method, path10, id) {
687
+ async function signRequest(method, path12, id) {
688
688
  const timestamp = Math.floor(Date.now() / 1e3).toString();
689
689
  const nonce = crypto.randomUUID();
690
- const message = `${method.toUpperCase()}|${path10}|${timestamp}|${nonce}`;
690
+ const message = `${method.toUpperCase()}|${path12}|${timestamp}|${nonce}`;
691
691
  const msgBytes = new TextEncoder().encode(message);
692
692
  const sig = await ed.signAsync(msgBytes, id.privateKey);
693
693
  return {
@@ -705,9 +705,9 @@ function fingerprintFilePath() {
705
705
  const p = getPlatformAdapter();
706
706
  return p.joinPath(beeoHome(), "identity", "fingerprint");
707
707
  }
708
- async function loadFromFile(path10) {
708
+ async function loadFromFile(path12) {
709
709
  const p = getPlatformAdapter();
710
- const raw = await p.readFile(path10);
710
+ const raw = await p.readFile(path12);
711
711
  const stored = JSON.parse(raw);
712
712
  const privateKey = fromBase64(stored.privateKey);
713
713
  if (privateKey.length !== 32) {
@@ -745,6 +745,62 @@ var init_keypair = __esm({
745
745
  }
746
746
  });
747
747
 
748
+ // ../core/dist/identity/qr.js
749
+ import QRCode from "qrcode";
750
+ async function qrModules(url) {
751
+ const segments = QRCode.create(url, { errorCorrectionLevel: "M" });
752
+ const size = segments.modules.size;
753
+ const modules = [];
754
+ for (let y = 0; y < size; y++) {
755
+ for (let x = 0; x < size; x++) {
756
+ modules.push(segments.modules.get(x, y) !== 0);
757
+ }
758
+ }
759
+ return { width: size, modules };
760
+ }
761
+ async function qrToTerminal(url) {
762
+ const { width, modules } = await qrModules(url);
763
+ const rows = [];
764
+ for (let i = 0; i < modules.length; i += width) {
765
+ rows.push(modules.slice(i, i + width));
766
+ }
767
+ const quiet = 2;
768
+ const fullWidth = width + quiet * 2;
769
+ const blankLine = " ".repeat(fullWidth);
770
+ const lines = [];
771
+ for (let i = 0; i < quiet; i++)
772
+ lines.push(blankLine);
773
+ let y = 0;
774
+ while (y < rows.length) {
775
+ const topRow = rows[y];
776
+ const botRow = y + 1 < rows.length ? rows[y + 1] : null;
777
+ let line = " ".repeat(quiet);
778
+ for (let x = 0; x < width; x++) {
779
+ const top = topRow[x];
780
+ const bot = botRow ? botRow[x] : false;
781
+ if (top && bot)
782
+ line += "\u2588";
783
+ else if (top && !bot)
784
+ line += "\u2580";
785
+ else if (!top && bot)
786
+ line += "\u2584";
787
+ else
788
+ line += " ";
789
+ }
790
+ line += " ".repeat(quiet);
791
+ lines.push(line);
792
+ y += 2;
793
+ }
794
+ for (let i = 0; i < quiet; i++)
795
+ lines.push(blankLine);
796
+ return lines.join("\n");
797
+ }
798
+ var init_qr = __esm({
799
+ "../core/dist/identity/qr.js"() {
800
+ "use strict";
801
+ }
802
+ });
803
+
748
804
  // ../core/dist/platform/client.js
749
805
  async function bindHttpError(resp, op) {
750
806
  const body = await resp.text().catch(() => "");
@@ -870,62 +926,6 @@ var init_process = __esm({
870
926
  }
871
927
  });
872
928
 
873
- // ../core/dist/identity/qr.js
874
- import QRCode from "qrcode";
875
- async function qrModules(url) {
876
- const segments = QRCode.create(url, { errorCorrectionLevel: "M" });
877
- const size = segments.modules.size;
878
- const modules = [];
879
- for (let y = 0; y < size; y++) {
880
- for (let x = 0; x < size; x++) {
881
- modules.push(segments.modules.get(x, y) !== 0);
882
- }
883
- }
884
- return { width: size, modules };
885
- }
886
- async function qrToTerminal(url) {
887
- const { width, modules } = await qrModules(url);
888
- const rows = [];
889
- for (let i = 0; i < modules.length; i += width) {
890
- rows.push(modules.slice(i, i + width));
891
- }
892
- const quiet = 2;
893
- const fullWidth = width + quiet * 2;
894
- const blankLine = " ".repeat(fullWidth);
895
- const lines = [];
896
- for (let i = 0; i < quiet; i++)
897
- lines.push(blankLine);
898
- let y = 0;
899
- while (y < rows.length) {
900
- const topRow = rows[y];
901
- const botRow = y + 1 < rows.length ? rows[y + 1] : null;
902
- let line = " ".repeat(quiet);
903
- for (let x = 0; x < width; x++) {
904
- const top = topRow[x];
905
- const bot = botRow ? botRow[x] : false;
906
- if (top && bot)
907
- line += "\u2588";
908
- else if (top && !bot)
909
- line += "\u2580";
910
- else if (!top && bot)
911
- line += "\u2584";
912
- else
913
- line += " ";
914
- }
915
- line += " ".repeat(quiet);
916
- lines.push(line);
917
- y += 2;
918
- }
919
- for (let i = 0; i < quiet; i++)
920
- lines.push(blankLine);
921
- return lines.join("\n");
922
- }
923
- var init_qr = __esm({
924
- "../core/dist/identity/qr.js"() {
925
- "use strict";
926
- }
927
- });
928
-
929
929
  // ../core/dist/bind/orchestrator.js
930
930
  async function bindAgent(opts) {
931
931
  const log = opts.log ?? ((m) => console.log(m));
@@ -1112,6 +1112,135 @@ var init_orchestrator = __esm({
1112
1112
  }
1113
1113
  });
1114
1114
 
1115
+ // ../core/dist/runtime/npm-prefix-self-heal.js
1116
+ function looksLikeEacces(stderr) {
1117
+ const s = stderr.toLowerCase();
1118
+ return s.includes("eacces") || s.includes("eperm") || s.includes("permission denied");
1119
+ }
1120
+ function looksLikeEnospc(stderr) {
1121
+ const s = stderr.toLowerCase();
1122
+ return s.includes("enospc") || s.includes("no space left");
1123
+ }
1124
+ async function trySwitchToUserPrefix(progress) {
1125
+ const p = getPlatformAdapter();
1126
+ const optOut = (p.env("BEEOS_NO_NPM_PREFIX_FIX") ?? "").trim();
1127
+ if (optOut === "1" || optOut.toLowerCase() === "true") {
1128
+ return {
1129
+ success: false,
1130
+ prefix: "",
1131
+ persisted: false,
1132
+ note: "BEEOS_NO_NPM_PREFIX_FIX=1 \u2014 refusing to auto-switch npm prefix."
1133
+ };
1134
+ }
1135
+ const home = p.homeDir();
1136
+ const newPrefix = p.joinPath(home, ".npm-global");
1137
+ const current = await readCurrentPrefix();
1138
+ if (current && current.startsWith(home)) {
1139
+ return {
1140
+ success: true,
1141
+ prefix: current,
1142
+ persisted: false,
1143
+ note: `npm prefix already user-owned: ${current}`
1144
+ };
1145
+ }
1146
+ progress.onStatus(`Switching npm global prefix to ${newPrefix} (user-owned).`);
1147
+ await p.mkdir(newPrefix);
1148
+ const setRes = await p.exec("npm", ["config", "set", "prefix", newPrefix]);
1149
+ if (setRes.code !== 0) {
1150
+ return {
1151
+ success: false,
1152
+ prefix: newPrefix,
1153
+ persisted: false,
1154
+ note: `npm config set prefix failed: ${(setRes.stderr || setRes.stdout).trim()}`
1155
+ };
1156
+ }
1157
+ const binDir = p.platform() === "win32" ? newPrefix : p.joinPath(newPrefix, "bin");
1158
+ const pathKey = p.platform() === "win32" ? "Path" : "PATH";
1159
+ const sep = p.platform() === "win32" ? ";" : ":";
1160
+ const existing = process.env[pathKey] ?? process.env.PATH ?? "";
1161
+ if (!existing.split(sep).includes(binDir)) {
1162
+ process.env[pathKey] = `${binDir}${sep}${existing}`;
1163
+ if (pathKey !== "PATH")
1164
+ process.env.PATH = process.env[pathKey];
1165
+ }
1166
+ let persisted = false;
1167
+ if (p.platform() !== "win32") {
1168
+ persisted = await persistShellRcBestEffort(binDir);
1169
+ }
1170
+ return {
1171
+ success: true,
1172
+ prefix: newPrefix,
1173
+ persisted,
1174
+ note: persisted ? `Persisted PATH to your shell rc.` : `PATH updated for this session; add '${binDir}' to your shell init to make it permanent.`
1175
+ };
1176
+ }
1177
+ async function readCurrentPrefix() {
1178
+ const p = getPlatformAdapter();
1179
+ try {
1180
+ const res = await p.exec("npm", ["prefix", "-g"], { timeout: 1e4 });
1181
+ if (res.code !== 0)
1182
+ return null;
1183
+ const trimmed = res.stdout.trim();
1184
+ return trimmed.length > 0 ? trimmed : null;
1185
+ } catch {
1186
+ return null;
1187
+ }
1188
+ }
1189
+ async function persistShellRcBestEffort(binDir) {
1190
+ const p = getPlatformAdapter();
1191
+ const rc = pickRcPath();
1192
+ if (!rc)
1193
+ return false;
1194
+ let existing = "";
1195
+ try {
1196
+ if (await p.exists(rc))
1197
+ existing = await p.readFile(rc);
1198
+ } catch {
1199
+ return false;
1200
+ }
1201
+ if (existing.includes(SENTINEL_BEGIN)) {
1202
+ return true;
1203
+ }
1204
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
1205
+ const block = `
1206
+ ${SENTINEL_BEGIN}
1207
+ # Added by @beeos/core npm-prefix-self-heal on ${stamp}
1208
+ # Reason: lazy install hit EACCES, switched npm prefix to a user-owned dir.
1209
+ # To revert: delete this block AND run \`npm config delete prefix\`.
1210
+ export PATH="${binDir}:$PATH"
1211
+ ${SENTINEL_END}
1212
+ `;
1213
+ try {
1214
+ await p.writeFile(rc, existing + block);
1215
+ return true;
1216
+ } catch {
1217
+ return false;
1218
+ }
1219
+ }
1220
+ function pickRcPath() {
1221
+ const p = getPlatformAdapter();
1222
+ const home = p.homeDir();
1223
+ const shell = p.env("SHELL") ?? "";
1224
+ const base = shell.split("/").pop() ?? "";
1225
+ switch (base) {
1226
+ case "zsh":
1227
+ return p.joinPath(home, ".zshrc");
1228
+ case "bash":
1229
+ return p.platform() === "darwin" ? p.joinPath(home, ".bash_profile") : p.joinPath(home, ".bashrc");
1230
+ default:
1231
+ return null;
1232
+ }
1233
+ }
1234
+ var SENTINEL_BEGIN, SENTINEL_END;
1235
+ var init_npm_prefix_self_heal = __esm({
1236
+ "../core/dist/runtime/npm-prefix-self-heal.js"() {
1237
+ "use strict";
1238
+ init_platform_adapter();
1239
+ SENTINEL_BEGIN = "# >>> beeos npm-prefix >>>";
1240
+ SENTINEL_END = "# <<< beeos npm-prefix <<<";
1241
+ }
1242
+ });
1243
+
1115
1244
  // ../core/dist/upgrade.js
1116
1245
  function readPinSourcesFromEnv() {
1117
1246
  const env = globalThis.process?.env ?? {};
@@ -1183,13 +1312,23 @@ async function upgradeBeeosSuite(opts) {
1183
1312
  if (needsInstall) {
1184
1313
  const args = ["install", "-g", ...specs.map((s) => s.spec)];
1185
1314
  progress?.onStatus(`npm ${args.join(" ")}`);
1186
- try {
1187
- const result2 = await p.exec("npm", args, { timeout: 6e5 });
1188
- if (result2.code !== 0) {
1189
- installFailure = (result2.stderr || result2.stdout || `npm install -g exited ${result2.code}`).trim();
1315
+ installFailure = await runNpmInstallOnce(args);
1316
+ if (installFailure && looksLikeEacces(installFailure)) {
1317
+ const reporter = progress ?? {
1318
+ onStatus() {
1319
+ },
1320
+ onComplete() {
1321
+ }
1322
+ };
1323
+ reporter.onStatus("npm install -g failed with EACCES \u2014 attempting prefix self-heal.");
1324
+ const heal = await trySwitchToUserPrefix(reporter);
1325
+ reporter.onStatus(heal.note);
1326
+ if (heal.success) {
1327
+ reporter.onStatus(`Retrying: npm ${args.join(" ")}`);
1328
+ installFailure = await runNpmInstallOnce(args);
1190
1329
  }
1191
- } catch (e) {
1192
- installFailure = e instanceof Error ? e.message : String(e);
1330
+ } else if (installFailure && looksLikeEnospc(installFailure)) {
1331
+ installFailure = "npm install -g failed because the filesystem is out of space (typically /usr/local/lib/node_modules or %APPDATA%\\npm). Free disk space and retry. Original error:\n" + installFailure;
1193
1332
  }
1194
1333
  } else {
1195
1334
  progress?.onStatus("Already up to date \u2014 skipping npm install -g");
@@ -1220,6 +1359,58 @@ async function upgradeBeeosSuite(opts) {
1220
1359
  }
1221
1360
  return result;
1222
1361
  }
1362
+ async function runNpmInstallOnce(args) {
1363
+ const p = getPlatformAdapter();
1364
+ const useInherit = shouldInheritStdio();
1365
+ try {
1366
+ const result = await p.exec("npm", args, {
1367
+ timeout: 6e5,
1368
+ stdio: useInherit ? "inherit" : "pipe"
1369
+ });
1370
+ if (result.code !== 0) {
1371
+ if (useInherit) {
1372
+ const probe = await probeEaccesViaSet().catch(() => null);
1373
+ if (probe)
1374
+ return `npm install -g exited ${result.code}; ${probe}`;
1375
+ return `npm install -g exited ${result.code}`;
1376
+ }
1377
+ return (result.stderr || result.stdout || `npm install -g exited ${result.code}`).trim();
1378
+ }
1379
+ return void 0;
1380
+ } catch (e) {
1381
+ return e instanceof Error ? e.message : String(e);
1382
+ }
1383
+ }
1384
+ function shouldInheritStdio() {
1385
+ const proc = globalThis.process;
1386
+ if (!proc)
1387
+ return false;
1388
+ const isTty = Boolean(proc.stdout?.isTTY);
1389
+ if (!isTty)
1390
+ return false;
1391
+ const env = proc.env ?? {};
1392
+ if (env.BEEOS_NPM_QUIET === "1")
1393
+ return false;
1394
+ const argv = proc.argv ?? [];
1395
+ if (argv.some((a) => a === "--json"))
1396
+ return false;
1397
+ return true;
1398
+ }
1399
+ async function probeEaccesViaSet() {
1400
+ const p = getPlatformAdapter();
1401
+ const res = await p.exec("npm", ["config", "get", "prefix"], {
1402
+ timeout: 1e4
1403
+ });
1404
+ if (res.code !== 0)
1405
+ return null;
1406
+ const prefix = res.stdout.trim();
1407
+ if (!prefix)
1408
+ return null;
1409
+ const home = p.homeDir();
1410
+ if (prefix.startsWith(home))
1411
+ return null;
1412
+ return `EACCES likely (prefix ${prefix} outside $HOME)`;
1413
+ }
1223
1414
  function trimOrUndef(v) {
1224
1415
  if (v === void 0)
1225
1416
  return void 0;
@@ -1231,6 +1422,7 @@ var init_upgrade = __esm({
1231
1422
  "../core/dist/upgrade.js"() {
1232
1423
  "use strict";
1233
1424
  init_platform_adapter();
1425
+ init_npm_prefix_self_heal();
1234
1426
  NPM_PKGS = {
1235
1427
  CLI: "@beeos-ai/cli",
1236
1428
  DEVICE_AGENT: "@beeos-ai/device-agent",
@@ -1371,6 +1563,75 @@ var init_device_setup = __esm({
1371
1563
  }
1372
1564
  });
1373
1565
 
1566
+ // ../core/dist/runtime/http-fetch-retry.js
1567
+ async function fetchWithRetry(url, opts) {
1568
+ const p = getPlatformAdapter();
1569
+ const max = opts.maxAttempts ?? 3;
1570
+ let lastError = "";
1571
+ let lastStatus = null;
1572
+ for (let attempt = 1; attempt <= max; attempt++) {
1573
+ let resp;
1574
+ try {
1575
+ resp = await p.fetch(url);
1576
+ } catch (e) {
1577
+ lastError = e instanceof Error ? e.message : String(e);
1578
+ opts.progress.onStatus(`${opts.label} attempt ${attempt}/${max} failed: ${lastError}`);
1579
+ if (attempt < max)
1580
+ await sleepBackoff(attempt);
1581
+ continue;
1582
+ }
1583
+ if (!resp.ok) {
1584
+ lastStatus = resp.status;
1585
+ opts.progress.onStatus(`${opts.label} attempt ${attempt}/${max}: HTTP ${resp.status} ${resp.statusText}`);
1586
+ if (resp.status === 401 || resp.status === 403 || resp.status === 404) {
1587
+ return {
1588
+ ok: false,
1589
+ status: resp.status,
1590
+ lastError: `HTTP ${resp.status} ${resp.statusText}`
1591
+ };
1592
+ }
1593
+ lastError = `HTTP ${resp.status} ${resp.statusText}`;
1594
+ if (attempt < max)
1595
+ await sleepBackoff(attempt);
1596
+ continue;
1597
+ }
1598
+ try {
1599
+ return { ok: true, bytes: new Uint8Array(await resp.arrayBuffer()) };
1600
+ } catch (e) {
1601
+ lastError = e instanceof Error ? e.message : String(e);
1602
+ opts.progress.onStatus(`${opts.label} stream interrupted on attempt ${attempt}/${max}: ${lastError}`);
1603
+ if (attempt < max)
1604
+ await sleepBackoff(attempt);
1605
+ }
1606
+ }
1607
+ opts.progress.onStatus(`${opts.label} gave up after ${max} attempts (last: ${lastError || "unknown"}).`);
1608
+ return { ok: false, status: lastStatus, lastError };
1609
+ }
1610
+ async function fetchWithRetryOrNull(url, opts) {
1611
+ const out = await fetchWithRetry(url, opts);
1612
+ return out.ok ? out.bytes : null;
1613
+ }
1614
+ async function fetchTextWithRetryOrNull(url, opts) {
1615
+ const bytes = await fetchWithRetryOrNull(url, opts);
1616
+ if (!bytes)
1617
+ return null;
1618
+ try {
1619
+ return new TextDecoder("utf-8").decode(bytes);
1620
+ } catch {
1621
+ return null;
1622
+ }
1623
+ }
1624
+ function sleepBackoff(attempt) {
1625
+ const ms = 1e3 * Math.pow(2, attempt - 1);
1626
+ return new Promise((r) => setTimeout(r, ms));
1627
+ }
1628
+ var init_http_fetch_retry = __esm({
1629
+ "../core/dist/runtime/http-fetch-retry.js"() {
1630
+ "use strict";
1631
+ init_platform_adapter();
1632
+ }
1633
+ });
1634
+
1374
1635
  // ../core/dist/adb-setup.js
1375
1636
  import { createHash } from "crypto";
1376
1637
  function managedAdbPath() {
@@ -1408,11 +1669,14 @@ async function installAdb(progress) {
1408
1669
  }
1409
1670
  const url = `${PLATFORM_TOOLS_BASE}/${zipName}`;
1410
1671
  progress.onStatus(`Downloading Android platform-tools (${zipName})...`);
1411
- const resp = await p.fetch(url);
1412
- if (!resp.ok) {
1413
- throw new Error(`platform-tools download failed: ${resp.status} ${resp.statusText}`);
1672
+ const result = await fetchWithRetry(url, {
1673
+ label: "platform-tools",
1674
+ progress
1675
+ });
1676
+ if (!result.ok) {
1677
+ throw new Error(`platform-tools download failed: ${result.lastError}` + (result.status ? ` (HTTP ${result.status})` : ""));
1414
1678
  }
1415
- const data = new Uint8Array(await resp.arrayBuffer());
1679
+ const data = result.bytes;
1416
1680
  verifyPlatformToolsChecksum(zipName, data, progress);
1417
1681
  const binDir = p.joinPath(beeoHome(), "bin");
1418
1682
  await p.mkdir(binDir);
@@ -1511,6 +1775,7 @@ var init_adb_setup = __esm({
1511
1775
  init_platform_adapter();
1512
1776
  init_paths();
1513
1777
  init_errors();
1778
+ init_http_fetch_retry();
1514
1779
  PLATFORM_TOOLS_BASE = "https://dl.google.com/android/repository";
1515
1780
  PLATFORM_TOOLS_REVISION = "r37.0.0";
1516
1781
  PLATFORM_TOOLS_ARCHIVE = {
@@ -1797,46 +2062,16 @@ function archiveName(cfg, target) {
1797
2062
  const suffix = target.includes("windows") ? "zip" : "tar.gz";
1798
2063
  return `${cfg.name}-${target}.${suffix}`;
1799
2064
  }
1800
- async function fetchArchiveBuffer(cfg, url, progress, maxAttempts = 3) {
1801
- const p = getPlatformAdapter();
1802
- let lastError = null;
1803
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1804
- let resp;
1805
- try {
1806
- resp = await p.fetch(url);
1807
- } catch (e) {
1808
- lastError = String(e);
1809
- progress.onStatus(`${cfg.name} download attempt ${attempt}/${maxAttempts} failed: ${lastError}`);
1810
- if (attempt < maxAttempts) {
1811
- await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
1812
- }
1813
- continue;
1814
- }
1815
- if (!resp.ok) {
1816
- progress.onStatus(`${cfg.name} download: ${resp.status} ${resp.statusText} (${url})`);
1817
- if (resp.status === 404) {
1818
- progress.onStatus(`Hint: the current release may not include the requested target. Try setting $${cfg.binaryEnv} to a locally built binary, ` + (cfg.releaseUrlEnv ? `or $${cfg.releaseUrlEnv} to a different release URL.` : `or check for a newer release.`));
1819
- }
1820
- if (resp.status === 404 || resp.status === 403 || resp.status === 401) {
1821
- return null;
1822
- }
1823
- lastError = `${resp.status} ${resp.statusText}`;
1824
- if (attempt < maxAttempts) {
1825
- await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
1826
- }
1827
- continue;
1828
- }
1829
- try {
1830
- return new Uint8Array(await resp.arrayBuffer());
1831
- } catch (e) {
1832
- lastError = String(e);
1833
- progress.onStatus(`${cfg.name} stream interrupted on attempt ${attempt}/${maxAttempts}: ${lastError}`);
1834
- if (attempt < maxAttempts) {
1835
- await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
1836
- }
1837
- }
2065
+ async function fetchArchiveBuffer(cfg, url, progress) {
2066
+ const out = await fetchWithRetry(url, {
2067
+ label: cfg.name,
2068
+ progress
2069
+ });
2070
+ if (out.ok)
2071
+ return out.bytes;
2072
+ if (out.status === 404) {
2073
+ progress.onStatus(`Hint: the current release may not include the requested target. Try setting $${cfg.binaryEnv} to a locally built binary, ` + (cfg.releaseUrlEnv ? `or $${cfg.releaseUrlEnv} to a different release URL.` : `or check for a newer release.`));
1838
2074
  }
1839
- progress.onStatus(`${cfg.name} download gave up after ${maxAttempts} attempts (last: ${lastError ?? "unknown"}).`);
1840
2075
  return null;
1841
2076
  }
1842
2077
  async function downloadManagedBinary(cfg, target, progress) {
@@ -1851,20 +2086,19 @@ async function downloadManagedBinary(cfg, target, progress) {
1851
2086
  const allowUnverified = (process.env?.BEEOS_ALLOW_UNVERIFIED_SIDECAR ?? "").toLowerCase() === "1" || (process.env?.BEEOS_ALLOW_UNVERIFIED_SIDECAR ?? "").toLowerCase() === "true";
1852
2087
  let expected = null;
1853
2088
  let digestFetchError = null;
1854
- try {
1855
- const digResp = await p.fetch(digestUrl);
1856
- if (digResp.ok) {
1857
- const text = (await digResp.text()).trim();
1858
- const first = text.split(/\s+/)[0] ?? "";
1859
- if (/^[a-f0-9]{64}$/i.test(first))
1860
- expected = first.toLowerCase();
1861
- else
1862
- digestFetchError = `malformed .sha256 body: ${text.slice(0, 80)}`;
1863
- } else {
1864
- digestFetchError = `HTTP ${digResp.status}`;
1865
- }
1866
- } catch (e) {
1867
- digestFetchError = e instanceof Error ? e.message : String(e);
2089
+ const digestText = await fetchTextWithRetryOrNull(digestUrl, {
2090
+ label: `${cfg.name}.sha256`,
2091
+ progress
2092
+ });
2093
+ if (digestText !== null) {
2094
+ const trimmed = digestText.trim();
2095
+ const first = trimmed.split(/\s+/)[0] ?? "";
2096
+ if (/^[a-f0-9]{64}$/i.test(first))
2097
+ expected = first.toLowerCase();
2098
+ else
2099
+ digestFetchError = `malformed .sha256 body: ${trimmed.slice(0, 80)}`;
2100
+ } else {
2101
+ digestFetchError = "fetch failed (see attempts above)";
1868
2102
  }
1869
2103
  if (expected) {
1870
2104
  const actual = createHash2("sha256").update(data).digest("hex");
@@ -1955,6 +2189,7 @@ var init_cargo_dist = __esm({
1955
2189
  init_platform_adapter();
1956
2190
  init_paths();
1957
2191
  init_errors();
2192
+ init_http_fetch_retry();
1958
2193
  }
1959
2194
  });
1960
2195
 
@@ -2301,8 +2536,8 @@ function normalizeOpenAiBaseUrl(input) {
2301
2536
  } catch {
2302
2537
  return input;
2303
2538
  }
2304
- const path10 = url.pathname.replace(/\/+$/, "");
2305
- if (path10 === "" || path10 === "/") {
2539
+ const path12 = url.pathname.replace(/\/+$/, "");
2540
+ if (path12 === "" || path12 === "/") {
2306
2541
  url.pathname = "/v1";
2307
2542
  return url.toString().replace(/\/+$/, "");
2308
2543
  }
@@ -2637,7 +2872,7 @@ async function identifyGateway(opts) {
2637
2872
  const url = `http://${host}:${port}/beeos/status`;
2638
2873
  const controller = globalThis.AbortController ? new AbortController() : null;
2639
2874
  const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
2640
- let resp = null;
2875
+ let resp;
2641
2876
  try {
2642
2877
  resp = await p.fetch(url, {
2643
2878
  method: "GET",
@@ -2799,12 +3034,12 @@ async function generateOpenclawConfig(ctx) {
2799
3034
  seedResult = await p.exec(ctx.agentBinary, ["setup", "--non-interactive", "--mode", "local"], { env });
2800
3035
  }
2801
3036
  if (seedResult.code === 0) {
2802
- for (const [path10, value] of [
3037
+ for (const [path12, value] of [
2803
3038
  ["gateway.auth.token", ctx.gatewayToken],
2804
3039
  ["gateway.remote.token", ctx.gatewayToken],
2805
3040
  ["gateway.bind", "lan"]
2806
3041
  ]) {
2807
- await runConfigSet(ctx.agentBinary, ctx.agentHome, path10, value, false, false);
3042
+ await runConfigSet(ctx.agentBinary, ctx.agentHome, path12, value, false, false);
2808
3043
  }
2809
3044
  await runConfigSet(ctx.agentBinary, ctx.agentHome, "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback", "true", true, false);
2810
3045
  await configurePluginViaCli(ctx);
@@ -2869,9 +3104,9 @@ async function writeConfigFallback(ctx) {
2869
3104
  await p.writeFile(configPath2, JSON.stringify(config, null, 2));
2870
3105
  await writePairedJson(ctx.agentHome, ctx.keyFile);
2871
3106
  }
2872
- async function runConfigSet(bin, home, path10, value, json, isSystemHome) {
3107
+ async function runConfigSet(bin, home, path12, value, json, isSystemHome) {
2873
3108
  const p = getPlatformAdapter();
2874
- const args = ["config", "set", path10, value];
3109
+ const args = ["config", "set", path12, value];
2875
3110
  if (json)
2876
3111
  args.push("--json");
2877
3112
  const env = {};
@@ -2879,7 +3114,7 @@ async function runConfigSet(bin, home, path10, value, json, isSystemHome) {
2879
3114
  env.OPENCLAW_STATE_DIR = home;
2880
3115
  const result = await p.exec(bin, args, { env });
2881
3116
  if (result.code !== 0) {
2882
- throw new Error(`\`openclaw config set ${path10}\` exited with ${result.code}`);
3117
+ throw new Error(`\`openclaw config set ${path12}\` exited with ${result.code}`);
2883
3118
  }
2884
3119
  }
2885
3120
  async function writePairedJson(home, keyFile) {
@@ -3512,7 +3747,7 @@ async function detectColdStartState() {
3512
3747
  const p = getPlatformAdapter();
3513
3748
  const port5900Listening = await p.tcpProbe("127.0.0.1", 5900, PORT_5900_PROBE_TIMEOUT_MS).catch(() => false);
3514
3749
  const passwordFile = p.joinPath(beeoHome(), "vnc.password");
3515
- let vncPasswordFileExists = false;
3750
+ let vncPasswordFileExists;
3516
3751
  try {
3517
3752
  const raw = await p.readFile(passwordFile);
3518
3753
  vncPasswordFileExists = raw.trim().length > 0;
@@ -3648,8 +3883,8 @@ launchctl kickstart -k system/com.apple.screensharing
3648
3883
  `;
3649
3884
  const aslEscaped = shellPayload.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
3650
3885
  const osascriptArg = `do shell script "${aslEscaped}" with administrator privileges`;
3651
- let stderr = "";
3652
- let code = 1;
3886
+ let stderr;
3887
+ let code;
3653
3888
  try {
3654
3889
  const result = await p.exec("osascript", ["-e", osascriptArg], {
3655
3890
  timeout: OSASCRIPT_TIMEOUT_MS
@@ -3782,7 +4017,7 @@ async function detectLinuxColdStartState() {
3782
4017
  const p = getPlatformAdapter();
3783
4018
  const port5901Listening = await p.tcpProbe("127.0.0.1", LINUX_VNC_PORT, PORT_PROBE_TIMEOUT_MS).catch(() => false);
3784
4019
  const beeoFile = p.joinPath(beeoHome(), "vnc.password");
3785
- let beeoPasswordFileExists = false;
4020
+ let beeoPasswordFileExists;
3786
4021
  try {
3787
4022
  const raw = await p.readFile(beeoFile);
3788
4023
  beeoPasswordFileExists = raw.trim().length > 0;
@@ -3790,7 +4025,7 @@ async function detectLinuxColdStartState() {
3790
4025
  beeoPasswordFileExists = false;
3791
4026
  }
3792
4027
  const systemFile = p.joinPath(p.homeDir(), ".vnc", "passwd");
3793
- let systemPasswordFileExists = false;
4028
+ let systemPasswordFileExists;
3794
4029
  try {
3795
4030
  const raw = await p.readFile(systemFile);
3796
4031
  systemPasswordFileExists = raw.length > 0;
@@ -4050,7 +4285,7 @@ async function detectWindowsColdStartState() {
4050
4285
  const p = getPlatformAdapter();
4051
4286
  const port5900Listening = await p.tcpProbe("127.0.0.1", WIN_VNC_PORT, PORT_PROBE_TIMEOUT_MS2).catch(() => false);
4052
4287
  const beeoFile = p.joinPath(beeoHome(), "vnc.password");
4053
- let beeoPasswordFileExists = false;
4288
+ let beeoPasswordFileExists;
4054
4289
  try {
4055
4290
  const raw = await p.readFile(beeoFile);
4056
4291
  beeoPasswordFileExists = raw.trim().length > 0;
@@ -5923,7 +6158,7 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
5923
6158
  return false;
5924
6159
  }
5925
6160
  }
5926
- let stale = false;
6161
+ let stale;
5927
6162
  try {
5928
6163
  const stat = await fs5.stat(lockFile);
5929
6164
  stale = Date.now() - stat.mtimeMs > STALE_LOCK_AGE_MS;
@@ -6167,6 +6402,7 @@ var init_dist = __esm({
6167
6402
  init_gateway_token();
6168
6403
  init_state();
6169
6404
  init_keypair();
6405
+ init_qr();
6170
6406
  init_client();
6171
6407
  init_orchestrator();
6172
6408
  init_process();
@@ -6185,6 +6421,10 @@ var init_dist = __esm({
6185
6421
  init_driver2();
6186
6422
  init_constants();
6187
6423
  init_agent_status();
6424
+ init_config();
6425
+ init_download();
6426
+ init_plugin();
6427
+ init_token();
6188
6428
  init_desktop_detect();
6189
6429
  init_macos_desktop_cold_start();
6190
6430
  init_desktop();
@@ -6197,6 +6437,8 @@ var init_dist = __esm({
6197
6437
  init_tail_logs();
6198
6438
  init_cli_version();
6199
6439
  init_upgrade();
6440
+ init_npm_prefix_self_heal();
6441
+ init_http_fetch_retry();
6200
6442
  }
6201
6443
  });
6202
6444
 
@@ -6341,6 +6583,60 @@ function printFleetNotRunningHint() {
6341
6583
  console.log(" device-agent fleet start # foreground (debug-friendly)");
6342
6584
  console.log("");
6343
6585
  }
6586
+ async function getFleetDeviceSnapshotBestEffort(serial, baseUrl = FLEET_STATUS_BASE_URL) {
6587
+ let res;
6588
+ try {
6589
+ res = await fetch(
6590
+ `${baseUrl}/fleet/devices/${encodeURIComponent(serial)}`,
6591
+ {
6592
+ method: "GET",
6593
+ signal: AbortSignal.timeout(FLEET_NOTIFY_TIMEOUT_MS)
6594
+ }
6595
+ );
6596
+ } catch {
6597
+ return null;
6598
+ }
6599
+ if (!res.ok) {
6600
+ return null;
6601
+ }
6602
+ let body;
6603
+ try {
6604
+ body = await res.json();
6605
+ } catch {
6606
+ return null;
6607
+ }
6608
+ return parseFleetDeviceSnapshot(body);
6609
+ }
6610
+ function parseFleetDeviceSnapshot(raw) {
6611
+ if (typeof raw !== "object" || raw === null) return null;
6612
+ const r = raw;
6613
+ if (typeof r.serial !== "string" || r.serial.length === 0) return null;
6614
+ if (!isFleetDeviceState(r.state)) return null;
6615
+ return {
6616
+ serial: r.serial,
6617
+ state: r.state,
6618
+ instanceId: typeof r.instanceId === "string" ? r.instanceId : null,
6619
+ mcpPid: typeof r.mcpPid === "number" ? r.mcpPid : null,
6620
+ daemonPid: typeof r.daemonPid === "number" ? r.daemonPid : null,
6621
+ sinceMs: typeof r.sinceMs === "number" ? r.sinceMs : 0
6622
+ };
6623
+ }
6624
+ function isFleetDeviceState(v) {
6625
+ switch (v) {
6626
+ case "adb_offline":
6627
+ case "adb_unauthorized":
6628
+ case "awaiting_bind":
6629
+ case "spawning":
6630
+ case "healthy":
6631
+ case "grace":
6632
+ case "degraded":
6633
+ case "gone":
6634
+ case "crashed":
6635
+ return true;
6636
+ default:
6637
+ return false;
6638
+ }
6639
+ }
6344
6640
  var FLEET_STATUS_BASE_URL, FLEET_NOTIFY_TIMEOUT_MS, FLEET_SHUTDOWN_TIMEOUT_MS;
6345
6641
  var init_fleet_notify = __esm({
6346
6642
  "src/commands/device/fleet-notify.ts"() {
@@ -6613,6 +6909,169 @@ var init_instance_picker = __esm({
6613
6909
  }
6614
6910
  });
6615
6911
 
6912
+ // src/commands/device/wait-for-fleet-healthy.ts
6913
+ import ora2 from "ora";
6914
+ async function waitForFleetHealthy(opts) {
6915
+ const timeoutMs = resolveTimeoutMs(opts.timeoutMs);
6916
+ if (timeoutMs <= 0) {
6917
+ return { kind: "skipped", reason: "opt_out" };
6918
+ }
6919
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
6920
+ const useSpinner = (opts.spinner ?? true) && Boolean(process.stdout?.isTTY);
6921
+ const reporter = useSpinner ? new SpinnerReporter() : new BareReporter();
6922
+ reporter.start(opts.serial, timeoutMs);
6923
+ const startedAt = Date.now();
6924
+ let nullStreak = 0;
6925
+ let lastSnapshot = null;
6926
+ try {
6927
+ while (true) {
6928
+ const snapshot = await getFleetDeviceSnapshotBestEffort(
6929
+ opts.serial,
6930
+ opts.fleetBaseUrl
6931
+ );
6932
+ const elapsedMs = Date.now() - startedAt;
6933
+ if (snapshot === null) {
6934
+ nullStreak += 1;
6935
+ if (nullStreak >= FLEET_DEAD_NULL_THRESHOLD) {
6936
+ reporter.skipFleetDown();
6937
+ return { kind: "skipped", reason: "fleet_not_running" };
6938
+ }
6939
+ } else {
6940
+ nullStreak = 0;
6941
+ lastSnapshot = snapshot;
6942
+ if (snapshot.state === "healthy") {
6943
+ reporter.online(snapshot, elapsedMs);
6944
+ return { kind: "online", elapsedMs, snapshot };
6945
+ }
6946
+ reporter.update(snapshot, elapsedMs);
6947
+ }
6948
+ if (elapsedMs + intervalMs >= timeoutMs) {
6949
+ reporter.timeout(lastSnapshot, elapsedMs);
6950
+ return { kind: "timeout", elapsedMs, lastSnapshot };
6951
+ }
6952
+ await sleep6(intervalMs);
6953
+ }
6954
+ } finally {
6955
+ reporter.dispose();
6956
+ }
6957
+ }
6958
+ function resolveTimeoutMs(optsTimeoutMs) {
6959
+ const envRaw = process.env.BEEOS_AGENT_WAIT_TIMEOUT_MS;
6960
+ if (envRaw !== void 0 && envRaw !== "") {
6961
+ const parsed = Number(envRaw);
6962
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
6963
+ }
6964
+ if (process.env.BEEOS_NO_WAIT_AGENT === "1") return 0;
6965
+ if (optsTimeoutMs !== void 0) return optsTimeoutMs;
6966
+ return DEFAULT_TIMEOUT_MS2;
6967
+ }
6968
+ function sleep6(ms) {
6969
+ return new Promise((resolve) => setTimeout(resolve, ms));
6970
+ }
6971
+ function formatSec(ms) {
6972
+ return `${(ms / 1e3).toFixed(1)}s`;
6973
+ }
6974
+ function formatOnline(snapshot, elapsedMs) {
6975
+ const mcp = snapshot.mcpPid ?? "?";
6976
+ const daemon = snapshot.daemonPid ?? "?";
6977
+ return `Device fleet healthy: mcp_pid=${mcp} daemon_pid=${daemon} (${formatSec(elapsedMs)})`;
6978
+ }
6979
+ function formatTimeout(serial, lastSnapshot, elapsedMs) {
6980
+ const state = lastSnapshot?.state ?? "unknown";
6981
+ const daemonPid = lastSnapshot?.daemonPid ?? null;
6982
+ return `Waiting for ${serial} healthy timed out after ${formatSec(elapsedMs)} \u2014 current state: ${state}, daemon_pid=${daemonPid ?? "null"}`;
6983
+ }
6984
+ var DEFAULT_TIMEOUT_MS2, DEFAULT_INTERVAL_MS, FLEET_DEAD_NULL_THRESHOLD, SpinnerReporter, BareReporter;
6985
+ var init_wait_for_fleet_healthy = __esm({
6986
+ "src/commands/device/wait-for-fleet-healthy.ts"() {
6987
+ "use strict";
6988
+ init_fleet_notify();
6989
+ DEFAULT_TIMEOUT_MS2 = 3e4;
6990
+ DEFAULT_INTERVAL_MS = 1e3;
6991
+ FLEET_DEAD_NULL_THRESHOLD = 3;
6992
+ SpinnerReporter = class {
6993
+ spinner = null;
6994
+ serial = "";
6995
+ start(serial, timeoutMs) {
6996
+ this.serial = serial;
6997
+ this.spinner = ora2({
6998
+ text: `Waiting for device-agent fleet to mark ${serial} healthy (timeout=${formatSec(timeoutMs)})...`,
6999
+ stream: process.stderr
7000
+ }).start();
7001
+ }
7002
+ update(snapshot, elapsedMs) {
7003
+ if (!this.spinner) return;
7004
+ this.spinner.text = `Waiting for ${this.serial} healthy \u2014 current state=${snapshot.state} (${formatSec(elapsedMs)})`;
7005
+ }
7006
+ online(snapshot, elapsedMs) {
7007
+ if (!this.spinner) return;
7008
+ this.spinner.succeed(formatOnline(snapshot, elapsedMs));
7009
+ this.spinner = null;
7010
+ console.log(
7011
+ " Agent may take ~1-2s more to register on dashboard."
7012
+ );
7013
+ }
7014
+ timeout(lastSnapshot, elapsedMs) {
7015
+ if (!this.spinner) return;
7016
+ this.spinner.warn(formatTimeout(this.serial, lastSnapshot, elapsedMs));
7017
+ this.spinner = null;
7018
+ console.log(
7019
+ ` See ~/.beeos/logs/services/device-agent-${this.serial}.log for details.`
7020
+ );
7021
+ }
7022
+ skipFleetDown() {
7023
+ if (!this.spinner) return;
7024
+ this.spinner.info(
7025
+ "Skipped fleet wait \u2014 supervisor not running. Run `beeos start` or `beeos doctor`."
7026
+ );
7027
+ this.spinner = null;
7028
+ }
7029
+ dispose() {
7030
+ if (this.spinner) {
7031
+ this.spinner.stop();
7032
+ this.spinner = null;
7033
+ }
7034
+ }
7035
+ };
7036
+ BareReporter = class {
7037
+ serial = "";
7038
+ lastState = null;
7039
+ start(serial, timeoutMs) {
7040
+ this.serial = serial;
7041
+ console.log(
7042
+ `Waiting for device-agent fleet to mark ${serial} healthy (timeout=${formatSec(timeoutMs)})...`
7043
+ );
7044
+ }
7045
+ update(snapshot, elapsedMs) {
7046
+ if (snapshot.state === this.lastState) return;
7047
+ this.lastState = snapshot.state;
7048
+ console.log(
7049
+ `[${formatSec(elapsedMs)}] ${this.serial} state=${snapshot.state}`
7050
+ );
7051
+ }
7052
+ online(snapshot, elapsedMs) {
7053
+ console.log(`\u2713 ${formatOnline(snapshot, elapsedMs)}`);
7054
+ console.log(
7055
+ " Agent may take ~1-2s more to register on dashboard."
7056
+ );
7057
+ }
7058
+ timeout(lastSnapshot, elapsedMs) {
7059
+ console.log(`\u26A0 ${formatTimeout(this.serial, lastSnapshot, elapsedMs)}`);
7060
+ console.log(
7061
+ ` See ~/.beeos/logs/services/device-agent-${this.serial}.log for details.`
7062
+ );
7063
+ }
7064
+ skipFleetDown() {
7065
+ console.log(
7066
+ "Skipped fleet wait \u2014 supervisor not running. Run `beeos start` or `beeos doctor`."
7067
+ );
7068
+ }
7069
+ dispose() {
7070
+ }
7071
+ };
7072
+ }
7073
+ });
7074
+
6616
7075
  // src/commands/device/state.ts
6617
7076
  import lockfile from "proper-lockfile";
6618
7077
  function deviceAgentTargetId(serial) {
@@ -6630,10 +7089,10 @@ function devicesPath() {
6630
7089
  }
6631
7090
  async function loadDeviceState() {
6632
7091
  const p = getPlatformAdapter();
6633
- const path10 = devicesPath();
6634
- if (!await p.exists(path10)) return { devices: [] };
7092
+ const path12 = devicesPath();
7093
+ if (!await p.exists(path12)) return { devices: [] };
6635
7094
  try {
6636
- const raw = await p.readFile(path10);
7095
+ const raw = await p.readFile(path12);
6637
7096
  return JSON.parse(raw);
6638
7097
  } catch {
6639
7098
  return { devices: [] };
@@ -6645,12 +7104,12 @@ async function saveDeviceState(state) {
6645
7104
  }
6646
7105
  async function withDeviceLock(fn) {
6647
7106
  const p = getPlatformAdapter();
6648
- const path10 = devicesPath();
6649
- await p.mkdir(p.dirname(path10));
6650
- if (!await p.exists(path10)) {
6651
- await p.writeFile(path10, JSON.stringify({ devices: [] }));
7107
+ const path12 = devicesPath();
7108
+ await p.mkdir(p.dirname(path12));
7109
+ if (!await p.exists(path12)) {
7110
+ await p.writeFile(path12, JSON.stringify({ devices: [] }));
6652
7111
  }
6653
- const release = await lockfile.lock(path10, { retries: 3 });
7112
+ const release = await lockfile.lock(path12, { retries: 3 });
6654
7113
  try {
6655
7114
  return await fn();
6656
7115
  } finally {
@@ -6859,10 +7318,17 @@ async function attach(options) {
6859
7318
  const outcome = await notifyFleetRestartDeviceBestEffort(serial);
6860
7319
  if (outcome === "not_running") {
6861
7320
  await maybeNotifyFleetWithHint(cfg);
7321
+ } else {
7322
+ await maybeWaitForFleetHealthy(serial, options);
6862
7323
  }
6863
7324
  return;
6864
7325
  }
6865
7326
  await maybeNotifyFleetWithHint(cfg);
7327
+ await maybeWaitForFleetHealthy(serial, options);
7328
+ }
7329
+ async function maybeWaitForFleetHealthy(serial, options) {
7330
+ if (options.wait === false) return;
7331
+ await waitForFleetHealthy({ serial });
6866
7332
  }
6867
7333
  async function attachAll(cfg, reporter, withVideo, options) {
6868
7334
  const all = await deviceRuntime.listAdbDevices();
@@ -6988,6 +7454,7 @@ async function attachAll(cfg, reporter, withVideo, options) {
6988
7454
  }
6989
7455
  console.log(`Attached ${committed.length} device(s); supervision via device-agent fleet.`);
6990
7456
  });
7457
+ const committedSerials = commits.filter((c) => c.ok).map((c) => c.serial);
6991
7458
  if (reboundSerials.length > 0) {
6992
7459
  let anyNotRunning = false;
6993
7460
  for (const serial of reboundSerials) {
@@ -6996,12 +7463,27 @@ async function attachAll(cfg, reporter, withVideo, options) {
6996
7463
  }
6997
7464
  if (anyNotRunning) {
6998
7465
  await maybeNotifyFleetWithHint(cfg);
6999
- } else if (reboundSerials.length < commits.filter((c) => c.ok).length) {
7466
+ } else if (reboundSerials.length < committedSerials.length) {
7000
7467
  await maybeNotifyFleetWithHint(cfg);
7001
7468
  }
7469
+ await maybeWaitForFleetHealthyMany(committedSerials, options);
7002
7470
  return;
7003
7471
  }
7004
7472
  await maybeNotifyFleetWithHint(cfg);
7473
+ await maybeWaitForFleetHealthyMany(committedSerials, options);
7474
+ }
7475
+ async function maybeWaitForFleetHealthyMany(serials, options) {
7476
+ if (options.wait === false || serials.length === 0) return;
7477
+ await Promise.allSettled(
7478
+ serials.map(
7479
+ (s) => (
7480
+ // Bare mode: ora doesn't compose with parallel callers cleanly,
7481
+ // so we route every serial through the line-per-state-change
7482
+ // reporter even on a TTY.
7483
+ waitForFleetHealthy({ serial: s, spinner: false })
7484
+ )
7485
+ )
7486
+ );
7005
7487
  }
7006
7488
  function commitAttachResults(state, commits) {
7007
7489
  const committed = [];
@@ -7316,14 +7798,18 @@ var init_attach = __esm({
7316
7798
  init_instance_picker();
7317
7799
  init_fallback_banner();
7318
7800
  init_fleet_notify();
7801
+ init_wait_for_fleet_healthy();
7319
7802
  init_state2();
7320
7803
  }
7321
7804
  });
7322
7805
 
7323
7806
  // src/commands/device/detach.ts
7807
+ import fs9 from "fs/promises";
7808
+ import path9 from "path";
7324
7809
  async function detach(options) {
7325
7810
  const cfg = await loadOrCreateConfig();
7326
7811
  const mgr = await getServiceManager();
7812
+ const rotated = [];
7327
7813
  await withDeviceLock(async () => {
7328
7814
  const state = await loadDeviceState();
7329
7815
  if (options.all) {
@@ -7331,11 +7817,19 @@ async function detach(options) {
7331
7817
  await removeTargetsForSerial(mgr, entry2.serial);
7332
7818
  legacyKillDeviceEntry(entry2);
7333
7819
  await notifyOffline(cfg.platform.api_url, entry2.instance_id);
7820
+ if (options.rotateIdentity) {
7821
+ if (await tryRotateIdentity(entry2.serial)) rotated.push(entry2.serial);
7822
+ }
7334
7823
  }
7335
7824
  const count = state.devices.length;
7336
7825
  state.devices = [];
7337
7826
  await saveDeviceState(state);
7338
7827
  console.log(`Detached ${count} device(s)`);
7828
+ if (options.rotateIdentity) {
7829
+ console.log(
7830
+ `Rotated identity for ${rotated.length} device(s); next attach will re-bind from scratch.`
7831
+ );
7832
+ }
7339
7833
  return;
7340
7834
  }
7341
7835
  if (!options.serial) {
@@ -7348,10 +7842,35 @@ async function detach(options) {
7348
7842
  legacyKillDeviceEntry(entry);
7349
7843
  await notifyOffline(cfg.platform.api_url, entry.instance_id);
7350
7844
  await saveDeviceState(state);
7845
+ if (options.rotateIdentity) {
7846
+ if (await tryRotateIdentity(entry.serial)) rotated.push(entry.serial);
7847
+ }
7351
7848
  console.log(`Detached device ${options.serial}`);
7849
+ if (options.rotateIdentity) {
7850
+ console.log(
7851
+ rotated.length > 0 ? `Rotated local identity (~/.beeos/identity/device-${options.serial}.key.json removed). Next attach will re-bind from scratch.` : `No local identity file found for ${options.serial} \u2014 nothing to rotate.`
7852
+ );
7853
+ }
7352
7854
  });
7353
7855
  await notifyFleetReloadBestEffort();
7354
7856
  }
7857
+ async function tryRotateIdentity(serial) {
7858
+ const keyPath = path9.join(
7859
+ beeoHome(),
7860
+ "identity",
7861
+ `device-${serial}.key.json`
7862
+ );
7863
+ try {
7864
+ await fs9.unlink(keyPath);
7865
+ return true;
7866
+ } catch (e) {
7867
+ if (e.code === "ENOENT") return false;
7868
+ console.error(
7869
+ ` identity file ${keyPath} could not be removed: ${e.message}`
7870
+ );
7871
+ return false;
7872
+ }
7873
+ }
7355
7874
  function legacyKillDeviceEntry(entry) {
7356
7875
  if (entry.pid && isProcessAlive(entry.pid)) killProcess(entry.pid);
7357
7876
  if (entry.bridge_pid && isProcessAlive(entry.bridge_pid)) killProcess(entry.bridge_pid);
@@ -7714,8 +8233,36 @@ var NodePlatformAdapter = class {
7714
8233
  }
7715
8234
  // ── Process execution ───────────────────────────────────
7716
8235
  exec(cmd, args, options) {
8236
+ const env = options?.env ? { ...process.env, ...options.env } : process.env;
8237
+ if (options?.stdio === "inherit") {
8238
+ return new Promise((resolve) => {
8239
+ const child = nodeSpawn(cmd, args, {
8240
+ cwd: options?.cwd,
8241
+ env,
8242
+ shell: process.platform === "win32",
8243
+ windowsHide: true,
8244
+ stdio: "inherit"
8245
+ });
8246
+ let timer = null;
8247
+ if (options?.timeout && options.timeout > 0) {
8248
+ timer = setTimeout(() => {
8249
+ try {
8250
+ child.kill("SIGTERM");
8251
+ } catch {
8252
+ }
8253
+ }, options.timeout);
8254
+ }
8255
+ child.on("error", () => {
8256
+ if (timer) clearTimeout(timer);
8257
+ resolve({ stdout: "", stderr: "", code: 1 });
8258
+ });
8259
+ child.on("exit", (code) => {
8260
+ if (timer) clearTimeout(timer);
8261
+ resolve({ stdout: "", stderr: "", code: code ?? 1 });
8262
+ });
8263
+ });
8264
+ }
7717
8265
  return new Promise((resolve) => {
7718
- const env = options?.env ? { ...process.env, ...options.env } : process.env;
7719
8266
  execFile6(
7720
8267
  cmd,
7721
8268
  args,
@@ -7899,10 +8446,9 @@ async function run(agentFramework, options) {
7899
8446
  const ttyHints = !options.json && process.stdin.isTTY === true;
7900
8447
  const desktopCapable = hasDesktopCapability(driver);
7901
8448
  const desktopPipeline = desktopCapable ? getDesktopPipeline() : null;
7902
- let coldStart = null;
7903
8449
  if (desktopPipeline) {
7904
8450
  const interactive = !options.json && process.stdin.isTTY === true && process.stdout.isTTY === true;
7905
- coldStart = await desktopPipeline.coldStart({
8451
+ const coldStart = await desktopPipeline.coldStart({
7906
8452
  prompt: interactive ? promptYesNo : void 0,
7907
8453
  log: options.json ? () => void 0 : (m) => console.log(m),
7908
8454
  yes: options.force === true
@@ -8638,8 +9184,8 @@ async function pickInstalledPackages() {
8638
9184
  // src/commands/doctor.ts
8639
9185
  init_dist();
8640
9186
  init_json_envelope();
8641
- import fs9 from "fs/promises";
8642
- import path9 from "path";
9187
+ import fs10 from "fs/promises";
9188
+ import path10 from "path";
8643
9189
  var LOG_SIZE_WARN_BYTES = 50 * 1024 * 1024;
8644
9190
  async function run8(options) {
8645
9191
  const state = await detectExistingInstall();
@@ -8656,8 +9202,13 @@ async function run8(options) {
8656
9202
  const resolvedAgentGatewayUrl = resolveAgentGatewayUrl(cfg);
8657
9203
  const agentGatewayHealth = await probeAgentGateway(resolvedAgentGatewayUrl);
8658
9204
  const tools = await collectToolStatus();
9205
+ const adbDaemon = tools.adb.path ? await probeAdbDaemon(tools.adb.path) : { reachable: false, deviceCount: 0, detail: "adb not on PATH" };
9206
+ const identityFile = await probeIdentityKeypair();
9207
+ const ports = await probePorts();
9208
+ const nodeEngines = await probeNodeEngines();
8659
9209
  const hasBoundDevices = state.devices.entries.length > 0;
8660
9210
  const shimReport = await collectShimReport(hasBoundDevices);
9211
+ const npmPrefix = await readCurrentPrefix();
8661
9212
  const warnings = [];
8662
9213
  const hints = [];
8663
9214
  if (!state.hasIdentity) {
@@ -8731,6 +9282,42 @@ async function run8(options) {
8731
9282
  warnings.push(
8732
9283
  "adb not found \u2014 `beeos device attach` will download Android platform-tools on first use"
8733
9284
  );
9285
+ } else if (!adbDaemon.reachable) {
9286
+ warnings.push(`adb daemon unreachable: ${adbDaemon.detail}`);
9287
+ hints.push(
9288
+ "try `adb kill-server && adb start-server` to reset; on Linux check udev rules; on Windows reinstall Android USB drivers"
9289
+ );
9290
+ }
9291
+ if (state.hasIdentity && !identityFile.parseable) {
9292
+ warnings.push(
9293
+ `identity keypair file present but unreadable/malformed (${identityFile.detail}). Binding will fail; backup and regenerate via \`beeos init\` (option [3] reset all).`
9294
+ );
9295
+ }
9296
+ if (ports.port7900Occupied) {
9297
+ warnings.push(
9298
+ "port 7900 is in use \u2014 device-mcp-server may not start cleanly on the next attach."
9299
+ );
9300
+ hints.push(
9301
+ "find the holder: `lsof -i :7900` (POSIX) or `netstat -ano | findstr :7900` (Windows)"
9302
+ );
9303
+ }
9304
+ if (ports.port7950Occupied) {
9305
+ warnings.push(
9306
+ "port 7950 is in use \u2014 fleet IPC may not bind on the next attach."
9307
+ );
9308
+ hints.push(
9309
+ "find the holder: `lsof -i :7950` (POSIX) or `netstat -ano | findstr :7950` (Windows)"
9310
+ );
9311
+ }
9312
+ for (const e of nodeEngines.entries) {
9313
+ if (e.satisfies === false) {
9314
+ warnings.push(
9315
+ `${e.pkg}@${e.installed} requires Node ${e.required} but you are on ${e.current}.`
9316
+ );
9317
+ hints.push(
9318
+ `upgrade Node (e.g. 'nvm install --lts' / 'fnm install --lts') and re-run \`beeos device attach\``
9319
+ );
9320
+ }
8734
9321
  }
8735
9322
  for (const e of shimReport.entries) {
8736
9323
  if (e.outcome === "outdated") {
@@ -8750,7 +9337,7 @@ async function run8(options) {
8750
9337
  const bloatedLogs = await checkLogFileSizes();
8751
9338
  for (const l of bloatedLogs) {
8752
9339
  warnings.push(
8753
- `Service log large: ${path9.basename(l.path)} = ${formatBytes(l.size)}`
9340
+ `Service log large: ${path10.basename(l.path)} = ${formatBytes(l.size)}`
8754
9341
  );
8755
9342
  hints.push(`truncate safely: : > ${l.path}`);
8756
9343
  }
@@ -8771,7 +9358,12 @@ async function run8(options) {
8771
9358
  },
8772
9359
  agentGateway: agentGatewayHealth,
8773
9360
  tools,
9361
+ adbDaemon,
9362
+ identityFile,
9363
+ ports,
9364
+ nodeEngines,
8774
9365
  npmShims: shimReport,
9366
+ npmPrefix,
8775
9367
  warnings,
8776
9368
  hints
8777
9369
  })
@@ -8798,8 +9390,20 @@ async function run8(options) {
8798
9390
  console.log(` - ${s.id.padEnd(28)} ${status2}${pid}`);
8799
9391
  }
8800
9392
  console.log(` adb : ${tools.adb.path ?? "not found"}`);
9393
+ if (tools.adb.path) {
9394
+ console.log(
9395
+ ` daemon : ${adbDaemon.reachable ? `reachable (${adbDaemon.deviceCount} device${adbDaemon.deviceCount === 1 ? "" : "s"})` : `unreachable \u2014 ${adbDaemon.detail}`}`
9396
+ );
9397
+ }
8801
9398
  console.log(` scrcpy-bridge: ${tools.scrcpyBridge.path ?? "not installed (auto on attach)"}`);
8802
9399
  console.log(` vnc-bridge : ${tools.vncBridge.path ?? "not installed (auto on --vnc-host)"}`);
9400
+ console.log(` npm prefix : ${npmPrefix ?? "(unknown \u2014 npm not on PATH?)"}`);
9401
+ console.log(
9402
+ ` identity : ${identityFile.parseable ? "ok" : identityFile.exists ? `corrupt (${identityFile.detail})` : "not yet created"}`
9403
+ );
9404
+ console.log(
9405
+ ` ports : 7900 ${ports.port7900Occupied ? "in use" : "free"}, 7950 ${ports.port7950Occupied ? "in use" : "free"}`
9406
+ );
8803
9407
  console.log("");
8804
9408
  console.log(" npm shims:");
8805
9409
  for (const e of shimReport.entries) {
@@ -8882,19 +9486,19 @@ function shimMarker(outcome) {
8882
9486
  }
8883
9487
  }
8884
9488
  async function checkLogFileSizes() {
8885
- const dir = path9.join(beeoHome(), "logs", "services");
9489
+ const dir = path10.join(beeoHome(), "logs", "services");
8886
9490
  let entries;
8887
9491
  try {
8888
- entries = await fs9.readdir(dir);
9492
+ entries = await fs10.readdir(dir);
8889
9493
  } catch {
8890
9494
  return [];
8891
9495
  }
8892
9496
  const bloated = [];
8893
9497
  for (const name of entries) {
8894
9498
  if (!name.endsWith(".log")) continue;
8895
- const full = path9.join(dir, name);
9499
+ const full = path10.join(dir, name);
8896
9500
  try {
8897
- const st = await fs9.stat(full);
9501
+ const st = await fs10.stat(full);
8898
9502
  if (st.isFile() && st.size >= LOG_SIZE_WARN_BYTES) {
8899
9503
  bloated.push({ path: full, size: st.size });
8900
9504
  }
@@ -8961,6 +9565,173 @@ function formatAgentGatewayStatus(h) {
8961
9565
  if (h.status === "unhealthy") return `HTTP ${h.httpStatus ?? "?"}`;
8962
9566
  return h.detail;
8963
9567
  }
9568
+ var ADB_PROBE_START_SERVER_TIMEOUT_MS = 3e3;
9569
+ var ADB_PROBE_DEVICES_TIMEOUT_MS = 1500;
9570
+ async function probeAdbDaemon(adbPath) {
9571
+ const p = getPlatformAdapter();
9572
+ try {
9573
+ const start = await p.exec(adbPath, ["start-server"], {
9574
+ timeout: ADB_PROBE_START_SERVER_TIMEOUT_MS
9575
+ });
9576
+ if (start.code !== 0) {
9577
+ return {
9578
+ reachable: false,
9579
+ deviceCount: 0,
9580
+ detail: `adb start-server exited ${start.code} (timeout ${ADB_PROBE_START_SERVER_TIMEOUT_MS}ms): ${(start.stderr || start.stdout).trim().slice(0, 120)}`
9581
+ };
9582
+ }
9583
+ } catch (e) {
9584
+ return {
9585
+ reachable: false,
9586
+ deviceCount: 0,
9587
+ detail: `adb start-server failed (timeout ${ADB_PROBE_START_SERVER_TIMEOUT_MS}ms): ${e instanceof Error ? e.message : String(e)}`
9588
+ };
9589
+ }
9590
+ try {
9591
+ const list2 = await p.exec(adbPath, ["devices"], {
9592
+ timeout: ADB_PROBE_DEVICES_TIMEOUT_MS
9593
+ });
9594
+ if (list2.code !== 0) {
9595
+ return {
9596
+ reachable: false,
9597
+ deviceCount: 0,
9598
+ detail: `adb devices exited ${list2.code} (timeout ${ADB_PROBE_DEVICES_TIMEOUT_MS}ms)`
9599
+ };
9600
+ }
9601
+ const lines = list2.stdout.split("\n").slice(1).map((l) => l.trim());
9602
+ const deviceCount = lines.filter((l) => l.endsWith(" device")).length;
9603
+ return {
9604
+ reachable: true,
9605
+ deviceCount,
9606
+ detail: `${deviceCount} device(s) authorized`
9607
+ };
9608
+ } catch (e) {
9609
+ return {
9610
+ reachable: false,
9611
+ deviceCount: 0,
9612
+ detail: `adb devices failed (timeout ${ADB_PROBE_DEVICES_TIMEOUT_MS}ms): ${e instanceof Error ? e.message : String(e)}`
9613
+ };
9614
+ }
9615
+ }
9616
+ async function probeIdentityKeypair() {
9617
+ const p = getPlatformAdapter();
9618
+ const kpPath = p.joinPath(beeoHome(), "identity", "keypair.json");
9619
+ if (!await p.exists(kpPath)) {
9620
+ return {
9621
+ exists: false,
9622
+ readable: false,
9623
+ parseable: false,
9624
+ detail: "not yet generated"
9625
+ };
9626
+ }
9627
+ let raw;
9628
+ try {
9629
+ raw = await p.readFile(kpPath);
9630
+ } catch (e) {
9631
+ return {
9632
+ exists: true,
9633
+ readable: false,
9634
+ parseable: false,
9635
+ detail: `read failed: ${e instanceof Error ? e.message : String(e)}`
9636
+ };
9637
+ }
9638
+ try {
9639
+ const parsed = JSON.parse(raw);
9640
+ if (typeof parsed.publicKey !== "string" || typeof parsed.privateKey !== "string") {
9641
+ return {
9642
+ exists: true,
9643
+ readable: true,
9644
+ parseable: false,
9645
+ detail: "missing publicKey / privateKey fields"
9646
+ };
9647
+ }
9648
+ return {
9649
+ exists: true,
9650
+ readable: true,
9651
+ parseable: true,
9652
+ detail: "ok"
9653
+ };
9654
+ } catch (e) {
9655
+ return {
9656
+ exists: true,
9657
+ readable: true,
9658
+ parseable: false,
9659
+ detail: `JSON parse failed: ${e instanceof Error ? e.message : String(e)}`
9660
+ };
9661
+ }
9662
+ }
9663
+ async function probePorts() {
9664
+ const p = getPlatformAdapter();
9665
+ const [p7900, p7950] = await Promise.all([
9666
+ p.tcpProbe("127.0.0.1", 7900, 500),
9667
+ p.tcpProbe("127.0.0.1", 7950, 500)
9668
+ ]);
9669
+ return { port7900Occupied: p7900, port7950Occupied: p7950 };
9670
+ }
9671
+ async function probeNodeEngines() {
9672
+ const targets = [NPM_PKGS.DEVICE_AGENT, NPM_PKGS.DEVICE_MCP_SERVER];
9673
+ const current = process.version.replace(/^v/, "");
9674
+ const entries = [];
9675
+ for (const pkg of targets) {
9676
+ const installed = await npmGlobalVersion(pkg).catch(() => null);
9677
+ if (!installed) {
9678
+ entries.push({
9679
+ pkg,
9680
+ installed: null,
9681
+ required: null,
9682
+ current,
9683
+ satisfies: null
9684
+ });
9685
+ continue;
9686
+ }
9687
+ const required = await readPkgEnginesNode(pkg);
9688
+ entries.push({
9689
+ pkg,
9690
+ installed,
9691
+ required,
9692
+ current,
9693
+ satisfies: required ? satisfiesMinNode(current, required) : null
9694
+ });
9695
+ }
9696
+ return { entries };
9697
+ }
9698
+ async function readPkgEnginesNode(pkg) {
9699
+ const p = getPlatformAdapter();
9700
+ const rootRes = await p.exec("npm", ["root", "-g"], { timeout: 5e3 });
9701
+ if (rootRes.code !== 0) return null;
9702
+ const root = rootRes.stdout.trim();
9703
+ if (!root) return null;
9704
+ const pkgJsonPath = path10.join(root, pkg, "package.json");
9705
+ try {
9706
+ const raw = await fs10.readFile(pkgJsonPath, "utf-8");
9707
+ const parsed = JSON.parse(raw);
9708
+ const node = parsed.engines?.node?.trim();
9709
+ return node && node.length > 0 ? node : null;
9710
+ } catch {
9711
+ return null;
9712
+ }
9713
+ }
9714
+ function satisfiesMinNode(current, spec) {
9715
+ const m = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(spec.trim());
9716
+ if (!m) return null;
9717
+ const want = [
9718
+ Number(m[1]),
9719
+ Number(m[2] ?? 0),
9720
+ Number(m[3] ?? 0)
9721
+ ];
9722
+ const have = parseSemver(current);
9723
+ if (!have) return null;
9724
+ for (let i = 0; i < 3; i++) {
9725
+ if (have[i] > want[i]) return true;
9726
+ if (have[i] < want[i]) return false;
9727
+ }
9728
+ return true;
9729
+ }
9730
+ function parseSemver(v) {
9731
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
9732
+ if (!m) return null;
9733
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
9734
+ }
8964
9735
 
8965
9736
  // src/commands/service.ts
8966
9737
  init_dist();
@@ -9178,6 +9949,194 @@ async function run10(idArg, options) {
9178
9949
  }
9179
9950
  }
9180
9951
 
9952
+ // src/commands/report.ts
9953
+ init_dist();
9954
+ init_json_envelope();
9955
+ import fs11 from "fs/promises";
9956
+ import { createReadStream, createWriteStream } from "fs";
9957
+ import path11 from "path";
9958
+ import { spawn as spawn3 } from "child_process";
9959
+ import { create as tarCreate } from "tar";
9960
+ var LOG_TAIL_BYTES = 1024 * 1024;
9961
+ async function run11(opts = {}) {
9962
+ const home = beeoHome();
9963
+ const reportsDir = path11.join(home, "reports");
9964
+ await fs11.mkdir(reportsDir, { recursive: true });
9965
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9966
+ const reportName = `beeos-report-${stamp}`;
9967
+ const stagingDir = path11.join(reportsDir, reportName);
9968
+ await fs11.mkdir(stagingDir, { recursive: true });
9969
+ try {
9970
+ if (!opts.skipDoctor) {
9971
+ const doctorJson = await runDoctorJson();
9972
+ await fs11.writeFile(
9973
+ path11.join(stagingDir, "doctor.json"),
9974
+ doctorJson,
9975
+ "utf-8"
9976
+ );
9977
+ }
9978
+ try {
9979
+ const cfgRaw = await fs11.readFile(configPath(), "utf-8");
9980
+ await fs11.writeFile(
9981
+ path11.join(stagingDir, "config.toml"),
9982
+ cfgRaw,
9983
+ "utf-8"
9984
+ );
9985
+ } catch {
9986
+ await fs11.writeFile(
9987
+ path11.join(stagingDir, "config.toml"),
9988
+ "# (no ~/.beeos/config.toml on this machine)\n",
9989
+ "utf-8"
9990
+ );
9991
+ }
9992
+ try {
9993
+ const state = await loadState();
9994
+ await fs11.writeFile(
9995
+ path11.join(stagingDir, "state.json"),
9996
+ JSON.stringify(redactSecrets(state), null, 2),
9997
+ "utf-8"
9998
+ );
9999
+ } catch {
10000
+ await fs11.writeFile(
10001
+ path11.join(stagingDir, "state.json"),
10002
+ "{}\n",
10003
+ "utf-8"
10004
+ );
10005
+ }
10006
+ const logsSrc = path11.join(home, "logs", "services");
10007
+ const logsDst = path11.join(stagingDir, "services-logs");
10008
+ await fs11.mkdir(logsDst, { recursive: true });
10009
+ let logEntries = [];
10010
+ try {
10011
+ logEntries = await fs11.readdir(logsSrc);
10012
+ } catch {
10013
+ }
10014
+ for (const name of logEntries) {
10015
+ if (!name.endsWith(".log")) continue;
10016
+ const src = path11.join(logsSrc, name);
10017
+ const dst = path11.join(logsDst, name);
10018
+ try {
10019
+ await tailCopy(src, dst, LOG_TAIL_BYTES);
10020
+ } catch {
10021
+ }
10022
+ }
10023
+ const tarballPath = path11.join(reportsDir, `${reportName}.tar.gz`);
10024
+ await tarCreate(
10025
+ { gzip: true, file: tarballPath, cwd: reportsDir },
10026
+ [reportName]
10027
+ );
10028
+ if (opts.json) {
10029
+ emitJsonEnvelope(
10030
+ jsonOk({
10031
+ tarball: tarballPath,
10032
+ included: [
10033
+ "doctor.json",
10034
+ "config.toml",
10035
+ "state.json",
10036
+ "services-logs/"
10037
+ ],
10038
+ excluded: [
10039
+ "identity/keypair.json",
10040
+ "identity/device-*.key.json"
10041
+ ]
10042
+ })
10043
+ );
10044
+ } else {
10045
+ console.log("");
10046
+ console.log(` Report: ${tarballPath}`);
10047
+ console.log("");
10048
+ console.log(" Attach this file to your support ticket.");
10049
+ console.log(
10050
+ " It contains: doctor.json, config.toml, state.json, last 1MiB of every service log."
10051
+ );
10052
+ console.log(" It does NOT contain: identity/keypair.json (private key).");
10053
+ console.log("");
10054
+ }
10055
+ } finally {
10056
+ await fs11.rm(stagingDir, { recursive: true, force: true }).catch(() => {
10057
+ });
10058
+ }
10059
+ }
10060
+ async function runDoctorJson() {
10061
+ const cliEntry = process.argv[1];
10062
+ if (!cliEntry) {
10063
+ return JSON.stringify(
10064
+ { ok: false, error: { code: "no_entry", message: "process.argv[1] missing" } },
10065
+ null,
10066
+ 2
10067
+ );
10068
+ }
10069
+ return new Promise((resolve) => {
10070
+ let stdout = "";
10071
+ let stderr = "";
10072
+ const child = spawn3(process.execPath, [cliEntry, "doctor", "--json"], {
10073
+ stdio: ["ignore", "pipe", "pipe"]
10074
+ });
10075
+ child.stdout?.on("data", (c) => {
10076
+ stdout += c.toString();
10077
+ });
10078
+ child.stderr?.on("data", (c) => {
10079
+ stderr += c.toString();
10080
+ });
10081
+ const timer = setTimeout(() => {
10082
+ try {
10083
+ child.kill("SIGTERM");
10084
+ } catch {
10085
+ }
10086
+ }, 3e4);
10087
+ child.on("close", (code) => {
10088
+ clearTimeout(timer);
10089
+ if (code === 0 && stdout.trim().length > 0) {
10090
+ resolve(stdout);
10091
+ } else {
10092
+ resolve(
10093
+ JSON.stringify(
10094
+ {
10095
+ ok: false,
10096
+ error: {
10097
+ code: "doctor_failed",
10098
+ exitCode: code,
10099
+ stderr: stderr.trim()
10100
+ }
10101
+ },
10102
+ null,
10103
+ 2
10104
+ )
10105
+ );
10106
+ }
10107
+ });
10108
+ });
10109
+ }
10110
+ async function tailCopy(src, dst, tailBytes) {
10111
+ const stat = await fs11.stat(src);
10112
+ const start = stat.size > tailBytes ? stat.size - tailBytes : 0;
10113
+ await new Promise((resolve, reject) => {
10114
+ const rs = createReadStream(src, { start });
10115
+ const ws = createWriteStream(dst);
10116
+ rs.on("error", reject);
10117
+ ws.on("error", reject);
10118
+ ws.on("finish", () => resolve());
10119
+ rs.pipe(ws);
10120
+ });
10121
+ }
10122
+ function redactSecrets(state) {
10123
+ const SECRET_KEY_RE = /(token|secret|password|api_?key|bearer|jwt|cookie)/i;
10124
+ function walk(value) {
10125
+ if (value === null || typeof value !== "object") return value;
10126
+ if (Array.isArray(value)) return value.map(walk);
10127
+ const out = {};
10128
+ for (const [k, v] of Object.entries(value)) {
10129
+ if (typeof v === "string" && SECRET_KEY_RE.test(k)) {
10130
+ out[k] = `<redacted ${v.length}b>`;
10131
+ } else {
10132
+ out[k] = walk(v);
10133
+ }
10134
+ }
10135
+ return out;
10136
+ }
10137
+ return walk(state);
10138
+ }
10139
+
9181
10140
  // src/index.ts
9182
10141
  setPlatformAdapter(new NodePlatformAdapter());
9183
10142
  var program = new Command();
@@ -9185,6 +10144,9 @@ var cliVersion = getCliVersion(import.meta.url, resolveActiveDistTag());
9185
10144
  program.name("beeos").version(cliVersion.display).description("BeeOS \u2014 run AI agents from your desktop");
9186
10145
  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));
9187
10146
  program.command("doctor").description("Inspect local BeeOS state (install, binding, services, warnings)").option("--json", "Output machine-readable JSON", false).action((opts) => run8(opts));
10147
+ program.command("report").description(
10148
+ "Bundle a diagnostic tarball (~/.beeos/reports/beeos-report-<ts>.tar.gz) for support tickets. Includes doctor.json + redacted config + last 1MiB of every service log. NEVER includes identity/keypair.json."
10149
+ ).option("--json", "Output machine-readable JSON envelope ({ok,data})", false).option("--skip-doctor", "Skip running `beeos doctor` as a subprocess", false).action((opts) => run11(opts));
9188
10150
  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));
9189
10151
  program.command("logs").description(
9190
10152
  "Tail logs for a BeeOS service. Pass an ADB serial or service id; omit to list every registered service's log file path."
@@ -9211,8 +10173,15 @@ deviceCmd.command("attach").description("Attach an ADB-connected device as an AI
9211
10173
  ).option(
9212
10174
  "--agent-gateway-url <url>",
9213
10175
  "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."
10176
+ ).option(
10177
+ "--no-wait",
10178
+ "Skip waiting for the fleet supervisor to mark the device healthy after bind. By default, attach polls fleet on 127.0.0.1:7950 for up to 30s (BEEOS_AGENT_WAIT_TIMEOUT_MS overrides) so terminal exit aligns with dashboard agent-online status. Set this for CI/scripts that don't care about post-bind dashboard sync."
9214
10179
  ).action(attach);
9215
- deviceCmd.command("detach").description("Detach a device agent").option("--serial <serial>", "ADB device serial number").option("--all", "Detach all devices", false).action(detach);
10180
+ deviceCmd.command("detach").description("Detach a device agent").option("--serial <serial>", "ADB device serial number").option("--all", "Detach all devices", false).option(
10181
+ "--rotate-identity",
10182
+ "Also delete ~/.beeos/identity/device-<serial>.key.json so the next `attach` re-binds from scratch (use this to switch the device to a different BeeOS account / org).",
10183
+ false
10184
+ ).action(detach);
9216
10185
  deviceCmd.command("list").description("List attached devices").option("--local", "Only show locally running device agents", false).action(list);
9217
10186
  deviceCmd.command("exec").description("Send a natural language command to a device").option("--serial <serial>", "ADB device serial number").argument("<prompt>", "The command/prompt to execute").action(exec);
9218
10187
  deviceCmd.command("upgrade").description("Upgrade the device-agent (and scrcpy/vnc bridges) to the latest version").option("--no-bridges", "Skip upgrading scrcpy-bridge / vnc-bridge binaries").action(upgrade);