@lincoln504/pi-research 1.2.0 → 1.3.0
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/dist/cli.mjs +1334 -750
- package/dist/thread-worker.mjs +194 -136
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CHANGELOG.md +114 -16
- package/package.json +1 -1
- package/scripts/cleanup.cjs +64 -2
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cleanup/research-cleanup.ts +12 -4
- package/src/cli.ts +228 -70
- package/src/config.ts +78 -2
- package/src/core/interfaces/health-check-interfaces.ts +3 -39
- package/src/core/interfaces/planning-interfaces.ts +8 -0
- package/src/core/interfaces/service-names.ts +0 -1
- package/src/core/interfaces/state-manager-interfaces.ts +10 -2
- package/src/core/llm/agentic-repair.ts +14 -0
- package/src/core/planning-service.ts +43 -1
- package/src/core/service-initialization.ts +6 -14
- package/src/core/service-interfaces.ts +1 -1
- package/src/healthcheck/index.ts +7 -2
- package/src/infrastructure/browser/browser-client.ts +64 -9
- package/src/infrastructure/browser/browser-server.ts +24 -7
- package/src/infrastructure/browser/browser-task-scheduler.ts +58 -12
- package/src/infrastructure/browser/cleanup-utils.ts +50 -2
- package/src/infrastructure/browser/client-agent.ts +6 -6
- package/src/infrastructure/browser/config.ts +14 -3
- package/src/infrastructure/browser/scheduler-factory.ts +18 -3
- package/src/infrastructure/browser/thread-worker-messaging.ts +54 -2
- package/src/infrastructure/browser/thread-worker-types.ts +3 -1
- package/src/infrastructure/browser/thread-worker.mjs +194 -136
- package/src/infrastructure/browser/worker-pool-manager.ts +7 -2
- package/src/infrastructure/embedding/embedding-factory.ts +35 -6
- package/src/infrastructure/embedding/embedding-server.ts +40 -7
- package/src/infrastructure/file-lock-service.ts +72 -2
- package/src/infrastructure/knowledge-store-service.ts +48 -3
- package/src/infrastructure/process-lifecycle-service.ts +28 -5
- package/src/infrastructure/research-run-semaphore.ts +37 -3
- package/src/infrastructure/state/state-browser-api.ts +5 -2
- package/src/infrastructure/state/state-browser-manager.ts +15 -1
- package/src/infrastructure/state/state-manager-service.ts +4 -4
- package/src/infrastructure/state/state-manager.ts +15 -3
- package/src/knowledge/chunker.ts +88 -1
- package/src/knowledge/embedder.ts +136 -43
- package/src/knowledge/store.ts +66 -12
- package/src/logger.ts +5 -1
- package/src/observers/research-observer-impl.ts +31 -0
- package/src/orchestration/abort-utils.ts +60 -0
- package/src/orchestration/deep-research-orchestrator.ts +12 -2
- package/src/orchestration/headless-observer.ts +8 -0
- package/src/orchestration/research-orchestration-service.ts +63 -11
- package/src/orchestration/research-session-service.ts +37 -11
- package/src/orchestration/researcher-executor.ts +100 -20
- package/src/orchestration/researcher.ts +39 -20
- package/src/orchestration/session-state.ts +11 -0
- package/src/sdk.ts +125 -19
- package/src/security/index.ts +51 -30
- package/src/security/nvd-client.ts +7 -2
- package/src/skill-install/skill-installer.ts +18 -2
- package/src/stackexchange/index.ts +20 -5
- package/src/stackexchange/output/table.ts +7 -2
- package/src/stackexchange/rest-client.ts +35 -1
- package/src/tools/research-tool-definition.ts +8 -2
- package/src/tools/scrape.ts +48 -25
- package/src/tools/search.ts +10 -1
- package/src/tools/security.ts +68 -7
- package/src/tui/research-health.ts +18 -2
- package/src/utils/circuit-breaker.ts +9 -2
- package/src/utils/disk-space-checker.ts +11 -6
- package/src/utils/json-utils.ts +116 -76
- package/src/utils/llm-usage.ts +17 -1
- package/src/utils/log-rotation.ts +42 -10
- package/src/utils/log-utils.ts +40 -0
- package/src/utils/metrics-summary.ts +27 -9
- package/src/utils/stdio-capture.ts +63 -22
- package/src/utils/text-utils.ts +8 -1
- package/src/utils/tool-usage-tracker.ts +9 -2
- package/src/web-research/browser-search.ts +26 -7
- package/src/web-research/scraper-utils.ts +13 -0
- package/src/web-research/search.ts +6 -2
- package/src/web-research/types.ts +4 -2
- package/src/web-research/web-scraper.ts +65 -4
- package/src/core/health-check-service.ts +0 -265
|
@@ -51,13 +51,17 @@ function redactSecrets(message) {
|
|
|
51
51
|
out = out.replace(JWT_PATTERN, "[REDACTED]");
|
|
52
52
|
out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
|
|
53
53
|
out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
|
|
54
|
+
out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
|
|
54
55
|
out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
|
|
55
56
|
out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
|
|
56
57
|
out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
|
|
57
58
|
out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
|
|
58
59
|
return out;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
function stripTerminalEscapes(message) {
|
|
62
|
+
return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
|
|
63
|
+
}
|
|
64
|
+
var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, TERMINAL_ESCAPE_PATTERN;
|
|
61
65
|
var init_log_utils = __esm({
|
|
62
66
|
"src/utils/log-utils.ts"() {
|
|
63
67
|
"use strict";
|
|
@@ -65,6 +69,7 @@ var init_log_utils = __esm({
|
|
|
65
69
|
MAX_LOG_MESSAGE_LENGTH = 1e4;
|
|
66
70
|
ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
67
71
|
URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
|
|
72
|
+
COOKIE_HEADER_PATTERN = /\b(set[_-]?cookie|cookie)\b(["']?\s*[:=]\s*["']?)([^\r\n]+)/gi;
|
|
68
73
|
SENSITIVE_KV_PATTERN = /\b(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key)\b(["']?\s*[:=]\s*["']?)([^\s"',&)]+)/gi;
|
|
69
74
|
KNOWN_TOKEN_PATTERN = /\b(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
|
|
70
75
|
JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
@@ -72,6 +77,106 @@ var init_log_utils = __esm({
|
|
|
72
77
|
BEARER_AUTH_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
|
|
73
78
|
PROVIDER_TOKEN_PATTERN = /\b(AIza[0-9A-Za-z_-]{35}|ya29\.[0-9A-Za-z_-]{20,}|[sp]k_(?:live|test)_[0-9A-Za-z]{16,})\b/g;
|
|
74
79
|
LONG_HEX_SECRET_PATTERN = /\b[0-9a-fA-F]{32,}\b/g;
|
|
80
|
+
CONTROL_CHARS_PATTERN = /[\x00-\x1f\x7f]/g;
|
|
81
|
+
TERMINAL_ESCAPE_PATTERN = new RegExp(
|
|
82
|
+
"\\x1b\\[[0-?]*[ -/]*[@-~]?|\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)?|\\x1b[PX^_][^\\x1b]*(?:\\x1b\\\\)?|\\x1b.?",
|
|
83
|
+
"g"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/utils/user-agent.ts
|
|
89
|
+
var USER_AGENTS;
|
|
90
|
+
var init_user_agent = __esm({
|
|
91
|
+
"src/utils/user-agent.ts"() {
|
|
92
|
+
"use strict";
|
|
93
|
+
USER_AGENTS = [
|
|
94
|
+
// Chrome on Windows (updated 2025)
|
|
95
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
96
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
97
|
+
// Chrome on macOS
|
|
98
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
99
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
100
|
+
// Chrome on Linux
|
|
101
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
102
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
103
|
+
// Firefox on Windows (updated 2025)
|
|
104
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
105
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
|
|
106
|
+
// Firefox on macOS
|
|
107
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
108
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// src/web-research/scraper-types.ts
|
|
114
|
+
var MAX_HTML_SIZE, MAX_PDF_SIZE, INTERNAL_NETWORK_PATTERNS, FILTERED_TAGS, BOT_PATTERNS, IMAGE_LINK_PATTERN, MARKDOWN_IMAGE_PATTERN;
|
|
115
|
+
var init_scraper_types = __esm({
|
|
116
|
+
"src/web-research/scraper-types.ts"() {
|
|
117
|
+
"use strict";
|
|
118
|
+
init_user_agent();
|
|
119
|
+
MAX_HTML_SIZE = 25 * 1024 * 1024;
|
|
120
|
+
MAX_PDF_SIZE = 100 * 1024 * 1024;
|
|
121
|
+
INTERNAL_NETWORK_PATTERNS = [
|
|
122
|
+
/^127\./,
|
|
123
|
+
// IPv4 loopback
|
|
124
|
+
/^0\./,
|
|
125
|
+
// IPv4 "this" network
|
|
126
|
+
/^::1$/,
|
|
127
|
+
// IPv6 loopback
|
|
128
|
+
/^fe80::/i,
|
|
129
|
+
// IPv6 link-local
|
|
130
|
+
/^f[cd][0-9a-f]{2}:/i,
|
|
131
|
+
// IPv6 unique local — full fc00::/7 block (fc00::–fdff::)
|
|
132
|
+
/^169\.254\./,
|
|
133
|
+
// IPv4 link-local (and metadata)
|
|
134
|
+
/^10\./,
|
|
135
|
+
// RFC 1918 Class A private
|
|
136
|
+
/^192\.168\./,
|
|
137
|
+
// RFC 1918 Class C private
|
|
138
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
139
|
+
// RFC 1918 Class B private
|
|
140
|
+
/^::ffff:(127\.|0\.|169\.254\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/i
|
|
141
|
+
// IPv4-mapped IPv6
|
|
142
|
+
];
|
|
143
|
+
FILTERED_TAGS = [
|
|
144
|
+
"nav",
|
|
145
|
+
"header",
|
|
146
|
+
"footer",
|
|
147
|
+
"aside",
|
|
148
|
+
"script",
|
|
149
|
+
"style",
|
|
150
|
+
"noscript",
|
|
151
|
+
"form",
|
|
152
|
+
"input",
|
|
153
|
+
"select",
|
|
154
|
+
"textarea",
|
|
155
|
+
"button",
|
|
156
|
+
"object",
|
|
157
|
+
"embed",
|
|
158
|
+
"svg",
|
|
159
|
+
"symbol",
|
|
160
|
+
"use",
|
|
161
|
+
"defs",
|
|
162
|
+
"path",
|
|
163
|
+
"circle",
|
|
164
|
+
"rect",
|
|
165
|
+
"line",
|
|
166
|
+
"polygon",
|
|
167
|
+
"img",
|
|
168
|
+
"iframe"
|
|
169
|
+
];
|
|
170
|
+
BOT_PATTERNS = [
|
|
171
|
+
["_cf_chl_", "Cloudflare challenge"],
|
|
172
|
+
["cf_chl_opt", "Cloudflare challenge opt"],
|
|
173
|
+
["cdn-cgi/challenge-platform", "Cloudflare challenge platform"],
|
|
174
|
+
["ddos-guard", "DDoS-Guard challenge"],
|
|
175
|
+
["Just a moment...", "Cloudflare interstitial"],
|
|
176
|
+
["Checking your browser before accessing", "Cloudflare interstitial"]
|
|
177
|
+
];
|
|
178
|
+
IMAGE_LINK_PATTERN = /\[([^\]]*)\]\((data:image\/[^)]+|[^)\s]+\.(?:svg|png|jpe?g|gif|webp|bmp|ico)(?:\?[^)]*)?)\)/gi;
|
|
179
|
+
MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((?:data:image\/[^)]+|[^)\s]+)\)/gi;
|
|
75
180
|
}
|
|
76
181
|
});
|
|
77
182
|
|
|
@@ -265,6 +370,14 @@ var init_log_rotation = __esm({
|
|
|
265
370
|
try {
|
|
266
371
|
if (fs2.existsSync(logFile)) {
|
|
267
372
|
fs2.unlinkSync(logFile);
|
|
373
|
+
try {
|
|
374
|
+
fs2.closeSync(fs2.openSync(logFile, "a", 384));
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
fs2.chmodSync(logFile, 384);
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
268
381
|
}
|
|
269
382
|
const files = fs2.readdirSync(logDir);
|
|
270
383
|
const baseName = path2.basename(logFile);
|
|
@@ -297,12 +410,16 @@ var init_log_rotation = __esm({
|
|
|
297
410
|
try {
|
|
298
411
|
fs2.renameSync(logFile, archivePath);
|
|
299
412
|
} catch (renameErr) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
413
|
+
this.logger.log("[Logger] Log rotation skipped (rename failed; file likely in use):", renameErr);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
fs2.closeSync(fs2.openSync(logFile, "a", 384));
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
fs2.chmodSync(logFile, 384);
|
|
422
|
+
} catch {
|
|
306
423
|
}
|
|
307
424
|
try {
|
|
308
425
|
const files = fs2.readdirSync(logDir);
|
|
@@ -372,10 +489,13 @@ var init_disk_space_checker = __esm({
|
|
|
372
489
|
const availableBytes = stats.bavail * stats.bsize;
|
|
373
490
|
if (availableBytes < this.MIN_DISK_SPACE_BYTES) {
|
|
374
491
|
this.hasDiskSpace = false;
|
|
375
|
-
|
|
376
|
-
|
|
492
|
+
try {
|
|
493
|
+
process.stderr.write(
|
|
494
|
+
`[pi-research] Insufficient disk space for logging: ${Math.round(availableBytes / 1024 / 1024)}MB available, minimum ${this.MIN_DISK_SPACE_BYTES / 1024 / 1024}MB required
|
|
377
495
|
`
|
|
378
|
-
|
|
496
|
+
);
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
379
499
|
} else {
|
|
380
500
|
this.hasDiskSpace = true;
|
|
381
501
|
}
|
|
@@ -482,7 +602,7 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
|
|
|
482
602
|
if (shouldRedirectStderr(stat)) {
|
|
483
603
|
savedFd2 = fs4.openSync("/dev/fd/2", "a");
|
|
484
604
|
fs4.closeSync(2);
|
|
485
|
-
const newFd = fs4.openSync(logFile, "a");
|
|
605
|
+
const newFd = fs4.openSync(logFile, "a", 384);
|
|
486
606
|
if (newFd === 2) {
|
|
487
607
|
fd2Redirected = true;
|
|
488
608
|
} else {
|
|
@@ -614,42 +734,52 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
|
|
|
614
734
|
}
|
|
615
735
|
} catch (_e) {
|
|
616
736
|
}
|
|
737
|
+
const frame = {
|
|
738
|
+
retired: false,
|
|
739
|
+
restore: () => {
|
|
740
|
+
process.stderr.write = originalStderrWrite;
|
|
741
|
+
process.stdout.write = originalStdoutWrite;
|
|
742
|
+
try {
|
|
743
|
+
fs4.writeSync = originalFsWriteSync;
|
|
744
|
+
} catch {
|
|
745
|
+
}
|
|
746
|
+
console.log = originalConsole.log;
|
|
747
|
+
console.info = originalConsole.info;
|
|
748
|
+
console.warn = originalConsole.warn;
|
|
749
|
+
console.error = originalConsole.error;
|
|
750
|
+
console.debug = originalConsole.debug;
|
|
751
|
+
if (fd2Redirected && savedFd2 >= 0) {
|
|
752
|
+
try {
|
|
753
|
+
fs4.closeSync(2);
|
|
754
|
+
const r = fs4.openSync(`/dev/fd/${savedFd2}`, "a");
|
|
755
|
+
if (r !== 2 && r >= 0) {
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
fs4.closeSync(savedFd2);
|
|
759
|
+
} catch {
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
captureStack.push(frame);
|
|
617
767
|
try {
|
|
618
768
|
return await task();
|
|
619
769
|
} finally {
|
|
620
|
-
process.stderr.write = originalStderrWrite;
|
|
621
|
-
process.stdout.write = originalStdoutWrite;
|
|
622
|
-
try {
|
|
623
|
-
fs4.writeSync = originalFsWriteSync;
|
|
624
|
-
} catch {
|
|
625
|
-
}
|
|
626
|
-
console.log = originalConsole.log;
|
|
627
|
-
console.info = originalConsole.info;
|
|
628
|
-
console.warn = originalConsole.warn;
|
|
629
|
-
console.error = originalConsole.error;
|
|
630
|
-
console.debug = originalConsole.debug;
|
|
631
770
|
if (sessionId) {
|
|
632
771
|
setSessionCapturing(sessionId, false);
|
|
633
772
|
} else {
|
|
634
773
|
unkeyedCaptureDepth = Math.max(0, unkeyedCaptureDepth - 1);
|
|
635
774
|
recomputeGlobalCapturing();
|
|
636
775
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const r = fs4.openSync(`/dev/fd/${savedFd2}`, "a");
|
|
641
|
-
if (r !== 2 && r >= 0) {
|
|
642
|
-
}
|
|
643
|
-
try {
|
|
644
|
-
fs4.closeSync(savedFd2);
|
|
645
|
-
} catch {
|
|
646
|
-
}
|
|
647
|
-
} catch {
|
|
648
|
-
}
|
|
776
|
+
frame.retired = true;
|
|
777
|
+
while (captureStack.length > 0 && captureStack[captureStack.length - 1].retired) {
|
|
778
|
+
captureStack.pop().restore();
|
|
649
779
|
}
|
|
650
780
|
}
|
|
651
781
|
}
|
|
652
|
-
var fs4, sessionCaptureStates, unkeyedCaptureDepth, isAnyLoggerCapturingOutput, NATIVE_LOG_PATTERNS, COMPLEX_TUI_PATTERNS, BOX_DRAWING_CHARS;
|
|
782
|
+
var fs4, sessionCaptureStates, unkeyedCaptureDepth, isAnyLoggerCapturingOutput, captureStack, NATIVE_LOG_PATTERNS, COMPLEX_TUI_PATTERNS, BOX_DRAWING_CHARS;
|
|
653
783
|
var init_stdio_capture = __esm({
|
|
654
784
|
"src/utils/stdio-capture.ts"() {
|
|
655
785
|
"use strict";
|
|
@@ -658,6 +788,7 @@ var init_stdio_capture = __esm({
|
|
|
658
788
|
sessionCaptureStates = /* @__PURE__ */ new Map();
|
|
659
789
|
unkeyedCaptureDepth = 0;
|
|
660
790
|
isAnyLoggerCapturingOutput = false;
|
|
791
|
+
captureStack = [];
|
|
661
792
|
NATIVE_LOG_PATTERNS = [
|
|
662
793
|
"Warning:",
|
|
663
794
|
"Error:",
|
|
@@ -699,7 +830,7 @@ var init_stdio_capture = __esm({
|
|
|
699
830
|
});
|
|
700
831
|
|
|
701
832
|
// src/logger.ts
|
|
702
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync, openSync, closeSync } from "node:fs";
|
|
833
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
703
834
|
import { appendFile as appendFile2 } from "node:fs/promises";
|
|
704
835
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
705
836
|
import * as path3 from "node:path";
|
|
@@ -859,7 +990,7 @@ var init_logger = __esm({
|
|
|
859
990
|
mkdirSync(this.logDir, { recursive: true, mode: 448 });
|
|
860
991
|
}
|
|
861
992
|
try {
|
|
862
|
-
|
|
993
|
+
closeSync2(openSync2(this.logFile, "wx", 384));
|
|
863
994
|
} catch {
|
|
864
995
|
}
|
|
865
996
|
} catch {
|
|
@@ -894,7 +1025,7 @@ var init_logger = __esm({
|
|
|
894
1025
|
if (this.consoleLog) {
|
|
895
1026
|
const color = level === "ERROR" /* ERROR */ ? "\x1B[31m" : level === "WARN" /* WARN */ ? "\x1B[33m" : level === "DEBUG" /* DEBUG */ ? "\x1B[90m" : "\x1B[36m";
|
|
896
1027
|
const reset = "\x1B[0m";
|
|
897
|
-
const msg = message.replace(/[\r\n]/g, "");
|
|
1028
|
+
const msg = stripTerminalEscapes(message.replace(/[\r\n]/g, ""));
|
|
898
1029
|
const rawSid = this.sessionId ?? "";
|
|
899
1030
|
const sid = rawSid.replace(/[\r\n]/g, "");
|
|
900
1031
|
const prefix = sid ? `[${sid}] ` : "";
|
|
@@ -1133,101 +1264,6 @@ var init_metrics = __esm({
|
|
|
1133
1264
|
}
|
|
1134
1265
|
});
|
|
1135
1266
|
|
|
1136
|
-
// src/utils/user-agent.ts
|
|
1137
|
-
var USER_AGENTS;
|
|
1138
|
-
var init_user_agent = __esm({
|
|
1139
|
-
"src/utils/user-agent.ts"() {
|
|
1140
|
-
"use strict";
|
|
1141
|
-
USER_AGENTS = [
|
|
1142
|
-
// Chrome on Windows (updated 2025)
|
|
1143
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
1144
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
1145
|
-
// Chrome on macOS
|
|
1146
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
1147
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
1148
|
-
// Chrome on Linux
|
|
1149
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
1150
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
1151
|
-
// Firefox on Windows (updated 2025)
|
|
1152
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
1153
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
|
|
1154
|
-
// Firefox on macOS
|
|
1155
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
1156
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
|
|
1157
|
-
];
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
// src/web-research/scraper-types.ts
|
|
1162
|
-
var MAX_HTML_SIZE, MAX_PDF_SIZE, INTERNAL_NETWORK_PATTERNS, FILTERED_TAGS, BOT_PATTERNS, IMAGE_LINK_PATTERN, MARKDOWN_IMAGE_PATTERN;
|
|
1163
|
-
var init_scraper_types = __esm({
|
|
1164
|
-
"src/web-research/scraper-types.ts"() {
|
|
1165
|
-
"use strict";
|
|
1166
|
-
init_user_agent();
|
|
1167
|
-
MAX_HTML_SIZE = 25 * 1024 * 1024;
|
|
1168
|
-
MAX_PDF_SIZE = 100 * 1024 * 1024;
|
|
1169
|
-
INTERNAL_NETWORK_PATTERNS = [
|
|
1170
|
-
/^127\./,
|
|
1171
|
-
// IPv4 loopback
|
|
1172
|
-
/^0\./,
|
|
1173
|
-
// IPv4 "this" network
|
|
1174
|
-
/^::1$/,
|
|
1175
|
-
// IPv6 loopback
|
|
1176
|
-
/^fe80::/i,
|
|
1177
|
-
// IPv6 link-local
|
|
1178
|
-
/^f[cd][0-9a-f]{2}:/i,
|
|
1179
|
-
// IPv6 unique local — full fc00::/7 block (fc00::–fdff::)
|
|
1180
|
-
/^169\.254\./,
|
|
1181
|
-
// IPv4 link-local (and metadata)
|
|
1182
|
-
/^10\./,
|
|
1183
|
-
// RFC 1918 Class A private
|
|
1184
|
-
/^192\.168\./,
|
|
1185
|
-
// RFC 1918 Class C private
|
|
1186
|
-
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
1187
|
-
// RFC 1918 Class B private
|
|
1188
|
-
/^::ffff:(127\.|0\.|169\.254\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/i
|
|
1189
|
-
// IPv4-mapped IPv6
|
|
1190
|
-
];
|
|
1191
|
-
FILTERED_TAGS = [
|
|
1192
|
-
"nav",
|
|
1193
|
-
"header",
|
|
1194
|
-
"footer",
|
|
1195
|
-
"aside",
|
|
1196
|
-
"script",
|
|
1197
|
-
"style",
|
|
1198
|
-
"noscript",
|
|
1199
|
-
"form",
|
|
1200
|
-
"input",
|
|
1201
|
-
"select",
|
|
1202
|
-
"textarea",
|
|
1203
|
-
"button",
|
|
1204
|
-
"object",
|
|
1205
|
-
"embed",
|
|
1206
|
-
"svg",
|
|
1207
|
-
"symbol",
|
|
1208
|
-
"use",
|
|
1209
|
-
"defs",
|
|
1210
|
-
"path",
|
|
1211
|
-
"circle",
|
|
1212
|
-
"rect",
|
|
1213
|
-
"line",
|
|
1214
|
-
"polygon",
|
|
1215
|
-
"img",
|
|
1216
|
-
"iframe"
|
|
1217
|
-
];
|
|
1218
|
-
BOT_PATTERNS = [
|
|
1219
|
-
["_cf_chl_", "Cloudflare challenge"],
|
|
1220
|
-
["cf_chl_opt", "Cloudflare challenge opt"],
|
|
1221
|
-
["cdn-cgi/challenge-platform", "Cloudflare challenge platform"],
|
|
1222
|
-
["ddos-guard", "DDoS-Guard challenge"],
|
|
1223
|
-
["Just a moment...", "Cloudflare interstitial"],
|
|
1224
|
-
["Checking your browser before accessing", "Cloudflare interstitial"]
|
|
1225
|
-
];
|
|
1226
|
-
IMAGE_LINK_PATTERN = /\[([^\]]*)\]\((data:image\/[^)]+|[^)\s]+\.(?:svg|png|jpe?g|gif|webp|bmp|ico)(?:\?[^)]*)?)\)/gi;
|
|
1227
|
-
MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((?:data:image\/[^)]+|[^)\s]+)\)/gi;
|
|
1228
|
-
}
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
1267
|
// src/utils/circuit-breaker.ts
|
|
1232
1268
|
var CircuitBreaker;
|
|
1233
1269
|
var init_circuit_breaker = __esm({
|
|
@@ -1277,7 +1313,7 @@ var init_circuit_breaker = __esm({
|
|
|
1277
1313
|
const result = await action();
|
|
1278
1314
|
const duration = Date.now() - startTime;
|
|
1279
1315
|
metrics.observe("circuit_breaker_call_duration_ms", duration, { breaker: this.options.name, status: "success" });
|
|
1280
|
-
this.onSuccess();
|
|
1316
|
+
this.onSuccess(admittedAsProbe);
|
|
1281
1317
|
return result;
|
|
1282
1318
|
} catch (error) {
|
|
1283
1319
|
const duration = Date.now() - startTime;
|
|
@@ -1295,9 +1331,10 @@ var init_circuit_breaker = __esm({
|
|
|
1295
1331
|
if (admittedAsProbe) this.halfOpenInFlight = Math.max(0, this.halfOpenInFlight - 1);
|
|
1296
1332
|
}
|
|
1297
1333
|
}
|
|
1298
|
-
onSuccess() {
|
|
1334
|
+
onSuccess(admittedAsProbe) {
|
|
1299
1335
|
metrics.increment("circuit_breaker_success_total", 1, { breaker: this.options.name, state: this.state });
|
|
1300
1336
|
if (this.state === "HALF_OPEN") {
|
|
1337
|
+
if (!admittedAsProbe) return;
|
|
1301
1338
|
this.successCount++;
|
|
1302
1339
|
if (this.successCount >= this.options.halfOpenMaxCalls) {
|
|
1303
1340
|
this.transitionTo("CLOSED");
|
|
@@ -1599,12 +1636,14 @@ function isPrivateIpv6(ip) {
|
|
|
1599
1636
|
if (normalized === "::1") return true;
|
|
1600
1637
|
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
|
|
1601
1638
|
if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) return true;
|
|
1639
|
+
if (normalized.startsWith("fec") || normalized.startsWith("fed") || normalized.startsWith("fee") || normalized.startsWith("fef")) return true;
|
|
1602
1640
|
const mapped = extractMappedIpv4(normalized);
|
|
1603
1641
|
if (mapped) {
|
|
1604
1642
|
return isPrivateIp(mapped);
|
|
1605
1643
|
}
|
|
1606
1644
|
if (normalized.startsWith("2002:")) return true;
|
|
1607
1645
|
if (/^2001:0{0,4}:/.test(normalized) || normalized.startsWith("2001::")) return true;
|
|
1646
|
+
if (normalized.startsWith("64:ff9b:")) return true;
|
|
1608
1647
|
if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
|
|
1609
1648
|
return false;
|
|
1610
1649
|
}
|
|
@@ -1857,6 +1896,7 @@ import { platform as platform2 } from "node:os";
|
|
|
1857
1896
|
|
|
1858
1897
|
// src/infrastructure/browser/thread-worker-messaging.ts
|
|
1859
1898
|
init_log_utils();
|
|
1899
|
+
init_scraper_types();
|
|
1860
1900
|
import { appendFileSync as appendFileSync2 } from "node:fs";
|
|
1861
1901
|
var workerId2 = "";
|
|
1862
1902
|
var pageCreationLock = Promise.resolve();
|
|
@@ -2149,17 +2189,31 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2149
2189
|
validateForWorkerSync(ip.includes(":") ? `http://[${ip}]/` : `http://${ip}/`);
|
|
2150
2190
|
}
|
|
2151
2191
|
const contentType = await response?.headerValue("content-type") || "";
|
|
2192
|
+
const status = response?.status?.() ?? 0;
|
|
2152
2193
|
if (contentType.includes("application/pdf")) {
|
|
2153
2194
|
if (!response) throw new Error(`[Worker] No response received for PDF URL: ${url}`);
|
|
2195
|
+
if (status >= 400) throw new Error(`HTTP ${status}`);
|
|
2196
|
+
const declaredLength = parseInt(await response.headerValue("content-length").catch(() => null) || "", 10);
|
|
2197
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_PDF_SIZE) {
|
|
2198
|
+
const sizeMB = Math.round(declaredLength / 1024 / 1024);
|
|
2199
|
+
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
2200
|
+
}
|
|
2154
2201
|
const buffer = await response.body();
|
|
2202
|
+
if (buffer.byteLength > MAX_PDF_SIZE) {
|
|
2203
|
+
const sizeMB = Math.round(buffer.byteLength / 1024 / 1024);
|
|
2204
|
+
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
2205
|
+
}
|
|
2155
2206
|
await Promise.allSettled(pendingAddrChecks);
|
|
2156
2207
|
if (poisonedError) throw poisonedError;
|
|
2157
2208
|
await page.close();
|
|
2158
|
-
return { contentType, buffer, jitter: 0 };
|
|
2209
|
+
return { contentType, bufferB64: buffer.toString("base64"), jitter: 0 };
|
|
2159
2210
|
}
|
|
2160
2211
|
let html = await page.content();
|
|
2161
2212
|
const cfPatterns = ["_cf_chl_", "cdn-cgi/challenge-platform", "cf_chl_opt", "Just a moment...", "Checking your browser before accessing"];
|
|
2162
2213
|
const hasCloudflare = cfPatterns.some((pattern) => html.includes(pattern));
|
|
2214
|
+
if (status >= 400 && !hasCloudflare) {
|
|
2215
|
+
throw new Error(`HTTP ${status}`);
|
|
2216
|
+
}
|
|
2163
2217
|
if (hasCloudflare) {
|
|
2164
2218
|
logToDebugFile2("WARN", `[Worker-${workerId2}] Cloudflare challenge detected for: ${url}`);
|
|
2165
2219
|
try {
|
|
@@ -2184,6 +2238,10 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2184
2238
|
await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch((err) => logToDebugFile2("DEBUG", `[ThreadWorker] Swallowed page close/wait error: ${err.message || String(err)}`));
|
|
2185
2239
|
html = await page.content();
|
|
2186
2240
|
}
|
|
2241
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
2242
|
+
const sizeMB = Math.round(html.length / 1024 / 1024);
|
|
2243
|
+
throw new Error(`Browser HTML too large (${sizeMB}MB, max 25MB)`);
|
|
2244
|
+
}
|
|
2187
2245
|
const jitter = process.env["PI_RESEARCH_MOCK_SCRAPE"] === "true" ? 0 : Math.floor(Math.random() * 1e3) + 500;
|
|
2188
2246
|
if (jitter > 0) await new Promise((r) => setTimeout(r, jitter));
|
|
2189
2247
|
const finalAddr = await finalMainFrameAddr;
|
|
@@ -186,8 +186,13 @@ export class WorkerPoolManager implements IService {
|
|
|
186
186
|
const myEpoch = ++this.poolEpoch;
|
|
187
187
|
this.pool = new FixedClusterPool(maxWorkers, workerPath, {
|
|
188
188
|
env: browserEnv,
|
|
189
|
-
// Prevent query leakage via process.argv in forked workers
|
|
190
|
-
|
|
189
|
+
// Prevent query leakage via process.argv in forked workers: cluster
|
|
190
|
+
// pools fork through cluster.setupPrimary(opts.settings), and
|
|
191
|
+
// cluster.settings.args defaults to process.argv.slice(2) — without
|
|
192
|
+
// this, every worker re-exposes the full CLI (research query
|
|
193
|
+
// included) in `ps`. NB `workerOptions` is a worker_threads-only
|
|
194
|
+
// poolifier knob; it is ignored by cluster pools.
|
|
195
|
+
settings: { args: [] },
|
|
191
196
|
errorHandler: (e: Error) => {
|
|
192
197
|
// Ignore events from a pool that has since been replaced/destroyed — a
|
|
193
198
|
// trailing error from an old pool's dying worker must not affect the live one.
|
|
@@ -76,6 +76,13 @@ export async function isEmbeddingLeaderAlive(
|
|
|
76
76
|
|
|
77
77
|
let _embeddingInstance: IEmbedder | null = null;
|
|
78
78
|
let _embeddingInitPromise: Promise<IEmbedder> | null = null;
|
|
79
|
+
// Config the in-flight init was started for. The join below must compare against
|
|
80
|
+
// these — the cached-instance path checks model/device before reuse, and the
|
|
81
|
+
// adoption paths refuse mismatched leaders, but a caller joining a promise built
|
|
82
|
+
// for a different config would silently receive (and stamp the cache with) the
|
|
83
|
+
// wrong-model embedder, defeating both guards.
|
|
84
|
+
let _initModel: string | null = null;
|
|
85
|
+
let _initDevice: string | null = null;
|
|
79
86
|
let _cachedModel: string | null = null;
|
|
80
87
|
let _cachedDevice: string | null = null;
|
|
81
88
|
|
|
@@ -130,8 +137,18 @@ export async function getEmbedder(config?: Config, _attempt = 0): Promise<IEmbed
|
|
|
130
137
|
|
|
131
138
|
// An internal retry (_attempt > 0) is re-entering from INSIDE the promise stored
|
|
132
139
|
// here, so handing that promise back would make it await itself. Only a fresh
|
|
133
|
-
// external call may join an in-flight init
|
|
134
|
-
|
|
140
|
+
// external call may join an in-flight init — and only one started for the SAME
|
|
141
|
+
// config: a mismatched joiner would get the wrong-model embedder (same-dimension
|
|
142
|
+
// models cross-embed silently). On mismatch, wait the in-flight init out (never
|
|
143
|
+
// race a second election in this process), then re-resolve for this config — the
|
|
144
|
+
// cached-instance check above disposes the mismatched instance.
|
|
145
|
+
if (_embeddingInitPromise && _attempt === 0) {
|
|
146
|
+
if (_initModel === cfg.EMBEDDING_MODEL && _initDevice === cfg.EMBEDDING_DEVICE) {
|
|
147
|
+
return _embeddingInitPromise;
|
|
148
|
+
}
|
|
149
|
+
await _embeddingInitPromise.catch(() => {});
|
|
150
|
+
return getEmbedder(cfg, _attempt);
|
|
151
|
+
}
|
|
135
152
|
|
|
136
153
|
let p: Promise<IEmbedder>;
|
|
137
154
|
|
|
@@ -222,7 +239,12 @@ export async function getEmbedder(config?: Config, _attempt = 0): Promise<IEmbed
|
|
|
222
239
|
if (!(await isEmbeddingLeaderAlive(info, processLifecycle))) {
|
|
223
240
|
// Candidate process died without cleaning up (or its PID was recycled by
|
|
224
241
|
// an unrelated process — the start-time pairing catches that too).
|
|
225
|
-
|
|
242
|
+
// serverId-scoped: several waiters typically detect the death in the same
|
|
243
|
+
// poll tick; the first one's clear+claim can commit before this one's clear
|
|
244
|
+
// runs, and an unconditional delete here would deregister that NEW claimant
|
|
245
|
+
// (whose Phase-3 port publish is CAS-guarded and silently no-ops) — leaving
|
|
246
|
+
// two concurrent model inits and an invisible leader.
|
|
247
|
+
await stateManager.clearEmbeddingServer({ serverId: info.serverId }).catch((err) => logger.debug('Swallowed clear embedding server error:', err));
|
|
226
248
|
break;
|
|
227
249
|
}
|
|
228
250
|
// Re-check model identity every poll: the leader may have changed while
|
|
@@ -246,7 +268,10 @@ export async function getEmbedder(config?: Config, _attempt = 0): Promise<IEmbed
|
|
|
246
268
|
const secondCheck = await isPortListening(info.port);
|
|
247
269
|
if (!secondCheck) {
|
|
248
270
|
logger.warn(`[EmbeddingFactory] Registered port ${info.port} is unreachable after two checks — clearing stale state`);
|
|
249
|
-
|
|
271
|
+
// serverId-scoped: this snapshot is up to ~2.5s old (two probes + sleep);
|
|
272
|
+
// the entry may already belong to a new claimant. See the dead-leader
|
|
273
|
+
// clear above for the failure this prevents.
|
|
274
|
+
await stateManager.clearEmbeddingServer({ serverId: info.serverId }).catch((err) => logger.debug('Swallowed clear embedding server error:', err));
|
|
250
275
|
break;
|
|
251
276
|
}
|
|
252
277
|
}
|
|
@@ -282,8 +307,10 @@ export async function getEmbedder(config?: Config, _attempt = 0): Promise<IEmbed
|
|
|
282
307
|
try {
|
|
283
308
|
port = await server.startServer();
|
|
284
309
|
} catch (err) {
|
|
285
|
-
// Init failed — clear our placeholder so others can try
|
|
286
|
-
|
|
310
|
+
// Init failed — clear our placeholder so others can try. serverId-scoped so a
|
|
311
|
+
// concurrent steal-and-reclaim (a waiter that judged us dead and claimed) is
|
|
312
|
+
// never deregistered by our own failure path.
|
|
313
|
+
await stateManager.clearEmbeddingServer({ serverId }).catch((err) => logger.debug('Swallowed clear embedding server error:', err));
|
|
287
314
|
throw err;
|
|
288
315
|
}
|
|
289
316
|
|
|
@@ -307,6 +334,8 @@ export async function getEmbedder(config?: Config, _attempt = 0): Promise<IEmbed
|
|
|
307
334
|
|
|
308
335
|
p = init();
|
|
309
336
|
_embeddingInitPromise = p;
|
|
337
|
+
_initModel = cfg.EMBEDDING_MODEL;
|
|
338
|
+
_initDevice = cfg.EMBEDDING_DEVICE;
|
|
310
339
|
// init() already assigns _embeddingInstance before it resolves, so do NOT
|
|
311
340
|
// re-assign it here. If clearEmbeddingInstance() ran (and disposed the server)
|
|
312
341
|
// between init resolving and this microtask, re-assigning would resurrect the
|