@shuyhere/bb-agent 0.0.9 → 0.0.11

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/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to BB-Agent will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.11] - 2026-04-07
9
+
10
+ ### Added
11
+
12
+ - startup update notices in the fullscreen transcript are now highlighted so available updates stand out clearly during startup
13
+ - read-tool line ranges in fullscreen tool activity now highlight the requested span, so values like `2148-2267/5006` stand out while the model is using tools
14
+
15
+ ### Improved
16
+
17
+ - npm install now caches verified native binaries by version/platform and reuses them on reinstall instead of re-downloading every time
18
+ - npm install now shows more frequent download progress with transfer rate information to make slow installs easier to understand
19
+ - npm install now avoids unnecessary re-verification on cache hits, making repeat installs faster
20
+
21
+ ## [0.0.10] - 2026-04-07
22
+
23
+ ### Fixed
24
+
25
+ - npm install now uses a longer timeout, retries release-binary downloads, and reports real download errors instead of incorrectly saying no matching prebuilt binary exists
26
+ - npm install now shows progress logs during native binary download and verification so first-time installs on macOS/Linux are less confusing
27
+ - fullscreen `/login` provider-family status now correctly shows OpenAI OAuth state after ChatGPT login instead of incorrectly showing the API key path as not authenticated
28
+
29
+ ### Changed
30
+
31
+ - README install docs now lead with `npm install -g @shuyhere/bb-agent`, move terminal/font guidance into Troubleshooting, and clearly separate npm install from building from source for development
32
+
8
33
  ## [0.0.9] - 2026-04-07
9
34
 
10
35
  ### Added
@@ -46,5 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
46
71
 
47
72
  - latest published package includes the post-0.0.7 startup, auth, model-default, and update-notice improvements
48
73
 
74
+ [0.0.11]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.11
75
+ [0.0.10]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.10
49
76
  [0.0.9]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.9
50
77
  [0.0.8]: https://github.com/shuyhere/bb-agent/releases/tag/v0.0.8
package/README.md CHANGED
@@ -2,44 +2,36 @@
2
2
 
3
3
  ![BB-Agent title figure](assets/title-figure.png)
4
4
 
5
- BB means Bridge Baby in Death Stranding. I named this project that way because while building it, I was also enjoying Death Stranding and loved the idea of connecting everyone together.
5
+ > BB means Bridge Baby in Death Stranding. I named this project that way because while building it, I was also enjoying Death Stranding and loved the idea of connecting everyone together.
6
6
 
7
7
  A Rust-native AI coding agent for the terminal — featuring a fullscreen TUI, multi-provider support, tool use, session persistence, branching, extensions, and skills.
8
8
 
9
9
  ## Install
10
10
 
11
- ### Terminal & Font Compatibility
12
-
13
- BB-Agent uses Unicode glyphs and ANSI color in the fullscreen TUI. For the best visual experience, use a modern terminal and a Unicode-capable monospace font such as:
14
-
15
- - JetBrains Mono
16
- - SF Mono / Menlo
17
- - Fira Code
18
- - Cascadia Mono
19
- - Nerd Font variants of the above
20
-
21
- If some symbols look broken, missing, or too narrow in your terminal:
11
+ ```bash
12
+ npm install -g @shuyhere/bb-agent
13
+ ```
22
14
 
23
- 1. switch to a Unicode-capable monospace font
24
- 2. make sure your terminal uses UTF-8
25
- 3. enable BB-Agent compatibility mode
15
+ ### 1. Install with npm
26
16
 
27
- Compatibility mode uses safer ASCII-style fallback glyphs for spinner/status/tool markers:
17
+ npm install downloads a small wrapper package first, then fetches the matching native BB-Agent binary from the GitHub release for your platform.
28
18
 
29
- ```bash
30
- BB_TUI_COMPAT=1 bb
31
- ```
19
+ What to expect:
20
+ - first install can take a bit because npm downloads and verifies the native binary
21
+ - the installer now prints progress and retry information while downloading
22
+ - after install, run `bb`
32
23
 
33
- Or set this in `~/.bb-agent/settings.json`:
24
+ Current GitHub release binaries are published for:
25
+ - Linux x86_64
26
+ - macOS x86_64
27
+ - macOS arm64 (Apple Silicon)
28
+ - Windows x86_64
34
29
 
35
- ```json
36
- {
37
- "compatibility_mode": true
38
- }
39
- ```
30
+ If no matching prebuilt binary is available, or the download fails, npm install will print source-build instructions instead.
40
31
 
32
+ ### 2. Build from source for development
41
33
 
42
- ### From source (all platforms macOS, Linux, Windows)
34
+ Use this if you want to develop BB-Agent itself, work on the Rust codebase, or install directly without the npm downloader.
43
35
 
44
36
  Requires [Rust](https://rustup.rs). Install Rust first if you don't have it:
45
37
 
@@ -48,7 +40,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
48
40
  source ~/.cargo/env
49
41
  ```
50
42
 
51
- Then build and install BB-Agent:
43
+ Then build and install BB-Agent from source:
52
44
 
53
45
  ```bash
54
46
  git clone https://github.com/shuyhere/bb-agent.git
@@ -58,16 +50,6 @@ cargo install --path crates/cli
58
50
 
59
51
  This compiles the `bb` binary and installs it to `~/.cargo/bin/bb` (which Rust adds to your PATH).
60
52
 
61
- ### npm (Linux/macOS/Windows — downloads matching prebuilt binary when available)
62
-
63
- ```bash
64
- npm install -g @shuyhere/bb-agent
65
- ```
66
-
67
- > If no matching prebuilt binary is available for your platform, npm install will print source-build instructions instead. After install, run `bb` to start.
68
- >
69
- > Current GitHub release binaries are published for Linux x86_64, macOS x86_64/arm64, and Windows x86_64.
70
-
71
53
  ## Getting Started
72
54
 
73
55
  ### 1. Start the TUI
@@ -205,6 +187,38 @@ BB-Agent uses layered configuration:
205
187
  | `bb-tui` | Terminal UI components and fullscreen experience |
206
188
  | `bb-cli` | The `bb` command-line application |
207
189
 
190
+ ## Troubleshooting
191
+
192
+ ### Terminal & Font Compatibility
193
+
194
+ BB-Agent uses Unicode glyphs and ANSI color in the fullscreen TUI. For the best visual experience, use a modern terminal and a Unicode-capable monospace font such as:
195
+
196
+ - JetBrains Mono
197
+ - SF Mono / Menlo
198
+ - Fira Code
199
+ - Cascadia Mono
200
+ - Nerd Font variants of the above
201
+
202
+ If some symbols look broken, missing, or too narrow in your terminal:
203
+
204
+ 1. switch to a Unicode-capable monospace font
205
+ 2. make sure your terminal uses UTF-8
206
+ 3. enable BB-Agent compatibility mode
207
+
208
+ Compatibility mode uses safer ASCII-style fallback glyphs for spinner/status/tool markers:
209
+
210
+ ```bash
211
+ BB_TUI_COMPAT=1 bb
212
+ ```
213
+
214
+ Or set this in `~/.bb-agent/settings.json`:
215
+
216
+ ```json
217
+ {
218
+ "compatibility_mode": true
219
+ }
220
+ ```
221
+
208
222
  ## Documentation
209
223
 
210
224
  - [Configuration Reference](docs/configuration.md) — settings.json, AGENTS.md, templates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shuyhere/bb-agent",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "BB-Agent — a Rust-native AI coding agent for the terminal",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,18 +2,21 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { execSync } = require("child_process");
5
+ const { execFileSync } = require("child_process");
6
6
  const fs = require("fs");
7
7
  const path = require("path");
8
8
  const os = require("os");
9
9
  const https = require("https");
10
+ const http = require("http");
10
11
 
11
12
  const packageJson = require("../package.json");
12
13
  const BINARY_RELEASE_TAG = `v${packageJson.version}`;
13
14
  const REPO = "shuyhere/bb-agent";
14
- const PACKAGE_ROOT = path.resolve(__dirname, "..");
15
15
  const NATIVE_DIR = path.join(__dirname, "..", "native");
16
- const DOWNLOAD_TIMEOUT_MS = 15_000;
16
+ const DOWNLOAD_TIMEOUT_MS = 120_000;
17
+ const DOWNLOAD_PROGRESS_INTERVAL_MS = 1_000;
18
+ const MAX_REDIRECTS = 8;
19
+ const MAX_DOWNLOAD_ATTEMPTS = 3;
17
20
 
18
21
  function isWindows() {
19
22
  return os.platform() === "win32";
@@ -44,119 +47,482 @@ function getTarget() {
44
47
  return `${a}-${p}`;
45
48
  }
46
49
 
47
- function downloadBinary(url, dest, timeoutMs) {
48
- return new Promise((resolve, reject) => {
49
- const timer = setTimeout(() => reject(new Error("Download timed out")), timeoutMs);
50
+ function assetNameForTarget(target) {
51
+ return isWindows() ? `bb-${target}.exe` : `bb-${target}`;
52
+ }
50
53
 
51
- const follow = (url, redirects = 0) => {
52
- if (redirects > 5) { clearTimeout(timer); return reject(new Error("Too many redirects")); }
54
+ function logLine(message = "") {
55
+ try {
56
+ process.stderr.write(`${message}\n`);
57
+ } catch (_) {}
58
+ }
53
59
 
54
- const mod = url.startsWith("https") ? https : require("http");
55
- const req = mod.get(url, (res) => {
56
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
57
- return follow(res.headers.location, redirects + 1);
58
- }
59
- if (res.statusCode !== 200) {
60
- clearTimeout(timer);
61
- return reject(new Error(`HTTP ${res.statusCode}`));
62
- }
63
- const file = fs.createWriteStream(dest);
64
- res.pipe(file);
65
- file.on("finish", () => { clearTimeout(timer); file.close(); resolve(); });
66
- file.on("error", (e) => { clearTimeout(timer); reject(e); });
67
- });
68
- req.on("error", (e) => { clearTimeout(timer); reject(e); });
69
- req.on("timeout", () => { req.destroy(); clearTimeout(timer); reject(new Error("Request timed out")); });
70
- };
71
- follow(url);
72
- });
60
+ function makeDownloadError(kind, message, statusCode) {
61
+ const err = new Error(message);
62
+ err.kind = kind;
63
+ if (statusCode) err.statusCode = statusCode;
64
+ return err;
73
65
  }
74
66
 
75
- function assetNameForTarget(target) {
76
- return isWindows() ? `bb-${target}.exe` : `bb-${target}`;
67
+ function formatBytes(bytes) {
68
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
69
+ const units = ["B", "KB", "MB", "GB"];
70
+ let value = bytes;
71
+ let unit = 0;
72
+ while (value >= 1024 && unit < units.length - 1) {
73
+ value /= 1024;
74
+ unit += 1;
75
+ }
76
+ return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
77
+ }
78
+
79
+ function formatRate(bytesPerSecond) {
80
+ if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s";
81
+ return `${formatBytes(bytesPerSecond)}/s`;
82
+ }
83
+
84
+ function ensureParentDir(filePath) {
85
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
86
+ }
87
+
88
+ function removeIfExists(filePath) {
89
+ try {
90
+ fs.unlinkSync(filePath);
91
+ } catch (_) {}
92
+ }
93
+
94
+ function binaryVersion(binaryPath) {
95
+ try {
96
+ const out = execFileSync(binaryPath, ["--version"], {
97
+ stdio: ["ignore", "pipe", "pipe"],
98
+ timeout: 2500,
99
+ encoding: "utf8",
100
+ });
101
+ return (out || "").trim();
102
+ } catch (err) {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ function binaryMatchesCurrentVersion(binaryPath) {
108
+ const version = binaryVersion(binaryPath);
109
+ if (!version) return false;
110
+ return version.includes(packageJson.version);
77
111
  }
78
112
 
79
113
  function hasBundledNativeBinary() {
80
114
  const dest = nativeBinaryPath();
81
115
  if (!fs.existsSync(dest)) return false;
116
+ if (!binaryMatchesCurrentVersion(dest)) return false;
82
117
  try {
83
- execSync(`"${dest}" --version`, { stdio: "pipe", timeout: 5000 });
118
+ fs.accessSync(dest, fs.constants.X_OK);
84
119
  return true;
85
120
  } catch {
86
121
  return false;
87
122
  }
88
123
  }
89
124
 
90
- async function tryDownloadPrebuilt(target) {
91
- const assetName = assetNameForTarget(target);
92
- const url = `https://github.com/${REPO}/releases/download/${BINARY_RELEASE_TAG}/${assetName}`;
125
+ function cacheRootDir() {
126
+ if (process.env.BB_INSTALL_CACHE_DIR && process.env.BB_INSTALL_CACHE_DIR.trim()) {
127
+ return process.env.BB_INSTALL_CACHE_DIR;
128
+ }
93
129
 
94
- fs.mkdirSync(NATIVE_DIR, { recursive: true });
95
- const dest = nativeBinaryPath();
130
+ const home = os.homedir();
131
+ if (isWindows()) {
132
+ return path.join(
133
+ process.env.LOCALAPPDATA || process.env.APPDATA || path.join(home, "AppData", "Local"),
134
+ "bb-agent"
135
+ );
136
+ }
137
+ if (os.platform() === "darwin") {
138
+ return path.join(home, "Library", "Caches", "bb-agent");
139
+ }
140
+ return path.join(process.env.XDG_CACHE_HOME || path.join(home, ".cache"), "bb-agent");
141
+ }
142
+
143
+ function cacheBinaryPath(target) {
144
+ return path.join(cacheRootDir(), "prebuilt", packageJson.version, assetNameForTarget(target));
145
+ }
96
146
 
147
+ function cacheMetadataPath(target) {
148
+ return `${cacheBinaryPath(target)}.json`;
149
+ }
150
+
151
+ function loadCacheMetadata(target) {
152
+ try {
153
+ return JSON.parse(fs.readFileSync(cacheMetadataPath(target), "utf8"));
154
+ } catch (_) {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function storeCacheMetadata(target, binaryPath) {
97
160
  try {
98
- console.log(`Downloading BB-Agent ${BINARY_RELEASE_TAG} for ${target}...`);
99
- await downloadBinary(url, dest, DOWNLOAD_TIMEOUT_MS);
161
+ const stat = fs.statSync(binaryPath);
162
+ ensureParentDir(cacheMetadataPath(target));
163
+ fs.writeFileSync(
164
+ cacheMetadataPath(target),
165
+ JSON.stringify(
166
+ {
167
+ version: packageJson.version,
168
+ target,
169
+ assetName: assetNameForTarget(target),
170
+ binaryName: nativeBinaryName(),
171
+ size: stat.size,
172
+ verifiedAt: new Date().toISOString(),
173
+ },
174
+ null,
175
+ 2
176
+ )
177
+ );
178
+ } catch (_) {}
179
+ }
180
+
181
+ function copyBinary(src, dest) {
182
+ ensureParentDir(dest);
183
+ fs.copyFileSync(src, dest);
184
+ if (!isWindows()) {
100
185
  fs.chmodSync(dest, 0o755);
186
+ }
187
+ }
188
+
189
+ function installFromVerifiedCache(target) {
190
+ const cached = cacheBinaryPath(target);
191
+ const meta = loadCacheMetadata(target);
192
+ if (!fs.existsSync(cached) || !meta) return false;
193
+ if (meta.version !== packageJson.version || meta.target !== target) return false;
194
+
195
+ let stat;
196
+ try {
197
+ stat = fs.statSync(cached);
198
+ } catch (_) {
199
+ return false;
200
+ }
201
+ if (!stat.isFile() || stat.size <= 0) return false;
202
+ if (meta.size && stat.size !== meta.size) return false;
203
+
204
+ logLine(`Using cached BB-Agent binary for ${target} (${formatBytes(stat.size)}).`);
205
+ copyBinary(cached, nativeBinaryPath());
206
+ return true;
207
+ }
208
+
209
+ function refreshCacheFromExistingBinary(target, sourcePath) {
210
+ if (!binaryMatchesCurrentVersion(sourcePath)) return false;
211
+ const cached = cacheBinaryPath(target);
212
+ copyBinary(sourcePath, cached);
213
+ storeCacheMetadata(target, cached);
214
+ return true;
215
+ }
216
+
217
+ function maybeRepairCache(target) {
218
+ const cached = cacheBinaryPath(target);
219
+ if (!fs.existsSync(cached)) return false;
101
220
 
102
- // Verify the binary is executable
221
+ const meta = loadCacheMetadata(target);
222
+ if (meta && meta.version === packageJson.version && meta.target === target && meta.size) {
103
223
  try {
104
- execSync(`"${dest}" --version`, { stdio: "pipe", timeout: 5000 });
105
- } catch {
106
- // Binary may not run on this platform (e.g. wrong arch) — remove it
107
- fs.unlinkSync(dest);
224
+ const stat = fs.statSync(cached);
225
+ if (stat.isFile() && stat.size === meta.size) {
226
+ return false;
227
+ }
228
+ } catch (_) {
108
229
  return false;
109
230
  }
231
+ }
110
232
 
111
- console.log("✓ BB-Agent binary installed successfully.");
112
- return true;
113
- } catch (err) {
114
- // Clean up partial download
115
- try { fs.unlinkSync(dest); } catch {}
233
+ logLine(`Checking cached BB-Agent binary for ${target}...`);
234
+ if (!binaryMatchesCurrentVersion(cached)) {
235
+ removeIfExists(cached);
236
+ removeIfExists(cacheMetadataPath(target));
116
237
  return false;
117
238
  }
239
+
240
+ storeCacheMetadata(target, cached);
241
+ logLine("Verified cached BB-Agent binary for reuse.");
242
+ return true;
243
+ }
244
+
245
+ function requestBinary(url, dest, redirects = 0) {
246
+ return new Promise((resolve, reject) => {
247
+ if (redirects > MAX_REDIRECTS) {
248
+ reject(makeDownloadError("redirect", "Too many redirects"));
249
+ return;
250
+ }
251
+
252
+ const client = url.startsWith("https:") ? https : http;
253
+ const req = client.get(
254
+ url,
255
+ {
256
+ headers: {
257
+ "User-Agent": `${packageJson.name}/${packageJson.version} (postinstall)`,
258
+ Accept: "application/octet-stream,application/octet-stream; q=0.9,*/*;q=0.1",
259
+ },
260
+ },
261
+ (res) => {
262
+ const status = res.statusCode || 0;
263
+
264
+ if (status >= 300 && status < 400 && res.headers.location) {
265
+ res.resume();
266
+ requestBinary(res.headers.location, dest, redirects + 1)
267
+ .then(resolve)
268
+ .catch(reject);
269
+ return;
270
+ }
271
+
272
+ if (status === 404) {
273
+ res.resume();
274
+ reject(makeDownloadError("not-found", `HTTP 404 for ${url}`, 404));
275
+ return;
276
+ }
277
+
278
+ if (status !== 200) {
279
+ res.resume();
280
+ reject(makeDownloadError("http", `HTTP ${status} for ${url}`, status));
281
+ return;
282
+ }
283
+
284
+ const totalBytes = Number(res.headers["content-length"] || 0);
285
+ const startedAt = Date.now();
286
+ let downloadedBytes = 0;
287
+ let lastLoggedAt = 0;
288
+
289
+ if (totalBytes > 0) {
290
+ logLine(`Release asset size: ${formatBytes(totalBytes)}.`);
291
+ } else {
292
+ logLine("Release asset size: unknown (streaming download).");
293
+ }
294
+
295
+ ensureParentDir(dest);
296
+ const file = fs.createWriteStream(dest);
297
+ let settled = false;
298
+
299
+ const finish = (fn, value) => {
300
+ if (settled) return;
301
+ settled = true;
302
+ clearTimeout(timeout);
303
+ fn(value);
304
+ };
305
+
306
+ res.on("data", (chunk) => {
307
+ downloadedBytes += chunk.length;
308
+ const now = Date.now();
309
+ if (now - lastLoggedAt >= DOWNLOAD_PROGRESS_INTERVAL_MS) {
310
+ lastLoggedAt = now;
311
+ const elapsedSeconds = Math.max((now - startedAt) / 1000, 0.001);
312
+ const rate = downloadedBytes / elapsedSeconds;
313
+ if (totalBytes > 0) {
314
+ const percent = Math.min(100, Math.round((downloadedBytes / totalBytes) * 100));
315
+ logLine(
316
+ `Download progress: ${percent}% (${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}, ${formatRate(rate)})`
317
+ );
318
+ } else {
319
+ logLine(`Downloaded ${formatBytes(downloadedBytes)} so far (${formatRate(rate)})...`);
320
+ }
321
+ }
322
+ });
323
+
324
+ file.on("finish", () => {
325
+ file.close((closeErr) => {
326
+ if (closeErr) {
327
+ finish(reject, makeDownloadError("write", closeErr.message));
328
+ } else {
329
+ const elapsedSeconds = Math.max((Date.now() - startedAt) / 1000, 0.001);
330
+ const rate = downloadedBytes / elapsedSeconds;
331
+ if (totalBytes > 0) {
332
+ logLine(
333
+ `Download complete: ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} in ${elapsedSeconds.toFixed(1)}s (${formatRate(rate)}).`
334
+ );
335
+ } else {
336
+ logLine(
337
+ `Download complete: ${formatBytes(downloadedBytes)} in ${elapsedSeconds.toFixed(1)}s (${formatRate(rate)}).`
338
+ );
339
+ }
340
+ finish(resolve);
341
+ }
342
+ });
343
+ });
344
+
345
+ file.on("error", (err) => {
346
+ try { file.close(() => {}); } catch (_) {}
347
+ removeIfExists(dest);
348
+ finish(reject, makeDownloadError("write", err.message));
349
+ });
350
+
351
+ res.on("error", (err) => {
352
+ try { file.close(() => {}); } catch (_) {}
353
+ removeIfExists(dest);
354
+ finish(reject, makeDownloadError("network", err.message));
355
+ });
356
+
357
+ res.pipe(file);
358
+ }
359
+ );
360
+
361
+ const timeout = setTimeout(() => {
362
+ req.destroy(makeDownloadError("timeout", `Download timed out after ${DOWNLOAD_TIMEOUT_MS}ms`));
363
+ }, DOWNLOAD_TIMEOUT_MS);
364
+
365
+ req.on("error", (err) => {
366
+ clearTimeout(timeout);
367
+ reject(makeDownloadError(err.kind || "network", err.message));
368
+ });
369
+ });
370
+ }
371
+
372
+ function verifyBinary(binaryPath) {
373
+ logLine("Verifying downloaded binary...");
374
+ const version = binaryVersion(binaryPath);
375
+ if (!version) {
376
+ return {
377
+ ok: false,
378
+ message: "binary verification failed",
379
+ };
380
+ }
381
+ if (!version.includes(packageJson.version)) {
382
+ return {
383
+ ok: false,
384
+ message: `expected version ${packageJson.version}, got '${version}'`,
385
+ };
386
+ }
387
+ return { ok: true, version };
118
388
  }
119
389
 
390
+ async function tryDownloadPrebuilt(target) {
391
+ const assetName = assetNameForTarget(target);
392
+ const url = `https://github.com/${REPO}/releases/download/${BINARY_RELEASE_TAG}/${assetName}`;
393
+
394
+ fs.mkdirSync(NATIVE_DIR, { recursive: true });
395
+ const dest = nativeBinaryPath();
396
+ const tmpDest = `${dest}.tmp`;
397
+
398
+ if (installFromVerifiedCache(target)) {
399
+ logLine("✓ BB-Agent binary installed successfully from cache.");
400
+ return { ok: true, source: "cache" };
401
+ }
402
+
403
+ if (maybeRepairCache(target) && installFromVerifiedCache(target)) {
404
+ logLine("✓ BB-Agent binary installed successfully from cache.");
405
+ return { ok: true, source: "cache" };
406
+ }
407
+
408
+ let lastError = null;
409
+ for (let attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt += 1) {
410
+ try {
411
+ logLine(
412
+ `Downloading BB-Agent ${BINARY_RELEASE_TAG} for ${target} (attempt ${attempt}/${MAX_DOWNLOAD_ATTEMPTS})...`
413
+ );
414
+ logLine("This may take a little while on first install because npm downloads the native binary from the GitHub release.");
415
+ removeIfExists(tmpDest);
416
+ await requestBinary(url, tmpDest, 0);
417
+ if (!isWindows()) {
418
+ fs.chmodSync(tmpDest, 0o755);
419
+ }
420
+
421
+ const verified = verifyBinary(tmpDest);
422
+ if (!verified.ok) {
423
+ removeIfExists(tmpDest);
424
+ return {
425
+ ok: false,
426
+ kind: "verify",
427
+ message: `Downloaded binary could not run: ${verified.message}`,
428
+ };
429
+ }
430
+
431
+ fs.renameSync(tmpDest, dest);
432
+ refreshCacheFromExistingBinary(target, dest);
433
+ logLine("Cached verified BB-Agent binary for future installs.");
434
+ logLine("✓ BB-Agent binary installed successfully.");
435
+ return { ok: true, source: "download" };
436
+ } catch (err) {
437
+ lastError = err;
438
+ removeIfExists(tmpDest);
439
+ if (err.kind === "not-found") {
440
+ return {
441
+ ok: false,
442
+ kind: "not-found",
443
+ message: `No release asset named ${assetName} was found for ${BINARY_RELEASE_TAG}.`,
444
+ };
445
+ }
446
+ if (attempt < MAX_DOWNLOAD_ATTEMPTS) {
447
+ logLine(`Download failed (${err.message}). Retrying...`);
448
+ await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
449
+ }
450
+ }
451
+ }
452
+
453
+ return {
454
+ ok: false,
455
+ kind: (lastError && lastError.kind) || "download",
456
+ message: (lastError && lastError.message) || "unknown download failure",
457
+ };
458
+ }
459
+
460
+ function printFallbackHelp(platform, reason) {
461
+ logLine("");
462
+ if (reason && reason.kind === "not-found") {
463
+ logLine(`BB-Agent ${packageJson.version}: matching prebuilt binary is not published for ${platform}.`);
464
+ } else if (reason) {
465
+ logLine(`BB-Agent ${packageJson.version}: failed to download the prebuilt binary for ${platform}.`);
466
+ logLine(`Reason: ${reason.message}`);
467
+ } else {
468
+ logLine(`BB-Agent ${packageJson.version}: matching prebuilt binary not available yet for ${platform}.`);
469
+ }
470
+ logLine("");
471
+ logLine("╔══════════════════════════════════════════════════════════════╗");
472
+ logLine(
473
+ "║ BB-Agent: npm could not install native binary for " +
474
+ platform.padEnd(16) +
475
+ " ║"
476
+ );
477
+ logLine("║ ║");
478
+ logLine("║ Install Rust (if needed): ║");
479
+ logLine("║ https://rustup.rs ║");
480
+ logLine("║ Then install with rustup for your platform ║");
481
+ logLine("║ ║");
482
+ logLine("║ Then build BB-Agent: ║");
483
+ logLine("║ git clone https://github.com/shuyhere/bb-agent.git ║");
484
+ logLine("║ cd bb-agent && cargo install --path crates/cli ║");
485
+ logLine("║ ║");
486
+ logLine("║ Then run: bb ║");
487
+ logLine("╚══════════════════════════════════════════════════════════════╝");
488
+ logLine("");
489
+ }
120
490
 
121
491
  async function main() {
122
492
  if (process.env.BB_SKIP_POSTINSTALL) {
123
493
  return;
124
494
  }
125
495
 
126
- if (hasBundledNativeBinary()) return;
496
+ if (hasBundledNativeBinary()) {
497
+ logLine(`BB-Agent ${packageJson.version} native binary already present; skipping download.`);
498
+ return;
499
+ }
127
500
 
128
501
  const target = getTarget();
502
+ const platform = `${os.platform()}-${os.arch()}`;
129
503
 
130
- // Try prebuilt binary
131
504
  if (target) {
132
- const ok = await tryDownloadPrebuilt(target);
133
- if (ok) return;
505
+ const result = await tryDownloadPrebuilt(target);
506
+ if (result.ok) {
507
+ return;
508
+ }
509
+ printFallbackHelp(platform, result);
510
+ return;
134
511
  }
135
512
 
136
- // No prebuilt available — print instructions instead of trying cargo build
137
- // (cargo build takes 5+ minutes and would appear to hang)
138
- const platform = `${os.platform()}-${os.arch()}`;
139
- console.log("");
140
- console.log(`BB-Agent ${packageJson.version}: matching prebuilt binary not available yet for ${platform}.`);
141
- console.log("");
142
- console.log("╔══════════════════════════════════════════════════════════════╗");
143
- console.log("║ BB-Agent: no prebuilt binary for " + platform.padEnd(19) + " ║");
144
- console.log("║ ║");
145
- console.log("║ Install Rust (if needed): ║");
146
- console.log("https://rustup.rs ║");
147
- console.log("║ Then install with rustup for your platform ║");
148
- console.log("║ ║");
149
- console.log("║ Then build BB-Agent: ║");
150
- console.log("║ git clone https://github.com/shuyhere/bb-agent.git ║");
151
- console.log("║ cd bb-agent && cargo install --path crates/cli ║");
152
- console.log("║ ║");
153
- console.log("║ Then run: bb ║");
154
- console.log("╚══════════════════════════════════════════════════════════════╝");
155
- console.log("");
156
- }
157
-
158
- main().catch((err) => {
159
- // Never fail npm install — just print instructions
160
- console.error("BB-Agent postinstall notice:", err.message);
161
- console.log("Install manually: git clone https://github.com/shuyhere/bb-agent.git && cd bb-agent && cargo install --path crates/cli");
162
- });
513
+ printFallbackHelp(platform, {
514
+ kind: "unsupported-platform",
515
+ message: `Unsupported target mapping for ${platform}`,
516
+ });
517
+ }
518
+
519
+ main()
520
+ .catch((err) => {
521
+ logLine(`BB-Agent postinstall notice: ${err && err.message ? err.message : String(err)}`);
522
+ logLine(
523
+ "Install manually: git clone https://github.com/shuyhere/bb-agent.git && cd bb-agent && cargo install --path crates/cli"
524
+ );
525
+ })
526
+ .finally(() => {
527
+ process.exitCode = 0;
528
+ });