@beeos-ai/cli 1.0.12 → 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;
@@ -2681,13 +3330,13 @@ function inferInitChoices(state) {
2681
3330
  if (state.hasIdentity && state.binding) {
2682
3331
  return {
2683
3332
  defaultDecision: "upgrade",
2684
- options: ["upgrade", "rebind-keep-key", "rebind-new-key", "skip"]
3333
+ options: ["upgrade", "rebind-new-key", "skip"]
2685
3334
  };
2686
3335
  }
2687
3336
  if (state.hasIdentity) {
2688
3337
  return {
2689
- defaultDecision: "rebind-keep-key",
2690
- options: ["upgrade", "rebind-keep-key", "rebind-new-key", "skip"]
3338
+ defaultDecision: "upgrade",
3339
+ options: ["upgrade", "rebind-new-key", "skip"]
2691
3340
  };
2692
3341
  }
2693
3342
  return { defaultDecision: "fresh", options: ["fresh", "skip"] };
@@ -2697,9 +3346,7 @@ function initDecisionLabel(decision) {
2697
3346
  case "fresh":
2698
3347
  return "Install + bind";
2699
3348
  case "upgrade":
2700
- return "Upgrade CLI & agents (keep binding)";
2701
- case "rebind-keep-key":
2702
- return "Re-bind (keep existing Ed25519 key)";
3349
+ return "Upgrade CLI & agents (keep binding & key)";
2703
3350
  case "rebind-new-key":
2704
3351
  return "Re-bind (rotate Ed25519 key)";
2705
3352
  case "skip":
@@ -2772,6 +3419,7 @@ var init_target_spec = __esm({
2772
3419
  "use strict";
2773
3420
  init_scrcpy_bridge();
2774
3421
  init_vnc_bridge();
3422
+ init_spawn_env2();
2775
3423
  }
2776
3424
  });
2777
3425
 
@@ -2971,13 +3619,23 @@ var init_launchd = __esm({
2971
3619
  await fs.mkdir(path2.dirname(plist), { recursive: true });
2972
3620
  await fs.mkdir(path2.dirname(logFile), { recursive: true });
2973
3621
  const resolvedSpec = await absolutizeCommand(spec);
2974
- await fs.writeFile(plist, renderPlist(resolvedSpec, label, logFile), {
2975
- mode: 420
2976
- });
2977
- const uid = process.getuid?.();
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?.();
2978
3635
  if (uid !== void 0) {
2979
3636
  try {
2980
3637
  await execFileP("launchctl", ["bootout", `gui/${uid}/${label}`]);
3638
+ await this.waitForBootout(label, uid);
2981
3639
  } catch {
2982
3640
  }
2983
3641
  try {
@@ -3000,6 +3658,24 @@ var init_launchd = __esm({
3000
3658
  const status2 = await this.status(spec.id);
3001
3659
  return status2 ?? fallbackStatus(spec, label, logFile, false);
3002
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
+ }
3003
3679
  async uninstall(id) {
3004
3680
  const label = launchdLabel(id);
3005
3681
  const plist = this.plistPath(label);
@@ -3970,6 +4646,7 @@ async function migrateLegacySupervisor(mgr) {
3970
4646
  const supervisorDir = path6.join(home, "supervisor");
3971
4647
  const stateFile = path6.join(supervisorDir, "state.json");
3972
4648
  const flagFile = path6.join(home, MIGRATION_FLAG);
4649
+ const lockFile = path6.join(home, MIGRATION_LOCK);
3973
4650
  if (fsSync4.existsSync(flagFile)) {
3974
4651
  return { ran: false, migrated: 0, errors: [], backupPath: null };
3975
4652
  }
@@ -3980,47 +4657,73 @@ async function migrateLegacySupervisor(mgr) {
3980
4657
  }
3981
4658
  return { ran: false, migrated: 0, errors: [], backupPath: null };
3982
4659
  }
3983
- const backupPath = `${stateFile}.migration-backup-${Date.now()}`;
3984
- try {
3985
- await fs5.copyFile(stateFile, backupPath);
3986
- } catch {
3987
- }
3988
- let parsed;
3989
- try {
3990
- const raw = await fs5.readFile(stateFile, "utf-8");
3991
- parsed = JSON.parse(raw);
3992
- } catch (e) {
4660
+ if (!await acquireLock(lockFile)) {
3993
4661
  return {
3994
4662
  ran: true,
3995
4663
  migrated: 0,
3996
- errors: [{ id: "<state.json>", error: `unreadable: ${e}` }],
3997
- 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
3998
4669
  };
3999
4670
  }
4000
- const targets = parsed.targets ?? [];
4001
- const errors = [];
4002
- let migrated = 0;
4003
- for (const t of targets) {
4004
- const canonicalId = safeId(t.id);
4671
+ try {
4672
+ const backupPath = `${stateFile}.migration-backup-${Date.now()}`;
4005
4673
  try {
4006
- const spec = {
4007
- id: canonicalId,
4008
- kind: t.kind,
4009
- command: t.command,
4010
- args: t.args ?? [],
4011
- env: t.env,
4012
- cwd: t.cwd,
4013
- restart: t.restart ?? "on-failure",
4014
- label: t.label
4015
- };
4016
- await mgr.install(spec);
4017
- 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);
4018
4681
  } catch (e) {
4019
- 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
+ };
4020
4688
  }
4021
- }
4022
- await stopLegacyDaemon();
4023
- 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();
4024
4727
  try {
4025
4728
  await fs5.writeFile(flagFile, (/* @__PURE__ */ new Date()).toISOString());
4026
4729
  } catch {
@@ -4029,8 +4732,56 @@ async function migrateLegacySupervisor(mgr) {
4029
4732
  await fs5.rm(supervisorDir, { recursive: true, force: true });
4030
4733
  } catch {
4031
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;
4032
4784
  }
4033
- return { ran: true, migrated, errors, backupPath };
4034
4785
  }
4035
4786
  async function stopLegacyDaemon() {
4036
4787
  if (process.platform === "darwin") {
@@ -4065,7 +4816,7 @@ async function stopLegacyDaemon() {
4065
4816
  }
4066
4817
  }
4067
4818
  }
4068
- 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;
4069
4820
  var init_migrate = __esm({
4070
4821
  "../core/dist/services/migrate.js"() {
4071
4822
  "use strict";
@@ -4075,11 +4826,91 @@ var init_migrate = __esm({
4075
4826
  LEGACY_LABEL = "ai.beeos.supervisor";
4076
4827
  LEGACY_SYSTEMD_UNIT = "beeos-supervisor.service";
4077
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";
4078
4909
  }
4079
4910
  });
4080
4911
 
4081
4912
  // ../core/dist/cli-version.js
4082
- import fs6 from "fs";
4913
+ import fs7 from "fs";
4083
4914
  import path7 from "path";
4084
4915
  import { fileURLToPath } from "url";
4085
4916
  function getCliVersion(moduleUrl, distTag) {
@@ -4107,8 +4938,8 @@ function readBakedDistTag() {
4107
4938
  let dir = here;
4108
4939
  for (let i = 0; i < 6; i++) {
4109
4940
  const candidate = path7.join(dir, "package.json");
4110
- if (fs6.existsSync(candidate)) {
4111
- const raw = fs6.readFileSync(candidate, "utf-8");
4941
+ if (fs7.existsSync(candidate)) {
4942
+ const raw = fs7.readFileSync(candidate, "utf-8");
4112
4943
  const pkg = JSON.parse(raw);
4113
4944
  if ((pkg.name === "@beeos-ai/cli" || pkg.name === "beeos") && pkg.beeos && typeof pkg.beeos.distTag === "string") {
4114
4945
  return pkg.beeos.distTag;
@@ -4133,8 +4964,8 @@ function readVersionFromModuleUrl(moduleUrl) {
4133
4964
  for (let i = 0; i < 6; i++) {
4134
4965
  const candidate = path7.join(here, "package.json");
4135
4966
  try {
4136
- if (fs6.existsSync(candidate)) {
4137
- const raw = fs6.readFileSync(candidate, "utf-8");
4967
+ if (fs7.existsSync(candidate)) {
4968
+ const raw = fs7.readFileSync(candidate, "utf-8");
4138
4969
  const pkg = JSON.parse(raw);
4139
4970
  if (typeof pkg.version === "string" && (pkg.name === "@beeos-ai/cli" || pkg.name === "beeos")) {
4140
4971
  return pkg.version;
@@ -4164,17 +4995,21 @@ var init_dist = __esm({
4164
4995
  init_types();
4165
4996
  init_paths();
4166
4997
  init_toml();
4998
+ init_spawn_env();
4167
4999
  init_binding();
4168
5000
  init_gateway_token();
5001
+ init_state();
4169
5002
  init_keypair();
4170
5003
  init_client();
4171
5004
  init_orchestrator();
4172
5005
  init_process();
5006
+ init_errors();
4173
5007
  init_device_setup();
4174
5008
  init_adb_setup();
4175
5009
  init_device();
4176
5010
  init_scrcpy_bridge();
4177
5011
  init_vnc_bridge();
5012
+ init_spawn_env2();
4178
5013
  init_agent_config();
4179
5014
  init_driver();
4180
5015
  init_detector();
@@ -4189,7 +5024,9 @@ var init_dist = __esm({
4189
5024
  init_factory();
4190
5025
  init_ids();
4191
5026
  init_migrate();
5027
+ init_tail_logs();
4192
5028
  init_cli_version();
5029
+ init_upgrade();
4193
5030
  }
4194
5031
  });
4195
5032
 
@@ -4268,6 +5105,34 @@ async function notifyFleetReloadBestEffort(baseUrl = FLEET_STATUS_BASE_URL) {
4268
5105
  return "unknown";
4269
5106
  }
4270
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
+ }
4271
5136
  function isConnectionRefused(e) {
4272
5137
  const code = extractErrorCode(e);
4273
5138
  return code === "ECONNREFUSED" || code === "ECONNRESET";
@@ -4292,12 +5157,13 @@ function printFleetNotRunningHint() {
4292
5157
  console.log(" device-agent fleet start # foreground (debug-friendly)");
4293
5158
  console.log("");
4294
5159
  }
4295
- var FLEET_STATUS_BASE_URL, FLEET_NOTIFY_TIMEOUT_MS;
5160
+ var FLEET_STATUS_BASE_URL, FLEET_NOTIFY_TIMEOUT_MS, FLEET_SHUTDOWN_TIMEOUT_MS;
4296
5161
  var init_fleet_notify = __esm({
4297
5162
  "src/commands/device/fleet-notify.ts"() {
4298
5163
  "use strict";
4299
5164
  FLEET_STATUS_BASE_URL = "http://127.0.0.1:7950";
4300
5165
  FLEET_NOTIFY_TIMEOUT_MS = 1e3;
5166
+ FLEET_SHUTDOWN_TIMEOUT_MS = 5e3;
4301
5167
  }
4302
5168
  });
4303
5169
 
@@ -4357,7 +5223,7 @@ async function removeTargetsForSerial(mgr, serial) {
4357
5223
  } catch {
4358
5224
  }
4359
5225
  }
4360
- var init_state = __esm({
5226
+ var init_state2 = __esm({
4361
5227
  "src/commands/device/state.ts"() {
4362
5228
  "use strict";
4363
5229
  init_dist();
@@ -4367,6 +5233,37 @@ var init_state = __esm({
4367
5233
  // src/commands/device/attach.ts
4368
5234
  import { spawn as spawn2 } from "child_process";
4369
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
+ }
4370
5267
  async function attach(options) {
4371
5268
  const cfg = await loadOrCreateConfig();
4372
5269
  const reporter = new CliReporter();
@@ -4390,17 +5287,19 @@ async function attach(options) {
4390
5287
  return;
4391
5288
  }
4392
5289
  const mgr = await getServiceManager();
5290
+ let prevInstanceId = null;
4393
5291
  await withDeviceLock(async () => {
4394
5292
  const state = await loadDeviceState();
4395
5293
  const existingIdx = state.devices.findIndex((d) => d.serial === serial);
4396
5294
  if (existingIdx >= 0) {
5295
+ prevInstanceId = state.devices[existingIdx].instance_id ?? null;
4397
5296
  await removeTargetsForSerial(mgr, serial);
4398
5297
  state.devices.splice(existingIdx, 1);
4399
5298
  }
4400
5299
  const httpPort = nextHttpPort(state, cfg.device.http_port);
4401
5300
  const keyFile = deviceRuntime.deviceKeyPath(serial);
4402
- const agentGatewayUrl = resolveAgentGatewayUrl(cfg);
4403
- await persistAgentConfigBestEffort({
5301
+ const agentGatewayUrl = resolvePerDeviceAgentGatewayUrl(cfg, options.agentGatewayUrl);
5302
+ const persistedAgentConfig = await persistAgentConfigBestEffort({
4404
5303
  agentGatewayUrl,
4405
5304
  keyFile,
4406
5305
  instanceId,
@@ -4415,8 +5314,12 @@ async function attach(options) {
4415
5314
  withVideo,
4416
5315
  vncHost: options.vncHost,
4417
5316
  vncPort: options.vncPort,
4418
- vncPassword: options.vncPassword
5317
+ vncPassword: options.vncPassword,
5318
+ agentGatewayUrl
4419
5319
  });
5320
+ const fp = fingerprintFromB64(pubkeyB64);
5321
+ const boundAt = Math.floor(Date.now() / 1e3);
5322
+ const backend = options.vncHost ? void 0 : "adb";
4420
5323
  state.devices.push({
4421
5324
  serial,
4422
5325
  name,
@@ -4430,11 +5333,37 @@ async function attach(options) {
4430
5333
  // path through fleet → mcp). When self-attach for
4431
5334
  // macOS/Linux/Windows lands, this field will be set
4432
5335
  // accordingly (`macos`/`linux`/`windows`).
4433
- backend: options.vncHost ? void 0 : "adb",
5336
+ backend,
4434
5337
  vnc_host: options.vncHost,
4435
- 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
4436
5345
  });
4437
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
+ );
4438
5367
  console.log(`Attached device ${serial} (${name}) \u2014 instance ${instanceId}`);
4439
5368
  console.log(` local http: :${httpPort} (if enabled)`);
4440
5369
  console.log(` logs: device-agent fleet logs ${serial}`);
@@ -4450,6 +5379,14 @@ async function attach(options) {
4450
5379
  throw e;
4451
5380
  }
4452
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
+ }
4453
5390
  await maybeNotifyFleetWithHint(cfg);
4454
5391
  }
4455
5392
  async function attachAll(cfg, reporter, withVideo, options) {
@@ -4461,9 +5398,13 @@ async function attachAll(cfg, reporter, withVideo, options) {
4461
5398
  console.log(`Discovered ${devices.length} device(s) via adb.`);
4462
5399
  await deviceRuntime.ensureAgent(reporter);
4463
5400
  reporter.stop();
4464
- const agentGatewayUrl = resolveAgentGatewayUrl(cfg);
5401
+ const agentGatewayUrl = resolvePerDeviceAgentGatewayUrl(cfg, options.agentGatewayUrl);
4465
5402
  const mgr = await getServiceManager();
4466
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
+ }
4467
5408
  const simulatedEntries = preState.devices.map((d) => ({ ...d }));
4468
5409
  function reserveHttpPort(serial) {
4469
5410
  const port = nextHttpPort({ devices: simulatedEntries }, cfg.device.http_port);
@@ -4490,7 +5431,8 @@ async function attachAll(cfg, reporter, withVideo, options) {
4490
5431
  serial: device.serial,
4491
5432
  instanceId,
4492
5433
  keyFile: deviceRuntime.deviceKeyPath(device.serial),
4493
- httpPort
5434
+ httpPort,
5435
+ fingerprint: fingerprintFromB64(pubkeyB64)
4494
5436
  });
4495
5437
  } catch (e) {
4496
5438
  console.error(` Failed to bind ${device.serial}: ${e}`);
@@ -4513,18 +5455,62 @@ async function attachAll(cfg, reporter, withVideo, options) {
4513
5455
  })
4514
5456
  )
4515
5457
  );
5458
+ const reboundSerials = [];
4516
5459
  await withDeviceLock(async () => {
4517
5460
  const state = await loadDeviceState();
4518
5461
  const { committed, failures } = commitAttachResults(state, commits);
4519
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
+ }
4520
5467
  for (const c of committed) {
4521
5468
  console.log(
4522
5469
  `Attached device ${c.serial} \u2014 instance ${c.entry.instance_id}` + (c.videoMode !== "none" ? ` (video: ${c.videoMode})` : "")
4523
5470
  );
4524
5471
  }
4525
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
+ }
4526
5499
  console.log(`Attached ${committed.length} device(s); supervision via device-agent fleet.`);
4527
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
+ }
4528
5514
  await maybeNotifyFleetWithHint(cfg);
4529
5515
  }
4530
5516
  function commitAttachResults(state, commits) {
@@ -4544,10 +5530,10 @@ function commitAttachResults(state, commits) {
4544
5530
  }
4545
5531
  async function runAttachStage2(params) {
4546
5532
  const { bind, cfg, mgr, reporter, agentGatewayUrl, withVideo, options } = params;
4547
- const { serial, instanceId, keyFile, httpPort } = bind;
5533
+ const { serial, instanceId, keyFile, httpPort, fingerprint: fp } = bind;
4548
5534
  try {
4549
5535
  await removeTargetsForSerial(mgr, serial);
4550
- await persistAgentConfigBestEffort({
5536
+ const persistedAgentConfig = await persistAgentConfigBestEffort({
4551
5537
  agentGatewayUrl,
4552
5538
  keyFile,
4553
5539
  instanceId,
@@ -4561,7 +5547,8 @@ async function runAttachStage2(params) {
4561
5547
  withVideo,
4562
5548
  vncHost: options.vncHost,
4563
5549
  vncPort: options.vncPort,
4564
- vncPassword: options.vncPassword
5550
+ vncPassword: options.vncPassword,
5551
+ agentGatewayUrl
4565
5552
  });
4566
5553
  const entry = {
4567
5554
  serial,
@@ -4572,9 +5559,25 @@ async function runAttachStage2(params) {
4572
5559
  video_mode: bridgeInfo.mode,
4573
5560
  backend: options.vncHost ? void 0 : "adb",
4574
5561
  vnc_host: options.vncHost,
4575
- 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
+ }
4576
5580
  };
4577
- return { ok: true, serial, entry, videoMode: bridgeInfo.mode };
4578
5581
  } catch (e) {
4579
5582
  await rollbackDeviceBind(cfg.platform.api_url, instanceId);
4580
5583
  return { ok: false, serial, error: e };
@@ -4686,7 +5689,7 @@ async function runDeviceAgentFleetEnable(cfg) {
4686
5689
  }
4687
5690
  async function registerVideoBridge(_mgr, reporter, params) {
4688
5691
  if (!params.withVideo) return { mode: "none" };
4689
- const agentGatewayUrl = resolveAgentGatewayUrl(params.cfg);
5692
+ const agentGatewayUrl = params.agentGatewayUrl ?? resolveAgentGatewayUrl(params.cfg);
4690
5693
  if (params.vncHost) {
4691
5694
  const binary2 = await ensureBridgeBinaryDegraded(
4692
5695
  vncBridgeRuntime.ensureInstalled.bind(vncBridgeRuntime),
@@ -4739,23 +5742,80 @@ async function ensureBridgeBinaryDegraded(fn, reporter, label) {
4739
5742
  return null;
4740
5743
  }
4741
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
+ }
4742
5753
  async function ensureAdbAvailable(reporter) {
4743
5754
  const existing = await findAdb();
4744
5755
  if (existing) return;
4745
5756
  if (process.env.BEEOS_SKIP_ADB_INSTALL === "1") {
4746
- throw new Error(
4747
- "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."
4748
- );
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
+ }
4749
5781
  }
4750
5782
  console.log("adb not found \u2014 downloading Android platform-tools (one-time, ~10MB)...");
4751
5783
  try {
4752
5784
  await ensureAdb(reporter, { autoInstall: true });
4753
5785
  } catch (e) {
4754
- throw new Error(
4755
- "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.",
4756
- { 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.`
4757
5801
  );
5802
+ return true;
4758
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());
4759
5819
  }
4760
5820
  var init_attach = __esm({
4761
5821
  "src/commands/device/attach.ts"() {
@@ -4764,7 +5824,7 @@ var init_attach = __esm({
4764
5824
  init_progress2();
4765
5825
  init_fallback_banner();
4766
5826
  init_fleet_notify();
4767
- init_state();
5827
+ init_state2();
4768
5828
  }
4769
5829
  });
4770
5830
 
@@ -4816,7 +5876,7 @@ var init_detach = __esm({
4816
5876
  "use strict";
4817
5877
  init_dist();
4818
5878
  init_fleet_notify();
4819
- init_state();
5879
+ init_state2();
4820
5880
  }
4821
5881
  });
4822
5882
 
@@ -4858,7 +5918,7 @@ var init_list = __esm({
4858
5918
  "src/commands/device/list.ts"() {
4859
5919
  "use strict";
4860
5920
  init_dist();
4861
- init_state();
5921
+ init_state2();
4862
5922
  }
4863
5923
  });
4864
5924
 
@@ -4894,14 +5954,49 @@ var init_exec = __esm({
4894
5954
  "src/commands/device/exec.ts"() {
4895
5955
  "use strict";
4896
5956
  init_dist();
4897
- init_state();
5957
+ init_state2();
4898
5958
  }
4899
5959
  });
4900
5960
 
4901
5961
  // src/commands/device/upgrade.ts
4902
5962
  async function upgrade(options = {}) {
4903
5963
  const reporter = new CliReporter();
4904
- 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
+ }
4905
6000
  if (options.bridges !== false) {
4906
6001
  try {
4907
6002
  await scrcpyBridgeRuntime.upgrade(reporter);
@@ -4915,11 +6010,12 @@ async function upgrade(options = {}) {
4915
6010
  }
4916
6011
  }
4917
6012
  }
4918
- var init_upgrade = __esm({
6013
+ var init_upgrade2 = __esm({
4919
6014
  "src/commands/device/upgrade.ts"() {
4920
6015
  "use strict";
4921
6016
  init_dist();
4922
6017
  init_progress2();
6018
+ init_fleet_notify();
4923
6019
  }
4924
6020
  });
4925
6021
 
@@ -4933,7 +6029,6 @@ async function refreshConfig(options = {}) {
4933
6029
  console.log("No matching devices in ~/.beeos/devices.json \u2014 nothing to refresh.");
4934
6030
  return;
4935
6031
  }
4936
- const mgr = await getServiceManager();
4937
6032
  let okCount = 0;
4938
6033
  let failCount = 0;
4939
6034
  for (const entry of targets) {
@@ -4951,27 +6046,44 @@ async function refreshConfig(options = {}) {
4951
6046
  failCount++;
4952
6047
  continue;
4953
6048
  }
4954
- const file = await persistAgentConfigBestEffort({
6049
+ const result = await persistAgentConfigBestEffort({
4955
6050
  agentGatewayUrl,
4956
6051
  keyFile: entry.key_file,
4957
6052
  instanceId: entry.instance_id,
4958
6053
  reporter
4959
6054
  });
4960
- if (!file) {
6055
+ if (!result) {
4961
6056
  console.error(
4962
6057
  ` Refresh failed for ${entry.serial} (see warning above) \u2014 config file left untouched.`
4963
6058
  );
4964
6059
  failCount++;
4965
6060
  continue;
4966
6061
  }
4967
- console.log(` Wrote ${file}`);
4968
- try {
4969
- 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") {
4970
6080
  console.log(` Restarted device-agent for ${entry.serial}`);
4971
- } catch (e) {
4972
- console.error(
4973
- ` 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`
4974
6084
  );
6085
+ } else {
6086
+ console.error(` fleet restart for ${entry.serial} returned an error`);
4975
6087
  }
4976
6088
  okCount++;
4977
6089
  }
@@ -5002,7 +6114,8 @@ var init_refresh_config = __esm({
5002
6114
  "use strict";
5003
6115
  init_dist();
5004
6116
  init_progress2();
5005
- init_state();
6117
+ init_fleet_notify();
6118
+ init_state2();
5006
6119
  }
5007
6120
  });
5008
6121
 
@@ -5024,9 +6137,9 @@ var init_device2 = __esm({
5024
6137
  init_detach();
5025
6138
  init_list();
5026
6139
  init_exec();
5027
- init_upgrade();
6140
+ init_upgrade2();
5028
6141
  init_refresh_config();
5029
- init_state();
6142
+ init_state2();
5030
6143
  }
5031
6144
  });
5032
6145
 
@@ -5039,7 +6152,7 @@ import {
5039
6152
  execFile as execFile6,
5040
6153
  spawn as nodeSpawn
5041
6154
  } from "child_process";
5042
- import fs7 from "fs";
6155
+ import fs8 from "fs";
5043
6156
  import fsp from "fs/promises";
5044
6157
  import net from "net";
5045
6158
  import os5 from "os";
@@ -5146,11 +6259,11 @@ var NodePlatformAdapter = class {
5146
6259
  let stderrFd;
5147
6260
  if (options?.stdoutFile) {
5148
6261
  await fsp.mkdir(path8.dirname(options.stdoutFile), { recursive: true });
5149
- stdoutFd = fs7.openSync(options.stdoutFile, "a");
6262
+ stdoutFd = fs8.openSync(options.stdoutFile, "a");
5150
6263
  }
5151
6264
  if (options?.stderrFile) {
5152
6265
  await fsp.mkdir(path8.dirname(options.stderrFile), { recursive: true });
5153
- stderrFd = options.stderrFile === options.stdoutFile ? stdoutFd : fs7.openSync(options.stderrFile, "a");
6266
+ stderrFd = options.stderrFile === options.stdoutFile ? stdoutFd : fs8.openSync(options.stderrFile, "a");
5154
6267
  }
5155
6268
  const child = nodeSpawn(cmd, args, {
5156
6269
  cwd: options?.cwd,
@@ -5167,8 +6280,8 @@ var NodePlatformAdapter = class {
5167
6280
  if (options?.detached) {
5168
6281
  child.unref();
5169
6282
  }
5170
- if (stdoutFd != null) fs7.closeSync(stdoutFd);
5171
- if (stderrFd != null && stderrFd !== stdoutFd) fs7.closeSync(stderrFd);
6283
+ if (stdoutFd != null) fs8.closeSync(stdoutFd);
6284
+ if (stderrFd != null && stderrFd !== stdoutFd) fs8.closeSync(stderrFd);
5172
6285
  const pid = child.pid;
5173
6286
  if (pid == null) {
5174
6287
  throw new Error(`Failed to spawn process: ${cmd} ${args.join(" ")}`);
@@ -5250,6 +6363,17 @@ init_dist();
5250
6363
  init_progress2();
5251
6364
  init_fallback_banner();
5252
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
5253
6377
  async function run(agentFramework, options) {
5254
6378
  const p = getPlatformAdapter();
5255
6379
  await ensureDirs();
@@ -5258,9 +6382,12 @@ async function run(agentFramework, options) {
5258
6382
  const descriptor = frameworkById(agentFramework);
5259
6383
  if (!descriptor || descriptor.status !== "available") {
5260
6384
  const avail = availableFrameworks().map((f) => f.id).join(", ");
5261
- throw new Error(
5262
- `Agent framework '${agentFramework}' is not available. Available: ${avail}.`
5263
- );
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
+ });
5264
6391
  }
5265
6392
  const driver = descriptor.driver;
5266
6393
  const identity = await loadOrCreateIdentity();
@@ -5268,12 +6395,17 @@ async function run(agentFramework, options) {
5268
6395
  const pubkey = publicKeyB64(identity);
5269
6396
  const keyFile = p.joinPath(beeoHome(), "identity", "keypair.json");
5270
6397
  const agentGatewayUrl = resolveAgentGatewayUrl(cfg);
5271
- const probe = await identifyGateway({ expectedFingerprint: fp });
5272
- if (!options.force && (probe.state === "foreign" || probe.state === "unknown")) {
5273
- 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})`;
5274
- throw new Error(
5275
- `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).`
5276
- );
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
+ }
5277
6409
  }
5278
6410
  reporter.onStatus(`Preparing ${driver.displayName}`);
5279
6411
  const ctx = {
@@ -5283,29 +6415,11 @@ async function run(agentFramework, options) {
5283
6415
  locationPreference: "auto",
5284
6416
  versionOverride: options.version
5285
6417
  };
5286
- 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;
5287
6421
  reporter.stop();
5288
- const mgr = await getServiceManager();
5289
- maybePrintFallbackBanner(mgr.kind);
5290
- const status2 = await mgr.install(spec);
5291
- if (spec.healthcheck) {
5292
- try {
5293
- await waitForHealthcheck(spec.healthcheck, 45e3);
5294
- } catch (err) {
5295
- if (err instanceof HealthcheckTimeoutError && hasStartupDiagnostics(driver)) {
5296
- const diag = await driver.diagnoseStartup(serviceLogPath(driver.id));
5297
- if (diag) {
5298
- throw new Error(
5299
- `${diag.reason}
5300
- ${diag.hints.map((h) => ` \u2022 ${h}`).join("\n")}`
5301
- );
5302
- }
5303
- }
5304
- throw err;
5305
- }
5306
- }
5307
6422
  const hostname = buildHostname();
5308
- const gatewayPid = status2.pid ?? void 0;
5309
6423
  const cachedBinding = await loadBindingInfo();
5310
6424
  const outcome = await bindAgent({
5311
6425
  apiUrl: cfg.platform.api_url,
@@ -5320,52 +6434,94 @@ ${diag.hints.map((h) => ` \u2022 ${h}`).join("\n")}`
5320
6434
  instance_id: cachedBinding.instance_id
5321
6435
  } : null
5322
6436
  });
5323
- if (outcome.status === "bound_offline") {
5324
- emit(options.json, {
5325
- status: "bound_offline",
5326
- public_key: pubkey,
5327
- fingerprint: fp,
5328
- gateway_pid: gatewayPid,
5329
- agent_gateway_url: agentGatewayUrl,
5330
- instance_id: outcome.instanceId
5331
- }, `Agent running (offline, cached instance: ${outcome.instanceId})`);
5332
- return;
5333
- }
5334
6437
  if (outcome.status === "pending_expired") {
5335
- throw new Error(
5336
- "Bind approval timed out \u2014 please re-run `beeos start` after approving in the dashboard."
5337
- );
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
+ });
5338
6444
  }
6445
+ const isOffline = outcome.status === "bound_offline";
5339
6446
  const boundInstanceId = outcome.instanceId;
5340
- try {
5341
- await saveBindingInfo({
5342
- fingerprint: fp,
5343
- instance_id: boundInstanceId,
5344
- bound_at: Math.floor(Date.now() / 1e3),
5345
- framework: agentFramework
5346
- });
5347
- emit(options.json, {
5348
- status: "bound",
5349
- public_key: pubkey,
5350
- fingerprint: fp,
5351
- gateway_pid: gatewayPid,
5352
- agent_gateway_url: agentGatewayUrl,
5353
- instance_id: boundInstanceId
5354
- }, `Agent bound to instance: ${boundInstanceId}`);
5355
- } catch (e) {
6447
+ if (!isOffline) {
5356
6448
  try {
5357
- await deleteInstance(cfg.platform.api_url, boundInstanceId);
5358
- } 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
+ });
5359
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) {
5360
6496
  try {
5361
- await removeBindingInfo();
5362
- } 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;
5363
6512
  }
5364
- throw new Error(
5365
- `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}`,
5366
- { cause: e }
5367
- );
5368
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);
5369
6525
  }
5370
6526
  function buildHostname() {
5371
6527
  const machine = os6.hostname();
@@ -5374,11 +6530,22 @@ function buildHostname() {
5374
6530
  }
5375
6531
  function emit(json, payload, human) {
5376
6532
  if (json) {
5377
- console.log(JSON.stringify(payload, null, 2));
6533
+ emitJsonEnvelope(jsonOk(payload));
5378
6534
  } else if (human) {
5379
6535
  console.log(human);
5380
6536
  }
5381
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
+ }
5382
6549
 
5383
6550
  // src/commands/stop.ts
5384
6551
  init_dist();
@@ -5397,30 +6564,56 @@ async function run2(agentFramework) {
5397
6564
 
5398
6565
  // src/commands/status.ts
5399
6566
  init_dist();
5400
- async function run3() {
6567
+ async function run3(options = {}) {
5401
6568
  const home = beeoHome();
5402
6569
  const cfg = await loadOrCreateConfig();
5403
- console.log(`BeeOS Home: ${home}`);
5404
- console.log(`API URL: ${cfg.platform.api_url}`);
5405
- console.log(`Agent Gateway: ${cfg.platform.agent_gateway_url}`);
5406
- console.log("");
5407
6570
  const p = getPlatformAdapter();
5408
6571
  const fpPath = p.joinPath(home, "identity", "fingerprint");
6572
+ let fingerprint2 = null;
5409
6573
  if (await p.exists(fpPath)) {
5410
6574
  const fp = (await p.readFile(fpPath)).trim();
5411
- console.log(`Identity fingerprint: ${fp}`);
5412
- } else {
5413
- console.log("Identity: not yet created");
6575
+ if (fp) fingerprint2 = fp;
5414
6576
  }
5415
- console.log("");
5416
6577
  const mgr = await getServiceManager();
5417
- const reason = activeFallbackReason();
5418
- console.log(`Service manager: ${mgr.kind}${reason ? ` (${reason})` : ""}`);
6578
+ const fallbackReason2 = activeFallbackReason();
5419
6579
  let services = [];
5420
6580
  try {
5421
6581
  services = await mgr.list();
5422
6582
  } catch {
5423
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})` : ""}`);
5424
6617
  if (services.length === 0) {
5425
6618
  console.log(" (no services registered)");
5426
6619
  } else {
@@ -5437,20 +6630,30 @@ function formatService(s) {
5437
6630
  // src/commands/update.ts
5438
6631
  init_dist();
5439
6632
  init_progress2();
5440
- async function run4(agentFramework) {
6633
+ async function run4(agentFramework, options = {}) {
5441
6634
  if (agentFramework === "device") {
5442
6635
  const reporter = new CliReporter();
5443
6636
  await deviceRuntime.update(reporter);
5444
6637
  reporter.stop();
5445
- console.log("device updated");
6638
+ if (options.json) {
6639
+ emitJsonEnvelope(jsonOk({ framework: "device", action: "updated" }));
6640
+ } else {
6641
+ console.log("device updated");
6642
+ }
5446
6643
  return;
5447
6644
  }
5448
6645
  const descriptor = frameworkById(agentFramework);
5449
6646
  if (!descriptor || descriptor.status !== "available") {
5450
6647
  const avail = availableFrameworks().map((f) => f.id).join(", ");
5451
- throw new Error(
5452
- `Unknown agent framework '${agentFramework}'. Available: ${avail}.`
5453
- );
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
+ });
5454
6657
  }
5455
6658
  const mgr = await getServiceManager();
5456
6659
  const services = await mgr.list().catch(() => []);
@@ -5459,7 +6662,11 @@ async function run4(agentFramework) {
5459
6662
  await run2(agentFramework);
5460
6663
  }
5461
6664
  await run(agentFramework, { force: true });
5462
- console.log(`${agentFramework} updated`);
6665
+ if (options.json) {
6666
+ emitJsonEnvelope(jsonOk({ framework: agentFramework, action: "updated" }));
6667
+ } else {
6668
+ console.log(`${agentFramework} updated`);
6669
+ }
5463
6670
  }
5464
6671
 
5465
6672
  // src/commands/bind-status.ts
@@ -5474,21 +6681,39 @@ async function run5(bindId, options) {
5474
6681
  if (await p.exists(fpPath)) {
5475
6682
  const fp = (await p.readFile(fpPath)).trim();
5476
6683
  if (fp) {
6684
+ const boundAt = Math.floor(Date.now() / 1e3);
6685
+ const cached2 = await safeLoadBindingInfo();
6686
+ const framework = cached2?.framework?.trim() || "openclaw";
5477
6687
  await saveBindingInfo({
5478
6688
  fingerprint: fp,
5479
6689
  instance_id: resp.instance_id,
5480
- bound_at: Math.floor(Date.now() / 1e3)
6690
+ bound_at: boundAt,
6691
+ framework
5481
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
+ );
5482
6705
  }
5483
6706
  }
5484
6707
  } catch {
5485
6708
  }
5486
6709
  }
5487
6710
  if (options.json) {
5488
- console.log(JSON.stringify({
5489
- status: resp.status,
5490
- instance_id: resp.instance_id ?? null
5491
- }, null, 2));
6711
+ emitJsonEnvelope(
6712
+ jsonOk({
6713
+ status: resp.status,
6714
+ instance_id: resp.instance_id ?? null
6715
+ })
6716
+ );
5492
6717
  } else {
5493
6718
  console.log(`Bind status: ${resp.status}`);
5494
6719
  if (resp.instance_id) {
@@ -5496,6 +6721,13 @@ async function run5(bindId, options) {
5496
6721
  }
5497
6722
  }
5498
6723
  }
6724
+ async function safeLoadBindingInfo() {
6725
+ try {
6726
+ return await loadBindingInfo();
6727
+ } catch {
6728
+ return null;
6729
+ }
6730
+ }
5499
6731
 
5500
6732
  // src/index.ts
5501
6733
  init_device2();
@@ -5506,7 +6738,7 @@ import readline2 from "readline";
5506
6738
 
5507
6739
  // src/commands/doctor.ts
5508
6740
  init_dist();
5509
- import fs8 from "fs/promises";
6741
+ import fs9 from "fs/promises";
5510
6742
  import path9 from "path";
5511
6743
  var LOG_SIZE_WARN_BYTES = 50 * 1024 * 1024;
5512
6744
  async function run6(options) {
@@ -5523,7 +6755,8 @@ async function run6(options) {
5523
6755
  }
5524
6756
  const resolvedAgentGatewayUrl = resolveAgentGatewayUrl(cfg);
5525
6757
  const agentGatewayHealth = await probeAgentGateway(resolvedAgentGatewayUrl);
5526
- const tools = await collectToolStatus(p);
6758
+ const tools = await collectToolStatus();
6759
+ const shimReport = await collectShimReport();
5527
6760
  const warnings = [];
5528
6761
  const hints = [];
5529
6762
  if (!state.hasIdentity) {
@@ -5532,6 +6765,14 @@ async function run6(options) {
5532
6765
  if (state.openclaw.gatewayRunning && !state.openclaw.found) {
5533
6766
  warnings.push("port 18789 is in use but no OpenClaw install detected \u2014 another app may conflict");
5534
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
+ }
5535
6776
  if (state.devices.entries.length > 0 && services.length === 0) {
5536
6777
  warnings.push(
5537
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."
@@ -5574,9 +6815,19 @@ async function run6(options) {
5574
6815
  "adb not found \u2014 `beeos device attach` will download Android platform-tools on first use"
5575
6816
  );
5576
6817
  }
5577
- if (!tools.python.path) {
5578
- warnings.push(
5579
- "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"
5580
6831
  );
5581
6832
  }
5582
6833
  const bloatedLogs = await checkLogFileSizes();
@@ -5587,29 +6838,26 @@ async function run6(options) {
5587
6838
  hints.push(`truncate safely: : > ${l.path}`);
5588
6839
  }
5589
6840
  if (options.json) {
5590
- console.log(
5591
- JSON.stringify(
5592
- {
5593
- platform: p.platform(),
5594
- arch: process.arch,
5595
- node: process.version,
5596
- beeosHome: state.beeosHome,
5597
- config: cfg,
5598
- state,
5599
- serviceManager: {
5600
- kind: mgr.kind,
5601
- available: serviceCheck.available,
5602
- fallbackReason: fallbackReason2,
5603
- services
5604
- },
5605
- agentGateway: agentGatewayHealth,
5606
- tools,
5607
- warnings,
5608
- 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
5609
6854
  },
5610
- null,
5611
- 2
5612
- )
6855
+ agentGateway: agentGatewayHealth,
6856
+ tools,
6857
+ npmShims: shimReport,
6858
+ warnings,
6859
+ hints
6860
+ })
5613
6861
  );
5614
6862
  return;
5615
6863
  }
@@ -5633,10 +6881,19 @@ async function run6(options) {
5633
6881
  console.log(` - ${s.id.padEnd(28)} ${status2}${pid}`);
5634
6882
  }
5635
6883
  console.log(` adb : ${tools.adb.path ?? "not found"}`);
5636
- console.log(` python3 : ${tools.python.path ?? "not found"}`);
5637
6884
  console.log(` scrcpy-bridge: ${tools.scrcpyBridge.path ?? "not installed (auto on attach)"}`);
5638
6885
  console.log(` vnc-bridge : ${tools.vncBridge.path ?? "not installed (auto on --vnc-host)"}`);
5639
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("");
5640
6897
  if (warnings.length > 0) {
5641
6898
  console.log(" Warnings:");
5642
6899
  for (const w of warnings) {
@@ -5654,38 +6911,57 @@ async function run6(options) {
5654
6911
  console.log("");
5655
6912
  }
5656
6913
  }
5657
- async function collectToolStatus(p) {
5658
- const [adbPath, pythonPath, scrcpyPath, vncPath] = await Promise.all([
6914
+ async function collectToolStatus() {
6915
+ const [adbPath, scrcpyPath, vncPath] = await Promise.all([
5659
6916
  findAdb().catch(() => null),
5660
- findPython(p).catch(() => null),
5661
6917
  scrcpyBridgeRuntime.findBinary().catch(() => null),
5662
6918
  vncBridgeRuntime.findBinary().catch(() => null)
5663
6919
  ]);
5664
6920
  return {
5665
6921
  adb: { path: adbPath },
5666
- python: { path: pythonPath },
5667
6922
  scrcpyBridge: { path: scrcpyPath },
5668
6923
  vncBridge: { path: vncPath }
5669
6924
  };
5670
6925
  }
5671
- async function findPython(p) {
5672
- const whichCmd = p.platform() === "win32" ? "where" : "which";
5673
- for (const bin of ["python3", "python"]) {
5674
- try {
5675
- const r = await p.exec(whichCmd, [bin]);
5676
- if (r.code === 0 && r.stdout.trim()) {
5677
- 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";
5678
6942
  }
5679
- } catch {
5680
- }
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 "?";
5681
6958
  }
5682
- return null;
5683
6959
  }
5684
6960
  async function checkLogFileSizes() {
5685
6961
  const dir = path9.join(beeoHome(), "logs", "services");
5686
6962
  let entries;
5687
6963
  try {
5688
- entries = await fs8.readdir(dir);
6964
+ entries = await fs9.readdir(dir);
5689
6965
  } catch {
5690
6966
  return [];
5691
6967
  }
@@ -5694,7 +6970,7 @@ async function checkLogFileSizes() {
5694
6970
  if (!name.endsWith(".log")) continue;
5695
6971
  const full = path9.join(dir, name);
5696
6972
  try {
5697
- const st = await fs8.stat(full);
6973
+ const st = await fs9.stat(full);
5698
6974
  if (st.isFile() && st.size >= LOG_SIZE_WARN_BYTES) {
5699
6975
  bloated.push({ path: full, size: st.size });
5700
6976
  }
@@ -5763,6 +7039,7 @@ function formatAgentGatewayStatus(h) {
5763
7039
  }
5764
7040
 
5765
7041
  // src/commands/init.ts
7042
+ init_progress2();
5766
7043
  init_fallback_banner();
5767
7044
  async function run7(options) {
5768
7045
  await ensureDirs();
@@ -5793,21 +7070,26 @@ async function run7(options) {
5793
7070
  return;
5794
7071
  }
5795
7072
  if (decision === "upgrade") {
5796
- 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");
5797
7078
  }
5798
7079
  if (decision === "rebind-new-key") {
5799
7080
  await rotateIdentity();
5800
- }
5801
- if (decision === "rebind-keep-key" || decision === "rebind-new-key") {
5802
7081
  await removeBindingInfo();
5803
7082
  }
5804
7083
  const frameworkId = await decideFramework(state, decision, options);
5805
7084
  const descriptor = frameworkById(frameworkId);
5806
7085
  if (!descriptor || descriptor.status !== "available") {
5807
7086
  const avail = availableFrameworks().map((f) => f.id).join(", ");
5808
- throw new Error(
5809
- `Agent framework '${frameworkId}' is not available. Available: ${avail}.`
5810
- );
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
+ });
5811
7093
  }
5812
7094
  await run(descriptor.id, {
5813
7095
  force: true,
@@ -5851,7 +7133,7 @@ async function decideFramework(state, decision, options) {
5851
7133
  if (options.framework && options.framework.trim()) {
5852
7134
  return options.framework.trim();
5853
7135
  }
5854
- if (state.binding && (decision === "upgrade" || decision === "rebind-keep-key")) {
7136
+ if (state.binding && decision === "upgrade") {
5855
7137
  const persisted = state.binding.framework?.trim();
5856
7138
  if (persisted) return persisted;
5857
7139
  return defaultFrameworkId();
@@ -5955,6 +7237,60 @@ function prompt(question) {
5955
7237
  });
5956
7238
  });
5957
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
+ }
5958
7294
 
5959
7295
  // src/commands/service.ts
5960
7296
  init_dist();
@@ -5963,18 +7299,14 @@ async function install(options) {
5963
7299
  const check = await mgr.selfCheck();
5964
7300
  const reason = activeFallbackReason();
5965
7301
  if (options.json) {
5966
- console.log(
5967
- JSON.stringify(
5968
- {
5969
- ok: check.available,
5970
- manager: mgr.kind,
5971
- fallbackReason: reason,
5972
- warnings: check.warnings,
5973
- hints: check.hints
5974
- },
5975
- null,
5976
- 2
5977
- )
7302
+ emitJsonEnvelope(
7303
+ jsonOk({
7304
+ available: check.available,
7305
+ manager: mgr.kind,
7306
+ fallbackReason: reason,
7307
+ warnings: check.warnings,
7308
+ hints: check.hints
7309
+ })
5978
7310
  );
5979
7311
  return;
5980
7312
  }
@@ -6008,7 +7340,7 @@ async function uninstall(options) {
6008
7340
  }
6009
7341
  }
6010
7342
  if (options.json) {
6011
- console.log(JSON.stringify({ removed, errors }, null, 2));
7343
+ emitJsonEnvelope(jsonOk({ removed, errors }));
6012
7344
  return;
6013
7345
  }
6014
7346
  if (removed.length === 0 && errors.length === 0) {
@@ -6027,12 +7359,8 @@ async function status(options) {
6027
7359
  } catch {
6028
7360
  }
6029
7361
  if (options.json) {
6030
- console.log(
6031
- JSON.stringify(
6032
- { manager: mgr.kind, fallbackReason: reason, services },
6033
- null,
6034
- 2
6035
- )
7362
+ emitJsonEnvelope(
7363
+ jsonOk({ manager: mgr.kind, fallbackReason: reason, services })
6036
7364
  );
6037
7365
  return;
6038
7366
  }
@@ -6055,13 +7383,15 @@ async function run8(options) {
6055
7383
  const mgr = await getServiceManager();
6056
7384
  const mig = await migrateLegacySupervisor(mgr);
6057
7385
  if (options.json) {
6058
- console.log(JSON.stringify({
6059
- ran: mig.ran,
6060
- migrated: mig.migrated,
6061
- backupPath: mig.backupPath ?? null,
6062
- errors: mig.errors,
6063
- serviceManager: mgr.kind
6064
- }, 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
+ );
6065
7395
  return;
6066
7396
  }
6067
7397
  if (!mig.ran) {
@@ -6083,6 +7413,98 @@ async function run8(options) {
6083
7413
  }
6084
7414
  }
6085
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
+
6086
7508
  // src/index.ts
6087
7509
  setPlatformAdapter(new NodePlatformAdapter());
6088
7510
  var program = new Command();
@@ -6091,15 +7513,18 @@ program.name("beeos").version(cliVersion.display).description("BeeOS \u2014 run
6091
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));
6092
7514
  program.command("doctor").description("Inspect local BeeOS state (install, binding, services, warnings)").option("--json", "Output machine-readable JSON", false).action((opts) => run6(opts));
6093
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));
6094
7519
  var serviceCmd = program.command("service").description("Inspect and manage the OS-native service manager (launchd / systemd --user / Task Scheduler)");
6095
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));
6096
7521
  serviceCmd.command("uninstall").description("Uninstall every BeeOS-managed OS service").option("--json", "Output machine-readable JSON", false).action((opts) => uninstall(opts));
6097
7522
  serviceCmd.command("status").description("List every BeeOS-managed OS service").option("--json", "Output machine-readable JSON", false).action((opts) => status(opts));
6098
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));
6099
7524
  program.command("stop").description("Stop a running agent").argument("<agent_type>", "Agent type to stop").action((agentFramework) => run2(agentFramework));
6100
- program.command("status").description("Show status of managed agents").action(run3);
6101
- program.command("update").description("Update an agent to the latest version").argument("<agent_type>", "Agent type to update").action((agentFramework) => run4(agentFramework));
6102
- 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);
6103
7528
  var deviceCmd = program.command("device").description("Manage device agents (Android/Desktop/ChromeOS)");
6104
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(
6105
7530
  "--no-video",
@@ -6107,7 +7532,10 @@ deviceCmd.command("attach").description("Attach an ADB-connected device as an AI
6107
7532
  ).option(
6108
7533
  "--vnc-host <host>",
6109
7534
  "Use vnc-bridge against a VNC server at this host (desktop/Linux/macOS). Implies video mode = vnc instead of scrcpy."
6110
- ).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);
6111
7539
  deviceCmd.command("detach").description("Detach a device agent").option("--serial <serial>", "ADB device serial number").option("--all", "Detach all devices", false).action(detach);
6112
7540
  deviceCmd.command("list").description("List attached devices").option("--local", "Only show locally running device agents", false).action(list);
6113
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);
@@ -6116,6 +7544,15 @@ deviceCmd.command("refresh-config").description(
6116
7544
  "Re-fetch this device's VLM/ARouter config from Agent Gateway and rewrite ~/.beeos/<instance_id>.config.json (use after dashboard changes)."
6117
7545
  ).option("--serial <serial>", "ADB device serial number").option("--all", "Refresh every attached device", false).action(refreshConfig);
6118
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
+ }
6119
7556
  console.error(err.message);
6120
7557
  process.exit(1);
6121
7558
  });