@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.
- package/LICENSE +1 -1
- package/README.md +55 -138
- package/dist/cli.mjs +23375 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1195 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- 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';
|