@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,120 @@
1
+ /**
2
+ * Browser Lifecycle
3
+ *
4
+ * Top-level lifecycle helpers that tie together the scheduler, worker pool,
5
+ * and state-manager. Used by SchedulerService.dispose() to tear down the
6
+ * browser subsystem cleanly.
7
+ */
8
+
9
+ import { logger } from '../../logger.ts';
10
+ import { tryGetService } from '../../core/service-registry.ts';
11
+ import { ServiceNames } from '../../core/service-interfaces.ts';
12
+ import type { ISchedulerInternals } from '../../core/interfaces/scheduler-interfaces.ts';
13
+ import { BrowserTaskScheduler } from './browser-task-scheduler.ts';
14
+ import type { WorkerPoolManager } from './worker-pool-manager.ts';
15
+
16
+
17
+ /**
18
+ * Stop the current browser manager / scheduler instance.
19
+ *
20
+ * Retrieves the cached scheduler from SchedulerService, calls shutdown() if
21
+ * it is a BrowserTaskScheduler, then clears all cached references so the next
22
+ * request starts fresh.
23
+ */
24
+ export async function stopBrowserManager(): Promise<void> {
25
+ try {
26
+ // Try to get the service synchronously first to avoid errors if not initialized
27
+ const schedulerService = tryGetService<ISchedulerInternals>(ServiceNames.SCHEDULER);
28
+
29
+ if (!schedulerService) {
30
+ logger.debug('[BrowserLifecycle] Scheduler service not available, nothing to stop');
31
+ return;
32
+ }
33
+
34
+ const instance = schedulerService.getSchedulerInstance();
35
+
36
+ if (instance instanceof BrowserTaskScheduler) {
37
+ await instance.shutdown();
38
+ } else if (instance && typeof (instance as any).shutdown === 'function') {
39
+ await (instance as any).shutdown();
40
+ }
41
+
42
+ // Clear all cached scheduler references
43
+ schedulerService.setSchedulerInstance(null);
44
+ schedulerService.setSchedulerVersion(null);
45
+ schedulerService.setSchedulerInitializationPromise(null);
46
+
47
+ logger.debug('[BrowserLifecycle] Browser manager stopped and references cleared');
48
+ } catch (err) {
49
+ const msg = err instanceof Error ? err.message : String(err);
50
+ logger.warn('[BrowserLifecycle] Error stopping browser manager:', msg);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Wait until any background scheduler shutdown (from a fire-and-forget
56
+ * forceSchedulerRestart) has fully completed, including the WorkerPoolManager's
57
+ * 1500ms IPC drain. Safe to call when no shutdown is in progress — resolves
58
+ * immediately.
59
+ *
60
+ * Strategy:
61
+ * 1. Await the pending shutdown promise stored in SchedulerService (set by
62
+ * forceSchedulerRestart when it fires-and-forgets oldScheduler.shutdown()).
63
+ * This is the authoritative signal that the full chain — including the
64
+ * WorkerPoolManager drain — has completed.
65
+ * 2. Fall back to polling WorkerPoolManager.isPoolShuttingDown() for any
66
+ * shutdown that was initiated outside of forceSchedulerRestart (e.g.,
67
+ * stopBrowserManager called directly on a BrowserTaskScheduler).
68
+ *
69
+ * Used primarily in integration tests to bridge the gap between a
70
+ * forceSchedulerRestart() call and subsequent browser task operations.
71
+ */
72
+ export async function waitForBrowserPoolIdle(maxWaitMs = 15000): Promise<void> {
73
+ const start = Date.now();
74
+
75
+ // Phase 1: await the tracked fire-and-forget shutdown promise.
76
+ // This covers the common case where forceSchedulerRestart was called: the promise
77
+ // resolves only after BrowserTaskScheduler.shutdown() — including the WorkerPoolManager
78
+ // drain — completes.
79
+ //
80
+ // Caveat: the retry path in task-execution-service may call forceSchedulerRestart again
81
+ // on the failed (Scheduler2) instance, which replaces pendingShutdownPromise with a
82
+ // quickly-resolving no-op promise (WorkerPoolManager.shutdown() is already running and
83
+ // returns immediately on the second call). To guard against this, we always fall through
84
+ // to Phase 2 and poll isPoolShuttingDown() after the promise resolves.
85
+ const schedulerService = tryGetService<ISchedulerInternals>(ServiceNames.SCHEDULER);
86
+ const pendingShutdown = schedulerService?.getPendingShutdownPromise();
87
+ if (pendingShutdown) {
88
+ const remainingMs = maxWaitMs - (Date.now() - start);
89
+ if (remainingMs > 0) {
90
+ let timeoutId: NodeJS.Timeout | undefined;
91
+ const timeoutPromise = new Promise<'timeout'>(resolve => {
92
+ timeoutId = setTimeout(() => resolve('timeout'), remainingMs);
93
+ });
94
+ const pendingPromise = pendingShutdown.then(() => 'done' as const);
95
+ pendingPromise.catch((err: any) => logger.debug(`[BrowserLifecycle] Background pending shutdown rejection: ${err instanceof Error ? err.message : String(err)}`));
96
+ const winner = await Promise.race([
97
+ pendingPromise,
98
+ timeoutPromise,
99
+ ]);
100
+ if (timeoutId) clearTimeout(timeoutId);
101
+ if (winner === 'timeout') {
102
+ logger.warn('[BrowserLifecycle] waitForBrowserPoolIdle: timed out awaiting pendingShutdown after', maxWaitMs, 'ms');
103
+ return;
104
+ }
105
+ }
106
+ // Falls through to Phase 2 — the pending promise may have been a no-op shutdown
107
+ // that resolved before the original drain finished.
108
+ }
109
+
110
+ // Phase 2: poll WorkerPoolManager.isPoolShuttingDown() until idle or timeout.
111
+ const poolManager = tryGetService<WorkerPoolManager>(ServiceNames.WORKER_POOL_MANAGER);
112
+ if (!poolManager || !poolManager.isPoolShuttingDown()) return;
113
+
114
+ const deadline = Date.now() + (maxWaitMs - (Date.now() - start));
115
+ while (Date.now() < deadline) {
116
+ await new Promise(resolve => setTimeout(resolve, 100));
117
+ if (!poolManager.isPoolShuttingDown()) return;
118
+ }
119
+ logger.warn('[BrowserLifecycle] waitForBrowserPoolIdle: pool still shutting down after', maxWaitMs, 'ms');
120
+ }
@@ -0,0 +1,174 @@
1
+ import * as http from 'node:http';
2
+ import * as crypto from 'node:crypto';
3
+ import { logger } from '../../logger.ts';
4
+ import type { SearchResult } from '../../web-research/types.ts';
5
+ import { isCloudflareBlockError, isPoolShutdownError } from './browser-error-utils.ts';
6
+
7
+ export interface BrowserServerOptions {
8
+ onSearch: (query: string) => Promise<SearchResult[]>;
9
+ onScrape: (url: string) => Promise<any>;
10
+ onHealthCheck: () => Promise<{ success: boolean }>;
11
+ }
12
+
13
+ /**
14
+ * FIX (#21): Shared secret for authenticating local HTTP requests.
15
+ * Generated once per server instance; clients must present it in the
16
+ * `X-Browser-Auth` header. This prevents unauthorized local processes
17
+ * from issuing search/scrape requests through the browser pool.
18
+ */
19
+ let _authSecret: string | null = null;
20
+
21
+ function getOrCreateAuthSecret(): string {
22
+ if (!_authSecret) {
23
+ _authSecret = crypto.randomBytes(32).toString('hex');
24
+ }
25
+ return _authSecret;
26
+ }
27
+
28
+ export function getBrowserServerAuthSecret(): string {
29
+ return getOrCreateAuthSecret();
30
+ }
31
+
32
+ export class BrowserServer {
33
+ private server: http.Server | null = null;
34
+ private port: number = 0;
35
+
36
+ constructor(private options: BrowserServerOptions) {}
37
+
38
+ async start(): Promise<number> {
39
+ return new Promise((resolve, reject) => {
40
+ this.server = http.createServer(async (req, res) => {
41
+ // FIX (#21): Validate auth token on every request
42
+ const rawAuth = req.headers['x-browser-auth'];
43
+ const authHeader = Array.isArray(rawAuth) ? rawAuth[0] : rawAuth;
44
+ const expected = Buffer.from(getOrCreateAuthSecret(), 'utf8');
45
+ const actual = Buffer.from(authHeader ?? '', 'utf8');
46
+ if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
47
+ res.writeHead(403, { 'Content-Type': 'application/json' });
48
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
49
+ return;
50
+ }
51
+
52
+ if (req.method !== 'POST') {
53
+ res.writeHead(405);
54
+ res.end('Method Not Allowed');
55
+ return;
56
+ }
57
+
58
+ let body = '';
59
+ let oversized = false;
60
+ const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB limit
61
+
62
+ req.on('data', chunk => {
63
+ body += chunk;
64
+ if (body.length > MAX_BODY_SIZE) {
65
+ oversized = true;
66
+ req.destroy(new Error('Payload too large'));
67
+ }
68
+ });
69
+
70
+ req.on('error', (err) => {
71
+ logger.error('[BrowserServer] Request stream error:', err);
72
+ // req.destroy() on oversize emits 'error', not 'end', so no response
73
+ // would otherwise be sent. Reply 413 best-effort before the socket
74
+ // tears down so the client gets a clear status, not a bare reset.
75
+ if (oversized && !res.headersSent) {
76
+ try {
77
+ res.writeHead(413, { 'Content-Type': 'application/json' });
78
+ res.end(JSON.stringify({ error: 'Payload too large' }));
79
+ } catch { /* socket may already be gone */ }
80
+ }
81
+ });
82
+
83
+ req.on('end', async () => {
84
+ try {
85
+ const data = JSON.parse(body);
86
+ let result: any;
87
+
88
+ switch (req.url) {
89
+ case '/search':
90
+ result = await this.options.onSearch(data.query);
91
+ break;
92
+ case '/scrape':
93
+ result = await this.options.onScrape(data.url);
94
+ break;
95
+ case '/healthcheck':
96
+ result = await this.options.onHealthCheck();
97
+ break;
98
+ default:
99
+ res.writeHead(404);
100
+ res.end('Not Found');
101
+ return;
102
+ }
103
+
104
+ res.writeHead(200, { 'Content-Type': 'application/json' });
105
+ res.end(JSON.stringify(result));
106
+ } catch (error) {
107
+ // A Cloudflare/bot challenge is an EXPECTED scrape outcome, not a
108
+ // server fault — log it at WARN so it doesn't inflate ERROR counts.
109
+ // Everything else is a genuine request-handling error.
110
+ if (isCloudflareBlockError(error)) {
111
+ logger.warn('[BrowserServer] Request blocked by bot protection:', error instanceof Error ? error.message : String(error));
112
+ } else if (isPoolShutdownError(error)) {
113
+ // Pool destroyed mid-request during quit/SIGTERM teardown — expected,
114
+ // not a server fault. DEBUG so a normal shutdown doesn't emit ERRORs.
115
+ logger.debug('[BrowserServer] Request abandoned during shutdown:', error instanceof Error ? error.message : String(error));
116
+ } else {
117
+ logger.error('[BrowserServer] Error handling request:', error);
118
+ }
119
+
120
+ // Extract safe error message - first line only to prevent stack trace exposure
121
+ let errorMessage = 'Unknown error';
122
+ if (error instanceof Error && error.message) {
123
+ errorMessage = (error.message.split('\n')[0] || '').trim();
124
+ } else if (typeof error === 'string') {
125
+ errorMessage = (error.split('\n')[0] || '').trim();
126
+ }
127
+
128
+ // Headers may already be out (e.g. res.end() threw mid-response) —
129
+ // writeHead() would then throw ERR_HTTP_HEADERS_SENT inside this async
130
+ // handler and surface as an unhandledRejection in the leader process.
131
+ if (!res.headersSent) {
132
+ res.writeHead(500, { 'Content-Type': 'application/json' });
133
+ res.end(JSON.stringify({ error: errorMessage }));
134
+ } else {
135
+ res.destroy();
136
+ }
137
+ }
138
+ });
139
+ });
140
+
141
+ this.server.listen(0, '127.0.0.1', () => {
142
+ const addr = this.server?.address();
143
+ if (addr && typeof addr === 'object') {
144
+ this.port = addr.port;
145
+ logger.log(`[BrowserServer] Listening on http://127.0.0.1:${this.port}`);
146
+ resolve(this.port);
147
+ } else {
148
+ reject(new Error('Failed to get server port'));
149
+ }
150
+ });
151
+
152
+ this.server.on('error', (err) => {
153
+ logger.error('[BrowserServer] Server error:', err);
154
+ reject(err);
155
+ });
156
+ });
157
+ }
158
+
159
+ async stop(): Promise<void> {
160
+ if (this.server) {
161
+ this.server.closeAllConnections?.();
162
+ return new Promise((resolve) => {
163
+ this.server?.close(() => {
164
+ this.server = null;
165
+ resolve();
166
+ });
167
+ });
168
+ }
169
+ }
170
+
171
+ getPort(): number {
172
+ return this.port;
173
+ }
174
+ }