@lincoln504/pi-research 1.7.8 → 1.7.10

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 (37) hide show
  1. package/README.md +5 -1
  2. package/agent-skill/pi-research/README.md +3 -3
  3. package/agent-skill/pi-research/SKILL.md +1 -1
  4. package/dist/cli-bin.mjs +3 -0
  5. package/dist/cli.mjs +673 -383
  6. package/dist/pdf-extract-worker.mjs +1 -1
  7. package/dist/thread-worker.mjs +6 -6
  8. package/docs/AGENT-SKILL.md +6 -4
  9. package/docs/ARCHITECTURE.md +64 -50
  10. package/docs/CHANGELOG.md +41 -0
  11. package/docs/CONFIGURATION.md +2 -2
  12. package/docs/PI-EXTENSION.md +4 -0
  13. package/docs/SDK.md +10 -7
  14. package/docs/THIRD-PARTY-NOTICES.md +11 -7
  15. package/package.json +10 -8
  16. package/scripts/camoufox-layout.cjs +455 -0
  17. package/scripts/cleanup.cjs +72 -29
  18. package/scripts/setup.cjs +73 -111
  19. package/src/cli-bin.mjs +3 -0
  20. package/src/cli.ts +12 -13
  21. package/src/core/pi-version.ts +9 -1
  22. package/src/healthcheck/index.ts +1 -1
  23. package/src/infrastructure/browser/browser-cleanup.ts +1 -1
  24. package/src/infrastructure/browser/browser-error-utils.ts +5 -4
  25. package/src/infrastructure/browser/camoufox-layout.ts +450 -0
  26. package/src/infrastructure/browser/config.ts +46 -103
  27. package/src/infrastructure/browser/ensure-browser.ts +90 -52
  28. package/src/infrastructure/browser/thread-worker-browser.ts +38 -42
  29. package/src/infrastructure/browser/thread-worker.mjs +6 -6
  30. package/src/infrastructure/knowledge-store-service.ts +6 -6
  31. package/src/orchestration/researcher-executor.ts +47 -1
  32. package/src/tui/research-health.ts +1 -1
  33. package/src/utils/log-utils.ts +1 -1
  34. package/src/web-research/browser-search.ts +8 -10
  35. package/src/web-research/pdf-extract-worker.mjs +1 -1
  36. package/src/web-research/scraper-utils.ts +11 -2
  37. 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,44 @@ 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
+ console.log('pi-research: downloading the ~1.3 GB Camoufox browser — this can take several minutes on a slow link…');
139
+ const pkgRoot = layout.findPackageRoot(projectRoot);
140
+ const result = layout.provisionBrowser(cachePath, runFetch, (m) => console.log(m), pkgRoot ? layout.loadPin(pkgRoot) : null);
141
+ if (result.status === 'failed') {
178
142
  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}`);
143
+ console.error('Run manually to fix: npx camoufox fetch');
144
+ console.error(`Reason: ${result.reason}`);
181
145
  // Exit 0 by default: the browser is fetched lazily on first use, so a restrictive
182
146
  // network must not fail a user's install. But that also meant nothing here could
183
147
  // ever turn CI red — a broken Windows launcher yielded a green `npm ci`. CI sets
@@ -187,23 +151,21 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
187
151
  process.env.PI_RESEARCH_STRICT_SETUP === 'true';
188
152
  process.exit(strict ? 1 : 0);
189
153
  }
190
- } else {
191
154
  browsersInstalled = true;
155
+ if (result.status === 'present') {
156
+ console.log(`pi-research: Camoufox already installed at ${cachePath}. Skipping fetch.`);
157
+ }
192
158
  }
193
159
  }
194
160
 
195
161
  // Verify
196
- const cachePath = camoufoxCachePath();
197
162
  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)}`);
163
+ const builds = layout.listInstalls(cachePath).map((b) => b.name);
164
+ const pkgRootForVerify = layout.findPackageRoot(projectRoot);
165
+ if (builds.length > 0 && layout.isUsable(cachePath, pkgRootForVerify ? layout.loadPin(pkgRootForVerify) : null)) {
166
+ console.log(`pi-research: camoufox ready (${builds.join(', ')})`);
167
+ } else if (browsersInstalled) {
168
+ console.warn(`pi-research: camoufox install at ${cachePath} could not be verified (no usable browser build found)`);
207
169
  }
208
170
  } else if (browsersInstalled) {
209
171
  console.warn(`pi-research: camoufox binary not found at expected path ${cachePath}`);
package/src/cli-bin.mjs CHANGED
@@ -43,6 +43,9 @@ const REQUIRED_PEERS = [
43
43
  // dist/cli.mjs imports it directly (TUI width helpers), so an install that skips
44
44
  // peers can lack it even though pi-coding-agent normally brings it in.
45
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',
46
49
  ];
47
50
 
48
51
  /**
package/src/cli.ts CHANGED
@@ -818,10 +818,11 @@ async function cmdStatus(json?: boolean): Promise<number> {
818
818
  // anything.
819
819
  //
820
820
  // The browser's NATIVE DEPENDENCIES are a separate and harsher question, and were
821
- // previously assumed away here with "nothing else fails". Measured: better-sqlite3,
822
- // which camoufox-js needs to launch at all, is built by a dependency install script
823
- // and does NOT survive a scripts-blocked install. When it is missing every browser
824
- // 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
825
826
  // working because onnxruntime and lancedb both ship their binaries. That is not a
826
827
  // capability note, so it gates `ready`: a build that cannot search cannot research.
827
828
  const browserInstalled = isBrowserAvailable();
@@ -843,17 +844,14 @@ async function cmdStatus(json?: boolean): Promise<number> {
843
844
  installed: browserInstalled,
844
845
  nativeDepsOk: nativeDeps.ok,
845
846
  nativeDepsError: nativeDeps.ok ? null : nativeDeps.error,
846
- // better-sqlite3 13 ships prebuilds/ for all eight platform/arch pairs and
847
- // loads them at runtime via node-gyp-build — no install script involved.
848
- // On modern npm (≥11.19) the injected `node-gyp rebuild` that a
849
- // binding.gyp-without-install-script triggers is skipped, so a missing
850
- // binding here means an old/broken install, not an approval problem:
851
- // 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.
852
850
  nativeDepsFix: nativeDeps.ok
853
851
  ? null
854
- : '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.',
855
853
  // Same remedy the health check prints, so both surfaces name one command.
856
- fix: browserInstalled ? null : 'npx camoufox-js fetch',
854
+ fix: browserInstalled ? null : 'npx camoufox fetch',
857
855
  note: browserInstalled
858
856
  ? null
859
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.',
@@ -1384,7 +1382,8 @@ COMMANDS
1384
1382
  state tree (<config dir>/research/state) and the cache, including
1385
1383
  downloaded embedding models. Keeps config.env and the knowledge
1386
1384
  store database. --purge-browsers also deletes the shared
1387
- stealth-browser cache (~/.cache/camoufox) other tools use.
1385
+ stealth-browser cache (~/.cache/camoufox, every installed
1386
+ browser build) other tools use.
1388
1387
 
1389
1388
  help, --help, -h Show this help.
1390
1389
  --version, -v Print the pi-research version.
@@ -122,8 +122,16 @@ export const PI_MIN_VERSION: SemverParts = { major: 0, minor: 99, patch: 0 };
122
122
  * modes), the `builtin:<name>` naming for built-in extensions and tools, and
123
123
  * `--no-extensions` now also disabling the built-in extensions. None of them
124
124
  * change what this package does.
125
+ *
126
+ * 1.1.0 (2026-10-07) is additive for this package. Diffed against 1.0.4 on 2026-10-08:
127
+ * `@earendil-works/pi-coding-agent` adds `durationMs` on the tool render context and
128
+ * `tool_execution_end`, `aborted` on `agent_settled` events, and the `+name`/`-name`
129
+ * tool modifiers; `@earendil-works/pi-ai` adds the Decisions-API classifier surface
130
+ * and an optional `AuthProvider.agentName`; `@earendil-works/pi-tui` adds the OSC 7501
131
+ * program-status helpers and `Text.setPaddingX`. `docs/extensions.md` is unchanged,
132
+ * and nothing was removed or re-signed, so the 0.99.0 floor is unchanged too.
125
133
  */
126
- export const PI_TESTED_MAX_VERSION: SemverParts = { major: 1, minor: 0, patch: 0 };
134
+ export const PI_TESTED_MAX_VERSION: SemverParts = { major: 1, minor: 1, patch: 0 };
127
135
 
128
136
  export type PiCompatibilityLevel = 'ok' | 'unparseable' | 'too-old' | 'untested';
129
137
 
@@ -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
  */