@beeos-ai/cli 1.0.7 → 1.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beeos-ai/cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "BeeOS CLI — run AI agents from your desktop",
6
6
  "bin": {
@@ -33,6 +33,26 @@ $MinNodeVersion = 18
33
33
 
34
34
  $CliPackage = if ($Version -eq "latest") { "@beeos-ai/cli@latest" } else { "@beeos-ai/cli@$Version" }
35
35
 
36
+ # Sibling packages installed alongside the CLI so that `beeos device attach`
37
+ # works on a clean machine without further `npm i -g` steps (a.k.a. the
38
+ # "fleet ready" install strategy — see agents/device-agent/ARCHITECTURE.md
39
+ # for the 0.2.0+ sibling-pair topology between device-agent and
40
+ # device-mcp-server). Versions are independent of `-Version` (which only
41
+ # pins the CLI) — both packages follow their own semver. Override via
42
+ # $env:BEEOS_DEVICE_AGENT_VERSION / $env:BEEOS_MCP_SERVER_VERSION when you
43
+ # need to pin a specific build (e.g. for a staging smoke test).
44
+ $DeviceAgentVersion = if ($env:BEEOS_DEVICE_AGENT_VERSION) { $env:BEEOS_DEVICE_AGENT_VERSION } else { "latest" }
45
+ $McpServerVersion = if ($env:BEEOS_MCP_SERVER_VERSION) { $env:BEEOS_MCP_SERVER_VERSION } else { "latest" }
46
+ $DeviceAgentPackage = "@beeos-ai/device-agent@$DeviceAgentVersion"
47
+ $McpServerPackage = "@beeos-ai/device-mcp-server@$McpServerVersion"
48
+
49
+ # Telemetry endpoint (override with $env:BEEOS_API_URL for staging).
50
+ if (-not $env:BEEOS_API_URL -or $env:BEEOS_API_URL.Length -eq 0) {
51
+ $script:BeeosApiUrl = "https://public-api.beeos.ai"
52
+ } else {
53
+ $script:BeeosApiUrl = $env:BEEOS_API_URL
54
+ }
55
+
36
56
  # ── Helpers ──────────────────────────────────────────────────
37
57
 
38
58
  function Write-BeeInfo { param([string]$Msg) Write-Host "[beeos] $Msg" -ForegroundColor Cyan }
@@ -40,14 +60,87 @@ function Write-BeeWarn { param([string]$Msg) Write-Host "[beeos] $Msg" -Foregro
40
60
  function Write-BeeError { param([string]$Msg) Write-Host "[beeos] $Msg" -ForegroundColor Red }
41
61
  function Write-BeeOk { param([string]$Msg) Write-Host "[beeos] $Msg" -ForegroundColor Green }
42
62
 
63
+ # ── Script-level telemetry (mirror of install.sh send_telemetry) ──
64
+ #
65
+ # Fire-and-forget POST to `/api/v1/telemetry/install`, capped at 2 s,
66
+ # error-swallowing. Matches `web/packages/cli/src/telemetry.ts` event
67
+ # names so dashboard queries don't have to special-case Windows.
68
+ #
69
+ # Opt-out: `$env:BEEOS_NO_TELEMETRY = "1"`.
70
+ function Send-Telemetry {
71
+ param(
72
+ [string]$Event,
73
+ [string]$ErrorCode = "",
74
+ [bool]$Success = $true
75
+ )
76
+ if ($env:BEEOS_NO_TELEMETRY -eq "1") { return }
77
+
78
+ $osArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
79
+ if (-not $osArch) { $osArch = "unknown" }
80
+ $nodeVer = ""
81
+ try {
82
+ $raw = & node -v 2>$null
83
+ if ($raw) { $nodeVer = $raw -replace '^v', '' }
84
+ } catch {}
85
+
86
+ $hostname = $env:COMPUTERNAME
87
+ if (-not $hostname) { $hostname = "unknown" }
88
+
89
+ $payload = @{
90
+ event = $Event
91
+ cli_version = ""
92
+ dist_tag = ""
93
+ os = "win32"
94
+ arch = $osArch
95
+ node_version = $nodeVer
96
+ success = $Success
97
+ error_code = $ErrorCode
98
+ host_hint = "$hostname-$osArch"
99
+ } | ConvertTo-Json -Compress
100
+
101
+ try {
102
+ # `-TimeoutSec 2` + `-UseBasicParsing` mirrors `curl --max-time 2`.
103
+ # Errors are fully swallowed (no Stop propagation).
104
+ Invoke-RestMethod `
105
+ -Method POST `
106
+ -Uri "$($script:BeeosApiUrl.TrimEnd('/'))/api/v1/telemetry/install" `
107
+ -ContentType "application/json" `
108
+ -Body $payload `
109
+ -TimeoutSec 2 `
110
+ -UseBasicParsing `
111
+ -ErrorAction SilentlyContinue | Out-Null
112
+ } catch {
113
+ # Telemetry MUST NOT break installs.
114
+ }
115
+ }
116
+
43
117
  # ── Platform detection ───────────────────────────────────────
44
118
 
45
119
  function Test-Platform {
46
120
  $arch = $env:PROCESSOR_ARCHITECTURE
47
121
  $effectiveArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $arch }
48
122
  Write-BeeInfo "detected: win32 $effectiveArch"
123
+
124
+ # Architecture policy (kept in sync with install.sh):
125
+ # - Non-interactive: hard exit 2 — silent warnings are invisible
126
+ # in piped installs (`irm | iex`) and would trap users on a
127
+ # platform we don't ship native sidecars for.
128
+ # - Interactive: warn and require explicit confirmation.
49
129
  if ($effectiveArch -notin @("AMD64","ARM64","x86_64")) {
50
- Write-BeeWarn "Unusual CPU architecture: $effectiveArch. Some sidecar binaries may be unavailable."
130
+ $reason = "Unsupported Windows architecture: $effectiveArch (need AMD64 or ARM64)"
131
+ Write-BeeWarn $reason
132
+ if (-not [Environment]::UserInteractive) {
133
+ Send-Telemetry -Event "install.bootstrap.arch_fail" -ErrorCode $reason -Success $false
134
+ Write-BeeError "Non-interactive shell — aborting."
135
+ exit 2
136
+ }
137
+ $go = Read-Host "Continue anyway? [y/N]"
138
+ if ($go -notmatch '^(y|yes)$') {
139
+ Send-Telemetry -Event "install.bootstrap.arch_fail" -ErrorCode $reason -Success $false
140
+ Write-BeeError "Aborting per user."
141
+ exit 2
142
+ }
143
+ Write-BeeInfo "Proceeding on unsupported arch by user confirmation."
51
144
  }
52
145
  }
53
146
 
@@ -174,6 +267,39 @@ function Read-ExistingInstallAction {
174
267
  }
175
268
  }
176
269
 
270
+ # ── Device-agent sibling suite installer ─────────────────────
271
+ #
272
+ # Installs `@beeos-ai/device-agent` + `@beeos-ai/device-mcp-server` globally
273
+ # so the very first `beeos device attach` on a clean machine does not need
274
+ # to fetch them separately. `@beeos-ai/device-common` is pulled
275
+ # transitively as a `dependencies` entry of both — no separate install
276
+ # needed.
277
+ #
278
+ # Failure here is NEVER fatal: the CLI itself was already installed
279
+ # successfully, and `beeos device attach` will prompt to install missing
280
+ # pieces via ensureDeviceAgent on first use. We just emit a WARN with the
281
+ # manual recovery command.
282
+ function Install-DeviceAgentSuite {
283
+ $npmPath = Get-Command npm -ErrorAction SilentlyContinue
284
+ if (-not $npmPath) {
285
+ Write-BeeWarn "npm not available — skipping device-agent suite install."
286
+ Write-BeeWarn "Manual fix: install Node + run ``npm i -g $DeviceAgentPackage $McpServerPackage``."
287
+ return
288
+ }
289
+
290
+ Write-BeeInfo "Installing device-agent suite ($DeviceAgentPackage, $McpServerPackage)..."
291
+ & npm install -g $DeviceAgentPackage $McpServerPackage
292
+ if ($LASTEXITCODE -eq 0) {
293
+ Write-BeeOk "device-agent suite installed."
294
+ } else {
295
+ Write-BeeWarn "Failed to install device-agent suite — ``beeos device attach`` will prompt for it on first use."
296
+ Write-BeeWarn "Manual fix: npm i -g $DeviceAgentPackage $McpServerPackage"
297
+ # Reset $LASTEXITCODE so subsequent `if ($LASTEXITCODE -ne 0)` checks
298
+ # in callers don't trip on this non-fatal failure.
299
+ $global:LASTEXITCODE = 0
300
+ }
301
+ }
302
+
177
303
  # ── Run CLI ──────────────────────────────────────────────────
178
304
 
179
305
  function Invoke-BeeosCli {
@@ -187,6 +313,11 @@ function Invoke-BeeosCli {
187
313
  if ($ExtraArgs) { $args += $ExtraArgs }
188
314
 
189
315
  if ($npxPath) {
316
+ # Temporary / npx path: deliberately do NOT install the device-agent
317
+ # suite globally. The user opted into the throwaway-install flow, so
318
+ # we honor that — if they later run `beeos device attach`,
319
+ # ensureDeviceAgent will guide them through a global install at that
320
+ # point.
190
321
  & npx --yes $CliPackage @args
191
322
  } else {
192
323
  Write-BeeWarn "npx not found, installing globally..."
@@ -200,6 +331,7 @@ function Invoke-BeeosCli {
200
331
  Write-BeeError " - EACCES / permission error: run PowerShell as Administrator"
201
332
  exit 1
202
333
  }
334
+ Install-DeviceAgentSuite
203
335
  & beeos @args
204
336
  }
205
337
  }
@@ -212,13 +344,16 @@ Write-Host " https://beeos.ai" -ForegroundColor DarkGray
212
344
  Write-Host ""
213
345
 
214
346
  Test-Platform
347
+ Send-Telemetry -Event "install.bootstrap.start"
215
348
 
216
349
  if (-not (Test-NodeVersion)) {
217
350
  if (-not (Install-NodeAuto)) {
351
+ Send-Telemetry -Event "install.bootstrap.node_fail" -ErrorCode "node_install_failed" -Success $false
218
352
  Show-InstallHints
219
353
  exit 1
220
354
  }
221
355
  if (-not (Test-NodeVersion)) {
356
+ Send-Telemetry -Event "install.bootstrap.node_fail" -ErrorCode "node_version_too_old" -Success $false
222
357
  Show-InstallHints
223
358
  exit 1
224
359
  }
@@ -247,6 +382,7 @@ if ($beeosCmd) {
247
382
  Write-BeeError " - EACCES / permission error: run PowerShell as Administrator"
248
383
  exit 1
249
384
  }
385
+ Install-DeviceAgentSuite
250
386
  }
251
387
  "rerun" {
252
388
  Write-BeeInfo "Reusing existing install."
@@ -257,6 +393,8 @@ if ($beeosCmd) {
257
393
  Write-BeeInfo "Running BeeOS CLI..."
258
394
  Write-Host ""
259
395
 
396
+ Send-Telemetry -Event "install.bootstrap.success"
397
+
260
398
  if ($Device) {
261
399
  Invoke-BeeosCli -Subcommand "device" -ExtraArgs (@("attach") + $Passthrough)
262
400
  } else {
@@ -27,17 +27,103 @@ RED="\033[31m"
27
27
  CYAN="\033[36m"
28
28
 
29
29
  CLI_PACKAGE="@beeos-ai/cli@latest"
30
+ # Sibling packages installed alongside the CLI so that `beeos device attach`
31
+ # works on a clean machine without further `npm i -g` steps (a.k.a. the
32
+ # "fleet ready" install strategy — see agents/device-agent/ARCHITECTURE.md
33
+ # for the 0.2.0+ sibling-pair topology between device-agent and
34
+ # device-mcp-server). Their versions are independent of `--version` (which
35
+ # only pins the CLI) — both packages follow their own semver. Override via
36
+ # BEEOS_DEVICE_AGENT_VERSION / BEEOS_MCP_SERVER_VERSION when you need to
37
+ # pin a specific build (e.g. for a staging smoke test).
38
+ DEVICE_AGENT_PACKAGE="@beeos-ai/device-agent@${BEEOS_DEVICE_AGENT_VERSION:-latest}"
39
+ MCP_SERVER_PACKAGE="@beeos-ai/device-mcp-server@${BEEOS_MCP_SERVER_VERSION:-latest}"
30
40
  MIN_NODE_VERSION=18
31
41
  NVM_VERSION="v0.40.1"
32
42
  FNM_INSTALL_URL="https://fnm.vercel.app/install"
33
43
 
34
44
  BEEOS_HOME="${BEEOS_HOME:-$HOME/.beeos}"
45
+ # Default telemetry endpoint. `$BEEOS_API_URL` lets developers aim at
46
+ # staging / a local gateway; empty-or-unset falls back to production.
47
+ BEEOS_API_URL="${BEEOS_API_URL:-https://public-api.beeos.ai}"
48
+
49
+ # Where the installer writes verbose stderr for Node auto-install.
50
+ # Overridable via `BEEOS_INSTALL_LOG=/path/to/file.log` so support can
51
+ # ask "run the one-liner with `BEEOS_INSTALL_LOG=~/beeos-install.log`
52
+ # and share the log" rather than asking the user to recreate the
53
+ # failure with manual `bash -x`.
54
+ #
55
+ # Pre-P1-12 we buried every `2>/dev/null` in `try_install_node`, which
56
+ # meant bug reports like "nvm install --lts failed" were unreproducible
57
+ # from anything but the user's terminal. We now preserve stderr in a
58
+ # per-PID file and reference it in the terminal hint on failure.
59
+ BEEOS_INSTALL_LOG="${BEEOS_INSTALL_LOG:-/tmp/beeos-install-$$.log}"
60
+ # Truncate on every run so the log reflects only the current attempt.
61
+ : > "$BEEOS_INSTALL_LOG" 2>/dev/null || true
35
62
 
36
63
  info() { printf "${BOLD}${CYAN}[beeos]${RESET} %s\n" "$*"; }
37
64
  warn() { printf "${BOLD}${YELLOW}[beeos]${RESET} %s\n" "$*"; }
38
65
  error() { printf "${BOLD}${RED}[beeos]${RESET} %s\n" "$*" >&2; }
39
66
  success() { printf "${BOLD}${GREEN}[beeos]${RESET} %s\n" "$*"; }
40
67
 
68
+ # ── Telemetry (script-level, pre-CLI) ────────────────────────
69
+ #
70
+ # Mirrors `web/packages/cli/src/telemetry.ts` so we can capture
71
+ # failures that happen BEFORE the Node CLI even starts (e.g. Node
72
+ # install failure, unsupported arch). Fire-and-forget with a 2 s cap —
73
+ # we must never block a real install on a flaky telemetry endpoint.
74
+ #
75
+ # Event names used here:
76
+ # - install.bootstrap.start — script entered `main()`
77
+ # - install.bootstrap.node_fail — could not install/find Node
78
+ # - install.bootstrap.arch_fail — unsupported arch
79
+ # - install.bootstrap.success — about to exec `beeos`
80
+ #
81
+ # `BEEOS_NO_TELEMETRY=1` short-circuits everything. This matches the
82
+ # Node CLI's opt-out env var verbatim so a user who disabled telemetry
83
+ # for the CLI doesn't unexpectedly get events from the bootstrap.
84
+ send_telemetry() {
85
+ local event="$1"
86
+ local error_code="${2:-}"
87
+ local success_flag="${3:-true}"
88
+
89
+ if [[ "${BEEOS_NO_TELEMETRY:-}" == "1" ]]; then
90
+ return 0
91
+ fi
92
+ if ! command -v curl &>/dev/null; then
93
+ return 0
94
+ fi
95
+
96
+ local os_kind="${OS_KIND:-unknown}"
97
+ local os_arch="${OS_ARCH:-unknown}"
98
+ local node_ver=""
99
+ if command -v node &>/dev/null; then
100
+ node_ver=$(node -v 2>/dev/null | sed 's/^v//' || true)
101
+ fi
102
+ local host_hint
103
+ host_hint="$(hostname 2>/dev/null || echo unknown)-${os_arch}"
104
+
105
+ # POST body kept deliberately small — matches Node CLI payload.
106
+ # Note: `cli_version` is empty until after npx install; we fill it
107
+ # only when the bootstrap telemetry is fired post-install, from the
108
+ # Node side. For pre-install events it stays "" and that's fine.
109
+ local body
110
+ body=$(cat <<JSON
111
+ {"event":"${event}","cli_version":"","dist_tag":"","os":"${os_kind}","arch":"${os_arch}","node_version":"${node_ver}","success":${success_flag},"error_code":"${error_code}","host_hint":"${host_hint}"}
112
+ JSON
113
+ )
114
+
115
+ # `--max-time 2` caps the whole request at 2 s. `-o /dev/null -s`
116
+ # silences output. Swallow non-zero exit — telemetry MUST NOT break
117
+ # an install.
118
+ curl -fsS -o /dev/null \
119
+ --max-time 2 \
120
+ -H "Content-Type: application/json" \
121
+ -X POST \
122
+ -d "$body" \
123
+ "${BEEOS_API_URL%/}/api/v1/telemetry/install" \
124
+ 2>/dev/null || true
125
+ }
126
+
41
127
  # ── Parse arguments ──────────────────────────────────────────
42
128
 
43
129
  CLI_VERSION=""
@@ -102,17 +188,46 @@ detect_platform() {
102
188
 
103
189
  info "detected: ${OS_KIND} ${OS_ARCH}${LIBC_KIND:+ (${LIBC_KIND})}"
104
190
 
191
+ # Architecture policy (kept in sync with install.ps1):
192
+ # - Non-interactive (no TTY): hard exit 2. The user cannot see a
193
+ # warning scroll past, and we must not silently install a CLI
194
+ # that will later fail to download its native sidecars.
195
+ # - Interactive TTY: warn and ask for confirmation. Power users
196
+ # sometimes run on odd arches (RISC-V Linux, IBM z, early
197
+ # Windows-on-ARM) for experimentation; bail out by default, but
198
+ # let them override when they know what they're doing.
199
+ local interactive=0
200
+ if [[ -t 0 ]] || [[ -t 1 ]]; then interactive=1; fi
201
+ local arch_bail arch_confirm
202
+ arch_bail() {
203
+ local reason="$1"
204
+ error "$reason"
205
+ send_telemetry "install.bootstrap.arch_fail" "$reason" false
206
+ exit 2
207
+ }
208
+ arch_confirm() {
209
+ local reason="$1"
210
+ warn "$reason"
211
+ if (( interactive == 0 )); then
212
+ arch_bail "$reason"
213
+ fi
214
+ local yn=""
215
+ read -rp "Continue anyway? [y/N]: " yn </dev/tty 2>/dev/null || yn=""
216
+ case "${yn,,}" in
217
+ y|yes) info "Proceeding on unsupported arch by user confirmation." ;;
218
+ *) arch_bail "$reason" ;;
219
+ esac
220
+ }
221
+
105
222
  case "$OS_KIND" in
106
223
  darwin)
107
224
  if [[ "$OS_ARCH" != "arm64" && "$OS_ARCH" != "x86_64" ]]; then
108
- error "Unsupported macOS architecture: $OS_ARCH (need arm64 or x86_64)"
109
- exit 2
225
+ arch_confirm "Unsupported macOS architecture: $OS_ARCH (need arm64 or x86_64)"
110
226
  fi
111
227
  ;;
112
228
  linux)
113
229
  if [[ "$OS_ARCH" != "arm64" && "$OS_ARCH" != "x86_64" ]]; then
114
- error "Unsupported Linux architecture: $OS_ARCH (need arm64 or x86_64)"
115
- exit 2
230
+ arch_confirm "Unsupported Linux architecture: $OS_ARCH (need arm64 or x86_64)"
116
231
  fi
117
232
  if [[ "$LIBC_KIND" == "musl" ]]; then
118
233
  # No musl prebuilt for scrcpy-bridge / vnc-bridge yet. The CLI
@@ -122,8 +237,7 @@ detect_platform() {
122
237
  fi
123
238
  ;;
124
239
  *)
125
- error "Unsupported OS: $OS_KIND (use install.ps1 for Windows)"
126
- exit 2
240
+ arch_bail "Unsupported OS: $OS_KIND (use install.ps1 for Windows)"
127
241
  ;;
128
242
  esac
129
243
  }
@@ -151,15 +265,26 @@ check_node() {
151
265
 
152
266
  try_install_node() {
153
267
  info "Node.js not found or too old. Attempting automatic installation..."
268
+ info "Verbose output appended to: ${BEEOS_INSTALL_LOG}"
154
269
  echo ""
155
270
 
271
+ # Every step below redirects stderr to $BEEOS_INSTALL_LOG (append)
272
+ # so that a post-mortem has the full picture. We deliberately do NOT
273
+ # use bare `2>/dev/null` anymore — it's the single biggest source of
274
+ # unactionable bug reports in this layer. Steps still short-circuit
275
+ # on success to avoid N-second retries when one tool works.
276
+ {
277
+ echo "=== try_install_node @ $(date -u +%FT%TZ) ==="
278
+ echo "OS_KIND=${OS_KIND} OS_ARCH=${OS_ARCH} LIBC_KIND=${LIBC_KIND:-}"
279
+ } >> "$BEEOS_INSTALL_LOG" 2>&1 || true
280
+
156
281
  # 1. Existing nvm
157
282
  export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
158
283
  if [[ -s "$NVM_DIR/nvm.sh" ]]; then
159
284
  info "Loading existing nvm..."
160
285
  # shellcheck source=/dev/null
161
286
  . "$NVM_DIR/nvm.sh"
162
- if nvm install --lts 2>/dev/null; then
287
+ if nvm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1; then
163
288
  success "Node.js $(node -v) installed via existing nvm"
164
289
  return 0
165
290
  fi
@@ -167,10 +292,11 @@ try_install_node() {
167
292
 
168
293
  # 2. Fresh nvm install
169
294
  info "Installing nvm ${NVM_VERSION}..."
170
- if curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash 2>/dev/null; then
295
+ if curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" 2>>"$BEEOS_INSTALL_LOG" \
296
+ | bash >> "$BEEOS_INSTALL_LOG" 2>&1; then
171
297
  # shellcheck source=/dev/null
172
- . "$NVM_DIR/nvm.sh" 2>/dev/null || true
173
- if nvm install --lts 2>/dev/null; then
298
+ . "$NVM_DIR/nvm.sh" >> "$BEEOS_INSTALL_LOG" 2>&1 || true
299
+ if nvm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1; then
174
300
  success "Node.js $(node -v) installed via nvm"
175
301
  return 0
176
302
  fi
@@ -179,10 +305,12 @@ try_install_node() {
179
305
  # 3. fnm — works on locked-down Linux without git/curl quirks
180
306
  if [[ "$OS_KIND" == "linux" ]] && ! command -v fnm &>/dev/null; then
181
307
  info "Trying fnm (single-user, no sudo)..."
182
- if curl -fsSL "$FNM_INSTALL_URL" | bash -s -- --skip-shell 2>/dev/null; then
308
+ if curl -fsSL "$FNM_INSTALL_URL" 2>>"$BEEOS_INSTALL_LOG" \
309
+ | bash -s -- --skip-shell >> "$BEEOS_INSTALL_LOG" 2>&1; then
183
310
  export PATH="$HOME/.local/share/fnm:$PATH"
184
- eval "$(fnm env --shell bash 2>/dev/null)" || true
185
- if fnm install --lts 2>/dev/null && fnm use lts-latest 2>/dev/null; then
311
+ eval "$(fnm env --shell bash 2>>"$BEEOS_INSTALL_LOG")" || true
312
+ if fnm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1 \
313
+ && fnm use lts-latest >> "$BEEOS_INSTALL_LOG" 2>&1; then
186
314
  success "Node.js $(node -v) installed via fnm"
187
315
  return 0
188
316
  fi
@@ -192,7 +320,7 @@ try_install_node() {
192
320
  # 4. macOS Homebrew
193
321
  if [[ "$OS_KIND" == "darwin" ]] && command -v brew &>/dev/null; then
194
322
  info "Trying Homebrew..."
195
- if brew install node 2>/dev/null; then
323
+ if brew install node >> "$BEEOS_INSTALL_LOG" 2>&1; then
196
324
  success "Node.js installed via Homebrew"
197
325
  return 0
198
326
  fi
@@ -205,6 +333,7 @@ try_install_node() {
205
333
 
206
334
  print_install_hints() {
207
335
  error "Could not install Node.js automatically."
336
+ error "See ${BEEOS_INSTALL_LOG} for full stderr of every attempted method."
208
337
  echo ""
209
338
  info "Please install Node.js >= ${MIN_NODE_VERSION} manually:"
210
339
  echo ""
@@ -294,14 +423,60 @@ prompt_existing_install_action() {
294
423
  esac
295
424
  }
296
425
 
426
+ # ── Device-agent sibling suite installer ─────────────────────
427
+ #
428
+ # Installs `@beeos-ai/device-agent` + `@beeos-ai/device-mcp-server` globally
429
+ # so the very first `beeos device attach` on a clean machine does not need
430
+ # to fetch them separately. `@beeos-ai/device-common` is pulled
431
+ # transitively as a `dependencies` entry of both — no separate install
432
+ # needed.
433
+ #
434
+ # Failure here is NEVER fatal: the CLI itself was already installed
435
+ # successfully, and `beeos device attach` will prompt to install missing
436
+ # pieces via ensureDeviceAgent on first use. We just emit a WARN with the
437
+ # manual recovery command.
438
+ install_device_agent_suite() {
439
+ if ! command -v npm &>/dev/null; then
440
+ warn "npm not available — skipping device-agent suite install."
441
+ warn "Manual fix: install Node + run \`npm i -g ${DEVICE_AGENT_PACKAGE} ${MCP_SERVER_PACKAGE}\`."
442
+ return 0
443
+ fi
444
+
445
+ info "Installing device-agent suite (${DEVICE_AGENT_PACKAGE}, ${MCP_SERVER_PACKAGE})..."
446
+ if npm install -g "$DEVICE_AGENT_PACKAGE" "$MCP_SERVER_PACKAGE"; then
447
+ success "device-agent suite installed."
448
+ else
449
+ warn "Failed to install device-agent suite — \`beeos device attach\` will prompt for it on first use."
450
+ warn "Manual fix: npm i -g ${DEVICE_AGENT_PACKAGE} ${MCP_SERVER_PACKAGE}"
451
+ fi
452
+ return 0
453
+ }
454
+
297
455
  # ── Main ─────────────────────────────────────────────────────
298
456
 
299
457
  run_cli() {
300
458
  local subcmd="$1"
301
459
  shift || true
302
460
 
461
+ # When invoked as `curl ... | bash`, our own stdin is a pipe that has
462
+ # already been fully consumed by the time we exec the CLI. Node's
463
+ # readline treats a closed / non-TTY stdin as "no answer", silently
464
+ # collapsing every interactive prompt to its default. Reopen stdin
465
+ # against the controlling terminal so `beeos init` can still ask for
466
+ # framework choice, bind confirmation, etc. `-t 1` guards us against
467
+ # true headless runs (CI) where /dev/tty doesn't exist.
468
+ local stdin_src="/dev/stdin"
469
+ if [ -t 1 ] && [ -e /dev/tty ]; then
470
+ stdin_src="/dev/tty"
471
+ fi
472
+
303
473
  if command -v npx &>/dev/null; then
304
- exec npx --yes "$CLI_PACKAGE" "$subcmd" "$@"
474
+ # Temporary / npx path: deliberately do NOT install the device-agent
475
+ # suite globally. The user opted into the throwaway-install flow, so
476
+ # we honor that — if they later run `beeos device attach`,
477
+ # ensureDeviceAgent will guide them through a global install at that
478
+ # point.
479
+ exec npx --yes "$CLI_PACKAGE" "$subcmd" "$@" <"$stdin_src"
305
480
  else
306
481
  warn "npx not found, installing globally..."
307
482
  if ! npm install -g "$CLI_PACKAGE"; then
@@ -314,7 +489,8 @@ run_cli() {
314
489
  error " use a node version manager (nvm / fnm) or sudo"
315
490
  exit 1
316
491
  fi
317
- exec beeos "$subcmd" "$@"
492
+ install_device_agent_suite
493
+ exec beeos "$subcmd" "$@" <"$stdin_src"
318
494
  fi
319
495
  }
320
496
 
@@ -325,13 +501,16 @@ main() {
325
501
  echo ""
326
502
 
327
503
  detect_platform
504
+ send_telemetry "install.bootstrap.start"
328
505
 
329
506
  if ! check_node; then
330
507
  if ! try_install_node; then
508
+ send_telemetry "install.bootstrap.node_fail" "node_install_failed" false
331
509
  print_install_hints
332
510
  exit 1
333
511
  fi
334
512
  if ! check_node; then
513
+ send_telemetry "install.bootstrap.node_fail" "node_version_too_old" false
335
514
  print_install_hints
336
515
  exit 1
337
516
  fi
@@ -361,6 +540,7 @@ main() {
361
540
  error " use a node version manager (nvm / fnm) or sudo"
362
541
  exit 1
363
542
  fi
543
+ install_device_agent_suite
364
544
  fi
365
545
  ;;
366
546
  rerun)
@@ -372,6 +552,8 @@ main() {
372
552
  info "Running BeeOS CLI..."
373
553
  echo ""
374
554
 
555
+ send_telemetry "install.bootstrap.success"
556
+
375
557
  if [[ "$MODE" == "device" ]]; then
376
558
  run_cli "device" "attach" "${PASSTHROUGH_ARGS[@]+"${PASSTHROUGH_ARGS[@]}"}"
377
559
  else