@lincoln504/pi-research 0.1.13 → 1.0.1

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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -138
  3. package/dist/cli.mjs +23375 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +130 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +211 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +122 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +66 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1195 -0
  30. package/src/config.ts +1032 -155
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +258 -11
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Browser Configuration
3
+ *
4
+ * Consolidated configuration utilities for browser management.
5
+ * Combines functionality from browser-config.ts and browser-configuration.ts.
6
+ */
7
+
8
+ import * as crypto from 'node:crypto';
9
+ import { join } from 'node:path';
10
+ import { existsSync, mkdirSync } from 'node:fs';
11
+ import { platform, homedir } from 'node:os';
12
+ import type { Config } from '../../config.ts';
13
+ import { getConfig } from '../../config.ts';
14
+ import { getLogger } from '../../logger.ts';
15
+
16
+ // ============================================================================
17
+ // Binary Cache Management
18
+ // ============================================================================
19
+
20
+ /**
21
+ * The directory where camoufox-js installs the browser on Windows.
22
+ *
23
+ * camoufox-js's userCacheDir("camoufox") resolves to
24
+ * os.homedir()/AppData/Local/camoufox/camoufox/Cache
25
+ * — note the DOUBLED "camoufox" segment, and that it uses os.homedir() rather
26
+ * than %LOCALAPPDATA%. We must mirror it exactly, otherwise isBrowserAvailable()
27
+ * looks in the wrong place and every browser test silently skips on Windows
28
+ * even though the binary downloaded and launches fine.
29
+ */
30
+ function getWindowsCamoufoxDir(): string {
31
+ return join(homedir(), 'AppData', 'Local', 'camoufox', 'camoufox', 'Cache');
32
+ }
33
+
34
+ /**
35
+ * Get the camoufox binary cache directory.
36
+ * Uses PLAYWRIGHT_BROWSERS_PATH if set, otherwise the standard user cache location
37
+ * that camoufox uses by default (~/.cache/camoufox on Linux).
38
+ * We do NOT override HOME — that trick was unreliable and caused install/runtime mismatches.
39
+ */
40
+ export function getBrowserCacheDir(): string {
41
+ if (process.env['PLAYWRIGHT_BROWSERS_PATH']) {
42
+ return process.env['PLAYWRIGHT_BROWSERS_PATH'];
43
+ }
44
+ // Mirror getCamoufoxBinaryPath() so both functions agree on the cache location
45
+ const osPlatform = platform();
46
+ if (osPlatform === 'win32') {
47
+ return getWindowsCamoufoxDir();
48
+ } else if (osPlatform === 'darwin') {
49
+ return join(homedir(), 'Library', 'Caches', 'camoufox');
50
+ } else {
51
+ const cacheHome = process.env['XDG_CACHE_HOME'] || join(homedir(), '.cache');
52
+ return join(cacheHome, 'camoufox');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Directory for transient per-worker browser profiles (Playwright/Camoufox
58
+ * create one isolated profile per browser instance).
59
+ *
60
+ * Defaults to a DISK-backed location (~/.cache/pi-research/profiles) rather than
61
+ * os.tmpdir(): on systems where /tmp is tmpfs (RAM-backed), placing several
62
+ * browser profiles there consumes RAM and, with an OOM killer like earlyoom
63
+ * active, can contribute to the whole session being killed. Override with
64
+ * PI_RESEARCH_TMP_DIR (config.TMP_DIR) — e.g. point it back at the system temp
65
+ * dir to deliberately use tmpfs/RAM when there is memory headroom.
66
+ *
67
+ * The directory is created if missing, because Playwright requires the parent
68
+ * of its mkdtemp profile dir to already exist.
69
+ */
70
+ export function getBrowserProfileDir(config?: Config): string {
71
+ const configured = (config || getConfig()).TMP_DIR;
72
+ let dir: string;
73
+ if (configured && configured.trim().length > 0) {
74
+ dir = configured;
75
+ } else {
76
+ const cacheHome = process.env['XDG_CACHE_HOME'] || join(homedir(), '.cache');
77
+ dir = join(cacheHome, 'pi-research', 'profiles');
78
+ }
79
+ if (!existsSync(dir)) {
80
+ try {
81
+ mkdirSync(dir, { recursive: true });
82
+ } catch (_e) {
83
+ // Ignore race / permission errors; Playwright will surface a clear
84
+ // error if the directory is genuinely unusable.
85
+ }
86
+ }
87
+ return dir;
88
+ }
89
+
90
+ /**
91
+ * Get environment for spawning browser worker processes.
92
+ * Does not override HOME so camoufox uses its natural install location.
93
+ *
94
+ * CRITICAL: Browser workers run as cluster child processes and inherit process.env.
95
+ * Since the config module loads .env file values into its internal config object
96
+ * but does NOT set them into process.env, we must explicitly inject config values
97
+ * here so workers can read them from their process.env copy.
98
+ */
99
+ export function getBrowserEnv(config?: Config): NodeJS.ProcessEnv {
100
+ const env: NodeJS.ProcessEnv = { ...process.env };
101
+ const customPath = process.env['PLAYWRIGHT_BROWSERS_PATH'];
102
+ if (customPath) {
103
+ env['PLAYWRIGHT_BROWSERS_PATH'] = customPath;
104
+ } else {
105
+ delete env['PLAYWRIGHT_BROWSERS_PATH'];
106
+ }
107
+ // Tell thread-workers where to write their lifecycle/error log. Precedence:
108
+ // 1. A user-set PI_RESEARCH_LOG_FILE (already copied from process.env above) wins —
109
+ // never clobber an explicit override.
110
+ // 2. Otherwise propagate the resolved main-process log path so workers log to the
111
+ // same file. That path already honors the user's PI_RESEARCH_LOG_PATH.
112
+ if (!env['PI_RESEARCH_LOG_FILE']) {
113
+ const logFilePath = getLogger().getLogFilePath();
114
+ if (logFilePath) {
115
+ env['PI_RESEARCH_LOG_FILE'] = logFilePath;
116
+ }
117
+ }
118
+ // Redirect the worker's temp dir to a disk-backed profile directory so
119
+ // Playwright/Camoufox per-instance profiles do not land on a tmpfs (RAM)
120
+ // /tmp. The worker's own logging is unaffected: it writes to
121
+ // PI_RESEARCH_LOG_FILE (set above), not os.tmpdir(). TMP/TEMP are set too
122
+ // for Windows. os.tmpdir() in the worker honours these.
123
+ const profileDir = getBrowserProfileDir(config);
124
+ env['TMPDIR'] = profileDir;
125
+ env['TMP'] = profileDir;
126
+ env['TEMP'] = profileDir;
127
+ // Inject config values that browser workers read from process.env.
128
+ // Without this, workers only see shell-set values — .env file values would be lost.
129
+ const c = config || getConfig();
130
+ env['PI_RESEARCH_SCRAPE_TIMEOUT_MS'] = String(c.SCRAPE_TIMEOUT_MS);
131
+ env['PI_RESEARCH_SEARCH_TIMEOUT_MS'] = String(c.SEARCH_TIMEOUT_MS);
132
+ env['PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS'] = String(c.HEALTH_CHECK_TIMEOUT_MS);
133
+ return env;
134
+ }
135
+
136
+ /**
137
+ * Ensure the browser cache directory exists (only relevant for custom PLAYWRIGHT_BROWSERS_PATH).
138
+ */
139
+ export function ensureBrowserCacheDir(): string {
140
+ const cacheDir = getBrowserCacheDir();
141
+ if (!existsSync(cacheDir)) {
142
+ try {
143
+ mkdirSync(cacheDir, { recursive: true });
144
+ } catch (_e) {
145
+ // Ignore race condition
146
+ }
147
+ }
148
+ return cacheDir;
149
+ }
150
+
151
+ /**
152
+ * Get the expected path where camoufox installs its binaries.
153
+ * Matches camoufox-js's own resolution logic.
154
+ */
155
+ export function getCamoufoxBinaryPath(): string {
156
+ const customPath = process.env['PLAYWRIGHT_BROWSERS_PATH'];
157
+ if (customPath) {
158
+ return customPath;
159
+ }
160
+
161
+ const osPlatform = platform();
162
+
163
+ if (osPlatform === 'win32') {
164
+ return getWindowsCamoufoxDir();
165
+ } else if (osPlatform === 'darwin') {
166
+ return join(homedir(), 'Library', 'Caches', 'camoufox');
167
+ } else {
168
+ // Linux and others
169
+ const cacheHome = process.env['XDG_CACHE_HOME'] || join(homedir(), '.cache');
170
+ return join(cacheHome, 'camoufox');
171
+ }
172
+ }
173
+
174
+ // ============================================================================
175
+ // Scheduler Configuration
176
+ // ============================================================================
177
+
178
+ /**
179
+ * Generate a version hash for the scheduler based on critical config values.
180
+ * This allows us to detect when configuration changes and invalidate the cache.
181
+ */
182
+ export function generateSchedulerVersion(config?: Config): string {
183
+ const c = config || getConfig();
184
+ const versionString = `v2:${c.WORKER_THREADS}:${c.WORKER_CONCURRENCY}:${c.MAX_CONCURRENT_RESEARCHERS}`;
185
+ return crypto.createHash('sha256').update(versionString).digest('hex').substring(0, 16);
186
+ }
187
+
188
+ /**
189
+ * Get the current number of worker threads from config.
190
+ * This is a function instead of a constant to allow config changes to take effect
191
+ * without requiring a process restart.
192
+ */
193
+ export function getMaxWorkers(config?: Config): number {
194
+ return (config || getConfig()).WORKER_THREADS;
195
+ }
196
+
197
+ /**
198
+ * Get the current scheduler version hash.
199
+ */
200
+ export function getSchedulerVersion(config?: Config): string {
201
+ return generateSchedulerVersion(config);
202
+ }
203
+
204
+ // ============================================================================
205
+ // Display Environment
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Resolve the camoufox headless mode for the current platform.
210
+ *
211
+ * Returns true (true headless — no visible window) on Windows and macOS.
212
+ * Historically Windows used headless:false because headless:true crashed Firefox
213
+ * (exit 0x80000003, camoufox-js issue #614); that is fixed in camoufox-js >=0.10
214
+ * (verified on Windows 11 x64 with 0.10.2: headless:true launches and navigates
215
+ * reliably and, crucially, NO browser window pops up on the desktop). Using
216
+ * headless:false on a real Windows desktop flashed visible, sometimes fullscreen,
217
+ * browser windows during every scrape — true headless avoids that entirely.
218
+ *
219
+ * Returns true in all other cases (macOS, Linux+X11, Linux+Wayland, Linux TTY).
220
+ *
221
+ * On a bare Linux TTY (no DISPLAY, no WAYLAND_DISPLAY) true headless is the
222
+ * default: Firefox renders fully offscreen, needs no display server and no Xvfb,
223
+ * and navigates reliably (verified: cold launch ~0.8s, page nav ~0.3s on a bare
224
+ * Debian box). The earlier 'virtual' default forced a hard Xvfb dependency that
225
+ * is absent on minimal servers — when Xvfb is missing camoufox-js throws at
226
+ * launch, and even when present, GTK/Xvfb rendering on a stripped box can stall
227
+ * navigation, surfacing as a misleading "connection timed out" health error.
228
+ *
229
+ * Xvfb ('virtual') remains available as an explicit opt-in for users who have
230
+ * installed it and want the marginal anti-detection benefit of a real virtual
231
+ * framebuffer: set PI_RESEARCH_USE_XVFB=true. It only takes effect on a bare
232
+ * Linux TTY (when a real display is present that display is used instead).
233
+ *
234
+ * Note: the JS camoufox port does not strip WAYLAND_DISPLAY / GDK_BACKEND /
235
+ * MOZ_ENABLE_WAYLAND before spawning Xvfb (unlike the Python port), so
236
+ * headless:'virtual' can still attach to a live compositor instead of the
237
+ * spawned Xvfb — another reason true headless is the safe default.
238
+ */
239
+ export function resolveHeadlessMode(): boolean | 'virtual' {
240
+ const osPlatform = platform();
241
+ // Windows + macOS: true headless (no visible window). camoufox-js >=0.10 no
242
+ // longer crashes on Windows with headless:true (see the doc comment above).
243
+ if (osPlatform !== 'linux') return true;
244
+ if (process.env['DISPLAY']) return true;
245
+ if (process.env['WAYLAND_DISPLAY']) return true;
246
+ // Bare Linux TTY: true headless by default (no Xvfb required); opt into Xvfb
247
+ // explicitly only when requested.
248
+ if (process.env['PI_RESEARCH_USE_XVFB'] === 'true') return 'virtual';
249
+ return true;
250
+ }
251
+
252
+ // ============================================================================
253
+ // Browser Availability Check
254
+ // ============================================================================
255
+
256
+ /**
257
+ * Check if the browser is available for meaningful browser pool testing.
258
+ *
259
+ * Returns false if:
260
+ * - camoufox-js package is not installed
261
+ * - camoufox binary is not present at expected path
262
+ * - FULL_MOCK_MODE is active (both search and scrape mocked)
263
+ *
264
+ * In FULL_MOCK_MODE, browser pool tests are not meaningful because:
265
+ * - The FixedClusterPool deadlocks in Vitest fork context when both search and scrape are mocked
266
+ * - Mocked tasks short-circuit in runTask() and don't exercise real browser behavior
267
+ * - These tests verify real browser behavior: crash recovery, IPC routing, profile locking, etc.
268
+ *
269
+ * Single point of control: all existing skipTests() checks use this function.
270
+ */
271
+ export function isBrowserAvailable(): boolean {
272
+ if (isFullMockMode()) return false;
273
+ try {
274
+ import.meta.resolve('camoufox-js');
275
+ return existsSync(getCamoufoxBinaryPath());
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Check whether search and scrape are mocked (used by tests that need real
283
+ * browser behavior — mock mode short-circuits in runTask()).
284
+ */
285
+ function isFullMockMode(): boolean {
286
+ return process.env['PI_RESEARCH_MOCK_SEARCH'] === 'true' &&
287
+ process.env['PI_RESEARCH_MOCK_SCRAPE'] === 'true';
288
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Runtime browser (camoufox) provisioning.
3
+ *
4
+ * Normally the camoufox browser is fetched by the `postinstall` script
5
+ * (scripts/setup.cjs) when pi-research is installed. But some install flows skip
6
+ * lifecycle scripts (e.g. `npm install --ignore-scripts`, or a bare `git clone`
7
+ * whose `npm install` has not yet run) and therefore never run our postinstall.
8
+ * On those the browser binary is absent and the first scrape would fail with
9
+ * "Camoufox is not installed".
10
+ *
11
+ * `ensureBrowserInstalled()` closes that gap: it is called once before the worker
12
+ * pool spawns and lazily fetches the browser if (and only if) it is missing.
13
+ *
14
+ * Non-regression guarantee for the other install paths: when the browser is
15
+ * already present — which it always is after the pi-extension / plain-npm
16
+ * postinstall fetch — this is a cheap `existsSync` check that returns
17
+ * immediately and changes nothing. It only ever does work when the binary is
18
+ * genuinely absent.
19
+ */
20
+
21
+ import { spawn } from 'node:child_process';
22
+ import { createRequire } from 'node:module';
23
+ import { createHash } from 'node:crypto';
24
+ import { existsSync, readdirSync, statSync, fstatSync, openSync, closeSync, rmSync } from 'node:fs';
25
+ import { join, dirname } from 'node:path';
26
+ import { tmpdir } from 'node:os';
27
+ import { logger } from '../../logger.ts';
28
+ import { getCamoufoxBinaryPath } from './config.ts';
29
+
30
+ /** Bound the ~100MB download so a stalled network fails fast instead of hanging forever. */
31
+ const FETCH_TIMEOUT_MS = 15 * 60 * 1000;
32
+ /** A lock older than this is considered stale (crashed mid-fetch) and may be stolen. */
33
+ const STALE_LOCK_MS = 20 * 60 * 1000;
34
+ /** When another process holds the fetch lock, poll this long for the browser to appear. */
35
+ const WAIT_FOR_PEER_MS = 16 * 60 * 1000;
36
+ const POLL_INTERVAL_MS = 2000;
37
+
38
+ /** In-process dedupe: concurrent pool inits / scrapers share one fetch. */
39
+ let inFlight: Promise<void> | null = null;
40
+
41
+ /**
42
+ * True when a camoufox browser binary is actually present (cache dir exists and
43
+ * contains at least one version subdirectory) — mirrors the check in setup.cjs.
44
+ */
45
+ export function isBrowserBinaryPresent(): boolean {
46
+ try {
47
+ const cacheDir = getCamoufoxBinaryPath();
48
+ if (!existsSync(cacheDir)) return false;
49
+ return readdirSync(cacheDir).some((entry) => {
50
+ try {
51
+ return statSync(join(cacheDir, entry)).isDirectory();
52
+ } catch {
53
+ return false;
54
+ }
55
+ });
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /** Resolve the camoufox-js CLI bin, mirroring scripts/setup.cjs (handles hoisting). */
62
+ function resolveCamoufoxBin(): string | null {
63
+ try {
64
+ const require = createRequire(import.meta.url);
65
+ const pkgJson = require.resolve('camoufox-js/package.json');
66
+ const bin = join(dirname(pkgJson), '..', '.bin', process.platform === 'win32' ? 'camoufox-js.cmd' : 'camoufox-js');
67
+ if (existsSync(bin)) return bin;
68
+ } catch {
69
+ /* fall through to npx */
70
+ }
71
+ return null;
72
+ }
73
+
74
+ const delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
75
+
76
+ /** Run `camoufox-js fetch`, resolving on exit 0 and rejecting otherwise / on timeout. */
77
+ function runFetch(): Promise<void> {
78
+ return new Promise<void>((resolve, reject) => {
79
+ const bin = resolveCamoufoxBin();
80
+ const [cmd, args] = bin ? [bin, ['fetch']] : ['npx', ['camoufox-js', 'fetch']];
81
+ logger.info(`[ensure-browser] Camoufox not found; fetching the browser (this runs once, ~100MB): ${cmd} ${args.join(' ')}`);
82
+
83
+ // Windows needs shell:true to run the .cmd shim (Node CVE-2024-27980 fix makes a
84
+ // shell-less spawn of .cmd throw EINVAL), but shell:true performs NO quoting, so a
85
+ // spaced install path (C:\Users\John Smith\…) must be quoted explicitly. The args
86
+ // here are fixed literals ('fetch'), never user input, so quoting the command alone
87
+ // is sufficient and this stays injection-free.
88
+ const useShell = process.platform === 'win32';
89
+ const child = spawn(useShell ? `"${cmd}"` : cmd, args, { stdio: 'inherit', shell: useShell });
90
+ const timer = setTimeout(() => {
91
+ child.kill('SIGKILL');
92
+ reject(new Error(`camoufox fetch timed out after ${FETCH_TIMEOUT_MS}ms`));
93
+ }, FETCH_TIMEOUT_MS);
94
+
95
+ child.on('error', (err) => {
96
+ clearTimeout(timer);
97
+ reject(err);
98
+ });
99
+ child.on('exit', (code) => {
100
+ clearTimeout(timer);
101
+ if (code === 0) resolve();
102
+ else reject(new Error(`camoufox fetch exited with code ${code}`));
103
+ });
104
+ });
105
+ }
106
+
107
+ /** Wait for a peer process (holding the lock) to finish fetching the browser. */
108
+ async function waitForPeer(): Promise<boolean> {
109
+ const deadline = Date.now() + WAIT_FOR_PEER_MS;
110
+ while (Date.now() < deadline) {
111
+ await delay(POLL_INTERVAL_MS);
112
+ if (isBrowserBinaryPresent()) return true;
113
+ }
114
+ return false;
115
+ }
116
+
117
+ async function provision(): Promise<void> {
118
+ // Fast path: already installed (pi-extension / plain-npm postinstall case).
119
+ if (isBrowserBinaryPresent()) return;
120
+
121
+ // Respect the same opt-out the postinstall fetch honours (used by CI/tests).
122
+ if (process.env['PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD'] === '1') {
123
+ logger.debug('[ensure-browser] Browser missing but PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1; skipping auto-fetch.');
124
+ return;
125
+ }
126
+
127
+ // Cross-process lock so two concurrent first-runs don't double-fetch. Scoped
128
+ // by a hash of the target cache dir so distinct cache locations (e.g. a
129
+ // PLAYWRIGHT_BROWSERS_PATH override, or different users) never share a lock.
130
+ const cacheKey = createHash('sha256').update(getCamoufoxBinaryPath()).digest('hex').slice(0, 16);
131
+ const lockPath = join(tmpdir(), `pi-research-camoufox-fetch-${cacheKey}.lock`);
132
+ let haveLock = false;
133
+ try {
134
+ // 'wx' = O_CREAT|O_EXCL: fails if another process holds it. Mode 0o600 keeps
135
+ // the (empty) lock owner-only, matching FileLockService and avoiding a
136
+ // world-readable temp file.
137
+ closeSync(openSync(lockPath, 'wx', 0o600));
138
+ haveLock = true;
139
+ } catch {
140
+ // Lock held — but steal it if it's stale (a crash left it behind). Read the
141
+ // mtime through a file descriptor rather than re-stat'ing by path (avoids a
142
+ // check-then-use race), then re-acquire with O_EXCL: a process that recreated
143
+ // the lock in the meantime makes our 'wx' create fail, so we never double-acquire.
144
+ try {
145
+ let mtimeMs: number;
146
+ const fd = openSync(lockPath, 'r');
147
+ try {
148
+ mtimeMs = fstatSync(fd).mtimeMs;
149
+ } finally {
150
+ closeSync(fd);
151
+ }
152
+ if (Date.now() - mtimeMs > STALE_LOCK_MS) {
153
+ rmSync(lockPath, { force: true });
154
+ closeSync(openSync(lockPath, 'wx', 0o600));
155
+ haveLock = true;
156
+ }
157
+ } catch {
158
+ /* someone won the race; fall through to wait */
159
+ }
160
+ }
161
+
162
+ if (!haveLock) {
163
+ // Another process is fetching — wait for it, then re-check.
164
+ logger.info('[ensure-browser] Another process is fetching the browser; waiting for it to finish…');
165
+ if (await waitForPeer()) return;
166
+ // Peer didn't deliver in time; fetch ourselves as a fallback.
167
+ }
168
+
169
+ try {
170
+ if (isBrowserBinaryPresent()) return; // re-check under lock
171
+ await runFetch();
172
+ if (isBrowserBinaryPresent()) {
173
+ logger.info('[ensure-browser] Camoufox browser is ready.');
174
+ } else {
175
+ logger.warn('[ensure-browser] camoufox fetch reported success but no binary was found.');
176
+ }
177
+ } finally {
178
+ if (haveLock) {
179
+ try {
180
+ rmSync(lockPath, { force: true });
181
+ } catch {
182
+ /* best-effort */
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Ensure the camoufox browser is installed, fetching it once if missing.
190
+ *
191
+ * Idempotent and concurrency-safe: concurrent callers share a single fetch, and
192
+ * a cross-process lock prevents two processes from fetching at the same time.
193
+ * Best-effort — if the fetch fails, the subsequent browser launch surfaces the
194
+ * existing actionable "run npx camoufox-js fetch" error rather than this throwing.
195
+ */
196
+ export function ensureBrowserInstalled(): Promise<void> {
197
+ if (!inFlight) {
198
+ inFlight = provision().catch((err) => {
199
+ // Reset so a later attempt (e.g. after the user fixes the network) can retry.
200
+ inFlight = null;
201
+ logger.warn(`[ensure-browser] Automatic browser provisioning failed: ${err instanceof Error ? err.message : String(err)}`);
202
+ });
203
+ }
204
+ return inFlight;
205
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Browser Infrastructure - Public API
3
+ *
4
+ * Barrel module for browser-related functionality. Source code imports
5
+ * directly from specific submodules; this file exists for test convenience.
6
+ */
7
+
8
+ export {
9
+ getMaxWorkers,
10
+ getSchedulerVersion,
11
+ isBrowserAvailable,
12
+ getCamoufoxBinaryPath,
13
+ getBrowserCacheDir,
14
+ } from './config.ts';
15
+
16
+ export {
17
+ isTransientSocketError,
18
+ isPoolShutdownError,
19
+ resetBrowserCircuitBreaker,
20
+ } from './browser-error-utils.ts';
21
+
22
+ export type { IScheduler } from '../../core/interfaces/scheduler-interfaces.ts';
23
+
24
+ export {
25
+ forceSchedulerRestart,
26
+ } from './scheduler-factory.ts';
27
+
28
+ export {
29
+ runBrowserTask,
30
+ runBrowserHealthCheck,
31
+ runWorkerSearch,
32
+ } from './task-execution-service.ts';
33
+
34
+ export {
35
+ stopBrowserManager,
36
+ waitForBrowserPoolIdle,
37
+ } from './browser-lifecycle.ts';