@beeos-ai/cli 1.0.13 → 1.0.14

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
@@ -91,6 +91,10 @@ function configPath() {
91
91
  const p = getPlatformAdapter();
92
92
  return p.joinPath(beeoHome(), "config.toml");
93
93
  }
94
+ function spawnEnvJsonPath() {
95
+ const p = getPlatformAdapter();
96
+ return p.joinPath(beeoHome(), "config.spawn-env.json");
97
+ }
94
98
  function bindingPath() {
95
99
  const p = getPlatformAdapter();
96
100
  return p.joinPath(beeoHome(), "binding.json");
@@ -110,6 +114,336 @@ var init_paths = __esm({
110
114
  }
111
115
  });
112
116
 
117
+ // ../core/dist/config/spawn-env.js
118
+ function projectSpawnEnv(cfg) {
119
+ const out = {};
120
+ const agw = cfg.platform.agent_gateway_url?.trim();
121
+ if (agw)
122
+ out.agent_gateway_url = agw;
123
+ const bridge = cfg.platform.bridge_url?.trim();
124
+ if (bridge)
125
+ out.bridge_url = bridge;
126
+ return out;
127
+ }
128
+ async function saveSpawnEnvSnapshot(cfg) {
129
+ const p = getPlatformAdapter();
130
+ const path10 = spawnEnvJsonPath();
131
+ const snapshot = projectSpawnEnv(cfg);
132
+ try {
133
+ await p.mkdir(p.dirname(path10));
134
+ await p.writeFile(path10, JSON.stringify(snapshot, null, 2) + "\n");
135
+ } catch (err) {
136
+ const message = err instanceof Error ? err.message : String(err);
137
+ console.error(`! warning: could not write ${path10}: ${message}
138
+ device-agent fleet will fall back to parsing config.toml directly.`);
139
+ }
140
+ }
141
+ var init_spawn_env = __esm({
142
+ "../core/dist/config/spawn-env.js"() {
143
+ "use strict";
144
+ init_platform_adapter();
145
+ init_paths();
146
+ }
147
+ });
148
+
149
+ // ../core/dist/errors.js
150
+ function isBeeosError(err) {
151
+ return err instanceof BeeosError;
152
+ }
153
+ function toJsonFailurePayload(err) {
154
+ return {
155
+ ok: false,
156
+ error: {
157
+ code: err.code,
158
+ message: err.message,
159
+ ...err.hint !== void 0 ? { hint: err.hint } : {},
160
+ ...err.details !== void 0 ? { details: err.details } : {}
161
+ }
162
+ };
163
+ }
164
+ function formatHumanError(err) {
165
+ const parts = [`error [${err.code}]: ${err.message}`];
166
+ if (err.hint)
167
+ parts.push(` $ ${err.hint}`);
168
+ return parts.join("\n");
169
+ }
170
+ var BeeosError;
171
+ var init_errors = __esm({
172
+ "../core/dist/errors.js"() {
173
+ "use strict";
174
+ BeeosError = class extends Error {
175
+ code;
176
+ hint;
177
+ details;
178
+ constructor(options) {
179
+ super(options.message, options.cause !== void 0 ? { cause: options.cause } : void 0);
180
+ this.name = "BeeosError";
181
+ this.code = options.code;
182
+ this.hint = options.hint;
183
+ this.details = options.details;
184
+ }
185
+ };
186
+ }
187
+ });
188
+
189
+ // ../core/dist/config/state.js
190
+ function statePath() {
191
+ const p = getPlatformAdapter();
192
+ return p.joinPath(beeoHome(), "state.json");
193
+ }
194
+ function legacyDevicesPath() {
195
+ const p = getPlatformAdapter();
196
+ return p.joinPath(beeoHome(), "devices.json");
197
+ }
198
+ function defaultState() {
199
+ return {
200
+ version: 2,
201
+ platform: {
202
+ api_url: DEFAULT_API_URL,
203
+ agent_gateway_url: DEFAULT_AGENT_GATEWAY_URL,
204
+ dashboard_base_url: DEFAULT_DASHBOARD_URL
205
+ },
206
+ agents: []
207
+ };
208
+ }
209
+ async function loadState() {
210
+ const p = getPlatformAdapter();
211
+ const path10 = statePath();
212
+ if (await p.exists(path10)) {
213
+ let raw;
214
+ try {
215
+ raw = await p.readFile(path10);
216
+ } catch (e) {
217
+ throw new BeeosError({
218
+ code: "state_corrupted",
219
+ message: `failed to read ${path10}: ${describeError(e)}`,
220
+ hint: "Inspect the file or move it aside and re-run `beeos doctor` to regenerate from legacy state.",
221
+ details: { path: path10 },
222
+ cause: e
223
+ });
224
+ }
225
+ let parsed;
226
+ try {
227
+ parsed = JSON.parse(raw);
228
+ } catch (e) {
229
+ throw new BeeosError({
230
+ code: "state_corrupted",
231
+ message: `${path10} is not valid JSON: ${describeError(e)}`,
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 }
234
+ });
235
+ }
236
+ return validateState(parsed, path10);
237
+ }
238
+ return migrateLegacy();
239
+ }
240
+ async function saveState(state) {
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");
245
+ if (p.platform() !== "win32") {
246
+ try {
247
+ await p.chmod(path10, 384);
248
+ } catch {
249
+ }
250
+ }
251
+ }
252
+ function upsertOpenClawAgent(state, entry) {
253
+ const next = state.agents.filter((a) => !(a.kind === "openclaw" && a.framework === entry.framework));
254
+ next.push(entry);
255
+ return { ...state, agents: next };
256
+ }
257
+ function upsertDeviceAgent(state, entry) {
258
+ const next = state.agents.filter((a) => !(a.kind === "device" && a.serial === entry.serial));
259
+ next.push(entry);
260
+ return { ...state, agents: next };
261
+ }
262
+ async function mutateStateBestEffort(mutator, ctx) {
263
+ try {
264
+ const state = await loadState();
265
+ const next = mutator(state);
266
+ if (next === null)
267
+ return;
268
+ await saveState(next);
269
+ } catch (err) {
270
+ const msg = err instanceof Error ? err.message : String(err);
271
+ const subject = ctx.subject ? ` for ${ctx.subject}` : "";
272
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
273
+ console.warn(`Warning: could not update ~/.beeos/state.json${subject} (${msg}). ${ctx.authority} is still authoritative; run \`beeos doctor\` to repair.`);
274
+ }
275
+ }
276
+ }
277
+ async function migrateLegacy() {
278
+ const state = defaultState();
279
+ await tryReadSpawnEnvIntoPlatform(state);
280
+ const binding = await tryReadBinding();
281
+ if (binding) {
282
+ const platformAgw = state.platform.agent_gateway_url;
283
+ state.agents.push({
284
+ kind: "openclaw",
285
+ framework: binding.framework ?? "openclaw",
286
+ instance_id: binding.instance_id,
287
+ bound_at: binding.bound_at,
288
+ fingerprint: binding.fingerprint,
289
+ // Old binding.json never recorded per-binding AGW, so fall back
290
+ // to whatever the platform projection shows; saveConfig keeps
291
+ // this fresh on every config write.
292
+ agent_gateway_url: platformAgw
293
+ });
294
+ }
295
+ const devices = await tryReadDevices();
296
+ for (const d of devices) {
297
+ if (d.instance_id == null) {
298
+ continue;
299
+ }
300
+ const entry = {
301
+ kind: "device",
302
+ instance_id: d.instance_id,
303
+ bound_at: d.bound_at ?? 0,
304
+ fingerprint: d.fingerprint ?? "",
305
+ agent_gateway_url: d.agent_gateway_url && d.agent_gateway_url !== "" ? d.agent_gateway_url : state.platform.agent_gateway_url,
306
+ serial: d.serial,
307
+ name: d.name,
308
+ key_file: d.key_file,
309
+ http_port: d.http_port,
310
+ ...d.video_mode !== void 0 ? { video_mode: d.video_mode } : {},
311
+ ...d.backend !== void 0 ? { backend: d.backend } : {},
312
+ ...d.vnc_host !== void 0 ? { vnc_host: d.vnc_host } : {},
313
+ ...d.vnc_port !== void 0 ? { vnc_port: d.vnc_port } : {}
314
+ };
315
+ state.agents.push(entry);
316
+ }
317
+ await tryFoldVlmConfigs(state);
318
+ return state;
319
+ }
320
+ async function tryReadBinding() {
321
+ const p = getPlatformAdapter();
322
+ const path10 = bindingPath();
323
+ if (!await p.exists(path10))
324
+ return null;
325
+ try {
326
+ const raw = await p.readFile(path10);
327
+ return JSON.parse(raw);
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+ async function tryReadDevices() {
333
+ const p = getPlatformAdapter();
334
+ const path10 = legacyDevicesPath();
335
+ if (!await p.exists(path10))
336
+ return [];
337
+ try {
338
+ const raw = await p.readFile(path10);
339
+ const parsed = JSON.parse(raw);
340
+ return Array.isArray(parsed?.devices) ? parsed.devices : [];
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+ async function tryReadSpawnEnvIntoPlatform(state) {
346
+ const p = getPlatformAdapter();
347
+ const path10 = spawnEnvJsonPath();
348
+ if (!await p.exists(path10))
349
+ return;
350
+ try {
351
+ const raw = await p.readFile(path10);
352
+ const parsed = JSON.parse(raw);
353
+ if (!parsed || typeof parsed !== "object")
354
+ return;
355
+ if (typeof parsed.agent_gateway_url === "string" && parsed.agent_gateway_url.length > 0) {
356
+ state.platform.agent_gateway_url = parsed.agent_gateway_url;
357
+ }
358
+ if (typeof parsed.bridge_url === "string" && parsed.bridge_url.length > 0) {
359
+ state.platform.bridge_url = parsed.bridge_url;
360
+ }
361
+ } catch {
362
+ }
363
+ }
364
+ async function tryFoldVlmConfigs(state) {
365
+ const p = getPlatformAdapter();
366
+ const root = beeoHome();
367
+ let entries = [];
368
+ try {
369
+ entries = await p.readdir(root);
370
+ } catch {
371
+ return;
372
+ }
373
+ const byInstanceId = /* @__PURE__ */ new Map();
374
+ for (const a of state.agents)
375
+ byInstanceId.set(a.instance_id, a);
376
+ for (const name of entries) {
377
+ if (!name.endsWith(".config.json"))
378
+ continue;
379
+ if (name === "config.spawn-env.json")
380
+ continue;
381
+ const instanceId = name.slice(0, -".config.json".length);
382
+ const target = byInstanceId.get(instanceId);
383
+ if (!target)
384
+ continue;
385
+ const filePath = p.joinPath(root, name);
386
+ try {
387
+ const raw = await p.readFile(filePath);
388
+ const parsed = JSON.parse(raw);
389
+ target.agent_config = parsed;
390
+ } catch {
391
+ }
392
+ }
393
+ }
394
+ function validateState(parsed, path10) {
395
+ if (!parsed || typeof parsed !== "object") {
396
+ throw new BeeosError({
397
+ code: "state_corrupted",
398
+ message: `${path10} did not deserialise to a JSON object.`,
399
+ hint: "Move the file aside and re-run `beeos doctor` to regenerate.",
400
+ details: { path: path10 }
401
+ });
402
+ }
403
+ const obj = parsed;
404
+ const version = obj.version;
405
+ if (version !== 2) {
406
+ throw new BeeosError({
407
+ code: "state_version_mismatch",
408
+ message: `${path10} declares version=${JSON.stringify(version)}; this CLI only understands version=2.`,
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 }
411
+ });
412
+ }
413
+ const platform = obj.platform;
414
+ if (!platform || typeof platform !== "object") {
415
+ throw new BeeosError({
416
+ code: "state_corrupted",
417
+ message: `${path10} is missing a \`platform\` object.`,
418
+ hint: "Move the file aside and re-run `beeos doctor`.",
419
+ details: { path: path10 }
420
+ });
421
+ }
422
+ if (!Array.isArray(obj.agents)) {
423
+ throw new BeeosError({
424
+ code: "state_corrupted",
425
+ message: `${path10} \`agents\` is not an array.`,
426
+ hint: "Move the file aside and re-run `beeos doctor`.",
427
+ details: { path: path10 }
428
+ });
429
+ }
430
+ return obj;
431
+ }
432
+ function describeError(e) {
433
+ if (e instanceof Error)
434
+ return e.message;
435
+ return String(e);
436
+ }
437
+ var init_state = __esm({
438
+ "../core/dist/config/state.js"() {
439
+ "use strict";
440
+ init_errors();
441
+ init_platform_adapter();
442
+ init_paths();
443
+ init_types();
444
+ }
445
+ });
446
+
113
447
  // ../core/dist/config/toml.js
114
448
  import * as TOML from "smol-toml";
115
449
  async function loadOrCreateConfig() {
@@ -119,6 +453,8 @@ async function loadOrCreateConfig() {
119
453
  const raw = await p.readFile(path10);
120
454
  const parsed = TOML.parse(raw);
121
455
  const cfg2 = mergeWithDefaults(parsed);
456
+ await saveSpawnEnvSnapshot(cfg2);
457
+ await refreshStatePlatformBestEffort(cfg2);
122
458
  return applyEnvOverrides(cfg2);
123
459
  }
124
460
  const cfg = defaultConfig();
@@ -144,12 +480,16 @@ async function saveConfig(cfg) {
144
480
  const p = getPlatformAdapter();
145
481
  const path10 = configPath();
146
482
  await p.mkdir(p.dirname(path10));
483
+ const platformBlock = {
484
+ api_url: cfg.platform.api_url,
485
+ agent_gateway_url: cfg.platform.agent_gateway_url,
486
+ dashboard_base_url: cfg.platform.dashboard_base_url
487
+ };
488
+ if (cfg.platform.bridge_url && cfg.platform.bridge_url.trim() !== "") {
489
+ platformBlock.bridge_url = cfg.platform.bridge_url;
490
+ }
147
491
  const serializable = {
148
- platform: {
149
- api_url: cfg.platform.api_url,
150
- agent_gateway_url: cfg.platform.agent_gateway_url,
151
- dashboard_base_url: cfg.platform.dashboard_base_url
152
- },
492
+ platform: platformBlock,
153
493
  device: {
154
494
  http_enabled: cfg.device.http_enabled,
155
495
  http_port: cfg.device.http_port
@@ -157,6 +497,20 @@ async function saveConfig(cfg) {
157
497
  };
158
498
  const raw = TOML.stringify(serializable);
159
499
  await p.writeFile(path10, raw);
500
+ await saveSpawnEnvSnapshot(cfg);
501
+ await refreshStatePlatformBestEffort(cfg);
502
+ }
503
+ async function refreshStatePlatformBestEffort(cfg) {
504
+ const platform = {
505
+ api_url: cfg.platform.api_url,
506
+ agent_gateway_url: cfg.platform.agent_gateway_url,
507
+ dashboard_base_url: cfg.platform.dashboard_base_url,
508
+ ...cfg.platform.bridge_url && cfg.platform.bridge_url.trim() !== "" ? { bridge_url: cfg.platform.bridge_url } : {}
509
+ };
510
+ await mutateStateBestEffort((state) => platformEqual(state.platform, platform) ? null : { ...state, platform }, { authority: "config.spawn-env.json (legacy mirror)" });
511
+ }
512
+ function platformEqual(a, b) {
513
+ return a.api_url === b.api_url && a.agent_gateway_url === b.agent_gateway_url && a.dashboard_base_url === b.dashboard_base_url && a.bridge_url === b.bridge_url;
160
514
  }
161
515
  function mergeWithDefaults(parsed) {
162
516
  const platform = parsed.platform ?? {};
@@ -207,6 +561,8 @@ var init_toml = __esm({
207
561
  "use strict";
208
562
  init_platform_adapter();
209
563
  init_paths();
564
+ init_spawn_env();
565
+ init_state();
210
566
  init_types();
211
567
  }
212
568
  });
@@ -587,14 +943,18 @@ async function bindAgent(opts) {
587
943
  const instanceId = await pollUntilBound(opts.apiUrl, bindId, pollTimeoutMs);
588
944
  return { status: "bound", instanceId, source: "fresh" };
589
945
  } catch (e) {
590
- const msg = e instanceof Error ? e.message : String(e);
591
- if (/timed out|expired/i.test(msg)) {
946
+ if (e instanceof BeeosError && e.code === "bind_pending_expired") {
592
947
  return { status: "pending_expired" };
593
948
  }
594
949
  throw e;
595
950
  }
596
951
  }
597
- throw new Error(`unexpected bind status: ${resp.status}`);
952
+ throw new BeeosError({
953
+ code: "bind_protocol_unexpected",
954
+ message: `Unexpected bind protocol response (status: '${resp.status}').`,
955
+ hint: "Re-run the command. If this persists, check the platform's `/api/v1/agent/bind` health and contact support with the requested fingerprint.",
956
+ details: { received: resp.status, fingerprint: opts.fingerprint }
957
+ });
598
958
  }
599
959
  async function presentBindUrl(bindUrl, forceHeadless, log = console.log) {
600
960
  const p = getPlatformAdapter();
@@ -624,7 +984,12 @@ async function pollUntilBound(apiUrl, bindId, timeoutMs) {
624
984
  const deadline = Date.now() + timeoutMs;
625
985
  while (true) {
626
986
  if (Date.now() > deadline) {
627
- throw new Error("Bind approval timed out \u2014 please re-run the command");
987
+ throw new BeeosError({
988
+ code: "bind_pending_expired",
989
+ message: "Bind approval timed out \u2014 please re-run the command.",
990
+ hint: "Approve the request in the dashboard sooner, or re-run with the same identity to start a fresh bind.",
991
+ details: { source: "client_timeout", bindId }
992
+ });
628
993
  }
629
994
  try {
630
995
  const resp = await pollBind(apiUrl, bindId);
@@ -632,12 +997,17 @@ async function pollUntilBound(apiUrl, bindId, timeoutMs) {
632
997
  return resp.instance_id ?? "";
633
998
  }
634
999
  if (resp.status === "expired") {
635
- throw new Error("Bind session expired \u2014 please re-run the command");
1000
+ throw new BeeosError({
1001
+ code: "bind_pending_expired",
1002
+ message: "Bind session expired \u2014 please re-run the command.",
1003
+ hint: "Re-run `beeos start` to start a fresh bind session.",
1004
+ details: { source: "server_expired", bindId }
1005
+ });
636
1006
  }
637
1007
  } catch (e) {
638
- const msg = e instanceof Error ? e.message : String(e);
639
- if (msg.includes("expired") || msg.includes("timed out"))
1008
+ if (e instanceof BeeosError && e.code === "bind_pending_expired") {
640
1009
  throw e;
1010
+ }
641
1011
  await sleep(2e3);
642
1012
  }
643
1013
  }
@@ -692,6 +1062,139 @@ var init_orchestrator = __esm({
692
1062
  init_client();
693
1063
  init_process();
694
1064
  init_qr();
1065
+ init_errors();
1066
+ }
1067
+ });
1068
+
1069
+ // ../core/dist/upgrade.js
1070
+ function readPinSourcesFromEnv() {
1071
+ const env = globalThis.process?.env ?? {};
1072
+ return {
1073
+ env: {
1074
+ [NPM_PKGS.CLI]: trimOrUndef(env.BEEOS_CLI_VERSION),
1075
+ [NPM_PKGS.DEVICE_AGENT]: trimOrUndef(env.BEEOS_DEVICE_AGENT_VERSION),
1076
+ [NPM_PKGS.DEVICE_MCP_SERVER]: trimOrUndef(env.BEEOS_MCP_SERVER_VERSION)
1077
+ },
1078
+ distTag: trimOrUndef(env.BEEOS_CLI_TAG)
1079
+ };
1080
+ }
1081
+ function resolveInstallSpec(pkg, sources) {
1082
+ const pin = sources.env?.[pkg];
1083
+ if (pin)
1084
+ return `${pkg}@${pin}`;
1085
+ const tag = sources.distTag ?? "latest";
1086
+ return `${pkg}@${tag}`;
1087
+ }
1088
+ async function npmRegistryVersion(spec) {
1089
+ const p = getPlatformAdapter();
1090
+ try {
1091
+ const result = await p.exec("npm", ["view", spec, "version"], { timeout: 3e4 });
1092
+ if (result.code !== 0)
1093
+ return null;
1094
+ const trimmed = result.stdout.trim();
1095
+ return trimmed || null;
1096
+ } catch {
1097
+ return null;
1098
+ }
1099
+ }
1100
+ async function npmGlobalVersion(pkg) {
1101
+ const p = getPlatformAdapter();
1102
+ try {
1103
+ const result = await p.exec("npm", ["ls", "-g", "--depth=0", "--json", pkg], { timeout: 3e4 });
1104
+ if (!result.stdout)
1105
+ return null;
1106
+ const parsed = JSON.parse(result.stdout);
1107
+ const ver = parsed.dependencies?.[pkg]?.version;
1108
+ return typeof ver === "string" ? ver : null;
1109
+ } catch {
1110
+ return null;
1111
+ }
1112
+ }
1113
+ async function upgradeBeeosSuite(opts) {
1114
+ const p = getPlatformAdapter();
1115
+ const sources = opts.sources ?? readPinSourcesFromEnv();
1116
+ const progress = opts.progress;
1117
+ const specs = opts.packages.map((pkg) => ({
1118
+ pkg,
1119
+ spec: resolveInstallSpec(pkg, sources)
1120
+ }));
1121
+ const beforeVers = /* @__PURE__ */ new Map();
1122
+ for (const { pkg } of specs) {
1123
+ beforeVers.set(pkg, await npmGlobalVersion(pkg));
1124
+ }
1125
+ let needsInstall = opts.forceInstall === true;
1126
+ if (!needsInstall) {
1127
+ for (const { pkg, spec } of specs) {
1128
+ const target = await npmRegistryVersion(spec);
1129
+ const current = beforeVers.get(pkg) ?? null;
1130
+ if (target === null || current === null || target !== current) {
1131
+ needsInstall = true;
1132
+ break;
1133
+ }
1134
+ }
1135
+ }
1136
+ let installFailure;
1137
+ if (needsInstall) {
1138
+ const args = ["install", "-g", ...specs.map((s) => s.spec)];
1139
+ progress?.onStatus(`npm ${args.join(" ")}`);
1140
+ try {
1141
+ const result2 = await p.exec("npm", args, { timeout: 6e5 });
1142
+ if (result2.code !== 0) {
1143
+ installFailure = (result2.stderr || result2.stdout || `npm install -g exited ${result2.code}`).trim();
1144
+ }
1145
+ } catch (e) {
1146
+ installFailure = e instanceof Error ? e.message : String(e);
1147
+ }
1148
+ } else {
1149
+ progress?.onStatus("Already up to date \u2014 skipping npm install -g");
1150
+ }
1151
+ const result = { packages: [], anyChanged: false, anyFailed: false };
1152
+ for (const { pkg, spec } of specs) {
1153
+ const before = beforeVers.get(pkg) ?? null;
1154
+ const after = await npmGlobalVersion(pkg);
1155
+ const changed = before !== after;
1156
+ const entry = {
1157
+ pkg,
1158
+ spec,
1159
+ before,
1160
+ after,
1161
+ changed,
1162
+ ...installFailure ? { failed: installFailure } : {}
1163
+ };
1164
+ result.packages.push(entry);
1165
+ if (changed)
1166
+ result.anyChanged = true;
1167
+ if (entry.failed)
1168
+ result.anyFailed = true;
1169
+ if (changed) {
1170
+ progress?.onStatus(`${pkg}: ${before ?? "(none)"} \u2192 ${after}`);
1171
+ } else if (entry.failed) {
1172
+ progress?.onStatus(`${pkg}: install failed (${entry.failed})`);
1173
+ }
1174
+ }
1175
+ return result;
1176
+ }
1177
+ function trimOrUndef(v) {
1178
+ if (v === void 0)
1179
+ return void 0;
1180
+ const t = v.trim();
1181
+ return t.length > 0 ? t : void 0;
1182
+ }
1183
+ var NPM_PKGS, ALL_BEEOS_PKGS;
1184
+ var init_upgrade = __esm({
1185
+ "../core/dist/upgrade.js"() {
1186
+ "use strict";
1187
+ init_platform_adapter();
1188
+ NPM_PKGS = {
1189
+ CLI: "@beeos-ai/cli",
1190
+ DEVICE_AGENT: "@beeos-ai/device-agent",
1191
+ DEVICE_MCP_SERVER: "@beeos-ai/device-mcp-server"
1192
+ };
1193
+ ALL_BEEOS_PKGS = [
1194
+ NPM_PKGS.CLI,
1195
+ NPM_PKGS.DEVICE_AGENT,
1196
+ NPM_PKGS.DEVICE_MCP_SERVER
1197
+ ];
695
1198
  }
696
1199
  });
697
1200
 
@@ -718,8 +1221,21 @@ function describeFound(bin, progress) {
718
1221
  }
719
1222
  }
720
1223
  async function upgradeDeviceAgent(progress) {
721
- progress.onStatus("device-agent (TS) \u2014 upgrade out-of-band:\n \u2022 dev: cd agents/device-agent && git pull && pnpm install && pnpm -r build\n \u2022 global: npm i -g @beeos-ai/device-agent@latest");
722
- progress.onComplete("device-agent upgrade hint printed");
1224
+ const outcome = await upgradeBeeosSuite({
1225
+ packages: [NPM_PKGS.DEVICE_AGENT, NPM_PKGS.DEVICE_MCP_SERVER],
1226
+ progress
1227
+ });
1228
+ if (outcome.anyFailed) {
1229
+ const failed = outcome.packages.find((p) => p.failed)?.failed;
1230
+ progress.onStatus(`device-agent upgrade did not finish cleanly (${failed ?? "unknown error"}).
1231
+ Manual fix: npm i -g @beeos-ai/device-agent @beeos-ai/device-mcp-server`);
1232
+ return;
1233
+ }
1234
+ if (!outcome.anyChanged) {
1235
+ progress.onComplete("device-agent already up to date");
1236
+ return;
1237
+ }
1238
+ progress.onComplete("device-agent upgraded");
723
1239
  }
724
1240
  async function findExistingTs() {
725
1241
  const p = getPlatformAdapter();
@@ -776,6 +1292,7 @@ var init_device_setup = __esm({
776
1292
  "../core/dist/device-setup.js"() {
777
1293
  "use strict";
778
1294
  init_platform_adapter();
1295
+ init_upgrade();
779
1296
  }
780
1297
  });
781
1298
 
@@ -870,6 +1387,14 @@ async function ensureAdb(progress, options = {}) {
870
1387
  return installAdb(progress);
871
1388
  return null;
872
1389
  }
1390
+ function detectAdbLicenseDecision() {
1391
+ const env = globalThis.process?.env ?? {};
1392
+ if (env.BEEOS_ACCEPT_ADB_LICENSE === "1")
1393
+ return "accepted";
1394
+ if (env.BEEOS_REJECT_ADB_LICENSE === "1")
1395
+ return "rejected";
1396
+ return "unknown";
1397
+ }
873
1398
  function adbZipName() {
874
1399
  const p = getPlatformAdapter();
875
1400
  return PLATFORM_TOOLS_ARCHIVE[p.platform()] ?? null;
@@ -884,7 +1409,12 @@ function verifyPlatformToolsChecksum(zipName, data, progress) {
884
1409
  progress.onStatus(`BEEOS_ADB_SKIP_CHECKSUM=1 \u2014 skipping SHA-256 verification (no pin recorded for ${zipName}).`);
885
1410
  return;
886
1411
  }
887
- throw new Error(`platform-tools download: no SHA-256 pin recorded for "${zipName}". Refusing to execute an unverified archive. Run scripts/refresh-adb-hashes.mjs ${PLATFORM_TOOLS_REVISION} to populate, or set BEEOS_ADB_SKIP_CHECKSUM=1 to override (offline mirror / pre-pin development only; not safe for CI/release).`);
1412
+ throw new BeeosError({
1413
+ code: "adb_checksum_missing",
1414
+ message: `platform-tools download: no SHA-256 pin recorded for "${zipName}".`,
1415
+ hint: `Run scripts/refresh-adb-hashes.mjs ${PLATFORM_TOOLS_REVISION} to populate the table, or set BEEOS_ADB_SKIP_CHECKSUM=1 to override (offline mirror / pre-pin development only; not safe for CI/release).`,
1416
+ details: { archive: zipName, revision: PLATFORM_TOOLS_REVISION }
1417
+ });
888
1418
  }
889
1419
  if (actual === expected)
890
1420
  return;
@@ -892,23 +1422,20 @@ function verifyPlatformToolsChecksum(zipName, data, progress) {
892
1422
  progress.onStatus(`BEEOS_ADB_SKIP_CHECKSUM=1 \u2014 mismatched SHA-256 IGNORED: got ${actual.slice(0, 16)}\u2026, expected ${expected.slice(0, 16)}\u2026.`);
893
1423
  return;
894
1424
  }
895
- throw new Error(`platform-tools SHA-256 mismatch for ${zipName}.
896
- expected: ${expected}
897
- got: ${actual}
898
- Refusing to execute the downloaded archive. Likely causes:
899
- - CDN mirror served a different revision than we pinned
900
- (rotate via scripts/refresh-adb-hashes.mjs after cross-checking
901
- against https://dl.google.com/android/repository/repository2-3.xml)
902
- - Archive corrupted in transit (retry)
903
- - Supply-chain tampering (investigate before overriding)
904
- Set BEEOS_ADB_SKIP_CHECKSUM=1 to bypass (not recommended).`);
905
- }
906
- var PLATFORM_TOOLS_BASE, PLATFORM_TOOLS_REVISION, PLATFORM_TOOLS_ARCHIVE, PLATFORM_TOOLS_SHA256;
1425
+ throw new BeeosError({
1426
+ code: "adb_checksum_mismatch",
1427
+ message: `platform-tools SHA-256 mismatch for ${zipName}.`,
1428
+ hint: "Likely causes:\n - CDN mirror served a different revision than we pinned (rotate via scripts/refresh-adb-hashes.mjs after cross-checking against https://dl.google.com/android/repository/repository2-3.xml)\n - Archive corrupted in transit (retry)\n - Supply-chain tampering (investigate before overriding)\nSet BEEOS_ADB_SKIP_CHECKSUM=1 to bypass (not recommended).",
1429
+ details: { archive: zipName, expected, actual }
1430
+ });
1431
+ }
1432
+ var PLATFORM_TOOLS_BASE, PLATFORM_TOOLS_REVISION, PLATFORM_TOOLS_ARCHIVE, PLATFORM_TOOLS_SHA256, ADB_LICENSE_URL;
907
1433
  var init_adb_setup = __esm({
908
1434
  "../core/dist/adb-setup.js"() {
909
1435
  "use strict";
910
1436
  init_platform_adapter();
911
1437
  init_paths();
1438
+ init_errors();
912
1439
  PLATFORM_TOOLS_BASE = "https://dl.google.com/android/repository";
913
1440
  PLATFORM_TOOLS_REVISION = "r37.0.0";
914
1441
  PLATFORM_TOOLS_ARCHIVE = {
@@ -917,8 +1444,41 @@ var init_adb_setup = __esm({
917
1444
  win32: `platform-tools_${PLATFORM_TOOLS_REVISION}-win.zip`
918
1445
  };
919
1446
  PLATFORM_TOOLS_SHA256 = {
920
- // Populated by scripts/refresh-adb-hashes.mjs — keys are the
921
- // archive filenames for the pinned PLATFORM_TOOLS_REVISION.
1447
+ // sha1 (google manifest): 8c4c926d0ca192376b2a04b0318484724319e67c
1448
+ "platform-tools_r37.0.0-darwin.zip": "094a1395683c509fd4d48667da0d8b5ef4d42b2abfcd29f2e8149e2f989357c7",
1449
+ // sha1 (google manifest): bcf323933980a59dccc3f14c339aed5fb2171163
1450
+ "platform-tools_r37.0.0-linux.zip": "198ae156ab285fa555987219af237b31102fefe8b9d2bc274708a8d4f2865a07",
1451
+ // sha1 (google manifest): f29bfb58d0d6f9a57d7dbcba6cc259f9ca6f58f1
1452
+ "platform-tools_r37.0.0-win.zip": "4fe305812db074cea32903a489d061eb4454cbc90a49e8fea677f4b7af764918"
1453
+ };
1454
+ ADB_LICENSE_URL = "https://developer.android.com/studio/terms";
1455
+ }
1456
+ });
1457
+
1458
+ // ../core/dist/runtime/spawn-env.js
1459
+ function buildDeviceAgentSpawnEnv(input) {
1460
+ const env = {};
1461
+ if (input.agentGatewayUrl) {
1462
+ env[DEVICE_AGENT_SPAWN_ENV_KEYS.AGENT_GATEWAY_URL] = input.agentGatewayUrl;
1463
+ }
1464
+ if (input.extra) {
1465
+ for (const [k, v] of Object.entries(input.extra)) {
1466
+ if (v)
1467
+ env[k] = v;
1468
+ }
1469
+ }
1470
+ return env;
1471
+ }
1472
+ var DEVICE_AGENT_SPAWN_ENV_KEYS;
1473
+ var init_spawn_env2 = __esm({
1474
+ "../core/dist/runtime/spawn-env.js"() {
1475
+ "use strict";
1476
+ DEVICE_AGENT_SPAWN_ENV_KEYS = {
1477
+ /**
1478
+ * Agent Gateway base URL. Used by device-agent for `/bootstrap`,
1479
+ * bridge config discovery, and Message Service token exchange.
1480
+ */
1481
+ AGENT_GATEWAY_URL: "AGENT_GATEWAY_URL"
922
1482
  };
923
1483
  }
924
1484
  });
@@ -933,6 +1493,7 @@ var init_device = __esm({
933
1493
  init_keypair();
934
1494
  init_device_setup();
935
1495
  init_progress();
1496
+ init_spawn_env2();
936
1497
  deviceRuntime = {
937
1498
  agentFramework() {
938
1499
  return "device";
@@ -1008,10 +1569,7 @@ var init_device = __esm({
1008
1569
  const logDir = p.joinPath(beeoHome(), "logs");
1009
1570
  await p.mkdir(logDir);
1010
1571
  const logFile = p.joinPath(logDir, `device-${serial}.log`);
1011
- const env = {};
1012
- if (agentGatewayUrl) {
1013
- env.AGENT_GATEWAY_URL = agentGatewayUrl;
1014
- }
1572
+ const env = buildDeviceAgentSpawnEnv({ agentGatewayUrl });
1015
1573
  const result = await p.spawn(cmd, args, {
1016
1574
  detached: true,
1017
1575
  stdoutFile: logFile,
@@ -1199,7 +1757,9 @@ async function downloadManagedBinary(cfg, target, progress) {
1199
1757
  if (!data)
1200
1758
  return null;
1201
1759
  const digestUrl = `${url}.sha256`;
1760
+ const allowUnverified = (process.env?.BEEOS_ALLOW_UNVERIFIED_SIDECAR ?? "").toLowerCase() === "1" || (process.env?.BEEOS_ALLOW_UNVERIFIED_SIDECAR ?? "").toLowerCase() === "true";
1202
1761
  let expected = null;
1762
+ let digestFetchError = null;
1203
1763
  try {
1204
1764
  const digResp = await p.fetch(digestUrl);
1205
1765
  if (digResp.ok) {
@@ -1207,18 +1767,35 @@ async function downloadManagedBinary(cfg, target, progress) {
1207
1767
  const first = text.split(/\s+/)[0] ?? "";
1208
1768
  if (/^[a-f0-9]{64}$/i.test(first))
1209
1769
  expected = first.toLowerCase();
1770
+ else
1771
+ digestFetchError = `malformed .sha256 body: ${text.slice(0, 80)}`;
1772
+ } else {
1773
+ digestFetchError = `HTTP ${digResp.status}`;
1210
1774
  }
1211
- } catch {
1775
+ } catch (e) {
1776
+ digestFetchError = e instanceof Error ? e.message : String(e);
1212
1777
  }
1213
1778
  if (expected) {
1214
1779
  const actual = createHash2("sha256").update(data).digest("hex");
1215
1780
  if (actual !== expected) {
1216
1781
  progress.onStatus(`${cfg.name} checksum mismatch (expected ${expected.slice(0, 12)}\u2026, got ${actual.slice(0, 12)}\u2026) \u2014 aborting install.`);
1217
- return null;
1782
+ throw new BeeosError({
1783
+ code: "sidecar_checksum_mismatch",
1784
+ message: `${cfg.name} archive integrity check failed at ${url}`,
1785
+ hint: "The downloaded archive does not match the publisher's checksum. Possible causes: MITM proxy rewriting the asset, a corrupted CDN cache, or a malicious replacement. Retry once; if it persists, file a bug at the sidecar's release repo.",
1786
+ details: { sidecar: cfg.name, url, expected, actual }
1787
+ });
1218
1788
  }
1219
1789
  progress.onStatus(`${cfg.name} checksum ok (${expected.slice(0, 12)}\u2026).`);
1790
+ } else if (allowUnverified) {
1791
+ progress.onStatus(`${cfg.name}: no .sha256 sidecar at ${digestUrl} (${digestFetchError ?? "unavailable"}) \u2014 BEEOS_ALLOW_UNVERIFIED_SIDECAR=1 set, continuing without integrity check (NOT recommended).`);
1220
1792
  } else {
1221
- progress.onStatus(`${cfg.name}: no .sha256 sidecar at ${digestUrl} \u2014 continuing without integrity check`);
1793
+ throw new BeeosError({
1794
+ code: "sidecar_checksum_missing",
1795
+ message: `${cfg.name}: no .sha256 sidecar at ${digestUrl}` + (digestFetchError ? ` (${digestFetchError})` : ""),
1796
+ hint: "The publisher must ship a .sha256 alongside every release asset (cargo-dist does this by default). If you are on a locked-down network that blocks .sha256 URLs, set BEEOS_ALLOW_UNVERIFIED_SIDECAR=1 to opt out \u2014 but understand you are accepting an unverified binary.",
1797
+ details: { sidecar: cfg.name, archiveUrl: url, digestUrl }
1798
+ });
1222
1799
  }
1223
1800
  const binDir = p.joinPath(beeoHome(), "bin");
1224
1801
  await p.mkdir(binDir);
@@ -1286,6 +1863,7 @@ var init_cargo_dist = __esm({
1286
1863
  "use strict";
1287
1864
  init_platform_adapter();
1288
1865
  init_paths();
1866
+ init_errors();
1289
1867
  }
1290
1868
  });
1291
1869
 
@@ -1549,7 +2127,7 @@ async function writeDeviceAgentConfig(input) {
1549
2127
  if (p.platform() !== "win32") {
1550
2128
  await p.chmod(file, 384);
1551
2129
  }
1552
- return file;
2130
+ return { filePath: file, payload };
1553
2131
  }
1554
2132
  async function persistAgentConfigBestEffort(input) {
1555
2133
  const reporter = input.reporter ?? noopReporter;
@@ -1563,7 +2141,7 @@ async function persistAgentConfigBestEffort(input) {
1563
2141
  if (result.keyMissing) {
1564
2142
  reporter.onStatus(` VLM API key not configured for instance ${input.instanceId} \u2014 set AROUTER_API_KEY in the dashboard, then run \`beeos device refresh-config\`.`);
1565
2143
  }
1566
- const file = await writeDeviceAgentConfig({
2144
+ return await writeDeviceAgentConfig({
1567
2145
  instanceId: input.instanceId,
1568
2146
  vlmApiKey: result.vlmApiKey,
1569
2147
  vlmModel: result.vlmModel,
@@ -1573,7 +2151,6 @@ async function persistAgentConfigBestEffort(input) {
1573
2151
  ...input.mobileUse ? { mobileUse: input.mobileUse } : {},
1574
2152
  ...input.grounder ? { grounder: input.grounder } : {}
1575
2153
  });
1576
- return file;
1577
2154
  } catch (e) {
1578
2155
  reporter.onStatus(` VLM config not fetched (${e.message}). Agent will run without ARouter \u2014 pass \`--vlm-api-key\` for standalone testing or run \`beeos device refresh-config\` later.`);
1579
2156
  return null;
@@ -1657,6 +2234,9 @@ function hasLocalDetection(driver) {
1657
2234
  function hasStartupDiagnostics(driver) {
1658
2235
  return typeof driver.diagnoseStartup === "function";
1659
2236
  }
2237
+ function hasPreflightConflictCheck(driver) {
2238
+ return typeof driver.preflightConflictCheck === "function";
2239
+ }
1660
2240
  var init_driver = __esm({
1661
2241
  "../core/dist/agent/driver.js"() {
1662
2242
  "use strict";
@@ -1728,10 +2308,16 @@ async function downloadAgent(npmPackage, requestedVersion, agentFramework, progr
1728
2308
  }
1729
2309
  return dest;
1730
2310
  }
2311
+ function pluginVersionPin() {
2312
+ const env = globalThis.process?.env;
2313
+ const fromEnv = env?.BEEOS_BEEOS_CLAW_VERSION?.trim();
2314
+ return fromEnv && fromEnv.length > 0 ? fromEnv : void 0;
2315
+ }
1731
2316
  async function downloadPlugin(pluginPackage, agentFramework, progress, installedVersion) {
1732
2317
  const p = getPlatformAdapter();
1733
2318
  const info = await fetchNpmPackageInfo(pluginPackage);
1734
- const version = info["dist-tags"]["latest"];
2319
+ const pin = pluginVersionPin();
2320
+ const version = pin ?? info["dist-tags"]["latest"];
1735
2321
  if (!version)
1736
2322
  throw new Error("no 'latest' tag for plugin");
1737
2323
  if (installedVersion && semverGte(installedVersion, version)) {
@@ -1852,13 +2438,22 @@ async function locateManagedBinary() {
1852
2438
  }
1853
2439
  async function downloadManagedOpenclaw(reporter, versionOverride) {
1854
2440
  reporter.onStatus(`Downloading ${OPENCLAW_NPM_PACKAGE}...`);
1855
- await downloadAgent(OPENCLAW_NPM_PACKAGE, versionOverride, OPENCLAW_ID, reporter);
2441
+ await downloadAgent(OPENCLAW_NPM_PACKAGE, resolveOpenclawVersion(versionOverride), OPENCLAW_ID, reporter);
1856
2442
  const binary = await locateManagedBinary();
1857
2443
  if (!binary) {
1858
2444
  throw new Error("openclaw binary not found after download \u2014 expected it under ~/.beeos/agents/openclaw/versions/current/");
1859
2445
  }
1860
2446
  return binary;
1861
2447
  }
2448
+ function resolveOpenclawVersion(explicit) {
2449
+ if (explicit && explicit.trim().length > 0)
2450
+ return explicit.trim();
2451
+ const env = globalThis.process?.env;
2452
+ const fromEnv = env?.BEEOS_OPENCLAW_VERSION?.trim();
2453
+ if (fromEnv && fromEnv.length > 0)
2454
+ return fromEnv;
2455
+ return void 0;
2456
+ }
1862
2457
  var init_download = __esm({
1863
2458
  "../core/dist/openclaw/download.js"() {
1864
2459
  "use strict";
@@ -1887,6 +2482,10 @@ async function readLocalPluginVersion(agentHome2) {
1887
2482
  }
1888
2483
  return null;
1889
2484
  }
2485
+ async function isPluginInstalled(agentHome2) {
2486
+ const p = getPlatformAdapter();
2487
+ return p.exists(p.joinPath(agentHome2, "extensions", "beeos-claw"));
2488
+ }
1890
2489
  async function findAgent(driver) {
1891
2490
  const managed = await locateManagedBinary();
1892
2491
  if (managed) {
@@ -2442,12 +3041,20 @@ function fallbackSystemHome() {
2442
3041
  async function isOpenclawGatewayRunning() {
2443
3042
  return getPlatformAdapter().tcpProbe(OPENCLAW_GATEWAY_HOST, OPENCLAW_GATEWAY_PORT, 500);
2444
3043
  }
3044
+ function describeError2(e) {
3045
+ if (e instanceof Error) {
3046
+ const msg = e.message.split(/\r?\n/, 1)[0]?.trim();
3047
+ return msg && msg.length > 0 ? msg : e.name;
3048
+ }
3049
+ return String(e);
3050
+ }
2445
3051
  var OpenclawDriver, openClawDriver;
2446
3052
  var init_driver2 = __esm({
2447
3053
  "../core/dist/openclaw/driver.js"() {
2448
3054
  "use strict";
2449
3055
  init_platform_adapter();
2450
3056
  init_paths();
3057
+ init_detector();
2451
3058
  init_constants();
2452
3059
  init_config();
2453
3060
  init_detect_local();
@@ -2461,10 +3068,12 @@ var init_driver2 = __esm({
2461
3068
  displayName = OPENCLAW_DISPLAY_NAME;
2462
3069
  async launch(ctx, reporter) {
2463
3070
  const p = getPlatformAdapter();
3071
+ const warnings = [];
2464
3072
  const picked = await pickLocation(ctx, reporter);
2465
3073
  try {
2466
3074
  await ensureOpenclawPlugin(picked.home, picked.binary, reporter);
2467
- } catch {
3075
+ } catch (e) {
3076
+ warnings.push(`plugin: failed to register beeos-claw plugin into ${picked.home} (${describeError2(e)}); the gateway will start but BeeOS-specific tools may not be available. Re-run \`beeos doctor\`.`);
2468
3077
  }
2469
3078
  const gatewayToken = await resolveGatewayToken(picked.home, picked.isSystemHome);
2470
3079
  if (picked.isSystemHome)
@@ -2491,14 +3100,16 @@ var init_driver2 = __esm({
2491
3100
  } else if (picked.isSystemHome) {
2492
3101
  try {
2493
3102
  await configurePluginViaCli(configCtx);
2494
- } catch {
3103
+ } catch (e) {
3104
+ warnings.push(`live-reconfigure: failed to update beeos-claw plugin section in running ${picked.home} (${describeError2(e)}); the foreign gateway may still be serving an older plugin config. Stop it and re-run \`beeos start openclaw --force\` to force a clean register.`);
2495
3105
  }
2496
3106
  try {
2497
3107
  await restartGatewayViaCli(configCtx);
2498
- } catch {
3108
+ } catch (e) {
3109
+ warnings.push(`live-reconfigure: failed to restart running gateway after re-configuring (${describeError2(e)}); changes may not take effect until the gateway is restarted manually.`);
2499
3110
  }
2500
3111
  }
2501
- return buildOpenclawServiceSpec({
3112
+ const spec = buildOpenclawServiceSpec({
2502
3113
  agentBinary: picked.binary,
2503
3114
  agentHome: picked.home,
2504
3115
  agentGatewayUrl: ctx.agentGatewayUrl,
@@ -2506,6 +3117,7 @@ var init_driver2 = __esm({
2506
3117
  isSystemHome: picked.isSystemHome,
2507
3118
  envOverrides: ctx.envOverrides
2508
3119
  });
3120
+ return { spec, warnings };
2509
3121
  }
2510
3122
  async detectLocal() {
2511
3123
  return detectLocalOpenclaw();
@@ -2513,6 +3125,34 @@ var init_driver2 = __esm({
2513
3125
  async diagnoseStartup(logPath) {
2514
3126
  return diagnoseOpenclawStartup(logPath);
2515
3127
  }
3128
+ /**
3129
+ * P0-C of the install-link review: previously this probe lived in
3130
+ * `commands/start.ts` with hard-coded "OpenClaw gateway port" copy
3131
+ * and `identifyGateway()` defaults — the abstraction promise that
3132
+ * "a new framework = a new registry entry" was broken because
3133
+ * adding e.g. Cline / Roo would have triggered a misleading
3134
+ * OpenClaw-flavoured error. The probe is now a driver-owned
3135
+ * capability; the CLI just routes the structured report into a
3136
+ * generic `gateway_port_conflict` BeeosError.
3137
+ */
3138
+ async preflightConflictCheck(ctx) {
3139
+ const probe = await identifyGateway({ expectedFingerprint: ctx.fingerprint });
3140
+ switch (probe.state) {
3141
+ case "unreachable":
3142
+ return { state: "unreachable" };
3143
+ case "own":
3144
+ return { state: "ours" };
3145
+ case "foreign":
3146
+ return {
3147
+ state: "foreign",
3148
+ detail: `a different beeos-claw gateway (fingerprint ${probe.fingerprint.slice(0, 16)}\u2026, plugin ${probe.pluginVersion || "?"})`
3149
+ };
3150
+ case "unknown":
3151
+ return { state: "unknown", reason: probe.reason };
3152
+ default:
3153
+ return { state: "unknown", reason: `unhandled probe state` };
3154
+ }
3155
+ }
2516
3156
  };
2517
3157
  openClawDriver = new OpenclawDriver();
2518
3158
  }
@@ -2573,23 +3213,27 @@ async function detectOpenclaw() {
2573
3213
  const location = await findAgent(openClawDriver);
2574
3214
  const running = await getPlatformAdapter().tcpProbe(OPENCLAW_GATEWAY_HOST, OPENCLAW_GATEWAY_PORT, 500);
2575
3215
  if (location.type === "managed") {
3216
+ const pluginInstalled = await isPluginInstalled(location.home).catch(() => false);
2576
3217
  return {
2577
3218
  found: true,
2578
3219
  source: "managed",
2579
3220
  binary: location.binary,
2580
3221
  home: location.home,
2581
3222
  version: null,
2582
- gatewayRunning: running
3223
+ gatewayRunning: running,
3224
+ pluginInstalled
2583
3225
  };
2584
3226
  }
2585
3227
  if (location.type === "system") {
3228
+ const pluginInstalled = await isPluginInstalled(location.home).catch(() => false);
2586
3229
  return {
2587
3230
  found: true,
2588
3231
  source: "system",
2589
3232
  binary: location.binary,
2590
3233
  home: location.home,
2591
3234
  version: location.version || null,
2592
- gatewayRunning: running
3235
+ gatewayRunning: running,
3236
+ pluginInstalled
2593
3237
  };
2594
3238
  }
2595
3239
  return {
@@ -2598,7 +3242,8 @@ async function detectOpenclaw() {
2598
3242
  binary: null,
2599
3243
  home: null,
2600
3244
  version: null,
2601
- gatewayRunning: running
3245
+ gatewayRunning: running,
3246
+ pluginInstalled: null
2602
3247
  };
2603
3248
  }
2604
3249
  async function detectDevices() {
@@ -2669,7 +3314,11 @@ function summarizeExistingInstall(state) {
2669
3314
  lines.push(`BeeOS home : ${state.beeosHome}${state.beeosHomeExists ? "" : " (not created)"}`);
2670
3315
  lines.push(`Identity : ${state.hasIdentity ? state.fingerprint ?? "(keypair present)" : "not created"}`);
2671
3316
  lines.push(`Binding : ${state.binding ? `bound \u2192 instance ${state.binding.instance_id}` : "not bound"}`);
2672
- lines.push(`OpenClaw : ${state.openclaw.found ? `${state.openclaw.source}${state.openclaw.version ? ` v${state.openclaw.version}` : ""} (gateway ${state.openclaw.gatewayRunning ? "running" : "stopped"})` : "not installed"}`);
3317
+ lines.push(`OpenClaw : ${state.openclaw.found ? `${state.openclaw.source}${state.openclaw.version ? ` v${state.openclaw.version}` : ""} (gateway ${state.openclaw.gatewayRunning ? "running" : "stopped"}` + // P2-N: surface "installed but plugin missing" so users
3318
+ // who hit a soft-failed launch see the discrepancy on the
3319
+ // first init/doctor pass instead of debugging a generic
3320
+ // "tools not available" later.
3321
+ (state.openclaw.pluginInstalled === false ? ", plugin missing" : "") + `)` : "not installed"}`);
2673
3322
  lines.push(`Devices : ${state.devices.entries.length === 0 && state.devices.keyedSerials.length === 0 ? "none attached" : `${state.devices.entries.length} attached, ${state.devices.keyedSerials.length} key(s)`}`);
2674
3323
  lines.push(`Supervisor : ${state.supervisor.ipcReachable ? `running (${state.supervisor.targets.length} target(s))` : state.supervisor.targets.length > 0 ? `stopped, ${state.supervisor.targets.length} persisted target(s)` : "not installed"}`);
2675
3324
  return lines;
@@ -2770,6 +3419,7 @@ var init_target_spec = __esm({
2770
3419
  "use strict";
2771
3420
  init_scrcpy_bridge();
2772
3421
  init_vnc_bridge();
3422
+ init_spawn_env2();
2773
3423
  }
2774
3424
  });
2775
3425
 
@@ -2969,13 +3619,23 @@ var init_launchd = __esm({
2969
3619
  await fs.mkdir(path2.dirname(plist), { recursive: true });
2970
3620
  await fs.mkdir(path2.dirname(logFile), { recursive: true });
2971
3621
  const resolvedSpec = await absolutizeCommand(spec);
2972
- await fs.writeFile(plist, renderPlist(resolvedSpec, label, logFile), {
2973
- mode: 420
2974
- });
2975
- const uid = process.getuid?.();
2976
- if (uid !== void 0) {
3622
+ const newContent = renderPlist(resolvedSpec, label, logFile);
3623
+ let existingContent = null;
3624
+ try {
3625
+ existingContent = await fs.readFile(plist, "utf-8");
3626
+ } catch {
3627
+ }
3628
+ if (existingContent === newContent) {
3629
+ const existing = await this.status(spec.id);
3630
+ if (existing?.running)
3631
+ return existing;
3632
+ }
3633
+ await fs.writeFile(plist, newContent, { mode: 420 });
3634
+ const uid = process.getuid?.();
3635
+ if (uid !== void 0) {
2977
3636
  try {
2978
3637
  await execFileP("launchctl", ["bootout", `gui/${uid}/${label}`]);
3638
+ await this.waitForBootout(label, uid);
2979
3639
  } catch {
2980
3640
  }
2981
3641
  try {
@@ -2998,6 +3658,24 @@ var init_launchd = __esm({
2998
3658
  const status2 = await this.status(spec.id);
2999
3659
  return status2 ?? fallbackStatus(spec, label, logFile, false);
3000
3660
  }
3661
+ /**
3662
+ * Poll `launchctl print` until the service is no longer registered
3663
+ * with launchd. `bootout` returns as soon as it sends SIGTERM, but
3664
+ * the old process may still be alive (and holding ports) for the
3665
+ * length of its graceful-shutdown window. `print` exits non-zero
3666
+ * once the job is gone, which is the signal we want.
3667
+ */
3668
+ async waitForBootout(label, uid, timeoutMs = 3e4) {
3669
+ const deadline = Date.now() + timeoutMs;
3670
+ while (Date.now() < deadline) {
3671
+ try {
3672
+ await execFileP("launchctl", ["print", `gui/${uid}/${label}`]);
3673
+ await new Promise((r) => setTimeout(r, 200));
3674
+ } catch {
3675
+ return;
3676
+ }
3677
+ }
3678
+ }
3001
3679
  async uninstall(id) {
3002
3680
  const label = launchdLabel(id);
3003
3681
  const plist = this.plistPath(label);
@@ -3968,6 +4646,7 @@ async function migrateLegacySupervisor(mgr) {
3968
4646
  const supervisorDir = path6.join(home, "supervisor");
3969
4647
  const stateFile = path6.join(supervisorDir, "state.json");
3970
4648
  const flagFile = path6.join(home, MIGRATION_FLAG);
4649
+ const lockFile = path6.join(home, MIGRATION_LOCK);
3971
4650
  if (fsSync4.existsSync(flagFile)) {
3972
4651
  return { ran: false, migrated: 0, errors: [], backupPath: null };
3973
4652
  }
@@ -3978,47 +4657,73 @@ async function migrateLegacySupervisor(mgr) {
3978
4657
  }
3979
4658
  return { ran: false, migrated: 0, errors: [], backupPath: null };
3980
4659
  }
3981
- const backupPath = `${stateFile}.migration-backup-${Date.now()}`;
3982
- try {
3983
- await fs5.copyFile(stateFile, backupPath);
3984
- } catch {
3985
- }
3986
- let parsed;
3987
- try {
3988
- const raw = await fs5.readFile(stateFile, "utf-8");
3989
- parsed = JSON.parse(raw);
3990
- } catch (e) {
4660
+ if (!await acquireLock(lockFile)) {
3991
4661
  return {
3992
4662
  ran: true,
3993
4663
  migrated: 0,
3994
- errors: [{ id: "<state.json>", error: `unreadable: ${e}` }],
3995
- backupPath
4664
+ errors: [{
4665
+ id: "<lock>",
4666
+ error: `migration-in-progress.lock held by another beeos process (${lockFile}); rerun once it exits`
4667
+ }],
4668
+ backupPath: null
3996
4669
  };
3997
4670
  }
3998
- const targets = parsed.targets ?? [];
3999
- const errors = [];
4000
- let migrated = 0;
4001
- for (const t of targets) {
4002
- const canonicalId = safeId(t.id);
4671
+ try {
4672
+ const backupPath = `${stateFile}.migration-backup-${Date.now()}`;
4003
4673
  try {
4004
- const spec = {
4005
- id: canonicalId,
4006
- kind: t.kind,
4007
- command: t.command,
4008
- args: t.args ?? [],
4009
- env: t.env,
4010
- cwd: t.cwd,
4011
- restart: t.restart ?? "on-failure",
4012
- label: t.label
4013
- };
4014
- await mgr.install(spec);
4015
- migrated++;
4674
+ await fs5.copyFile(stateFile, backupPath);
4675
+ } catch {
4676
+ }
4677
+ let parsed;
4678
+ try {
4679
+ const raw = await fs5.readFile(stateFile, "utf-8");
4680
+ parsed = JSON.parse(raw);
4016
4681
  } catch (e) {
4017
- errors.push({ id: canonicalId, error: e instanceof Error ? e.message : String(e) });
4682
+ return {
4683
+ ran: true,
4684
+ migrated: 0,
4685
+ errors: [{ id: "<state.json>", error: `unreadable: ${e}` }],
4686
+ backupPath
4687
+ };
4018
4688
  }
4019
- }
4020
- await stopLegacyDaemon();
4021
- if (errors.length === 0) {
4689
+ const targets = parsed.targets ?? [];
4690
+ const errors = [];
4691
+ const installed = [];
4692
+ for (const t of targets) {
4693
+ const canonicalId = safeId(t.id);
4694
+ try {
4695
+ const spec = {
4696
+ id: canonicalId,
4697
+ kind: t.kind,
4698
+ command: t.command,
4699
+ args: t.args ?? [],
4700
+ env: t.env,
4701
+ cwd: t.cwd,
4702
+ restart: t.restart ?? "on-failure",
4703
+ label: t.label
4704
+ };
4705
+ await mgr.install(spec);
4706
+ installed.push(canonicalId);
4707
+ } catch (e) {
4708
+ errors.push({ id: canonicalId, error: e instanceof Error ? e.message : String(e) });
4709
+ break;
4710
+ }
4711
+ }
4712
+ if (errors.length > 0) {
4713
+ for (const id of installed) {
4714
+ try {
4715
+ await mgr.uninstall(id);
4716
+ } catch {
4717
+ }
4718
+ }
4719
+ return {
4720
+ ran: true,
4721
+ migrated: 0,
4722
+ errors,
4723
+ backupPath
4724
+ };
4725
+ }
4726
+ await stopLegacyDaemon();
4022
4727
  try {
4023
4728
  await fs5.writeFile(flagFile, (/* @__PURE__ */ new Date()).toISOString());
4024
4729
  } catch {
@@ -4027,8 +4732,56 @@ async function migrateLegacySupervisor(mgr) {
4027
4732
  await fs5.rm(supervisorDir, { recursive: true, force: true });
4028
4733
  } catch {
4029
4734
  }
4735
+ return { ran: true, migrated: installed.length, errors: [], backupPath };
4736
+ } finally {
4737
+ try {
4738
+ await fs5.unlink(lockFile);
4739
+ } catch {
4740
+ }
4741
+ }
4742
+ }
4743
+ async function acquireLock(lockFile) {
4744
+ try {
4745
+ const handle = await fs5.open(lockFile, "wx");
4746
+ try {
4747
+ await handle.write(`${process.pid}
4748
+ ${(/* @__PURE__ */ new Date()).toISOString()}
4749
+ `);
4750
+ } finally {
4751
+ await handle.close();
4752
+ }
4753
+ return true;
4754
+ } catch (e) {
4755
+ if (e.code !== "EEXIST") {
4756
+ return false;
4757
+ }
4758
+ }
4759
+ let stale = false;
4760
+ try {
4761
+ const stat = await fs5.stat(lockFile);
4762
+ stale = Date.now() - stat.mtimeMs > STALE_LOCK_AGE_MS;
4763
+ } catch {
4764
+ stale = true;
4765
+ }
4766
+ if (!stale)
4767
+ return false;
4768
+ try {
4769
+ await fs5.unlink(lockFile);
4770
+ } catch {
4771
+ }
4772
+ try {
4773
+ const handle = await fs5.open(lockFile, "wx");
4774
+ try {
4775
+ await handle.write(`${process.pid}
4776
+ ${(/* @__PURE__ */ new Date()).toISOString()}
4777
+ `);
4778
+ } finally {
4779
+ await handle.close();
4780
+ }
4781
+ return true;
4782
+ } catch {
4783
+ return false;
4030
4784
  }
4031
- return { ran: true, migrated, errors, backupPath };
4032
4785
  }
4033
4786
  async function stopLegacyDaemon() {
4034
4787
  if (process.platform === "darwin") {
@@ -4063,7 +4816,7 @@ async function stopLegacyDaemon() {
4063
4816
  }
4064
4817
  }
4065
4818
  }
4066
- var execFileP5, LEGACY_LABEL, LEGACY_SYSTEMD_UNIT, MIGRATION_FLAG;
4819
+ var execFileP5, LEGACY_LABEL, LEGACY_SYSTEMD_UNIT, MIGRATION_FLAG, MIGRATION_LOCK, STALE_LOCK_AGE_MS;
4067
4820
  var init_migrate = __esm({
4068
4821
  "../core/dist/services/migrate.js"() {
4069
4822
  "use strict";
@@ -4073,11 +4826,91 @@ var init_migrate = __esm({
4073
4826
  LEGACY_LABEL = "ai.beeos.supervisor";
4074
4827
  LEGACY_SYSTEMD_UNIT = "beeos-supervisor.service";
4075
4828
  MIGRATION_FLAG = "migrated-to-services.flag";
4829
+ MIGRATION_LOCK = "migration-in-progress.lock";
4830
+ STALE_LOCK_AGE_MS = 5 * 60 * 1e3;
4831
+ }
4832
+ });
4833
+
4834
+ // ../core/dist/services/tail-logs.js
4835
+ import fs6 from "fs/promises";
4836
+ async function readLastLines(file, n) {
4837
+ let text = "";
4838
+ let size = 0;
4839
+ try {
4840
+ text = await fs6.readFile(file, "utf8");
4841
+ const stat = await fs6.stat(file);
4842
+ size = stat.size;
4843
+ } catch (e) {
4844
+ if (e.code !== "ENOENT")
4845
+ throw e;
4846
+ }
4847
+ if (text === "")
4848
+ return { text: "", size };
4849
+ const trimmed = text.endsWith("\n") ? text.slice(0, -1) : text;
4850
+ const lines = trimmed.split("\n");
4851
+ const sliced = lines.slice(Math.max(0, lines.length - n));
4852
+ return { text: sliced.join("\n") + "\n", size };
4853
+ }
4854
+ async function tailLogFile(opts) {
4855
+ const write = opts.write ?? ((s) => process.stdout.write(s));
4856
+ const prefix = opts.prefix ?? "";
4857
+ const initial = opts.initialLines ?? 50;
4858
+ const pollMs = opts.pollIntervalMs ?? 200;
4859
+ const writePrefixed = (chunk) => {
4860
+ if (chunk.length === 0)
4861
+ return;
4862
+ if (prefix === "") {
4863
+ write(chunk);
4864
+ return;
4865
+ }
4866
+ const endsWithNewline = chunk.endsWith("\n");
4867
+ const lines = (endsWithNewline ? chunk.slice(0, -1) : chunk).split("\n");
4868
+ const out = lines.map((l) => `${prefix}${l}`).join("\n");
4869
+ write(endsWithNewline ? out + "\n" : out);
4870
+ };
4871
+ const initialRead = await readLastLines(opts.logFile, initial);
4872
+ writePrefixed(initialRead.text);
4873
+ if (!opts.follow)
4874
+ return;
4875
+ let lastSize = initialRead.size;
4876
+ while (!opts.signal?.aborted) {
4877
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
4878
+ if (opts.signal?.aborted)
4879
+ break;
4880
+ let stat;
4881
+ try {
4882
+ stat = await fs6.stat(opts.logFile);
4883
+ } catch (e) {
4884
+ if (e.code === "ENOENT") {
4885
+ lastSize = 0;
4886
+ continue;
4887
+ }
4888
+ throw e;
4889
+ }
4890
+ if (stat.size === lastSize)
4891
+ continue;
4892
+ if (stat.size < lastSize) {
4893
+ lastSize = 0;
4894
+ }
4895
+ const handle = await fs6.open(opts.logFile, "r");
4896
+ try {
4897
+ const buf = Buffer.alloc(stat.size - lastSize);
4898
+ const { bytesRead } = await handle.read(buf, 0, buf.length, lastSize);
4899
+ writePrefixed(buf.subarray(0, bytesRead).toString("utf8"));
4900
+ lastSize = lastSize + bytesRead;
4901
+ } finally {
4902
+ await handle.close();
4903
+ }
4904
+ }
4905
+ }
4906
+ var init_tail_logs = __esm({
4907
+ "../core/dist/services/tail-logs.js"() {
4908
+ "use strict";
4076
4909
  }
4077
4910
  });
4078
4911
 
4079
4912
  // ../core/dist/cli-version.js
4080
- import fs6 from "fs";
4913
+ import fs7 from "fs";
4081
4914
  import path7 from "path";
4082
4915
  import { fileURLToPath } from "url";
4083
4916
  function getCliVersion(moduleUrl, distTag) {
@@ -4105,8 +4938,8 @@ function readBakedDistTag() {
4105
4938
  let dir = here;
4106
4939
  for (let i = 0; i < 6; i++) {
4107
4940
  const candidate = path7.join(dir, "package.json");
4108
- if (fs6.existsSync(candidate)) {
4109
- const raw = fs6.readFileSync(candidate, "utf-8");
4941
+ if (fs7.existsSync(candidate)) {
4942
+ const raw = fs7.readFileSync(candidate, "utf-8");
4110
4943
  const pkg = JSON.parse(raw);
4111
4944
  if ((pkg.name === "@beeos-ai/cli" || pkg.name === "beeos") && pkg.beeos && typeof pkg.beeos.distTag === "string") {
4112
4945
  return pkg.beeos.distTag;
@@ -4131,8 +4964,8 @@ function readVersionFromModuleUrl(moduleUrl) {
4131
4964
  for (let i = 0; i < 6; i++) {
4132
4965
  const candidate = path7.join(here, "package.json");
4133
4966
  try {
4134
- if (fs6.existsSync(candidate)) {
4135
- const raw = fs6.readFileSync(candidate, "utf-8");
4967
+ if (fs7.existsSync(candidate)) {
4968
+ const raw = fs7.readFileSync(candidate, "utf-8");
4136
4969
  const pkg = JSON.parse(raw);
4137
4970
  if (typeof pkg.version === "string" && (pkg.name === "@beeos-ai/cli" || pkg.name === "beeos")) {
4138
4971
  return pkg.version;
@@ -4162,17 +4995,21 @@ var init_dist = __esm({
4162
4995
  init_types();
4163
4996
  init_paths();
4164
4997
  init_toml();
4998
+ init_spawn_env();
4165
4999
  init_binding();
4166
5000
  init_gateway_token();
5001
+ init_state();
4167
5002
  init_keypair();
4168
5003
  init_client();
4169
5004
  init_orchestrator();
4170
5005
  init_process();
5006
+ init_errors();
4171
5007
  init_device_setup();
4172
5008
  init_adb_setup();
4173
5009
  init_device();
4174
5010
  init_scrcpy_bridge();
4175
5011
  init_vnc_bridge();
5012
+ init_spawn_env2();
4176
5013
  init_agent_config();
4177
5014
  init_driver();
4178
5015
  init_detector();
@@ -4187,7 +5024,9 @@ var init_dist = __esm({
4187
5024
  init_factory();
4188
5025
  init_ids();
4189
5026
  init_migrate();
5027
+ init_tail_logs();
4190
5028
  init_cli_version();
5029
+ init_upgrade();
4191
5030
  }
4192
5031
  });
4193
5032
 
@@ -4266,6 +5105,34 @@ async function notifyFleetReloadBestEffort(baseUrl = FLEET_STATUS_BASE_URL) {
4266
5105
  return "unknown";
4267
5106
  }
4268
5107
  }
5108
+ async function notifyFleetRestartDeviceBestEffort(serial, baseUrl = FLEET_STATUS_BASE_URL) {
5109
+ try {
5110
+ const res = await fetch(
5111
+ `${baseUrl}/fleet/devices/${encodeURIComponent(serial)}/restart`,
5112
+ {
5113
+ method: "POST",
5114
+ signal: AbortSignal.timeout(FLEET_NOTIFY_TIMEOUT_MS)
5115
+ }
5116
+ );
5117
+ return res.ok ? "ok" : "unknown";
5118
+ } catch (e) {
5119
+ if (isConnectionRefused(e)) return "not_running";
5120
+ return "unknown";
5121
+ }
5122
+ }
5123
+ async function notifyFleetShutdownBestEffort(baseUrl = FLEET_STATUS_BASE_URL) {
5124
+ try {
5125
+ const res = await fetch(`${baseUrl}/fleet/shutdown`, {
5126
+ method: "POST",
5127
+ signal: AbortSignal.timeout(FLEET_SHUTDOWN_TIMEOUT_MS)
5128
+ });
5129
+ if (res.status === 501) return "unknown";
5130
+ return res.ok ? "ok" : "unknown";
5131
+ } catch (e) {
5132
+ if (isConnectionRefused(e)) return "not_running";
5133
+ return "unknown";
5134
+ }
5135
+ }
4269
5136
  function isConnectionRefused(e) {
4270
5137
  const code = extractErrorCode(e);
4271
5138
  return code === "ECONNREFUSED" || code === "ECONNRESET";
@@ -4290,12 +5157,13 @@ function printFleetNotRunningHint() {
4290
5157
  console.log(" device-agent fleet start # foreground (debug-friendly)");
4291
5158
  console.log("");
4292
5159
  }
4293
- var FLEET_STATUS_BASE_URL, FLEET_NOTIFY_TIMEOUT_MS;
5160
+ var FLEET_STATUS_BASE_URL, FLEET_NOTIFY_TIMEOUT_MS, FLEET_SHUTDOWN_TIMEOUT_MS;
4294
5161
  var init_fleet_notify = __esm({
4295
5162
  "src/commands/device/fleet-notify.ts"() {
4296
5163
  "use strict";
4297
5164
  FLEET_STATUS_BASE_URL = "http://127.0.0.1:7950";
4298
5165
  FLEET_NOTIFY_TIMEOUT_MS = 1e3;
5166
+ FLEET_SHUTDOWN_TIMEOUT_MS = 5e3;
4299
5167
  }
4300
5168
  });
4301
5169
 
@@ -4355,7 +5223,7 @@ async function removeTargetsForSerial(mgr, serial) {
4355
5223
  } catch {
4356
5224
  }
4357
5225
  }
4358
- var init_state = __esm({
5226
+ var init_state2 = __esm({
4359
5227
  "src/commands/device/state.ts"() {
4360
5228
  "use strict";
4361
5229
  init_dist();
@@ -4365,6 +5233,37 @@ var init_state = __esm({
4365
5233
  // src/commands/device/attach.ts
4366
5234
  import { spawn as spawn2 } from "child_process";
4367
5235
  import readline from "readline";
5236
+ function resolvePerDeviceAgentGatewayUrl(cfg, override) {
5237
+ if (override === void 0) return resolveAgentGatewayUrl(cfg);
5238
+ const trimmed = override.trim();
5239
+ if (trimmed === "") {
5240
+ throw new BeeosError({
5241
+ code: "invalid_argument",
5242
+ message: "--agent-gateway-url cannot be empty.",
5243
+ hint: "Pass an http(s) URL, e.g. --agent-gateway-url https://agent-gw.staging.beeos.ai"
5244
+ });
5245
+ }
5246
+ let parsed;
5247
+ try {
5248
+ parsed = new URL(trimmed);
5249
+ } catch {
5250
+ throw new BeeosError({
5251
+ code: "invalid_argument",
5252
+ message: `--agent-gateway-url is not a valid URL: ${trimmed}`,
5253
+ hint: "Pass an http(s) URL, e.g. --agent-gateway-url https://agent-gw.staging.beeos.ai",
5254
+ details: { value: trimmed }
5255
+ });
5256
+ }
5257
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
5258
+ throw new BeeosError({
5259
+ code: "invalid_argument",
5260
+ message: `--agent-gateway-url must use http(s); got ${parsed.protocol}`,
5261
+ hint: "Pass an http(s) URL, e.g. --agent-gateway-url https://agent-gw.staging.beeos.ai",
5262
+ details: { value: trimmed, protocol: parsed.protocol }
5263
+ });
5264
+ }
5265
+ return trimmed;
5266
+ }
4368
5267
  async function attach(options) {
4369
5268
  const cfg = await loadOrCreateConfig();
4370
5269
  const reporter = new CliReporter();
@@ -4388,17 +5287,19 @@ async function attach(options) {
4388
5287
  return;
4389
5288
  }
4390
5289
  const mgr = await getServiceManager();
5290
+ let prevInstanceId = null;
4391
5291
  await withDeviceLock(async () => {
4392
5292
  const state = await loadDeviceState();
4393
5293
  const existingIdx = state.devices.findIndex((d) => d.serial === serial);
4394
5294
  if (existingIdx >= 0) {
5295
+ prevInstanceId = state.devices[existingIdx].instance_id ?? null;
4395
5296
  await removeTargetsForSerial(mgr, serial);
4396
5297
  state.devices.splice(existingIdx, 1);
4397
5298
  }
4398
5299
  const httpPort = nextHttpPort(state, cfg.device.http_port);
4399
5300
  const keyFile = deviceRuntime.deviceKeyPath(serial);
4400
- const agentGatewayUrl = resolveAgentGatewayUrl(cfg);
4401
- await persistAgentConfigBestEffort({
5301
+ const agentGatewayUrl = resolvePerDeviceAgentGatewayUrl(cfg, options.agentGatewayUrl);
5302
+ const persistedAgentConfig = await persistAgentConfigBestEffort({
4402
5303
  agentGatewayUrl,
4403
5304
  keyFile,
4404
5305
  instanceId,
@@ -4413,8 +5314,12 @@ async function attach(options) {
4413
5314
  withVideo,
4414
5315
  vncHost: options.vncHost,
4415
5316
  vncPort: options.vncPort,
4416
- vncPassword: options.vncPassword
5317
+ vncPassword: options.vncPassword,
5318
+ agentGatewayUrl
4417
5319
  });
5320
+ const fp = fingerprintFromB64(pubkeyB64);
5321
+ const boundAt = Math.floor(Date.now() / 1e3);
5322
+ const backend = options.vncHost ? void 0 : "adb";
4418
5323
  state.devices.push({
4419
5324
  serial,
4420
5325
  name,
@@ -4428,11 +5333,37 @@ async function attach(options) {
4428
5333
  // path through fleet → mcp). When self-attach for
4429
5334
  // macOS/Linux/Windows lands, this field will be set
4430
5335
  // accordingly (`macos`/`linux`/`windows`).
4431
- backend: options.vncHost ? void 0 : "adb",
5336
+ backend,
4432
5337
  vnc_host: options.vncHost,
4433
- vnc_port: options.vncPort
5338
+ vnc_port: options.vncPort,
5339
+ // P0-D: pin the resolved Agent Gateway URL on the entry so
5340
+ // the fleet supervisor reuses *this* URL on every restart
5341
+ // instead of re-resolving from the (possibly drifting) config
5342
+ // — and so multi-region staging users can attach devices to
5343
+ // different gateways without splitting fleets.
5344
+ agent_gateway_url: agentGatewayUrl
4434
5345
  });
4435
5346
  await saveDeviceState(state);
5347
+ const stateEntry = {
5348
+ kind: "device",
5349
+ instance_id: instanceId,
5350
+ bound_at: boundAt,
5351
+ fingerprint: fp,
5352
+ agent_gateway_url: agentGatewayUrl,
5353
+ serial,
5354
+ name,
5355
+ key_file: keyFile,
5356
+ http_port: httpPort,
5357
+ video_mode: bridgeInfo.mode,
5358
+ ...backend ? { backend } : {},
5359
+ ...options.vncHost ? { vnc_host: options.vncHost } : {},
5360
+ ...options.vncPort ? { vnc_port: options.vncPort } : {},
5361
+ ...persistedAgentConfig ? { agent_config: persistedAgentConfig.payload } : {}
5362
+ };
5363
+ await mutateStateBestEffort(
5364
+ (s) => upsertDeviceAgent(s, withCarriedForwardAgentConfig(s, stateEntry)),
5365
+ { authority: "devices.json", subject: serial }
5366
+ );
4436
5367
  console.log(`Attached device ${serial} (${name}) \u2014 instance ${instanceId}`);
4437
5368
  console.log(` local http: :${httpPort} (if enabled)`);
4438
5369
  console.log(` logs: device-agent fleet logs ${serial}`);
@@ -4448,6 +5379,14 @@ async function attach(options) {
4448
5379
  throw e;
4449
5380
  }
4450
5381
  });
5382
+ const isRebind = prevInstanceId !== null && prevInstanceId !== instanceId;
5383
+ if (isRebind) {
5384
+ const outcome = await notifyFleetRestartDeviceBestEffort(serial);
5385
+ if (outcome === "not_running") {
5386
+ await maybeNotifyFleetWithHint(cfg);
5387
+ }
5388
+ return;
5389
+ }
4451
5390
  await maybeNotifyFleetWithHint(cfg);
4452
5391
  }
4453
5392
  async function attachAll(cfg, reporter, withVideo, options) {
@@ -4459,9 +5398,13 @@ async function attachAll(cfg, reporter, withVideo, options) {
4459
5398
  console.log(`Discovered ${devices.length} device(s) via adb.`);
4460
5399
  await deviceRuntime.ensureAgent(reporter);
4461
5400
  reporter.stop();
4462
- const agentGatewayUrl = resolveAgentGatewayUrl(cfg);
5401
+ const agentGatewayUrl = resolvePerDeviceAgentGatewayUrl(cfg, options.agentGatewayUrl);
4463
5402
  const mgr = await getServiceManager();
4464
5403
  const preState = await loadDeviceState();
5404
+ const prevInstanceIdBySerial = /* @__PURE__ */ new Map();
5405
+ for (const d of preState.devices) {
5406
+ if (d.instance_id) prevInstanceIdBySerial.set(d.serial, d.instance_id);
5407
+ }
4465
5408
  const simulatedEntries = preState.devices.map((d) => ({ ...d }));
4466
5409
  function reserveHttpPort(serial) {
4467
5410
  const port = nextHttpPort({ devices: simulatedEntries }, cfg.device.http_port);
@@ -4488,7 +5431,8 @@ async function attachAll(cfg, reporter, withVideo, options) {
4488
5431
  serial: device.serial,
4489
5432
  instanceId,
4490
5433
  keyFile: deviceRuntime.deviceKeyPath(device.serial),
4491
- httpPort
5434
+ httpPort,
5435
+ fingerprint: fingerprintFromB64(pubkeyB64)
4492
5436
  });
4493
5437
  } catch (e) {
4494
5438
  console.error(` Failed to bind ${device.serial}: ${e}`);
@@ -4511,18 +5455,62 @@ async function attachAll(cfg, reporter, withVideo, options) {
4511
5455
  })
4512
5456
  )
4513
5457
  );
5458
+ const reboundSerials = [];
4514
5459
  await withDeviceLock(async () => {
4515
5460
  const state = await loadDeviceState();
4516
5461
  const { committed, failures } = commitAttachResults(state, commits);
4517
5462
  for (const f of failures) console.error(`Failed to attach ${f.serial}: ${f.error}`);
5463
+ for (const c of committed) {
5464
+ const prev = prevInstanceIdBySerial.get(c.serial);
5465
+ if (prev && prev !== c.entry.instance_id) reboundSerials.push(c.serial);
5466
+ }
4518
5467
  for (const c of committed) {
4519
5468
  console.log(
4520
5469
  `Attached device ${c.serial} \u2014 instance ${c.entry.instance_id}` + (c.videoMode !== "none" ? ` (video: ${c.videoMode})` : "")
4521
5470
  );
4522
5471
  }
4523
5472
  await saveDeviceState(state);
5473
+ for (const c of committed) {
5474
+ const e = c.entry;
5475
+ const mirror = commits.find(
5476
+ (x) => x.ok && x.serial === c.serial
5477
+ ).stateMirror;
5478
+ const stateEntry = {
5479
+ kind: "device",
5480
+ instance_id: e.instance_id ?? "",
5481
+ bound_at: mirror.boundAt,
5482
+ fingerprint: mirror.fingerprint,
5483
+ agent_gateway_url: e.agent_gateway_url && e.agent_gateway_url !== "" ? e.agent_gateway_url : mirror.agentGatewayUrl,
5484
+ serial: e.serial,
5485
+ name: e.name,
5486
+ key_file: e.key_file,
5487
+ http_port: e.http_port,
5488
+ ...e.video_mode ? { video_mode: e.video_mode } : {},
5489
+ ...e.backend ? { backend: e.backend } : {},
5490
+ ...e.vnc_host ? { vnc_host: e.vnc_host } : {},
5491
+ ...e.vnc_port ? { vnc_port: e.vnc_port } : {},
5492
+ ...mirror.agentConfig ? { agent_config: mirror.agentConfig } : {}
5493
+ };
5494
+ await mutateStateBestEffort(
5495
+ (s) => upsertDeviceAgent(s, withCarriedForwardAgentConfig(s, stateEntry)),
5496
+ { authority: "devices.json", subject: e.serial }
5497
+ );
5498
+ }
4524
5499
  console.log(`Attached ${committed.length} device(s); supervision via device-agent fleet.`);
4525
5500
  });
5501
+ if (reboundSerials.length > 0) {
5502
+ let anyNotRunning = false;
5503
+ for (const serial of reboundSerials) {
5504
+ const outcome = await notifyFleetRestartDeviceBestEffort(serial);
5505
+ if (outcome === "not_running") anyNotRunning = true;
5506
+ }
5507
+ if (anyNotRunning) {
5508
+ await maybeNotifyFleetWithHint(cfg);
5509
+ } else if (reboundSerials.length < commits.filter((c) => c.ok).length) {
5510
+ await maybeNotifyFleetWithHint(cfg);
5511
+ }
5512
+ return;
5513
+ }
4526
5514
  await maybeNotifyFleetWithHint(cfg);
4527
5515
  }
4528
5516
  function commitAttachResults(state, commits) {
@@ -4542,10 +5530,10 @@ function commitAttachResults(state, commits) {
4542
5530
  }
4543
5531
  async function runAttachStage2(params) {
4544
5532
  const { bind, cfg, mgr, reporter, agentGatewayUrl, withVideo, options } = params;
4545
- const { serial, instanceId, keyFile, httpPort } = bind;
5533
+ const { serial, instanceId, keyFile, httpPort, fingerprint: fp } = bind;
4546
5534
  try {
4547
5535
  await removeTargetsForSerial(mgr, serial);
4548
- await persistAgentConfigBestEffort({
5536
+ const persistedAgentConfig = await persistAgentConfigBestEffort({
4549
5537
  agentGatewayUrl,
4550
5538
  keyFile,
4551
5539
  instanceId,
@@ -4559,7 +5547,8 @@ async function runAttachStage2(params) {
4559
5547
  withVideo,
4560
5548
  vncHost: options.vncHost,
4561
5549
  vncPort: options.vncPort,
4562
- vncPassword: options.vncPassword
5550
+ vncPassword: options.vncPassword,
5551
+ agentGatewayUrl
4563
5552
  });
4564
5553
  const entry = {
4565
5554
  serial,
@@ -4570,9 +5559,25 @@ async function runAttachStage2(params) {
4570
5559
  video_mode: bridgeInfo.mode,
4571
5560
  backend: options.vncHost ? void 0 : "adb",
4572
5561
  vnc_host: options.vncHost,
4573
- vnc_port: options.vncPort
5562
+ vnc_port: options.vncPort,
5563
+ // P0-D: see attach() above — every new entry self-pins to the
5564
+ // resolved Agent Gateway URL so the fleet supervisor (and any
5565
+ // future per-device override flows) honour the binding's
5566
+ // origin region.
5567
+ agent_gateway_url: agentGatewayUrl
5568
+ };
5569
+ return {
5570
+ ok: true,
5571
+ serial,
5572
+ entry,
5573
+ videoMode: bridgeInfo.mode,
5574
+ stateMirror: {
5575
+ agentGatewayUrl,
5576
+ fingerprint: fp,
5577
+ boundAt: Math.floor(Date.now() / 1e3),
5578
+ agentConfig: persistedAgentConfig?.payload ?? null
5579
+ }
4574
5580
  };
4575
- return { ok: true, serial, entry, videoMode: bridgeInfo.mode };
4576
5581
  } catch (e) {
4577
5582
  await rollbackDeviceBind(cfg.platform.api_url, instanceId);
4578
5583
  return { ok: false, serial, error: e };
@@ -4684,7 +5689,7 @@ async function runDeviceAgentFleetEnable(cfg) {
4684
5689
  }
4685
5690
  async function registerVideoBridge(_mgr, reporter, params) {
4686
5691
  if (!params.withVideo) return { mode: "none" };
4687
- const agentGatewayUrl = resolveAgentGatewayUrl(params.cfg);
5692
+ const agentGatewayUrl = params.agentGatewayUrl ?? resolveAgentGatewayUrl(params.cfg);
4688
5693
  if (params.vncHost) {
4689
5694
  const binary2 = await ensureBridgeBinaryDegraded(
4690
5695
  vncBridgeRuntime.ensureInstalled.bind(vncBridgeRuntime),
@@ -4737,23 +5742,80 @@ async function ensureBridgeBinaryDegraded(fn, reporter, label) {
4737
5742
  return null;
4738
5743
  }
4739
5744
  }
5745
+ function withCarriedForwardAgentConfig(state, entry) {
5746
+ if (entry.agent_config) return entry;
5747
+ const previous = state.agents.find(
5748
+ (a) => a.kind === "device" && a.serial === entry.serial
5749
+ );
5750
+ if (!previous?.agent_config) return entry;
5751
+ return { ...entry, agent_config: previous.agent_config };
5752
+ }
4740
5753
  async function ensureAdbAvailable(reporter) {
4741
5754
  const existing = await findAdb();
4742
5755
  if (existing) return;
4743
5756
  if (process.env.BEEOS_SKIP_ADB_INSTALL === "1") {
4744
- throw new Error(
4745
- "adb not found on PATH and BEEOS_SKIP_ADB_INSTALL=1 is set.\nInstall Android platform-tools manually and re-run, or unset the env var."
4746
- );
5757
+ throw new BeeosError({
5758
+ code: "adb_install_skipped",
5759
+ message: "adb not found on PATH and BEEOS_SKIP_ADB_INSTALL=1 is set.",
5760
+ hint: "Install Android platform-tools manually and re-run, or unset BEEOS_SKIP_ADB_INSTALL."
5761
+ });
5762
+ }
5763
+ const decision = detectAdbLicenseDecision();
5764
+ if (decision === "rejected") {
5765
+ throw new BeeosError({
5766
+ code: "adb_install_rejected",
5767
+ message: "BEEOS_REJECT_ADB_LICENSE=1 is set \u2014 adb auto-install refused.",
5768
+ hint: `Pre-install Android platform-tools and point $BEEOS_ADB_BIN at it, or unset BEEOS_REJECT_ADB_LICENSE and review the license at ${ADB_LICENSE_URL}.`,
5769
+ details: { licenseUrl: ADB_LICENSE_URL }
5770
+ });
5771
+ }
5772
+ if (decision === "unknown") {
5773
+ const proceed = await confirmAdbLicensePrompt();
5774
+ if (!proceed) {
5775
+ throw new BeeosError({
5776
+ code: "adb_install_declined",
5777
+ message: "adb auto-install declined at the license prompt.",
5778
+ hint: "Pre-install Android platform-tools manually and re-run, or set BEEOS_ACCEPT_ADB_LICENSE=1 in your environment."
5779
+ });
5780
+ }
4747
5781
  }
4748
5782
  console.log("adb not found \u2014 downloading Android platform-tools (one-time, ~10MB)...");
4749
5783
  try {
4750
5784
  await ensureAdb(reporter, { autoInstall: true });
4751
5785
  } catch (e) {
4752
- throw new Error(
4753
- "Failed to install adb automatically: " + String(e) + "\nWorkarounds:\n \u2022 macOS: brew install android-platform-tools\n \u2022 Linux: apt install android-tools-adb (or equivalent)\n \u2022 Windows: install Android SDK Platform-Tools from https://developer.android.com/tools/releases/platform-tools\n \u2022 Or point $BEEOS_ADB_BIN at an existing adb binary.",
4754
- { cause: e }
5786
+ throw new BeeosError({
5787
+ code: "adb_install_failed",
5788
+ message: `Failed to install adb automatically: ${String(e)}`,
5789
+ hint: "Workarounds:\n \u2022 macOS: brew install android-platform-tools\n \u2022 Linux: apt install android-tools-adb (or equivalent)\n \u2022 Windows: install Android SDK Platform-Tools from https://developer.android.com/tools/releases/platform-tools\n \u2022 Or point $BEEOS_ADB_BIN at an existing adb binary.",
5790
+ cause: e
5791
+ });
5792
+ }
5793
+ }
5794
+ async function confirmAdbLicensePrompt() {
5795
+ const isTty = Boolean(process.stdout.isTTY);
5796
+ if (!isTty) {
5797
+ console.log(
5798
+ ` Android platform-tools license: ${ADB_LICENSE_URL}
5799
+ Non-interactive shell \u2014 auto-accepting. Set BEEOS_ACCEPT_ADB_LICENSE=1
5800
+ in your environment to silence this banner.`
4755
5801
  );
5802
+ return true;
4756
5803
  }
5804
+ console.log(
5805
+ `
5806
+ Android SDK Platform-Tools (adb) is licensed under the Android SDK License Agreement:
5807
+ ${ADB_LICENSE_URL}
5808
+ Set BEEOS_ACCEPT_ADB_LICENSE=1 in your environment to skip this prompt next time.
5809
+ `
5810
+ );
5811
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5812
+ const ans = await new Promise((resolve) => {
5813
+ rl.question("Agree to the Android SDK license and download platform-tools? [Y/n]: ", (a) => {
5814
+ rl.close();
5815
+ resolve(a);
5816
+ });
5817
+ });
5818
+ return /^(y(es)?|)$/i.test(ans.trim());
4757
5819
  }
4758
5820
  var init_attach = __esm({
4759
5821
  "src/commands/device/attach.ts"() {
@@ -4762,7 +5824,7 @@ var init_attach = __esm({
4762
5824
  init_progress2();
4763
5825
  init_fallback_banner();
4764
5826
  init_fleet_notify();
4765
- init_state();
5827
+ init_state2();
4766
5828
  }
4767
5829
  });
4768
5830
 
@@ -4814,7 +5876,7 @@ var init_detach = __esm({
4814
5876
  "use strict";
4815
5877
  init_dist();
4816
5878
  init_fleet_notify();
4817
- init_state();
5879
+ init_state2();
4818
5880
  }
4819
5881
  });
4820
5882
 
@@ -4856,7 +5918,7 @@ var init_list = __esm({
4856
5918
  "src/commands/device/list.ts"() {
4857
5919
  "use strict";
4858
5920
  init_dist();
4859
- init_state();
5921
+ init_state2();
4860
5922
  }
4861
5923
  });
4862
5924
 
@@ -4892,14 +5954,49 @@ var init_exec = __esm({
4892
5954
  "src/commands/device/exec.ts"() {
4893
5955
  "use strict";
4894
5956
  init_dist();
4895
- init_state();
5957
+ init_state2();
4896
5958
  }
4897
5959
  });
4898
5960
 
4899
5961
  // src/commands/device/upgrade.ts
4900
5962
  async function upgrade(options = {}) {
4901
5963
  const reporter = new CliReporter();
4902
- await deviceRuntime.upgradeAgent(reporter);
5964
+ const outcome = await upgradeBeeosSuite({
5965
+ packages: [NPM_PKGS.DEVICE_AGENT, NPM_PKGS.DEVICE_MCP_SERVER],
5966
+ progress: reporter
5967
+ });
5968
+ reporter.stop();
5969
+ if (outcome.anyFailed) {
5970
+ const failed = outcome.packages.find((p) => p.failed);
5971
+ console.error(
5972
+ `device-agent upgrade had errors: ${failed?.failed ?? "unknown"}
5973
+ Manual fix:
5974
+ npm i -g @beeos-ai/device-agent @beeos-ai/device-mcp-server`
5975
+ );
5976
+ }
5977
+ for (const pkg of outcome.packages) {
5978
+ if (pkg.changed) {
5979
+ console.log(` ${pkg.pkg}: ${pkg.before ?? "(none)"} \u2192 ${pkg.after}`);
5980
+ } else if (!pkg.failed) {
5981
+ console.log(` ${pkg.pkg}: ${pkg.after ?? "(absent)"} (already current)`);
5982
+ }
5983
+ }
5984
+ if (outcome.anyChanged) {
5985
+ const reloadOutcome = await notifyFleetShutdownBestEffort();
5986
+ switch (reloadOutcome) {
5987
+ case "ok":
5988
+ console.log(" Fleet asked to restart \u2014 launchd will bring it up with the new code.");
5989
+ break;
5990
+ case "not_running":
5991
+ console.log(" Fleet is not running \u2014 nothing to restart.");
5992
+ break;
5993
+ case "unknown":
5994
+ console.log(
5995
+ " Fleet restart could not be confirmed; if you see stale behaviour run\n launchctl kickstart -k gui/$(id -u)/ai.beeos.device-fleet # macOS\n or stop & start the fleet manually."
5996
+ );
5997
+ break;
5998
+ }
5999
+ }
4903
6000
  if (options.bridges !== false) {
4904
6001
  try {
4905
6002
  await scrcpyBridgeRuntime.upgrade(reporter);
@@ -4913,11 +6010,12 @@ async function upgrade(options = {}) {
4913
6010
  }
4914
6011
  }
4915
6012
  }
4916
- var init_upgrade = __esm({
6013
+ var init_upgrade2 = __esm({
4917
6014
  "src/commands/device/upgrade.ts"() {
4918
6015
  "use strict";
4919
6016
  init_dist();
4920
6017
  init_progress2();
6018
+ init_fleet_notify();
4921
6019
  }
4922
6020
  });
4923
6021
 
@@ -4931,7 +6029,6 @@ async function refreshConfig(options = {}) {
4931
6029
  console.log("No matching devices in ~/.beeos/devices.json \u2014 nothing to refresh.");
4932
6030
  return;
4933
6031
  }
4934
- const mgr = await getServiceManager();
4935
6032
  let okCount = 0;
4936
6033
  let failCount = 0;
4937
6034
  for (const entry of targets) {
@@ -4949,27 +6046,44 @@ async function refreshConfig(options = {}) {
4949
6046
  failCount++;
4950
6047
  continue;
4951
6048
  }
4952
- const file = await persistAgentConfigBestEffort({
6049
+ const result = await persistAgentConfigBestEffort({
4953
6050
  agentGatewayUrl,
4954
6051
  keyFile: entry.key_file,
4955
6052
  instanceId: entry.instance_id,
4956
6053
  reporter
4957
6054
  });
4958
- if (!file) {
6055
+ if (!result) {
4959
6056
  console.error(
4960
6057
  ` Refresh failed for ${entry.serial} (see warning above) \u2014 config file left untouched.`
4961
6058
  );
4962
6059
  failCount++;
4963
6060
  continue;
4964
6061
  }
4965
- console.log(` Wrote ${file}`);
4966
- try {
4967
- await mgr.restart(deviceAgentTargetId(entry.serial));
6062
+ console.log(` Wrote ${result.filePath}`);
6063
+ const instanceId = entry.instance_id;
6064
+ const payload = result.payload;
6065
+ await mutateStateBestEffort(
6066
+ (state) => {
6067
+ const idx = state.agents.findIndex(
6068
+ (a) => a.kind === "device" && a.instance_id === instanceId
6069
+ );
6070
+ if (idx < 0) return null;
6071
+ const next = { ...state.agents[idx], agent_config: payload };
6072
+ const agents = state.agents.slice();
6073
+ agents[idx] = next;
6074
+ return { ...state, agents };
6075
+ },
6076
+ { authority: "Per-instance config.json", subject: instanceId }
6077
+ );
6078
+ const outcome = await notifyFleetRestartDeviceBestEffort(entry.serial);
6079
+ if (outcome === "ok") {
4968
6080
  console.log(` Restarted device-agent for ${entry.serial}`);
4969
- } catch (e) {
4970
- console.error(
4971
- ` Service restart skipped for ${entry.serial}: ${e.message}`
6081
+ } else if (outcome === "not_running") {
6082
+ console.log(
6083
+ ` fleet not running for ${entry.serial} \u2014 config will load on next start`
4972
6084
  );
6085
+ } else {
6086
+ console.error(` fleet restart for ${entry.serial} returned an error`);
4973
6087
  }
4974
6088
  okCount++;
4975
6089
  }
@@ -5000,7 +6114,8 @@ var init_refresh_config = __esm({
5000
6114
  "use strict";
5001
6115
  init_dist();
5002
6116
  init_progress2();
5003
- init_state();
6117
+ init_fleet_notify();
6118
+ init_state2();
5004
6119
  }
5005
6120
  });
5006
6121
 
@@ -5022,9 +6137,9 @@ var init_device2 = __esm({
5022
6137
  init_detach();
5023
6138
  init_list();
5024
6139
  init_exec();
5025
- init_upgrade();
6140
+ init_upgrade2();
5026
6141
  init_refresh_config();
5027
- init_state();
6142
+ init_state2();
5028
6143
  }
5029
6144
  });
5030
6145
 
@@ -5037,7 +6152,7 @@ import {
5037
6152
  execFile as execFile6,
5038
6153
  spawn as nodeSpawn
5039
6154
  } from "child_process";
5040
- import fs7 from "fs";
6155
+ import fs8 from "fs";
5041
6156
  import fsp from "fs/promises";
5042
6157
  import net from "net";
5043
6158
  import os5 from "os";
@@ -5144,11 +6259,11 @@ var NodePlatformAdapter = class {
5144
6259
  let stderrFd;
5145
6260
  if (options?.stdoutFile) {
5146
6261
  await fsp.mkdir(path8.dirname(options.stdoutFile), { recursive: true });
5147
- stdoutFd = fs7.openSync(options.stdoutFile, "a");
6262
+ stdoutFd = fs8.openSync(options.stdoutFile, "a");
5148
6263
  }
5149
6264
  if (options?.stderrFile) {
5150
6265
  await fsp.mkdir(path8.dirname(options.stderrFile), { recursive: true });
5151
- stderrFd = options.stderrFile === options.stdoutFile ? stdoutFd : fs7.openSync(options.stderrFile, "a");
6266
+ stderrFd = options.stderrFile === options.stdoutFile ? stdoutFd : fs8.openSync(options.stderrFile, "a");
5152
6267
  }
5153
6268
  const child = nodeSpawn(cmd, args, {
5154
6269
  cwd: options?.cwd,
@@ -5165,8 +6280,8 @@ var NodePlatformAdapter = class {
5165
6280
  if (options?.detached) {
5166
6281
  child.unref();
5167
6282
  }
5168
- if (stdoutFd != null) fs7.closeSync(stdoutFd);
5169
- if (stderrFd != null && stderrFd !== stdoutFd) fs7.closeSync(stderrFd);
6283
+ if (stdoutFd != null) fs8.closeSync(stdoutFd);
6284
+ if (stderrFd != null && stderrFd !== stdoutFd) fs8.closeSync(stderrFd);
5170
6285
  const pid = child.pid;
5171
6286
  if (pid == null) {
5172
6287
  throw new Error(`Failed to spawn process: ${cmd} ${args.join(" ")}`);
@@ -5248,6 +6363,17 @@ init_dist();
5248
6363
  init_progress2();
5249
6364
  init_fallback_banner();
5250
6365
  import os6 from "os";
6366
+
6367
+ // src/json-envelope.ts
6368
+ init_dist();
6369
+ function jsonOk(data) {
6370
+ return { ok: true, data };
6371
+ }
6372
+ function emitJsonEnvelope(env) {
6373
+ console.log(JSON.stringify(env, null, 2));
6374
+ }
6375
+
6376
+ // src/commands/start.ts
5251
6377
  async function run(agentFramework, options) {
5252
6378
  const p = getPlatformAdapter();
5253
6379
  await ensureDirs();
@@ -5256,9 +6382,12 @@ async function run(agentFramework, options) {
5256
6382
  const descriptor = frameworkById(agentFramework);
5257
6383
  if (!descriptor || descriptor.status !== "available") {
5258
6384
  const avail = availableFrameworks().map((f) => f.id).join(", ");
5259
- throw new Error(
5260
- `Agent framework '${agentFramework}' is not available. Available: ${avail}.`
5261
- );
6385
+ throw new BeeosError({
6386
+ code: "framework_unavailable",
6387
+ message: `Agent framework '${agentFramework}' is not available.`,
6388
+ hint: `Available frameworks: ${avail}.`,
6389
+ details: { requested: agentFramework, available: avail.split(/,\s*/) }
6390
+ });
5262
6391
  }
5263
6392
  const driver = descriptor.driver;
5264
6393
  const identity = await loadOrCreateIdentity();
@@ -5266,12 +6395,17 @@ async function run(agentFramework, options) {
5266
6395
  const pubkey = publicKeyB64(identity);
5267
6396
  const keyFile = p.joinPath(beeoHome(), "identity", "keypair.json");
5268
6397
  const agentGatewayUrl = resolveAgentGatewayUrl(cfg);
5269
- const probe = await identifyGateway({ expectedFingerprint: fp });
5270
- if (!options.force && (probe.state === "foreign" || probe.state === "unknown")) {
5271
- const detail = probe.state === "foreign" ? `a different beeos-claw gateway (fingerprint ${probe.fingerprint.slice(0, 16)}\u2026, plugin ${probe.pluginVersion || "?"})` : `an unrecognised process (${probe.reason})`;
5272
- throw new Error(
5273
- `OpenClaw gateway port is already in use by ${detail}. Refusing to install over it \u2014 this would make both agents fight for the same port and leak your fingerprint to the other process. Run \`beeos doctor\` to diagnose, or re-run with \`--force\` if you are certain (will terminate the existing listener).`
5274
- );
6398
+ if (hasPreflightConflictCheck(driver) && !options.force) {
6399
+ const report = await driver.preflightConflictCheck({ fingerprint: fp });
6400
+ if (report.state === "foreign" || report.state === "unknown") {
6401
+ const detail = report.state === "foreign" ? report.detail : `an unrecognised process (${report.reason})`;
6402
+ throw new BeeosError({
6403
+ code: "gateway_port_conflict",
6404
+ message: `${driver.displayName} port is already in use by ${detail}.`,
6405
+ hint: `Refusing to install over it \u2014 clobbering would make both agents fight for the same port and leak your fingerprint to the foreign process. Run \`beeos doctor\` to diagnose, or re-run with \`--force\` to terminate the existing listener.`,
6406
+ details: { driver: driver.id, state: report.state }
6407
+ });
6408
+ }
5275
6409
  }
5276
6410
  reporter.onStatus(`Preparing ${driver.displayName}`);
5277
6411
  const ctx = {
@@ -5281,29 +6415,11 @@ async function run(agentFramework, options) {
5281
6415
  locationPreference: "auto",
5282
6416
  versionOverride: options.version
5283
6417
  };
5284
- const spec = await driver.launch(ctx, reporter);
6418
+ const launchOutcome = await driver.launch(ctx, reporter);
6419
+ const spec = launchOutcome.spec;
6420
+ const launchWarnings = launchOutcome.warnings;
5285
6421
  reporter.stop();
5286
- const mgr = await getServiceManager();
5287
- maybePrintFallbackBanner(mgr.kind);
5288
- const status2 = await mgr.install(spec);
5289
- if (spec.healthcheck) {
5290
- try {
5291
- await waitForHealthcheck(spec.healthcheck, 45e3);
5292
- } catch (err) {
5293
- if (err instanceof HealthcheckTimeoutError && hasStartupDiagnostics(driver)) {
5294
- const diag = await driver.diagnoseStartup(serviceLogPath(driver.id));
5295
- if (diag) {
5296
- throw new Error(
5297
- `${diag.reason}
5298
- ${diag.hints.map((h) => ` \u2022 ${h}`).join("\n")}`
5299
- );
5300
- }
5301
- }
5302
- throw err;
5303
- }
5304
- }
5305
6422
  const hostname = buildHostname();
5306
- const gatewayPid = status2.pid ?? void 0;
5307
6423
  const cachedBinding = await loadBindingInfo();
5308
6424
  const outcome = await bindAgent({
5309
6425
  apiUrl: cfg.platform.api_url,
@@ -5318,52 +6434,94 @@ ${diag.hints.map((h) => ` \u2022 ${h}`).join("\n")}`
5318
6434
  instance_id: cachedBinding.instance_id
5319
6435
  } : null
5320
6436
  });
5321
- if (outcome.status === "bound_offline") {
5322
- emit(options.json, {
5323
- status: "bound_offline",
5324
- public_key: pubkey,
5325
- fingerprint: fp,
5326
- gateway_pid: gatewayPid,
5327
- agent_gateway_url: agentGatewayUrl,
5328
- instance_id: outcome.instanceId
5329
- }, `Agent running (offline, cached instance: ${outcome.instanceId})`);
5330
- return;
5331
- }
5332
6437
  if (outcome.status === "pending_expired") {
5333
- throw new Error(
5334
- "Bind approval timed out \u2014 please re-run `beeos start` after approving in the dashboard."
5335
- );
6438
+ throw new BeeosError({
6439
+ code: "bind_pending_expired",
6440
+ message: "Bind approval timed out before the dashboard confirmed it.",
6441
+ hint: "Re-run `beeos start` and approve the new request in the dashboard.",
6442
+ details: { framework: agentFramework }
6443
+ });
5336
6444
  }
6445
+ const isOffline = outcome.status === "bound_offline";
5337
6446
  const boundInstanceId = outcome.instanceId;
5338
- try {
5339
- await saveBindingInfo({
5340
- fingerprint: fp,
5341
- instance_id: boundInstanceId,
5342
- bound_at: Math.floor(Date.now() / 1e3),
5343
- framework: agentFramework
5344
- });
5345
- emit(options.json, {
5346
- status: "bound",
5347
- public_key: pubkey,
5348
- fingerprint: fp,
5349
- gateway_pid: gatewayPid,
5350
- agent_gateway_url: agentGatewayUrl,
5351
- instance_id: boundInstanceId
5352
- }, `Agent bound to instance: ${boundInstanceId}`);
5353
- } catch (e) {
6447
+ if (!isOffline) {
5354
6448
  try {
5355
- await deleteInstance(cfg.platform.api_url, boundInstanceId);
5356
- } catch {
6449
+ const boundAt = Math.floor(Date.now() / 1e3);
6450
+ await saveBindingInfo({
6451
+ fingerprint: fp,
6452
+ instance_id: boundInstanceId,
6453
+ bound_at: boundAt,
6454
+ framework: agentFramework
6455
+ });
6456
+ const entry = {
6457
+ kind: "openclaw",
6458
+ framework: agentFramework,
6459
+ instance_id: boundInstanceId,
6460
+ bound_at: boundAt,
6461
+ fingerprint: fp,
6462
+ agent_gateway_url: agentGatewayUrl
6463
+ };
6464
+ await mutateStateBestEffort(
6465
+ (state) => upsertOpenClawAgent(state, entry),
6466
+ { authority: "binding.json" }
6467
+ );
6468
+ } catch (e) {
6469
+ try {
6470
+ await deleteInstance(cfg.platform.api_url, boundInstanceId);
6471
+ } catch {
6472
+ }
6473
+ try {
6474
+ await removeBindingInfo();
6475
+ } catch {
6476
+ }
6477
+ throw new BeeosError({
6478
+ code: "bind_post_bind_failed",
6479
+ message: `Agent bound to instance ${boundInstanceId} but post-bind step failed.`,
6480
+ hint: `Best-effort cleanup attempted; if the instance is still visible in the dashboard, delete it manually. Underlying error: ${e instanceof Error ? e.message : String(e)}`,
6481
+ cause: e,
6482
+ details: { instance_id: boundInstanceId }
6483
+ });
5357
6484
  }
6485
+ }
6486
+ const isRebind = !isOffline && cachedBinding !== null && cachedBinding.instance_id !== boundInstanceId;
6487
+ const mgr = await getServiceManager();
6488
+ maybePrintFallbackBanner(mgr.kind);
6489
+ let status2 = await mgr.install(spec);
6490
+ if (isRebind) {
6491
+ await mgr.restart(spec.id);
6492
+ const refreshed = await mgr.status(spec.id);
6493
+ if (refreshed) status2 = refreshed;
6494
+ }
6495
+ if (spec.healthcheck) {
5358
6496
  try {
5359
- await removeBindingInfo();
5360
- } catch {
6497
+ await waitForHealthcheck(spec.healthcheck, 45e3);
6498
+ } catch (err) {
6499
+ if (err instanceof HealthcheckTimeoutError && hasStartupDiagnostics(driver)) {
6500
+ const diag = await driver.diagnoseStartup(serviceLogPath(driver.id));
6501
+ if (diag) {
6502
+ throw new BeeosError({
6503
+ code: "service_install_failed",
6504
+ message: diag.reason,
6505
+ hint: diag.hints.length > 0 ? diag.hints.map((h) => `\u2022 ${h}`).join("\n ") : void 0,
6506
+ cause: err,
6507
+ details: { driver: driver.id }
6508
+ });
6509
+ }
6510
+ }
6511
+ throw err;
5361
6512
  }
5362
- throw new Error(
5363
- `Agent bound to instance ${boundInstanceId} but post-bind step failed. Attempted best-effort cleanup; if the instance is still visible in the dashboard, delete it manually. Underlying error: ${e}`,
5364
- { cause: e }
5365
- );
5366
6513
  }
6514
+ const gatewayPid = status2.pid ?? void 0;
6515
+ emit(options.json, {
6516
+ status: isOffline ? "bound_offline" : "bound",
6517
+ public_key: pubkey,
6518
+ fingerprint: fp,
6519
+ gateway_pid: gatewayPid,
6520
+ agent_gateway_url: agentGatewayUrl,
6521
+ instance_id: boundInstanceId,
6522
+ warnings: launchWarnings.length > 0 ? [...launchWarnings] : void 0
6523
+ }, isOffline ? `Agent running (offline, cached instance: ${boundInstanceId})` : `Agent bound to instance: ${boundInstanceId}`);
6524
+ if (!options.json) printLaunchWarnings(launchWarnings);
5367
6525
  }
5368
6526
  function buildHostname() {
5369
6527
  const machine = os6.hostname();
@@ -5372,11 +6530,22 @@ function buildHostname() {
5372
6530
  }
5373
6531
  function emit(json, payload, human) {
5374
6532
  if (json) {
5375
- console.log(JSON.stringify(payload, null, 2));
6533
+ emitJsonEnvelope(jsonOk(payload));
5376
6534
  } else if (human) {
5377
6535
  console.log(human);
5378
6536
  }
5379
6537
  }
6538
+ function printLaunchWarnings(warnings) {
6539
+ if (warnings.length === 0) return;
6540
+ console.log("");
6541
+ console.log("Warnings:");
6542
+ for (const w of warnings) {
6543
+ console.log(` ! ${w}`);
6544
+ }
6545
+ console.log(
6546
+ " Run `beeos doctor` for a full health report."
6547
+ );
6548
+ }
5380
6549
 
5381
6550
  // src/commands/stop.ts
5382
6551
  init_dist();
@@ -5395,30 +6564,56 @@ async function run2(agentFramework) {
5395
6564
 
5396
6565
  // src/commands/status.ts
5397
6566
  init_dist();
5398
- async function run3() {
6567
+ async function run3(options = {}) {
5399
6568
  const home = beeoHome();
5400
6569
  const cfg = await loadOrCreateConfig();
5401
- console.log(`BeeOS Home: ${home}`);
5402
- console.log(`API URL: ${cfg.platform.api_url}`);
5403
- console.log(`Agent Gateway: ${cfg.platform.agent_gateway_url}`);
5404
- console.log("");
5405
6570
  const p = getPlatformAdapter();
5406
6571
  const fpPath = p.joinPath(home, "identity", "fingerprint");
6572
+ let fingerprint2 = null;
5407
6573
  if (await p.exists(fpPath)) {
5408
6574
  const fp = (await p.readFile(fpPath)).trim();
5409
- console.log(`Identity fingerprint: ${fp}`);
5410
- } else {
5411
- console.log("Identity: not yet created");
6575
+ if (fp) fingerprint2 = fp;
5412
6576
  }
5413
- console.log("");
5414
6577
  const mgr = await getServiceManager();
5415
- const reason = activeFallbackReason();
5416
- console.log(`Service manager: ${mgr.kind}${reason ? ` (${reason})` : ""}`);
6578
+ const fallbackReason2 = activeFallbackReason();
5417
6579
  let services = [];
5418
6580
  try {
5419
6581
  services = await mgr.list();
5420
6582
  } catch {
5421
6583
  }
6584
+ if (options.json) {
6585
+ emitJsonEnvelope(
6586
+ jsonOk({
6587
+ beeos_home: home,
6588
+ api_url: cfg.platform.api_url,
6589
+ agent_gateway_url: cfg.platform.agent_gateway_url,
6590
+ fingerprint: fingerprint2,
6591
+ service_manager: {
6592
+ kind: mgr.kind,
6593
+ fallback_reason: fallbackReason2 ?? null
6594
+ },
6595
+ services: services.map((s) => ({
6596
+ id: s.id,
6597
+ installed: s.installed,
6598
+ running: s.running,
6599
+ pid: s.pid ?? null,
6600
+ last_exit_code: s.lastExitCode ?? null
6601
+ }))
6602
+ })
6603
+ );
6604
+ return;
6605
+ }
6606
+ console.log(`BeeOS Home: ${home}`);
6607
+ console.log(`API URL: ${cfg.platform.api_url}`);
6608
+ console.log(`Agent Gateway: ${cfg.platform.agent_gateway_url}`);
6609
+ console.log("");
6610
+ if (fingerprint2) {
6611
+ console.log(`Identity fingerprint: ${fingerprint2}`);
6612
+ } else {
6613
+ console.log("Identity: not yet created");
6614
+ }
6615
+ console.log("");
6616
+ console.log(`Service manager: ${mgr.kind}${fallbackReason2 ? ` (${fallbackReason2})` : ""}`);
5422
6617
  if (services.length === 0) {
5423
6618
  console.log(" (no services registered)");
5424
6619
  } else {
@@ -5435,20 +6630,30 @@ function formatService(s) {
5435
6630
  // src/commands/update.ts
5436
6631
  init_dist();
5437
6632
  init_progress2();
5438
- async function run4(agentFramework) {
6633
+ async function run4(agentFramework, options = {}) {
5439
6634
  if (agentFramework === "device") {
5440
6635
  const reporter = new CliReporter();
5441
6636
  await deviceRuntime.update(reporter);
5442
6637
  reporter.stop();
5443
- console.log("device updated");
6638
+ if (options.json) {
6639
+ emitJsonEnvelope(jsonOk({ framework: "device", action: "updated" }));
6640
+ } else {
6641
+ console.log("device updated");
6642
+ }
5444
6643
  return;
5445
6644
  }
5446
6645
  const descriptor = frameworkById(agentFramework);
5447
6646
  if (!descriptor || descriptor.status !== "available") {
5448
6647
  const avail = availableFrameworks().map((f) => f.id).join(", ");
5449
- throw new Error(
5450
- `Unknown agent framework '${agentFramework}'. Available: ${avail}.`
5451
- );
6648
+ throw new BeeosError({
6649
+ code: "framework_unavailable",
6650
+ message: `Unknown agent framework '${agentFramework}'.`,
6651
+ hint: avail.length > 0 ? `Available frameworks: ${avail}.` : "No frameworks are registered \u2014 reinstall the CLI.",
6652
+ details: {
6653
+ requested: agentFramework,
6654
+ available: avail.length > 0 ? avail.split(/,\s*/) : []
6655
+ }
6656
+ });
5452
6657
  }
5453
6658
  const mgr = await getServiceManager();
5454
6659
  const services = await mgr.list().catch(() => []);
@@ -5457,7 +6662,11 @@ async function run4(agentFramework) {
5457
6662
  await run2(agentFramework);
5458
6663
  }
5459
6664
  await run(agentFramework, { force: true });
5460
- console.log(`${agentFramework} updated`);
6665
+ if (options.json) {
6666
+ emitJsonEnvelope(jsonOk({ framework: agentFramework, action: "updated" }));
6667
+ } else {
6668
+ console.log(`${agentFramework} updated`);
6669
+ }
5461
6670
  }
5462
6671
 
5463
6672
  // src/commands/bind-status.ts
@@ -5472,21 +6681,39 @@ async function run5(bindId, options) {
5472
6681
  if (await p.exists(fpPath)) {
5473
6682
  const fp = (await p.readFile(fpPath)).trim();
5474
6683
  if (fp) {
6684
+ const boundAt = Math.floor(Date.now() / 1e3);
6685
+ const cached2 = await safeLoadBindingInfo();
6686
+ const framework = cached2?.framework?.trim() || "openclaw";
5475
6687
  await saveBindingInfo({
5476
6688
  fingerprint: fp,
5477
6689
  instance_id: resp.instance_id,
5478
- bound_at: Math.floor(Date.now() / 1e3)
6690
+ bound_at: boundAt,
6691
+ framework
5479
6692
  });
6693
+ const entry = {
6694
+ kind: "openclaw",
6695
+ framework,
6696
+ instance_id: resp.instance_id,
6697
+ bound_at: boundAt,
6698
+ fingerprint: fp,
6699
+ agent_gateway_url: resolveAgentGatewayUrl(cfg)
6700
+ };
6701
+ await mutateStateBestEffort(
6702
+ (state) => upsertOpenClawAgent(state, entry),
6703
+ { authority: "binding.json" }
6704
+ );
5480
6705
  }
5481
6706
  }
5482
6707
  } catch {
5483
6708
  }
5484
6709
  }
5485
6710
  if (options.json) {
5486
- console.log(JSON.stringify({
5487
- status: resp.status,
5488
- instance_id: resp.instance_id ?? null
5489
- }, null, 2));
6711
+ emitJsonEnvelope(
6712
+ jsonOk({
6713
+ status: resp.status,
6714
+ instance_id: resp.instance_id ?? null
6715
+ })
6716
+ );
5490
6717
  } else {
5491
6718
  console.log(`Bind status: ${resp.status}`);
5492
6719
  if (resp.instance_id) {
@@ -5494,6 +6721,13 @@ async function run5(bindId, options) {
5494
6721
  }
5495
6722
  }
5496
6723
  }
6724
+ async function safeLoadBindingInfo() {
6725
+ try {
6726
+ return await loadBindingInfo();
6727
+ } catch {
6728
+ return null;
6729
+ }
6730
+ }
5497
6731
 
5498
6732
  // src/index.ts
5499
6733
  init_device2();
@@ -5504,7 +6738,7 @@ import readline2 from "readline";
5504
6738
 
5505
6739
  // src/commands/doctor.ts
5506
6740
  init_dist();
5507
- import fs8 from "fs/promises";
6741
+ import fs9 from "fs/promises";
5508
6742
  import path9 from "path";
5509
6743
  var LOG_SIZE_WARN_BYTES = 50 * 1024 * 1024;
5510
6744
  async function run6(options) {
@@ -5521,7 +6755,8 @@ async function run6(options) {
5521
6755
  }
5522
6756
  const resolvedAgentGatewayUrl = resolveAgentGatewayUrl(cfg);
5523
6757
  const agentGatewayHealth = await probeAgentGateway(resolvedAgentGatewayUrl);
5524
- const tools = await collectToolStatus(p);
6758
+ const tools = await collectToolStatus();
6759
+ const shimReport = await collectShimReport();
5525
6760
  const warnings = [];
5526
6761
  const hints = [];
5527
6762
  if (!state.hasIdentity) {
@@ -5530,6 +6765,14 @@ async function run6(options) {
5530
6765
  if (state.openclaw.gatewayRunning && !state.openclaw.found) {
5531
6766
  warnings.push("port 18789 is in use but no OpenClaw install detected \u2014 another app may conflict");
5532
6767
  }
6768
+ if (state.openclaw.found && state.openclaw.pluginInstalled === false) {
6769
+ warnings.push(
6770
+ `OpenClaw is installed at ${state.openclaw.home ?? "?"} but the beeos-claw plugin is not registered. BeeOS-specific tools may be unavailable to agents until this is fixed.`
6771
+ );
6772
+ hints.push(
6773
+ "re-run `beeos start openclaw --force` to re-install the plugin, or check ~/.beeos/logs/services/openclaw.log for the original failure"
6774
+ );
6775
+ }
5533
6776
  if (state.devices.entries.length > 0 && services.length === 0) {
5534
6777
  warnings.push(
5535
6778
  "devices tracked via legacy devices.json \u2014 no OS services registered. Re-run `beeos device attach` to migrate them to the OS service manager."
@@ -5572,9 +6815,19 @@ async function run6(options) {
5572
6815
  "adb not found \u2014 `beeos device attach` will download Android platform-tools on first use"
5573
6816
  );
5574
6817
  }
5575
- if (!tools.python.path) {
5576
- warnings.push(
5577
- "python3 not found on PATH \u2014 device-agent uses a uv-managed interpreter; install python 3.11+ if uv is unavailable"
6818
+ for (const e of shimReport.entries) {
6819
+ if (e.outcome === "outdated") {
6820
+ warnings.push(
6821
+ `${e.pkg}: installed ${e.installed ?? "(missing)"}, npm latest ${e.latest}.`
6822
+ );
6823
+ } else if (e.outcome === "missing") {
6824
+ warnings.push(`${e.pkg}: not installed globally \u2014 run 'beeos init' to install.`);
6825
+ } else if (e.outcome === "error") {
6826
+ }
6827
+ }
6828
+ if (shimReport.entries.some((e) => e.outcome === "outdated" || e.outcome === "missing")) {
6829
+ hints.push(
6830
+ "run `beeos init` and pick option [1] to upgrade CLI + agents to the latest npm release"
5578
6831
  );
5579
6832
  }
5580
6833
  const bloatedLogs = await checkLogFileSizes();
@@ -5585,29 +6838,26 @@ async function run6(options) {
5585
6838
  hints.push(`truncate safely: : > ${l.path}`);
5586
6839
  }
5587
6840
  if (options.json) {
5588
- console.log(
5589
- JSON.stringify(
5590
- {
5591
- platform: p.platform(),
5592
- arch: process.arch,
5593
- node: process.version,
5594
- beeosHome: state.beeosHome,
5595
- config: cfg,
5596
- state,
5597
- serviceManager: {
5598
- kind: mgr.kind,
5599
- available: serviceCheck.available,
5600
- fallbackReason: fallbackReason2,
5601
- services
5602
- },
5603
- agentGateway: agentGatewayHealth,
5604
- tools,
5605
- warnings,
5606
- hints
6841
+ emitJsonEnvelope(
6842
+ jsonOk({
6843
+ platform: p.platform(),
6844
+ arch: process.arch,
6845
+ node: process.version,
6846
+ beeosHome: state.beeosHome,
6847
+ config: cfg,
6848
+ state,
6849
+ serviceManager: {
6850
+ kind: mgr.kind,
6851
+ available: serviceCheck.available,
6852
+ fallbackReason: fallbackReason2,
6853
+ services
5607
6854
  },
5608
- null,
5609
- 2
5610
- )
6855
+ agentGateway: agentGatewayHealth,
6856
+ tools,
6857
+ npmShims: shimReport,
6858
+ warnings,
6859
+ hints
6860
+ })
5611
6861
  );
5612
6862
  return;
5613
6863
  }
@@ -5631,10 +6881,19 @@ async function run6(options) {
5631
6881
  console.log(` - ${s.id.padEnd(28)} ${status2}${pid}`);
5632
6882
  }
5633
6883
  console.log(` adb : ${tools.adb.path ?? "not found"}`);
5634
- console.log(` python3 : ${tools.python.path ?? "not found"}`);
5635
6884
  console.log(` scrcpy-bridge: ${tools.scrcpyBridge.path ?? "not installed (auto on attach)"}`);
5636
6885
  console.log(` vnc-bridge : ${tools.vncBridge.path ?? "not installed (auto on --vnc-host)"}`);
5637
6886
  console.log("");
6887
+ console.log(" npm shims:");
6888
+ for (const e of shimReport.entries) {
6889
+ const marker = shimMarker(e.outcome);
6890
+ const installed = e.installed ?? "(missing)";
6891
+ const latest = e.latest ?? "(unknown)";
6892
+ console.log(
6893
+ ` ${marker} ${e.pkg.padEnd(34)} installed=${installed.padEnd(8)} npm=${latest}`
6894
+ );
6895
+ }
6896
+ console.log("");
5638
6897
  if (warnings.length > 0) {
5639
6898
  console.log(" Warnings:");
5640
6899
  for (const w of warnings) {
@@ -5652,38 +6911,57 @@ async function run6(options) {
5652
6911
  console.log("");
5653
6912
  }
5654
6913
  }
5655
- async function collectToolStatus(p) {
5656
- const [adbPath, pythonPath, scrcpyPath, vncPath] = await Promise.all([
6914
+ async function collectToolStatus() {
6915
+ const [adbPath, scrcpyPath, vncPath] = await Promise.all([
5657
6916
  findAdb().catch(() => null),
5658
- findPython(p).catch(() => null),
5659
6917
  scrcpyBridgeRuntime.findBinary().catch(() => null),
5660
6918
  vncBridgeRuntime.findBinary().catch(() => null)
5661
6919
  ]);
5662
6920
  return {
5663
6921
  adb: { path: adbPath },
5664
- python: { path: pythonPath },
5665
6922
  scrcpyBridge: { path: scrcpyPath },
5666
6923
  vncBridge: { path: vncPath }
5667
6924
  };
5668
6925
  }
5669
- async function findPython(p) {
5670
- const whichCmd = p.platform() === "win32" ? "where" : "which";
5671
- for (const bin of ["python3", "python"]) {
5672
- try {
5673
- const r = await p.exec(whichCmd, [bin]);
5674
- if (r.code === 0 && r.stdout.trim()) {
5675
- return r.stdout.trim().split("\n")[0].trim();
6926
+ async function collectShimReport() {
6927
+ const sources = readPinSourcesFromEnv();
6928
+ const entries = await Promise.all(
6929
+ ALL_BEEOS_PKGS.map(async (pkg) => {
6930
+ const spec = resolveInstallSpec(pkg, sources);
6931
+ const [installed, latest] = await Promise.all([
6932
+ npmGlobalVersion(pkg).catch(() => null),
6933
+ npmRegistryVersion(spec).catch(() => null)
6934
+ ]);
6935
+ let outcome;
6936
+ if (latest === null) outcome = "error";
6937
+ else if (installed === null) outcome = "missing";
6938
+ else if (installed === latest) outcome = "ok";
6939
+ else outcome = "outdated";
6940
+ if (pkg === NPM_PKGS.CLI && installed === null) {
6941
+ outcome = "missing";
5676
6942
  }
5677
- } catch {
5678
- }
6943
+ return { pkg, installed, latest, spec, outcome };
6944
+ })
6945
+ );
6946
+ return { entries };
6947
+ }
6948
+ function shimMarker(outcome) {
6949
+ switch (outcome) {
6950
+ case "ok":
6951
+ return "\u2713";
6952
+ case "outdated":
6953
+ return "\u2191";
6954
+ case "missing":
6955
+ return "\u2717";
6956
+ case "error":
6957
+ return "?";
5679
6958
  }
5680
- return null;
5681
6959
  }
5682
6960
  async function checkLogFileSizes() {
5683
6961
  const dir = path9.join(beeoHome(), "logs", "services");
5684
6962
  let entries;
5685
6963
  try {
5686
- entries = await fs8.readdir(dir);
6964
+ entries = await fs9.readdir(dir);
5687
6965
  } catch {
5688
6966
  return [];
5689
6967
  }
@@ -5692,7 +6970,7 @@ async function checkLogFileSizes() {
5692
6970
  if (!name.endsWith(".log")) continue;
5693
6971
  const full = path9.join(dir, name);
5694
6972
  try {
5695
- const st = await fs8.stat(full);
6973
+ const st = await fs9.stat(full);
5696
6974
  if (st.isFile() && st.size >= LOG_SIZE_WARN_BYTES) {
5697
6975
  bloated.push({ path: full, size: st.size });
5698
6976
  }
@@ -5761,6 +7039,7 @@ function formatAgentGatewayStatus(h) {
5761
7039
  }
5762
7040
 
5763
7041
  // src/commands/init.ts
7042
+ init_progress2();
5764
7043
  init_fallback_banner();
5765
7044
  async function run7(options) {
5766
7045
  await ensureDirs();
@@ -5791,7 +7070,11 @@ async function run7(options) {
5791
7070
  return;
5792
7071
  }
5793
7072
  if (decision === "upgrade") {
5794
- console.log("Upgrading / ensuring OpenClaw is running...\n");
7073
+ if (await shouldUpgradeBeeosSuite(options)) {
7074
+ const exited = await upgradeAndMaybeExit(options);
7075
+ if (exited) return;
7076
+ }
7077
+ console.log("Ensuring OpenClaw is running...\n");
5795
7078
  }
5796
7079
  if (decision === "rebind-new-key") {
5797
7080
  await rotateIdentity();
@@ -5801,9 +7084,12 @@ async function run7(options) {
5801
7084
  const descriptor = frameworkById(frameworkId);
5802
7085
  if (!descriptor || descriptor.status !== "available") {
5803
7086
  const avail = availableFrameworks().map((f) => f.id).join(", ");
5804
- throw new Error(
5805
- `Agent framework '${frameworkId}' is not available. Available: ${avail}.`
5806
- );
7087
+ throw new BeeosError({
7088
+ code: "framework_unavailable",
7089
+ message: `Agent framework '${frameworkId}' is not available. Available: ${avail}.`,
7090
+ hint: avail.length > 0 ? `Re-run with --framework <one-of-the-above>` : "No frameworks are registered \u2014 reinstall the CLI.",
7091
+ details: { requested: frameworkId, available: availableFrameworks().map((f) => f.id) }
7092
+ });
5807
7093
  }
5808
7094
  await run(descriptor.id, {
5809
7095
  force: true,
@@ -5951,6 +7237,60 @@ function prompt(question) {
5951
7237
  });
5952
7238
  });
5953
7239
  }
7240
+ async function shouldUpgradeBeeosSuite(options) {
7241
+ if (options.headless) return false;
7242
+ if (process.env.BEEOS_INIT_SKIP_NPM_UPGRADE === "1") return false;
7243
+ return true;
7244
+ }
7245
+ async function upgradeAndMaybeExit(options) {
7246
+ if (options.json) {
7247
+ return false;
7248
+ }
7249
+ console.log("Checking for newer @beeos-ai/cli + agents on npm...");
7250
+ const reporter = new CliReporter();
7251
+ let outcome;
7252
+ try {
7253
+ outcome = await upgradeBeeosSuite({
7254
+ packages: ALL_BEEOS_PKGS,
7255
+ progress: reporter
7256
+ });
7257
+ } catch (e) {
7258
+ reporter.stop();
7259
+ console.log(
7260
+ ` (upgrade skipped \u2014 ${e instanceof Error ? e.message : String(e)})
7261
+ Tip: re-run \`npm install -g @beeos-ai/cli @beeos-ai/device-agent @beeos-ai/device-mcp-server\` manually.`
7262
+ );
7263
+ return false;
7264
+ }
7265
+ reporter.stop();
7266
+ if (outcome.anyFailed) {
7267
+ const failed = outcome.packages.find((p) => p.failed)?.failed ?? "unknown error";
7268
+ console.log(` \u26A0 npm install failed: ${failed}`);
7269
+ console.log(
7270
+ " Manual fix:\n npm i -g @beeos-ai/cli @beeos-ai/device-agent @beeos-ai/device-mcp-server\n"
7271
+ );
7272
+ return false;
7273
+ }
7274
+ if (!outcome.anyChanged) {
7275
+ console.log(" All BeeOS npm packages are up to date.\n");
7276
+ return false;
7277
+ }
7278
+ for (const pkg of outcome.packages) {
7279
+ if (pkg.changed) {
7280
+ console.log(` ${pkg.pkg}: ${pkg.before ?? "(none)"} \u2192 ${pkg.after}`);
7281
+ }
7282
+ }
7283
+ const cliEntry = outcome.packages.find((p) => p.pkg === NPM_PKGS.CLI);
7284
+ if (cliEntry?.changed) {
7285
+ console.log("");
7286
+ console.log(" CLI itself was upgraded \u2014 please re-run `beeos init` to continue");
7287
+ console.log(" with the new code. The current process is still running the old version.");
7288
+ console.log("");
7289
+ return true;
7290
+ }
7291
+ console.log(" Agents updated. Continuing init...\n");
7292
+ return false;
7293
+ }
5954
7294
 
5955
7295
  // src/commands/service.ts
5956
7296
  init_dist();
@@ -5959,18 +7299,14 @@ async function install(options) {
5959
7299
  const check = await mgr.selfCheck();
5960
7300
  const reason = activeFallbackReason();
5961
7301
  if (options.json) {
5962
- console.log(
5963
- JSON.stringify(
5964
- {
5965
- ok: check.available,
5966
- manager: mgr.kind,
5967
- fallbackReason: reason,
5968
- warnings: check.warnings,
5969
- hints: check.hints
5970
- },
5971
- null,
5972
- 2
5973
- )
7302
+ emitJsonEnvelope(
7303
+ jsonOk({
7304
+ available: check.available,
7305
+ manager: mgr.kind,
7306
+ fallbackReason: reason,
7307
+ warnings: check.warnings,
7308
+ hints: check.hints
7309
+ })
5974
7310
  );
5975
7311
  return;
5976
7312
  }
@@ -6004,7 +7340,7 @@ async function uninstall(options) {
6004
7340
  }
6005
7341
  }
6006
7342
  if (options.json) {
6007
- console.log(JSON.stringify({ removed, errors }, null, 2));
7343
+ emitJsonEnvelope(jsonOk({ removed, errors }));
6008
7344
  return;
6009
7345
  }
6010
7346
  if (removed.length === 0 && errors.length === 0) {
@@ -6023,12 +7359,8 @@ async function status(options) {
6023
7359
  } catch {
6024
7360
  }
6025
7361
  if (options.json) {
6026
- console.log(
6027
- JSON.stringify(
6028
- { manager: mgr.kind, fallbackReason: reason, services },
6029
- null,
6030
- 2
6031
- )
7362
+ emitJsonEnvelope(
7363
+ jsonOk({ manager: mgr.kind, fallbackReason: reason, services })
6032
7364
  );
6033
7365
  return;
6034
7366
  }
@@ -6051,13 +7383,15 @@ async function run8(options) {
6051
7383
  const mgr = await getServiceManager();
6052
7384
  const mig = await migrateLegacySupervisor(mgr);
6053
7385
  if (options.json) {
6054
- console.log(JSON.stringify({
6055
- ran: mig.ran,
6056
- migrated: mig.migrated,
6057
- backupPath: mig.backupPath ?? null,
6058
- errors: mig.errors,
6059
- serviceManager: mgr.kind
6060
- }, null, 2));
7386
+ emitJsonEnvelope(
7387
+ jsonOk({
7388
+ ran: mig.ran,
7389
+ migrated: mig.migrated,
7390
+ backupPath: mig.backupPath ?? null,
7391
+ errors: mig.errors,
7392
+ serviceManager: mgr.kind
7393
+ })
7394
+ );
6061
7395
  return;
6062
7396
  }
6063
7397
  if (!mig.ran) {
@@ -6079,6 +7413,98 @@ async function run8(options) {
6079
7413
  }
6080
7414
  }
6081
7415
 
7416
+ // src/commands/logs.ts
7417
+ init_dist();
7418
+ function resolveLogTarget(input, services) {
7419
+ const exact = services.find((s) => s.id === input);
7420
+ if (exact) {
7421
+ return { id: exact.id, logFile: exact.logFile, matchedFrom: "exact" };
7422
+ }
7423
+ const safe = safeId(input);
7424
+ const safeMatch = services.find((s) => s.id === safe);
7425
+ if (safeMatch) {
7426
+ return { id: safeMatch.id, logFile: safeMatch.logFile, matchedFrom: "safeId" };
7427
+ }
7428
+ const heuristicId = `device-agent-${safe}`;
7429
+ const heuristic = services.find((s) => s.id === heuristicId);
7430
+ if (heuristic) {
7431
+ return { id: heuristic.id, logFile: heuristic.logFile, matchedFrom: "adbSerialHeuristic" };
7432
+ }
7433
+ return {
7434
+ id: safe,
7435
+ logFile: serviceLogPath(safe),
7436
+ matchedFrom: "fallback"
7437
+ };
7438
+ }
7439
+ async function run9(idArg, options) {
7440
+ const mgr = await getServiceManager();
7441
+ let services = [];
7442
+ try {
7443
+ services = await mgr.list();
7444
+ } catch {
7445
+ }
7446
+ if (!idArg) {
7447
+ if (options.json) {
7448
+ emitJsonEnvelope(
7449
+ jsonOk({
7450
+ manager: mgr.kind,
7451
+ services: services.map((s) => ({ id: s.id, logFile: s.logFile }))
7452
+ })
7453
+ );
7454
+ return;
7455
+ }
7456
+ if (services.length === 0) {
7457
+ console.log("No registered BeeOS services. Start one with `beeos start <agent>` or `beeos device attach`.");
7458
+ return;
7459
+ }
7460
+ console.log(`Service manager: ${mgr.kind}`);
7461
+ console.log("Available services:");
7462
+ for (const s of services) {
7463
+ console.log(` ${s.id.padEnd(28)} ${s.logFile}`);
7464
+ }
7465
+ console.log("\nUsage: beeos logs <id> [-f] [-n <lines>]");
7466
+ return;
7467
+ }
7468
+ const target = resolveLogTarget(idArg, services);
7469
+ if (options.json) {
7470
+ emitJsonEnvelope(
7471
+ jsonOk({
7472
+ input: idArg,
7473
+ resolved: {
7474
+ id: target.id,
7475
+ logFile: target.logFile,
7476
+ matchedFrom: target.matchedFrom
7477
+ }
7478
+ })
7479
+ );
7480
+ return;
7481
+ }
7482
+ if (target.matchedFrom === "fallback") {
7483
+ console.error(
7484
+ `! No registered service matched "${idArg}". Falling back to canonical path:
7485
+ ${target.logFile}
7486
+ (the file may not exist yet if the service has never run.)`
7487
+ );
7488
+ }
7489
+ const ac = new AbortController();
7490
+ const onSigint = () => {
7491
+ ac.abort();
7492
+ };
7493
+ if (options.follow) {
7494
+ process.on("SIGINT", onSigint);
7495
+ }
7496
+ try {
7497
+ await tailLogFile({
7498
+ logFile: target.logFile,
7499
+ initialLines: options.lines ?? 50,
7500
+ follow: !!options.follow,
7501
+ signal: ac.signal
7502
+ });
7503
+ } finally {
7504
+ if (options.follow) process.off("SIGINT", onSigint);
7505
+ }
7506
+ }
7507
+
6082
7508
  // src/index.ts
6083
7509
  setPlatformAdapter(new NodePlatformAdapter());
6084
7510
  var program = new Command();
@@ -6087,15 +7513,18 @@ program.name("beeos").version(cliVersion.display).description("BeeOS \u2014 run
6087
7513
  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));
6088
7514
  program.command("doctor").description("Inspect local BeeOS state (install, binding, services, warnings)").option("--json", "Output machine-readable JSON", false).action((opts) => run6(opts));
6089
7515
  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));
7516
+ program.command("logs").description(
7517
+ "Tail logs for a BeeOS service. Pass an ADB serial or service id; omit to list every registered service's log file path."
7518
+ ).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));
6090
7519
  var serviceCmd = program.command("service").description("Inspect and manage the OS-native service manager (launchd / systemd --user / Task Scheduler)");
6091
7520
  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));
6092
7521
  serviceCmd.command("uninstall").description("Uninstall every BeeOS-managed OS service").option("--json", "Output machine-readable JSON", false).action((opts) => uninstall(opts));
6093
7522
  serviceCmd.command("status").description("List every BeeOS-managed OS service").option("--json", "Output machine-readable JSON", false).action((opts) => status(opts));
6094
7523
  program.command("start").description("Install and start an agent").argument("<agent_type>", "Agent type to start (e.g. openclaw)").option("--version <version>", "Pin to a specific version instead of latest").option("--force", "Skip confirmation prompts", false).option("--json", "Output machine-readable JSON to stdout (implies --force)", false).option("--no-browser", "Don't auto-open browser for bind confirmation").action((agentFramework, options) => run(agentFramework, options));
6095
7524
  program.command("stop").description("Stop a running agent").argument("<agent_type>", "Agent type to stop").action((agentFramework) => run2(agentFramework));
6096
- program.command("status").description("Show status of managed agents").action(run3);
6097
- program.command("update").description("Update an agent to the latest version").argument("<agent_type>", "Agent type to update").action((agentFramework) => run4(agentFramework));
6098
- program.command("bind-status").description("Check agent binding status").argument("<bind_id>", "Bind session ID returned by `beeos start --json`").option("--json", "Output machine-readable JSON", false).action(run5);
7525
+ program.command("status").description("Show status of managed agents").option("--json", "Output machine-readable JSON envelope ({ok,data})", false).action((opts) => run3(opts));
7526
+ program.command("update").description("Update an agent to the latest version").argument("<agent_type>", "Agent type to update").option("--json", "Output machine-readable JSON envelope ({ok,data})", false).action((agentFramework, opts) => run4(agentFramework, opts));
7527
+ program.command("bind-status").description("Check agent binding status").argument("<bind_id>", "Bind session ID returned by `beeos start --json`").option("--json", "Output machine-readable JSON envelope ({ok,data})", false).action(run5);
6099
7528
  var deviceCmd = program.command("device").description("Manage device agents (Android/Desktop/ChromeOS)");
6100
7529
  deviceCmd.command("attach").description("Attach an ADB-connected device as an AI agent").option("--serial <serial>", "ADB device serial number").option("--name <name>", "Human-readable device name").option("--all", "Attach all connected ADB devices", false).option(
6101
7530
  "--no-video",
@@ -6103,7 +7532,10 @@ deviceCmd.command("attach").description("Attach an ADB-connected device as an AI
6103
7532
  ).option(
6104
7533
  "--vnc-host <host>",
6105
7534
  "Use vnc-bridge against a VNC server at this host (desktop/Linux/macOS). Implies video mode = vnc instead of scrcpy."
6106
- ).option("--vnc-port <port>", "VNC TCP port (default 5900)", (v) => Number(v)).option("--vnc-password <password>", "VNC password (forwarded via env)").action(attach);
7535
+ ).option("--vnc-port <port>", "VNC TCP port (default 5900)", (v) => Number(v)).option("--vnc-password <password>", "VNC password (forwarded via env)").option(
7536
+ "--agent-gateway-url <url>",
7537
+ "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."
7538
+ ).action(attach);
6107
7539
  deviceCmd.command("detach").description("Detach a device agent").option("--serial <serial>", "ADB device serial number").option("--all", "Detach all devices", false).action(detach);
6108
7540
  deviceCmd.command("list").description("List attached devices").option("--local", "Only show locally running device agents", false).action(list);
6109
7541
  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);
@@ -6112,6 +7544,15 @@ deviceCmd.command("refresh-config").description(
6112
7544
  "Re-fetch this device's VLM/ARouter config from Agent Gateway and rewrite ~/.beeos/<instance_id>.config.json (use after dashboard changes)."
6113
7545
  ).option("--serial <serial>", "ADB device serial number").option("--all", "Refresh every attached device", false).action(refreshConfig);
6114
7546
  program.parseAsync(process.argv).catch((err) => {
7547
+ const wantsJson = process.argv.includes("--json");
7548
+ if (isBeeosError(err)) {
7549
+ if (wantsJson) {
7550
+ console.log(JSON.stringify(toJsonFailurePayload(err), null, 2));
7551
+ } else {
7552
+ console.error(formatHumanError(err));
7553
+ }
7554
+ process.exit(1);
7555
+ }
6115
7556
  console.error(err.message);
6116
7557
  process.exit(1);
6117
7558
  });