bloby-bot 0.70.13 → 0.71.0

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.
Files changed (45) hide show
  1. package/bin/cli.js +223 -45
  2. package/dist-bloby/assets/{bloby-CU9KhQdP.js → bloby-es6cZJzs.js} +6 -6
  3. package/dist-bloby/assets/globals-DBqwNiJV.css +2 -0
  4. package/dist-bloby/assets/{highlighted-body-OFNGDK62-D0Tm_wgU.js → highlighted-body-OFNGDK62-8PiOHw9p.js} +1 -1
  5. package/dist-bloby/assets/mermaid-GHXKKRXX-BJWX8urU.js +1 -0
  6. package/dist-bloby/assets/{onboard-GfjHF9nm.js → onboard-BKgy17OU.js} +1 -1
  7. package/dist-bloby/bloby.html +3 -3
  8. package/dist-bloby/onboard.html +3 -3
  9. package/package.json +2 -3
  10. package/scripts/install +141 -34
  11. package/scripts/install.ps1 +111 -15
  12. package/scripts/install.sh +141 -34
  13. package/shared/config.ts +37 -2
  14. package/supervisor/channels/manager.ts +68 -33
  15. package/supervisor/channels/telegram.ts +57 -16
  16. package/supervisor/channels/types.ts +4 -1
  17. package/supervisor/channels/whatsapp.ts +57 -10
  18. package/supervisor/chat/src/components/Chat/AudioBubble.tsx +1 -1
  19. package/supervisor/chat/src/components/Chat/AuthedImage.tsx +16 -3
  20. package/supervisor/chat/src/components/Chat/BlobyImageCard.tsx +2 -2
  21. package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +25 -8
  22. package/supervisor/chat/src/components/Chat/InputBar.tsx +62 -7
  23. package/supervisor/chat/src/components/Chat/MessageBubble.tsx +37 -18
  24. package/supervisor/chat/src/components/Chat/MessageList.tsx +3 -3
  25. package/supervisor/chat/src/hooks/useChat.ts +52 -0
  26. package/supervisor/chat/src/lib/authedFile.ts +24 -12
  27. package/supervisor/file-saver.ts +92 -19
  28. package/supervisor/harnesses/attachment-policy.ts +111 -0
  29. package/supervisor/harnesses/claude.ts +62 -15
  30. package/supervisor/harnesses/codex.ts +69 -43
  31. package/supervisor/harnesses/pi/index.ts +84 -49
  32. package/supervisor/harnesses/pi/providers/humanize-error.ts +25 -0
  33. package/supervisor/harnesses/pi/providers/stream-anthropic.ts +8 -0
  34. package/supervisor/harnesses/pi/providers/stream-google.ts +5 -0
  35. package/supervisor/harnesses/pi/providers/stream-openai-completions.ts +15 -6
  36. package/supervisor/harnesses/pi/providers/types.ts +18 -1
  37. package/supervisor/harnesses/pi/session.ts +28 -1
  38. package/supervisor/index.ts +57 -16
  39. package/supervisor/widget.js +19 -5
  40. package/worker/db.ts +2 -0
  41. package/dist-bloby/assets/globals-DlPtwiZL.css +0 -2
  42. package/dist-bloby/assets/mermaid-GHXKKRXX-B95J3s3s.js +0 -1
  43. package/supervisor/public/headphones_spritesheet.webp +0 -0
  44. package/supervisor/public/spritesheet.webp +0 -0
  45. /package/dist-bloby/assets/{globals-mGpojCOe.js → globals-DN3F0CQE.js} +0 -0
@@ -6,6 +6,30 @@
6
6
 
7
7
  $ErrorActionPreference = "Stop"
8
8
 
9
+ # TLS 1.2 floor for Windows PowerShell 5.1 (which can default to TLS 1.0/1.1 and
10
+ # fail against nodejs.org/npm). OR-in so we never DROP TLS 1.3 on PowerShell 7+.
11
+ try {
12
+ [Net.ServicePointManager]::SecurityProtocol = `
13
+ [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
14
+ } catch {}
15
+
16
+ # Get-Url <url> <outFile> — download with a timeout and up to 3 attempts so a
17
+ # single transient blip doesn't abort the whole install. PS 5.1-compatible
18
+ # (no -MaximumRetryCount, which is PowerShell 6+ only).
19
+ function Get-Url($url, $outFile) {
20
+ $attempt = 0
21
+ while ($true) {
22
+ $attempt++
23
+ try {
24
+ Invoke-WebRequest -Uri $url -OutFile $outFile -UseBasicParsing -TimeoutSec 60
25
+ return
26
+ } catch {
27
+ if ($attempt -ge 3) { throw }
28
+ Start-Sleep -Seconds 2
29
+ }
30
+ }
31
+ }
32
+
9
33
  $MIN_NODE_MAJOR = 18
10
34
  $NODE_VERSION = "22.14.0"
11
35
  $BLOBY_HOME = Join-Path $env:USERPROFILE ".bloby"
@@ -125,25 +149,64 @@ function Install-Node {
125
149
 
126
150
  Write-Down "Downloading Node.js v${NODE_VERSION}..."
127
151
 
128
- $nodeUrl = "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-win-${NODEARCH}.zip"
152
+ $nodeFile = "node-v${NODE_VERSION}-win-${NODEARCH}.zip"
153
+ $nodeUrl = "https://nodejs.org/dist/v${NODE_VERSION}/$nodeFile"
129
154
  $tmpFile = Join-Path ([System.IO.Path]::GetTempPath()) "node-bloby.zip"
130
155
 
131
- Invoke-WebRequest -Uri $nodeUrl -OutFile $tmpFile -UseBasicParsing
156
+ Get-Url $nodeUrl $tmpFile
132
157
 
133
- # Extract
134
- New-Item -ItemType Directory -Path $TOOLS_DIR -Force | Out-Null
135
- if (Test-Path $NODE_DIR) { Remove-Item $NODE_DIR -Recurse -Force }
158
+ # Integrity: verify against nodejs.org SHASUMS256.txt before extracting. A
159
+ # mismatch is fatal; an unreachable sums file degrades to a warning.
160
+ try {
161
+ $sums = (Invoke-WebRequest -Uri "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" -UseBasicParsing -TimeoutSec 30).Content
162
+ $line = $sums -split "`n" | Where-Object { $_ -match ([regex]::Escape($nodeFile) + '\s*$') } | Select-Object -First 1
163
+ if ($line) {
164
+ $expectedHash = ($line -split '\s+')[0]
165
+ $actualHash = (Get-FileHash -Path $tmpFile -Algorithm SHA256).Hash
166
+ if ($actualHash -ieq $expectedHash) {
167
+ Write-Check "Checksum verified"
168
+ } else {
169
+ Write-Host " ✗ Node.js checksum mismatch — aborting (corrupt or tampered download)" -ForegroundColor Red
170
+ Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue
171
+ exit 1
172
+ }
173
+ } else {
174
+ Write-Host " ! Could not find checksum entry — skipping verification" -ForegroundColor Yellow
175
+ }
176
+ } catch {
177
+ Write-Host " ! Could not fetch checksums — skipping verification" -ForegroundColor Yellow
178
+ }
136
179
 
180
+ # Extract into a staging dir, verify it runs, then swap. Move-Item cannot
181
+ # overwrite a populated directory, so the existing node is moved aside and
182
+ # removed only AFTER the new tree is verified — an interrupt never strands us.
183
+ New-Item -ItemType Directory -Path $TOOLS_DIR -Force | Out-Null
137
184
  $tmpExtract = Join-Path ([System.IO.Path]::GetTempPath()) "node-bloby-extract"
138
185
  if (Test-Path $tmpExtract) { Remove-Item $tmpExtract -Recurse -Force }
139
186
 
140
187
  Expand-Archive -Path $tmpFile -DestinationPath $tmpExtract -Force
141
188
  $extracted = Get-ChildItem $tmpExtract | Select-Object -First 1
142
- Move-Item -Path $extracted.FullName -Destination $NODE_DIR -Force
189
+ $nodeNew = "$NODE_DIR.new"
190
+ if (Test-Path $nodeNew) { Remove-Item $nodeNew -Recurse -Force }
191
+ Move-Item -Path $extracted.FullName -Destination $nodeNew -Force
143
192
 
144
193
  Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue
145
194
  Remove-Item $tmpExtract -Recurse -Force -ErrorAction SilentlyContinue
146
195
 
196
+ $stagedNode = Join-Path $nodeNew "node.exe"
197
+ $stagedOk = $false
198
+ try { $stagedOk = [bool](& $stagedNode -v 2>$null) } catch {}
199
+ if (-not $stagedOk) {
200
+ Write-Host " ✗ Node.js download failed (extracted binary does not run)" -ForegroundColor Red
201
+ Remove-Item $nodeNew -Recurse -Force -ErrorAction SilentlyContinue
202
+ exit 1
203
+ }
204
+
205
+ if (Test-Path "$NODE_DIR.old") { Remove-Item "$NODE_DIR.old" -Recurse -Force -ErrorAction SilentlyContinue }
206
+ if (Test-Path $NODE_DIR) { Move-Item $NODE_DIR "$NODE_DIR.old" -Force }
207
+ Move-Item $nodeNew $NODE_DIR -Force
208
+ if (Test-Path "$NODE_DIR.old") { Remove-Item "$NODE_DIR.old" -Recurse -Force -ErrorAction SilentlyContinue }
209
+
147
210
  # Verify
148
211
  $nodeBin = Join-Path $NODE_DIR "node.exe"
149
212
  if (-not (Test-Path $nodeBin)) {
@@ -174,9 +237,13 @@ function Install-Bloby {
174
237
 
175
238
  Write-Down "Installing bloby..."
176
239
 
177
- $tarballUrl = (& $NPM view bloby-bot dist.tarball 2>$null).Trim()
240
+ # Capture first, THEN .Trim() calling .Trim() on a null (offline npm) throws
241
+ # under $ErrorActionPreference='Stop' and skips the friendly message below.
242
+ $tarballUrl = ""
243
+ try { $tarballUrl = (& $NPM view bloby-bot dist.tarball 2>$null) } catch {}
244
+ if ($tarballUrl) { $tarballUrl = $tarballUrl.Trim() }
178
245
  if (-not $tarballUrl) {
179
- Write-Host " ✗ Failed to fetch package info from npm" -ForegroundColor Red
246
+ Write-Host " ✗ Failed to fetch package info from npm (are you online?)" -ForegroundColor Red
180
247
  exit 1
181
248
  }
182
249
 
@@ -186,7 +253,7 @@ function Install-Bloby {
186
253
 
187
254
  try {
188
255
  $tarball = Join-Path $tmpDir "bloby.tgz"
189
- Invoke-WebRequest -Uri $tarballUrl -OutFile $tarball -UseBasicParsing
256
+ Get-Url $tarballUrl $tarball
190
257
 
191
258
  tar xzf $tarball -C $tmpDir
192
259
 
@@ -255,21 +322,39 @@ function Install-Bloby {
255
322
  if (-not ((Test-Path $npmrc) -and (Select-String -Path $npmrc -Pattern '^legacy-peer-deps' -Quiet))) {
256
323
  Add-Content -Path $npmrc -Value 'legacy-peer-deps=true'
257
324
  }
325
+ # Toggle EAP to Continue around the native npm call so a stderr line doesn't
326
+ # raise a NativeCommandError; then gate on the real exit code. A broken dep
327
+ # tree is fatal here (matching install.sh) instead of being silently ignored.
258
328
  Push-Location $BLOBY_HOME
259
- try {
260
- & $NPM install --omit=dev 2>$null
261
- } catch {}
329
+ $eap = $ErrorActionPreference; $ErrorActionPreference = "Continue"
330
+ $npmOut = & $NPM install --omit=dev 2>&1
331
+ $npmCode = $LASTEXITCODE
332
+ $ErrorActionPreference = $eap
262
333
  Pop-Location
334
+ if ($npmCode -ne 0) {
335
+ Write-Host " ✗ Dependency install failed:" -ForegroundColor Red
336
+ $npmOut | ForEach-Object { Write-Host " $_" }
337
+ exit 1
338
+ }
263
339
 
264
340
  # Install workspace dependencies (rebuilds native modules for this platform)
265
341
  $wsDir = Join-Path $BLOBY_HOME "workspace"
266
342
  if (Test-Path (Join-Path $wsDir "package.json")) {
267
343
  Write-Down "Installing workspace dependencies..."
268
344
  Push-Location $wsDir
269
- try {
270
- & $NPM install --omit=dev 2>$null
271
- } catch {}
345
+ $eap = $ErrorActionPreference; $ErrorActionPreference = "Continue"
346
+ $wsOut = & $NPM install --omit=dev 2>&1
347
+ $wsCode = $LASTEXITCODE
348
+ $ErrorActionPreference = $eap
272
349
  Pop-Location
350
+ if ($wsCode -ne 0) {
351
+ Write-Host " ✗ Workspace dependency install failed:" -ForegroundColor Red
352
+ if ("$wsOut" -match 'Visual Studio|gyp ERR|MSBuild|node-gyp|Python') {
353
+ Write-Host " ! Native build tools missing. Install the Visual Studio Build Tools (Desktop C++ workload) + Python 3, then re-run. See https://github.com/nodejs/node-gyp#on-windows" -ForegroundColor Yellow
354
+ }
355
+ $wsOut | ForEach-Object { Write-Host " $_" }
356
+ exit 1
357
+ }
273
358
  }
274
359
 
275
360
  # Verify
@@ -336,6 +421,17 @@ Install-Bloby
336
421
  Create-Wrapper
337
422
  Setup-Path
338
423
 
424
+ # Smoke-test: the wrapper + node + cli must actually run, not just exist on disk.
425
+ $blobyCmd = Join-Path $BIN_DIR "bloby.cmd"
426
+ $smokeOk = $false
427
+ try { $smokeOk = [bool](& $blobyCmd --version 2>$null) } catch {}
428
+ if (-not $smokeOk) {
429
+ Write-Host ""
430
+ Write-Host " ✗ Bloby installed but failed to run (bloby --version)." -ForegroundColor Red
431
+ Write-Host " Open a NEW terminal and run 'bloby --version'; if it still fails, re-run this installer." -ForegroundColor DarkGray
432
+ exit 1
433
+ }
434
+
339
435
  Write-Host ""
340
436
  if ($vtSupported) {
341
437
  Write-Host " ${PINK}${BOLD}✔ Bloby is installed!${RSET}"
@@ -7,6 +7,23 @@ set -e
7
7
  # Downloads Node.js + Bloby into ~/.bloby — no system dependencies needed.
8
8
  # ─────────────────────────────────────────────────────────────────────────────
9
9
 
10
+ # Repair $HOME before deriving any install path. Under `sudo`, cron, CI, or a
11
+ # minimal shell, $HOME can be empty or "/" — without this we'd install into the
12
+ # wrong place (or /). Colors aren't defined yet, so this early error is plain.
13
+ if [ -z "${HOME:-}" ] || [ ! -d "${HOME:-}" ]; then
14
+ _whoami=$(id -un 2>/dev/null || echo "")
15
+ if [ -n "$_whoami" ] && command -v getent >/dev/null 2>&1; then
16
+ HOME=$(getent passwd "$_whoami" 2>/dev/null | cut -d: -f6)
17
+ elif [ -n "$_whoami" ] && command -v dscl >/dev/null 2>&1; then
18
+ HOME=$(dscl . -read "/Users/$_whoami" NFSHomeDirectory 2>/dev/null | awk '{print $2}')
19
+ fi
20
+ export HOME
21
+ fi
22
+ if [ -z "${HOME:-}" ] || [ ! -d "$HOME" ]; then
23
+ printf 'Error: could not determine your home directory ($HOME is unset).\n' >&2
24
+ exit 1
25
+ fi
26
+
10
27
  MIN_NODE_MAJOR=18
11
28
  NODE_VERSION="22.14.0"
12
29
  BLOBY_HOME="$HOME/.bloby"
@@ -14,6 +31,11 @@ TOOLS_DIR="$BLOBY_HOME/tools"
14
31
  NODE_DIR="$TOOLS_DIR/node"
15
32
  BIN_DIR="$BLOBY_HOME/bin"
16
33
  USE_SYSTEM_NODE=false
34
+ LIBC=""
35
+ # Temp paths the cleanup trap removes — kept empty until we own them so an early
36
+ # exit can never `rm -rf` an inherited value (e.g. the system $TMPDIR).
37
+ WORK_DIR=""
38
+ TMPFILE=""
17
39
 
18
40
  # Brand colors: #00ADFE (light) and #0158FB (deep) -- Morphy palette, 24-bit truecolor
19
41
  BLUE='\033[38;2;0;173;254m'
@@ -37,15 +59,47 @@ if [ ! -t 1 ]; then
37
59
  BLUE='' PINK='' YELLOW='' RED='' DIM='' BOLD='' RESET='' G1='' G2='' G3='' G4='' G5='' G6='' G7=''
38
60
  fi
39
61
 
40
- # Cleanup on exit (restore cursor, reset colors, remove temp files)
62
+ # Cleanup on exit (restore cursor, reset colors, remove temp files).
63
+ # Only remove paths we actually created — never an inherited/unset value.
41
64
  cleanup() {
42
65
  printf '\033[?25h' # show cursor
43
66
  printf "${RESET}"
44
- rm -f "$TMPFILE" 2>/dev/null
45
- rm -rf "$TMPDIR" 2>/dev/null
67
+ [ -n "$TMPFILE" ] && rm -f "$TMPFILE" 2>/dev/null
68
+ [ -n "$WORK_DIR" ] && rm -rf "$WORK_DIR" 2>/dev/null
69
+ return 0
46
70
  }
47
71
  trap cleanup EXIT INT TERM
48
72
 
73
+ # ─── Download + integrity helpers ───────────────────────────────────────────
74
+
75
+ # download_file <url> <dest> — curl/wget with HTTPS floor, timeout, and retry.
76
+ download_file() {
77
+ if command -v curl >/dev/null 2>&1; then
78
+ curl -fsSL --proto '=https' --tlsv1.2 --connect-timeout 20 --retry 3 --retry-delay 2 -o "$2" "$1"
79
+ elif command -v wget >/dev/null 2>&1; then
80
+ wget -q --https-only --tries=3 --timeout=20 -O "$2" "$1"
81
+ else
82
+ printf " ${RED}✗${RESET} curl or wget is required to install Bloby\n"
83
+ exit 1
84
+ fi
85
+ }
86
+
87
+ # verify_sha256 <file> <expected-hex> — 0 match, 1 mismatch, 2 no tool available.
88
+ verify_sha256() {
89
+ _actual=""
90
+ if command -v sha256sum >/dev/null 2>&1; then
91
+ _actual=$(sha256sum "$1" 2>/dev/null | awk '{print $1}')
92
+ elif command -v shasum >/dev/null 2>&1; then
93
+ _actual=$(shasum -a 256 "$1" 2>/dev/null | awk '{print $1}')
94
+ else
95
+ return 2
96
+ fi
97
+ # No hash computed (tool failed / file vanished) → "skip" (2), not "mismatch" (1),
98
+ # so we warn rather than aborting with a misleading "tampered download".
99
+ [ -n "$_actual" ] || return 2
100
+ [ "$_actual" = "$2" ]
101
+ }
102
+
49
103
  printf "\n"
50
104
  printf "${G1}${BOLD} █▄ ${RESET}\n"
51
105
  printf "${G2}${BOLD} ▄ ▄ ██ ${RESET}\n"
@@ -83,6 +137,14 @@ detect_platform() {
83
137
  ;;
84
138
  esac
85
139
 
140
+ # Detect musl (Alpine): nodejs.org ships glibc builds only, so a bundled Node
141
+ # would segfault at runtime. Flag it now and require a system Node instead.
142
+ if [ "$PLATFORM" = "linux" ]; then
143
+ if [ -f /etc/alpine-release ] || (ldd --version 2>&1 | grep -qi musl); then
144
+ LIBC="musl"
145
+ fi
146
+ fi
147
+
86
148
  printf " ${DIM}Platform: ${PLATFORM}/${NODEARCH}${RESET}\n"
87
149
  }
88
150
 
@@ -115,35 +177,68 @@ install_node() {
115
177
  fi
116
178
  fi
117
179
 
118
- printf " ${BLUE}↓${RESET} Downloading Node.js v${NODE_VERSION}...\n"
180
+ # nodejs.org ships glibc-linked builds; on musl they segfault at runtime.
181
+ if [ "$LIBC" = "musl" ]; then
182
+ printf " ${RED}✗${RESET} Alpine/musl detected — Bloby's bundled Node.js requires glibc.\n"
183
+ printf " ${DIM}Install Node.js >= ${MIN_NODE_MAJOR} (e.g. ${BOLD}apk add nodejs npm${RESET}${DIM}) and re-run this installer.${RESET}\n"
184
+ exit 1
185
+ fi
119
186
 
120
- NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-${PLATFORM}-${NODEARCH}.tar.xz"
121
- TMPFILE=$(mktemp /tmp/node-XXXXXX.tar.xz)
187
+ printf " ${BLUE}↓${RESET} Downloading Node.js v${NODE_VERSION}...\n"
122
188
 
123
- # Download
124
- if command -v curl >/dev/null 2>&1; then
125
- curl -fsSL -o "$TMPFILE" "$NODE_URL"
126
- elif command -v wget >/dev/null 2>&1; then
127
- wget -qO "$TMPFILE" "$NODE_URL"
189
+ NODE_FILE="node-v${NODE_VERSION}-${PLATFORM}-${NODEARCH}.tar.xz"
190
+ NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_FILE}"
191
+ # Trailing X's, NO suffix: BSD mktemp (macOS) only substitutes trailing X's,
192
+ # so a ".tar.xz" suffix would create a literal, predictable, non-unique file.
193
+ TMPFILE=$(mktemp "${TMPDIR:-/tmp}/bloby-node-XXXXXX")
194
+ download_file "$NODE_URL" "$TMPFILE"
195
+
196
+ # Integrity: verify against nodejs.org SHASUMS256.txt before extracting. A
197
+ # mismatch is fatal; an unreachable sums file or missing hash tool degrades to
198
+ # a warning so installs still proceed (the download itself is TLS-protected).
199
+ printf " ${DIM}Verifying download...${RESET}\n"
200
+ EXPECTED_SHA=""
201
+ SUMS=$(curl -fsSL --proto '=https' --tlsv1.2 --connect-timeout 20 "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" 2>/dev/null \
202
+ || wget -q --https-only --timeout=20 -O- "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" 2>/dev/null \
203
+ || echo "")
204
+ if [ -n "$SUMS" ]; then
205
+ EXPECTED_SHA=$(printf '%s\n' "$SUMS" | awk -v f="$NODE_FILE" '$2==f {print $1; exit}')
206
+ fi
207
+ if [ -n "$EXPECTED_SHA" ]; then
208
+ vrc=0
209
+ verify_sha256 "$TMPFILE" "$EXPECTED_SHA" || vrc=$?
210
+ if [ "$vrc" = "0" ]; then
211
+ printf " ${BLUE}✔${RESET} Checksum verified\n"
212
+ elif [ "$vrc" = "2" ]; then
213
+ printf " ${YELLOW}!${RESET} Could not compute checksum — skipping verification\n"
214
+ else
215
+ printf " ${RED}✗${RESET} Node.js checksum mismatch — aborting (corrupt or tampered download)\n"
216
+ exit 1
217
+ fi
128
218
  else
129
- printf " ${RED}✗${RESET} curl or wget required\n"
130
- exit 1
219
+ printf " ${YELLOW}!${RESET} Could not fetch checksums — skipping verification\n"
131
220
  fi
132
221
 
133
- # Extract
222
+ # Extract into a staging dir, verify it runs, then atomically swap in. An
223
+ # interrupt mid-extract no longer wipes the existing (working) node.
134
224
  mkdir -p "$TOOLS_DIR"
135
- rm -rf "$NODE_DIR"
136
- mkdir -p "$NODE_DIR"
137
-
138
- tar xf "$TMPFILE" -C "$NODE_DIR" --strip-components=1
139
- rm -f "$TMPFILE"
140
-
141
- # Verify
142
- if [ ! -x "$NODE_DIR/bin/node" ]; then
143
- printf " ${RED}✗${RESET} Node.js download failed\n"
225
+ NODE_NEW="$NODE_DIR.new"
226
+ rm -rf "$NODE_NEW"
227
+ mkdir -p "$NODE_NEW"
228
+ tar xf "$TMPFILE" -C "$NODE_NEW" --strip-components=1
229
+ rm -f "$TMPFILE"; TMPFILE=""
230
+
231
+ if ! "$NODE_NEW/bin/node" -v >/dev/null 2>&1; then
232
+ printf " ${RED}✗${RESET} Node.js download failed (extracted binary does not run)\n"
233
+ rm -rf "$NODE_NEW"
144
234
  exit 1
145
235
  fi
146
236
 
237
+ rm -rf "$NODE_DIR.old"
238
+ [ -e "$NODE_DIR" ] && mv "$NODE_DIR" "$NODE_DIR.old"
239
+ mv "$NODE_NEW" "$NODE_DIR"
240
+ rm -rf "$NODE_DIR.old"
241
+
147
242
  printf " ${BLUE}✔${RESET} Node.js v${NODE_VERSION} installed\n"
148
243
  }
149
244
 
@@ -174,19 +269,15 @@ install_bloby() {
174
269
  exit 1
175
270
  fi
176
271
 
177
- # Download and extract tarball
178
- TMPDIR=$(mktemp -d)
179
- if command -v curl >/dev/null 2>&1; then
180
- curl -fsSL -o "$TMPDIR/bloby.tgz" "$TARBALL_URL"
181
- elif command -v wget >/dev/null 2>&1; then
182
- wget -qO "$TMPDIR/bloby.tgz" "$TARBALL_URL"
183
- fi
272
+ # Download and extract tarball (download_file guards on missing curl/wget)
273
+ WORK_DIR=$(mktemp -d)
274
+ download_file "$TARBALL_URL" "$WORK_DIR/bloby.tgz"
184
275
 
185
- tar xzf "$TMPDIR/bloby.tgz" -C "$TMPDIR"
186
- EXTRACTED="$TMPDIR/package"
276
+ tar xzf "$WORK_DIR/bloby.tgz" -C "$WORK_DIR"
277
+ EXTRACTED="$WORK_DIR/package"
187
278
 
188
279
  if [ ! -d "$EXTRACTED" ]; then
189
- rm -rf "$TMPDIR"
280
+ rm -rf "$WORK_DIR"; WORK_DIR=""
190
281
  printf " ${RED}✗${RESET} Installation failed\n"
191
282
  exit 1
192
283
  fi
@@ -219,7 +310,7 @@ install_bloby() {
219
310
  fi
220
311
  fi
221
312
 
222
- rm -rf "$TMPDIR"
313
+ rm -rf "$WORK_DIR"; WORK_DIR=""
223
314
 
224
315
  # Install dependencies inside ~/.bloby/
225
316
  # claude-agent-sdk 0.3.x moved @anthropic-ai/sdk + @modelcontextprotocol/sdk to
@@ -245,6 +336,15 @@ install_bloby() {
245
336
  WS_INSTALL_LOG=$(mktemp)
246
337
  if ! (cd "$BLOBY_HOME/workspace" && "$NPM" install --omit=dev > "$WS_INSTALL_LOG" 2>&1); then
247
338
  printf " ${RED}✗${RESET} Workspace dependency install failed:\n"
339
+ # Native modules (better-sqlite3 etc.) need a compiler toolchain — detect
340
+ # the common "missing build tools" failure and point at the exact fix.
341
+ if grep -qiE 'make: .*command not found|gyp ERR! find Python|no developer tools|xcode-select|command not found: make|g\+\+: not found' "$WS_INSTALL_LOG"; then
342
+ if [ "$PLATFORM" = "darwin" ]; then
343
+ printf " ${YELLOW}!${RESET} Native build tools missing. Run ${BOLD}xcode-select --install${RESET} then re-run this installer.\n"
344
+ else
345
+ printf " ${YELLOW}!${RESET} Native build tools missing. On Debian/Ubuntu: ${BOLD}sudo apt-get install -y build-essential python3${RESET}, then re-run.\n"
346
+ fi
347
+ fi
248
348
  cat "$WS_INSTALL_LOG"
249
349
  rm -f "$WS_INSTALL_LOG"
250
350
  exit 1
@@ -348,6 +448,13 @@ install_bloby
348
448
  create_wrapper
349
449
  setup_path
350
450
 
451
+ # Smoke-test: the wrapper + node + cli must actually run, not just exist on disk.
452
+ if ! "$BIN_DIR/bloby" --version >/dev/null 2>&1; then
453
+ printf "\n ${RED}✗${RESET} Bloby installed but failed to run (\`bloby --version\`).\n"
454
+ printf " ${DIM}Open a NEW terminal and run ${BOLD}bloby --version${RESET}${DIM}; if it still fails, re-run this installer.${RESET}\n\n"
455
+ exit 1
456
+ fi
457
+
351
458
  printf "\n"
352
459
  printf " ${PINK}${BOLD}✔ Bloby is installed!${RESET}\n"
353
460
  printf "\n"
package/shared/config.ts CHANGED
@@ -97,7 +97,20 @@ const MODEL_MIGRATIONS: Record<string, string> = {
97
97
 
98
98
  export function loadConfig(): BotConfig {
99
99
  if (!fs.existsSync(paths.config)) throw new Error('No config. Run `bloby init`.');
100
- const config = JSON.parse(fs.readFileSync(paths.config, 'utf-8'));
100
+ let config: BotConfig;
101
+ try {
102
+ config = JSON.parse(fs.readFileSync(paths.config, 'utf-8'));
103
+ } catch {
104
+ // Torn/truncated write — recover from the .bak mirror rather than crashing
105
+ // the supervisor (or, worse, letting the CLI regenerate a fresh wallet).
106
+ const bak = `${paths.config}.bak`;
107
+ if (fs.existsSync(bak)) {
108
+ config = JSON.parse(fs.readFileSync(bak, 'utf-8'));
109
+ try { saveConfig(config); } catch {}
110
+ } else {
111
+ throw new Error('config.json is corrupt and no backup (.bak) was found.');
112
+ }
113
+ }
101
114
  let dirty = false;
102
115
 
103
116
  // Backward compat: migrate old { enabled: boolean } → { mode }
@@ -119,5 +132,27 @@ export function loadConfig(): BotConfig {
119
132
 
120
133
  export function saveConfig(config: BotConfig): void {
121
134
  fs.mkdirSync(DATA_DIR, { recursive: true });
122
- fs.writeFileSync(paths.config, JSON.stringify(config, null, 2));
135
+ // Atomic write: temp file + rename, then mirror to .bak. config.json holds the
136
+ // funded wallet and is written by BOTH this supervisor and the CLI, so a plain
137
+ // writeFileSync can be observed half-written (the Jun-9 0-byte-creds class of
138
+ // bug, here applied to the one file that must never be lost).
139
+ const json = JSON.stringify(config, null, 2);
140
+ const tmp = `${paths.config}.${process.pid}.tmp`;
141
+ try {
142
+ fs.writeFileSync(tmp, json);
143
+ try {
144
+ fs.renameSync(tmp, paths.config);
145
+ } catch (err) {
146
+ // Windows: rename over a file another process holds open can EPERM/EEXIST.
147
+ const code = (err as NodeJS.ErrnoException)?.code;
148
+ if (process.platform === 'win32' && (code === 'EPERM' || code === 'EEXIST')) {
149
+ fs.copyFileSync(tmp, paths.config);
150
+ fs.unlinkSync(tmp);
151
+ } else throw err;
152
+ }
153
+ try { fs.copyFileSync(paths.config, `${paths.config}.bak`); } catch {}
154
+ } catch (err) {
155
+ try { fs.unlinkSync(tmp); } catch {}
156
+ throw err;
157
+ }
123
158
  }