@lincoln504/pi-research 1.7.7 → 1.7.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.
Files changed (36) hide show
  1. package/agent-skill/pi-research/README.md +3 -3
  2. package/agent-skill/pi-research/SKILL.md +1 -1
  3. package/dist/cli-bin.mjs +10 -1
  4. package/dist/cli.mjs +692 -387
  5. package/dist/pdf-extract-worker.mjs +12 -3
  6. package/dist/thread-worker.mjs +41 -12
  7. package/docs/AGENT-SKILL.md +3 -3
  8. package/docs/ARCHITECTURE.md +64 -50
  9. package/docs/CHANGELOG.md +31 -0
  10. package/docs/CONFIGURATION.md +2 -2
  11. package/docs/SDK.md +7 -6
  12. package/docs/THIRD-PARTY-NOTICES.md +11 -7
  13. package/package.json +10 -8
  14. package/scripts/camoufox-layout.cjs +455 -0
  15. package/scripts/cleanup.cjs +72 -29
  16. package/scripts/setup.cjs +72 -111
  17. package/src/cli-bin.mjs +10 -1
  18. package/src/cli.ts +27 -15
  19. package/src/healthcheck/index.ts +1 -1
  20. package/src/infrastructure/browser/browser-cleanup.ts +1 -1
  21. package/src/infrastructure/browser/browser-error-utils.ts +5 -4
  22. package/src/infrastructure/browser/camoufox-layout.ts +450 -0
  23. package/src/infrastructure/browser/config.ts +46 -103
  24. package/src/infrastructure/browser/ensure-browser.ts +63 -49
  25. package/src/infrastructure/browser/thread-worker-browser.ts +38 -42
  26. package/src/infrastructure/browser/thread-worker.mjs +41 -12
  27. package/src/infrastructure/knowledge-store-service.ts +6 -6
  28. package/src/knowledge/store.ts +1 -1
  29. package/src/orchestration/researcher-executor.ts +47 -1
  30. package/src/tui/research-health.ts +1 -1
  31. package/src/utils/log-utils.ts +19 -4
  32. package/src/utils/research-export.ts +1 -1
  33. package/src/web-research/browser-search.ts +8 -10
  34. package/src/web-research/pdf-extract-worker.mjs +12 -3
  35. package/src/web-research/scraper-utils.ts +42 -10
  36. package/src/web-research/web-scraper.ts +1 -1
package/scripts/setup.cjs CHANGED
@@ -12,7 +12,8 @@
12
12
  * of hiding behind the graceful path.
13
13
  *
14
14
  * Environment:
15
- * PLAYWRIGHT_BROWSERS_PATH - override browser install location
15
+ * CAMOUFOX_INSTALL_DIR / PLAYWRIGHT_BROWSERS_PATH - relocate the browser cache (Linux: XDG_CACHE_HOME,
16
+ * Windows: LOCALAPPDATA; ignored on macOS)
16
17
  * PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 - skip download entirely
17
18
  * PLAYWRIGHT_INSTALL_DEPS=true - also install Linux system deps (or pass --system-deps)
18
19
  */
@@ -26,7 +27,6 @@ const path = require('path');
26
27
  const projectRoot = path.join(__dirname, '..');
27
28
 
28
29
  const isLinux = process.platform === 'linux';
29
- const isDarwin = process.platform === 'darwin';
30
30
  const isWindows = process.platform === 'win32';
31
31
 
32
32
  const [nodeMajor, nodeMinor, nodePatch] = process.version.replace('v', '').split('.').map((n) => parseInt(n, 10));
@@ -37,73 +37,55 @@ if (belowMinimum) {
37
37
  console.warn(`WARNING: Node.js ${process.version} is below the minimum (>=22.22.2). Upgrade to 22.22.2+.`);
38
38
  }
39
39
 
40
+ const layout = require('./camoufox-layout.cjs');
41
+
40
42
  /**
41
- * Resolve the camoufox-js binary path.
43
+ * Resolve the `camoufox` CLI entry of @camoufox/camoufox.
42
44
  *
43
- * In a typical npm install, dependencies are hoisted to the root node_modules,
44
- * so the package's own node_modules/ may be empty. We use require.resolve to
45
- * find the real location of camoufox-js and derive the bin from there.
45
+ * Dependencies are hoisted, so the package's own node_modules may be empty:
46
+ * require.resolve finds the real location. We run the CLI's JS entry with
47
+ * process.execPath instead of the .bin shim: no .cmd quoting on Windows, no
48
+ * shell, and an argv array that cannot be reinterpreted.
46
49
  */
47
- function resolveCamoufoxBin() {
50
+ function resolveCamoufoxCli() {
48
51
  try {
49
- const pkgDir = path.dirname(require.resolve('camoufox-js/package.json', { paths: [projectRoot] }));
50
- const binDir = path.join(pkgDir, '..', '.bin');
51
- // On Windows the executable is the .cmd shim; the extensionless file is a sh
52
- // script that only runs via PATHEXT luck. Mirror src ensure-browser.ts.
53
- const bin = path.join(binDir, isWindows ? 'camoufox-js.cmd' : 'camoufox-js');
54
- if (existsSync(bin)) return bin;
52
+ const pkgJson = require.resolve('@camoufox/camoufox/package.json', { paths: [projectRoot] });
53
+ const pkg = require(pkgJson);
54
+ const rel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin && pkg.bin.camoufox;
55
+ if (rel) {
56
+ const entry = path.join(path.dirname(pkgJson), rel);
57
+ if (existsSync(entry)) return entry;
58
+ }
55
59
  } catch (_) { /* fall through */ }
56
60
  return null;
57
61
  }
58
62
 
59
- /**
60
- * The custom camoufox location, if the user set one. Mirrors
61
- * getCustomCamoufoxDir() in src/infrastructure/browser/config.ts — the two must
62
- * agree or install and lookup land in different directories.
63
- *
64
- * CAMOUFOX_INSTALL_DIR first: it is camoufox-js's own variable and the only one
65
- * that can relocate the install (camoufox-js <0.12.0 hardcoded
66
- * userCacheDir("camoufox") and honoured nothing — inert there; effective on the
67
- * 0.12.0+ refresh). PLAYWRIGHT_BROWSERS_PATH is kept as the documented alias
68
- * and is exported BELOW as CAMOUFOX_INSTALL_DIR. See
69
- * src/infrastructure/browser/config.ts and thread-worker-browser.ts.
70
- */
71
- function customCamoufoxDir() {
72
- return process.env.CAMOUFOX_INSTALL_DIR || process.env.PLAYWRIGHT_BROWSERS_PATH || null;
73
- }
74
-
75
- function camoufoxCachePath() {
76
- const customPath = customCamoufoxDir();
77
- if (customPath) return customPath;
78
-
79
- if (isWindows) {
80
- // Mirror camoufox-js userCacheDir("camoufox") exactly: homedir-based with a
81
- // DOUBLED "camoufox" segment. Must match src getWindowsCamoufoxDir().
82
- return path.join(homedir(), 'AppData', 'Local', 'camoufox', 'camoufox', 'Cache');
83
- }
84
- if (isDarwin) return path.join(homedir(), 'Library', 'Caches', 'camoufox');
85
-
86
- const cacheHome = process.env.XDG_CACHE_HOME || path.join(homedir(), '.cache');
87
- return path.join(cacheHome, 'camoufox');
88
- }
89
-
90
63
  let browsersInstalled = false;
91
64
  // Set when a fetch cannot possibly succeed (an unusable configured install dir),
92
65
  // so the download is skipped rather than run to completion and then fail. Kept
93
66
  // separate from browsersInstalled so the summary below still tells the truth.
94
67
  let fetchSkipped = false;
95
68
 
69
+ // @camoufox/camoufox derives its install dir ONLY from the platform cache location
70
+ // and ignores CAMOUFOX_INSTALL_DIR / PLAYWRIGHT_BROWSERS_PATH. The layout module turns
71
+ // a user's custom dir into the variable it does read (XDG_CACHE_HOME / LOCALAPPDATA).
72
+ // The same function runs at runtime (src/infrastructure/browser/config.ts), so the
73
+ // install and the lookup cannot disagree.
74
+ const install = layout.resolveInstall(process.env, process.platform, homedir());
75
+ const cachePath = install.dir;
76
+ if (install.ignoredCustom) {
77
+ console.warn(
78
+ `pi-research: CAMOUFOX_INSTALL_DIR / PLAYWRIGHT_BROWSERS_PATH (${install.custom}) is ignored on macOS: ` +
79
+ `the Camoufox launcher always uses ${cachePath} and cannot be relocated without moving HOME.`,
80
+ );
81
+ }
82
+
96
83
  if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
97
84
  console.log('pi-research: skipping browser download (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1)');
98
85
  } else {
99
- const env = { ...process.env };
100
- // Make the fetch honour the custom location. Without this the download always
101
- // went to camoufox's default cache while camoufoxCachePath() reported the
102
- // custom one: the "already installed" probe never matched, every install
103
- // re-fetched, and at runtime the browser was reported missing despite having
104
- // downloaded successfully.
105
- const customDir = customCamoufoxDir();
106
- if (customDir) env.CAMOUFOX_INSTALL_DIR = customDir;
86
+ const env = { ...process.env, ...install.overrides };
87
+ delete env.CAMOUFOX_INSTALL_DIR;
88
+ delete env.PLAYWRIGHT_BROWSERS_PATH;
107
89
 
108
90
  const installDeps = process.argv.includes('--system-deps') || process.env.PLAYWRIGHT_INSTALL_DEPS === 'true';
109
91
  if (installDeps && isLinux) {
@@ -122,62 +104,43 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
122
104
  }
123
105
  }
124
106
 
125
- const cachePath = camoufoxCachePath();
126
- let alreadyInstalled = false;
127
- if (existsSync(cachePath)) {
107
+ // A configured install dir whose parent exists but cannot be used as a directory (a
108
+ // plain file, a broken mount) makes the fetch impossible: it would download the whole
109
+ // ~1.3GB archive and then fail to place it. Report and skip instead.
110
+ if (install.custom && existsSync(install.custom)) {
128
111
  try {
129
- const versions = readdirSync(cachePath).filter(f => statSync(path.join(cachePath, f)).isDirectory());
130
- if (versions.length > 0) {
131
- alreadyInstalled = true;
132
- console.log(`pi-research: Camoufox already installed at ${cachePath}. Skipping fetch.`);
133
- }
112
+ if (!statSync(install.custom).isDirectory()) throw new Error('not a directory');
113
+ readdirSync(install.custom);
134
114
  } catch (e) {
135
- // A configured install dir that exists but cannot be READ as a directory
136
- // (a plain file, a broken symlink mount, no permission) makes the fetch
137
- // below impossible: camoufox downloads the whole ~663MB archive and then
138
- // fails to place it. Report and skip instead, which is what the user's
139
- // install needs either way (the catch further down exits 0, and the
140
- // browser is re-fetchable once the path is usable).
141
- console.warn(`pi-research: cannot use the browser install directory ${cachePath}: ${e instanceof Error ? e.message : String(e)}`);
142
- if (customCamoufoxDir()) {
143
- console.warn('pi-research: skipping the browser fetch because CAMOUFOX_INSTALL_DIR / PLAYWRIGHT_BROWSERS_PATH points at a path that is not a usable directory. Fix or unset it, then run: npx camoufox-js fetch');
144
- fetchSkipped = true;
145
- }
115
+ console.warn(`pi-research: cannot use the browser install directory ${install.custom}: ${e instanceof Error ? e.message : String(e)}`);
116
+ console.warn('pi-research: skipping the browser fetch because CAMOUFOX_INSTALL_DIR / PLAYWRIGHT_BROWSERS_PATH points at a path that is not a usable directory. Fix or unset it, then run: npx camoufox fetch');
117
+ fetchSkipped = true;
146
118
  }
147
119
  }
148
120
 
149
- if (fetchSkipped) {
150
- // Reported above; the fetch cannot work until the configured path is fixed.
151
- } else if (!alreadyInstalled) {
152
- try {
153
- const bin = resolveCamoufoxBin();
154
- // spawnSync with an ARGV ARRAY, never a shell string. `bin` is a filesystem
155
- // path derived from the install directory, and execSync runs through
156
- // `/bin/sh -c` where `$(…)`, backticks and quotes stay live INSIDE double
157
- // quotes — so a package installed under a directory whose name contains
158
- // shell metacharacters would execute them at install time. Windows still
159
- // needs a shell to run the `.cmd` shim, and there the path is quoted; the
160
- // runtime twin in ensure-browser.ts uses the same shape.
161
- // Bound the ~500MB camoufox-js 0.12 browser download (was ~100MB on the
162
- // 0.10 pin — the old 15-minute bound could kill a download that would
163
- // have succeeded on a slow link) so a stalled/interrupted network fails
164
- // eventually instead of hanging `npm install` forever (the catch below
165
- // exits 0, and the browser is re-fetchable manually). 45 min covers a
166
- // working link down to ~180 KB/s; a dead link aborts on its own sooner.
167
- const spawnOpts = { stdio: 'inherit', env, timeout: 45 * 60 * 1000 };
168
- const res = bin
169
- ? (isWindows
170
- ? spawnSync(`"${bin}"`, ['fetch'], { ...spawnOpts, shell: true })
171
- : spawnSync(bin, ['fetch'], spawnOpts))
172
- : spawnSync('npx', ['camoufox-js', 'fetch'], { ...spawnOpts, shell: isWindows });
121
+ if (!fetchSkipped) {
122
+ const cli = resolveCamoufoxCli();
123
+ // The fetch runs through process.execPath with an ARGV ARRAY, never a shell string:
124
+ // `cli` is a filesystem path derived from the install directory, and a shell string
125
+ // would let a directory name containing `$(…)` execute at install time. The runtime
126
+ // twin in ensure-browser.ts uses the same shape.
127
+ // Bound the ~1.3GB browser download so a stalled network fails eventually instead of
128
+ // hanging `npm install` forever (the failure path below exits 0 and the browser is
129
+ // re-fetchable). 45 min covers a working link down to ~500 KB/s.
130
+ const runFetch = () => {
131
+ if (!cli) throw new Error('@camoufox/camoufox is not installed (cannot find its CLI)');
132
+ const res = spawnSync(process.execPath, [cli, 'fetch'], { stdio: 'inherit', env, timeout: 45 * 60 * 1000 });
173
133
  if (res.error) throw res.error;
174
134
  if (res.status !== 0) throw new Error(`camoufox fetch exited with code ${res.status}`);
175
- browsersInstalled = true;
176
- } catch (error) {
177
- const msg = error instanceof Error ? error.message : String(error);
135
+ return true;
136
+ };
137
+
138
+ const pkgRoot = layout.findPackageRoot(projectRoot);
139
+ const result = layout.provisionBrowser(cachePath, runFetch, (m) => console.log(m), pkgRoot ? layout.loadPin(pkgRoot) : null);
140
+ if (result.status === 'failed') {
178
141
  console.error('ERROR: Camoufox browser install failed — pi-research will not work.');
179
- console.error('Run manually to fix: npx camoufox-js fetch');
180
- console.error(`Reason: ${msg}`);
142
+ console.error('Run manually to fix: npx camoufox fetch');
143
+ console.error(`Reason: ${result.reason}`);
181
144
  // Exit 0 by default: the browser is fetched lazily on first use, so a restrictive
182
145
  // network must not fail a user's install. But that also meant nothing here could
183
146
  // ever turn CI red — a broken Windows launcher yielded a green `npm ci`. CI sets
@@ -187,23 +150,21 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
187
150
  process.env.PI_RESEARCH_STRICT_SETUP === 'true';
188
151
  process.exit(strict ? 1 : 0);
189
152
  }
190
- } else {
191
153
  browsersInstalled = true;
154
+ if (result.status === 'present') {
155
+ console.log(`pi-research: Camoufox already installed at ${cachePath}. Skipping fetch.`);
156
+ }
192
157
  }
193
158
  }
194
159
 
195
160
  // Verify
196
- const cachePath = camoufoxCachePath();
197
161
  if (existsSync(cachePath)) {
198
- try {
199
- const versions = readdirSync(cachePath).filter(f => statSync(path.join(cachePath, f)).isDirectory());
200
- console.log(`pi-research: camoufox ready (${versions.join(', ') || 'installed'})`);
201
- } catch (e) {
202
- // Not "ready": the path exists but cannot be read as an install dir (a plain
203
- // file, no permission, a broken mount). Claiming readiness here is how a
204
- // broken CAMOUFOX_INSTALL_DIR turned into a mysterious "browser missing at
205
- // runtime" much later. The fetch path above already printed the remedy.
206
- console.warn(`pi-research: camoufox install path ${cachePath} could not be verified: ${e instanceof Error ? e.message : String(e)}`);
162
+ const builds = layout.listInstalls(cachePath).map((b) => b.name);
163
+ const pkgRootForVerify = layout.findPackageRoot(projectRoot);
164
+ if (builds.length > 0 && layout.isUsable(cachePath, pkgRootForVerify ? layout.loadPin(pkgRootForVerify) : null)) {
165
+ console.log(`pi-research: camoufox ready (${builds.join(', ')})`);
166
+ } else if (browsersInstalled) {
167
+ console.warn(`pi-research: camoufox install at ${cachePath} could not be verified (no usable browser build found)`);
207
168
  }
208
169
  } else if (browsersInstalled) {
209
170
  console.warn(`pi-research: camoufox binary not found at expected path ${cachePath}`);
package/src/cli-bin.mjs CHANGED
@@ -37,7 +37,16 @@
37
37
  import { existsSync } from 'node:fs';
38
38
  import { fileURLToPath } from 'node:url';
39
39
 
40
- const REQUIRED_PEERS = ['@earendil-works/pi-ai', '@earendil-works/pi-coding-agent'];
40
+ const REQUIRED_PEERS = [
41
+ '@earendil-works/pi-ai',
42
+ '@earendil-works/pi-coding-agent',
43
+ // dist/cli.mjs imports it directly (TUI width helpers), so an install that skips
44
+ // peers can lack it even though pi-coding-agent normally brings it in.
45
+ '@earendil-works/pi-tui',
46
+ // Host-provided too (peerDependency since 1.7.9): dist/cli.mjs imports it for
47
+ // every tool schema, and pi supplies its own copy when it loads the extension.
48
+ 'typebox',
49
+ ];
41
50
 
42
51
  /**
43
52
  * Probe one peer dependency.
package/src/cli.ts CHANGED
@@ -580,6 +580,20 @@ interface ResearchArgs {
580
580
  json?: boolean;
581
581
  }
582
582
 
583
+
584
+ /**
585
+ * Depth for a CLI research run. An explicit --depth 0 always runs quick mode (the
586
+ * documented CLI/skill entry). A CONFIG-derived default of 0 only does when
587
+ * QUICK_RESEARCH is on; otherwise it resolves to 1, as on the pi tool path.
588
+ */
589
+ export function resolveCliDepth(
590
+ explicit: number | undefined,
591
+ cfg: { QUICK_RESEARCH: boolean; DEFAULT_RESEARCH_DEPTH: number },
592
+ ): 0 | 1 | 2 | 3 {
593
+ const d = explicit ?? (cfg.QUICK_RESEARCH ? cfg.DEFAULT_RESEARCH_DEPTH : Math.max(1, cfg.DEFAULT_RESEARCH_DEPTH));
594
+ return d as 0 | 1 | 2 | 3;
595
+ }
596
+
583
597
  /**
584
598
  * Research subcommand. Initializes the SDK, runs deep/quick research, prints the
585
599
  * markdown report to stdout, and always shuts down (even on failure).
@@ -607,8 +621,7 @@ export async function cmdResearch(args: ResearchArgs): Promise<number> {
607
621
  return EXIT.USAGE;
608
622
  }
609
623
 
610
- const depth: 0 | 1 | 2 | 3 =
611
- (args.depth ?? getConfig(process.cwd(), 'cli').DEFAULT_RESEARCH_DEPTH) as 0 | 1 | 2 | 3;
624
+ const depth = resolveCliDepth(args.depth, getConfig(process.cwd(), 'cli'));
612
625
  // det.model already reflects --model-first precedence (detectCredentials above).
613
626
  const runModel = det.model;
614
627
  toStderr(`[pi-research] starting research (depth ${depth})${runModel ? ` with ${runModel}` : ''}…\n`);
@@ -805,10 +818,11 @@ async function cmdStatus(json?: boolean): Promise<number> {
805
818
  // anything.
806
819
  //
807
820
  // The browser's NATIVE DEPENDENCIES are a separate and harsher question, and were
808
- // previously assumed away here with "nothing else fails". Measured: better-sqlite3,
809
- // which camoufox-js needs to launch at all, is built by a dependency install script
810
- // and does NOT survive a scripts-blocked install. When it is missing every browser
811
- // worker dies and SEARCH IS DEAD — not degraded — while the knowledge store keeps
821
+ // previously assumed away here with "nothing else fails". The launcher's native
822
+ // module (`impit`) resolves a prebuilt platform package; if npm drops it (npm/cli#4828)
823
+ // the module fails at first use. (Historically this was better-sqlite3, a dependency
824
+ // of the camoufox-js launcher this package replaced with @camoufox/camoufox.)
825
+ // When it is missing every browser worker dies and SEARCH IS DEAD — not degraded — while the knowledge store keeps
812
826
  // working because onnxruntime and lancedb both ship their binaries. That is not a
813
827
  // capability note, so it gates `ready`: a build that cannot search cannot research.
814
828
  const browserInstalled = isBrowserAvailable();
@@ -830,17 +844,14 @@ async function cmdStatus(json?: boolean): Promise<number> {
830
844
  installed: browserInstalled,
831
845
  nativeDepsOk: nativeDeps.ok,
832
846
  nativeDepsError: nativeDeps.ok ? null : nativeDeps.error,
833
- // better-sqlite3 13 ships prebuilds/ for all eight platform/arch pairs and
834
- // loads them at runtime via node-gyp-build — no install script involved.
835
- // On modern npm (≥11.19) the injected `node-gyp rebuild` that a
836
- // binding.gyp-without-install-script triggers is skipped, so a missing
837
- // binding here means an old/broken install, not an approval problem:
838
- // point at the upgrade that fixes it.
847
+ // impit ships its binding as a per-platform optional package, no install script
848
+ // involved: a missing binding means npm dropped that package, and a reinstall
849
+ // restores it.
839
850
  nativeDepsFix: nativeDeps.ok
840
851
  ? null
841
- : 'Upgrade npm and reinstall: npm install -g npm@12, then reinstall @lincoln504/pi-research. better-sqlite3 13 ships prebuilt bindings (no script needed); older npm needlessly recompiles it and fails on machines without a C++ toolchain.',
852
+ : 'Reinstall @lincoln504/pi-research (npm install). The browser launcher\'s native module (impit) ships as a separate platform package that npm sometimes drops (npm/cli#4828); a fresh install restores it.',
842
853
  // Same remedy the health check prints, so both surfaces name one command.
843
- fix: browserInstalled ? null : 'npx camoufox-js fetch',
854
+ fix: browserInstalled ? null : 'npx camoufox fetch',
844
855
  note: browserInstalled
845
856
  ? null
846
857
  : 'Stealth browser not installed — scraping falls back to plain fetch, which some sites block. npm 12 blocks install scripts by default, which skips this package\'s postinstall; the engine is fetched on first use instead.',
@@ -1371,7 +1382,8 @@ COMMANDS
1371
1382
  state tree (<config dir>/research/state) and the cache, including
1372
1383
  downloaded embedding models. Keeps config.env and the knowledge
1373
1384
  store database. --purge-browsers also deletes the shared
1374
- stealth-browser cache (~/.cache/camoufox) other tools use.
1385
+ stealth-browser cache (~/.cache/camoufox, every installed
1386
+ browser build) other tools use.
1375
1387
 
1376
1388
  help, --help, -h Show this help.
1377
1389
  --version, -v Print the pi-research version.
@@ -41,7 +41,7 @@ export async function checkBrowserCapability(): Promise<{ healthy: boolean; erro
41
41
  }
42
42
  return { healthy: true, diagnostic: { status: mockMode ? 'mocked' : 'available' } };
43
43
  } else {
44
- return { healthy: false, error: 'Camoufox (browser) not found. Run "npx camoufox-js fetch" to install the browser.' };
44
+ return { healthy: false, error: 'Camoufox (browser) not found. Run "npx camoufox fetch" to install the browser.' };
45
45
  }
46
46
  }
47
47
 
@@ -42,7 +42,7 @@ export async function cleanupOrphanedCamoufoxProcesses(): Promise<void> {
42
42
  * Marker that identifies a browser process as one WE launched (Camoufox via
43
43
  * Playwright), never the user's own system Firefox. We match on the launched
44
44
  * EXECUTABLE — `camoufox-bin` (Linux), `camoufox.exe` (Windows) or
45
- * `…/MacOS/camoufox` (macOS), per camoufox-js's LAUNCH_FILE — plus our
45
+ * `…/MacOS/camoufox` (macOS), per @camoufox/camoufox's LAUNCH_FILE — plus our
46
46
  * `…/pi-research/profiles/…` profile path (the fallback when PI_RESEARCH_TMP_DIR
47
47
  * relocates profiles). Anchoring on the executable name (rather than a bare
48
48
  * `camoufox` substring) keeps an incidental `tail camoufox.log`, `grep camoufox`,
@@ -129,10 +129,11 @@ export function isPoolShutdownError(error: unknown): boolean {
129
129
  * anything about the page, the network or the load on the machine.
130
130
  *
131
131
  * The case that produced this: npm 12 turns `allowScripts` OFF by default, so a
132
- * plain `npm install` no longer runs dependency lifecycle scripts — including the
133
- * one that fetches/builds `better-sqlite3`, which `camoufox-js` needs to launch a
134
- * browser at all. Every browser worker then dies with "Could not locate the
135
- * bindings file", every query fails, and the run reported that DuckDuckGo might be
132
+ * plain `npm install` no longer runs dependency lifecycle scripts, and npm can drop a
133
+ * native dependency's platform package (npm/cli#4828). Historically this was
134
+ * `better-sqlite3` (a dependency of the old `camoufox-js` launcher; the launcher is now
135
+ * `@camoufox/camoufox`, whose native module is `impit`). Every browser worker then dies
136
+ * with "Could not locate the bindings file", every query fails, and the run reported that DuckDuckGo might be
136
137
  * unreachable or the machine under extreme load. Neither was true, and the advice
137
138
  * that follows from each is useless. An install fault has to say so.
138
139
  */