@lincoln504/pi-research 0.0.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/LICENSE +21 -0
- package/README.md +131 -0
- package/config/searxng/default-settings.yml +189 -0
- package/config/searxng/limiter.toml +34 -0
- package/config/tooling/eslint.config.js +73 -0
- package/config/tooling/vitest.config.integration.ts +17 -0
- package/config/tooling/vitest.config.ts +40 -0
- package/config/tooling/vitest.config.unit.ts +12 -0
- package/docs/ARCHITECTURE.md +109 -0
- package/docs/assets/README-banner.jpg +0 -0
- package/package.json +92 -0
- package/scripts/setup.js +174 -0
- package/src/config.ts +206 -0
- package/src/constants.ts +148 -0
- package/src/healthcheck/index.ts +309 -0
- package/src/index.ts +71 -0
- package/src/infrastructure/searxng-lifecycle.ts +577 -0
- package/src/infrastructure/searxng-manager.ts +1173 -0
- package/src/infrastructure/state-manager.ts +918 -0
- package/src/logger.ts +246 -0
- package/src/orchestration/deep-research-orchestrator.ts +848 -0
- package/src/orchestration/deep-research-reducer.ts +129 -0
- package/src/orchestration/deep-research-types.ts +54 -0
- package/src/orchestration/id-utils.ts +63 -0
- package/src/orchestration/researcher.ts +104 -0
- package/src/orchestration/session-context.ts +43 -0
- package/src/prompts/research-tool-usage.md +45 -0
- package/src/prompts/researcher.md +166 -0
- package/src/prompts/system-coordinator.md +24 -0
- package/src/prompts/system-lead-evaluator.md +225 -0
- package/src/security/cisa-kev.ts +242 -0
- package/src/security/github-advisories.ts +607 -0
- package/src/security/index.ts +406 -0
- package/src/security/nvd.ts +532 -0
- package/src/security/osv.ts +487 -0
- package/src/security/types.ts +138 -0
- package/src/stackexchange/cache.ts +111 -0
- package/src/stackexchange/index.ts +352 -0
- package/src/stackexchange/output/compact.ts +64 -0
- package/src/stackexchange/output/index.ts +7 -0
- package/src/stackexchange/output/json.ts +21 -0
- package/src/stackexchange/output/table.ts +123 -0
- package/src/stackexchange/queries.ts +168 -0
- package/src/stackexchange/rest-client.ts +127 -0
- package/src/stackexchange/types.ts +129 -0
- package/src/tool.ts +430 -0
- package/src/tools/grep.ts +335 -0
- package/src/tools/index.ts +77 -0
- package/src/tools/scrape.ts +451 -0
- package/src/tools/search.ts +188 -0
- package/src/tools/security.ts +292 -0
- package/src/tools/stackexchange.ts +101 -0
- package/src/tui/research-panel.ts +576 -0
- package/src/tui/searxng-status.ts +80 -0
- package/src/types/extension-context.ts +60 -0
- package/src/types/llm.ts +111 -0
- package/src/utils/inject-date.ts +29 -0
- package/src/utils/input-validation.ts +103 -0
- package/src/utils/json-utils.ts +262 -0
- package/src/utils/make-resource-loader.ts +43 -0
- package/src/utils/research-error.ts +205 -0
- package/src/utils/research-export.ts +199 -0
- package/src/utils/searxng-config.ts +94 -0
- package/src/utils/session-state.ts +295 -0
- package/src/utils/shared-links.ts +123 -0
- package/src/utils/shutdown-manager.ts +54 -0
- package/src/utils/text-utils.ts +83 -0
- package/src/utils/tool-usage-tracker.ts +135 -0
- package/src/web-research/dependencies.ts +20 -0
- package/src/web-research/retry-utils.ts +232 -0
- package/src/web-research/scrapers.ts +935 -0
- package/src/web-research/search.ts +235 -0
- package/src/web-research/types.ts +47 -0
- package/src/web-research/utils.ts +158 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Retry and Timeout Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for API rate limiting, retry logic with exponential backoff,
|
|
5
|
+
* and timeout signal creation with Node.js compatibility.
|
|
6
|
+
*
|
|
7
|
+
* Used by: NVD, GitHub Advisories, OSV, CISA KEV, Search, Delegate Research
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { logger } from '../logger.ts';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Type Definitions
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents a timeout handle that can be optionally unreferenced
|
|
18
|
+
*/
|
|
19
|
+
interface TimeoutHandle {
|
|
20
|
+
unref?(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Retry Options
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export interface RetryOptions {
|
|
28
|
+
readonly maxRetries: number;
|
|
29
|
+
readonly initialDelay: number;
|
|
30
|
+
readonly maxDelay: number;
|
|
31
|
+
readonly label?: string;
|
|
32
|
+
readonly isTransientError?: (error: unknown) => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Timeout Signal Helper
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Helper function to create timeout signal.
|
|
41
|
+
* Handles Node.js version compatibility for AbortSignal.any() and AbortSignal.timeout()
|
|
42
|
+
*/
|
|
43
|
+
export function createTimeoutSignal(timeoutMs: number, signal?: AbortSignal): AbortSignal {
|
|
44
|
+
// Check if modern AbortSignal methods are available (Node.js 20+)
|
|
45
|
+
if (typeof AbortSignal.timeout === 'function') {
|
|
46
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
47
|
+
if (signal && typeof AbortSignal.any === 'function') {
|
|
48
|
+
return AbortSignal.any([signal, timeoutSignal]);
|
|
49
|
+
}
|
|
50
|
+
return timeoutSignal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback for older Node.js versions
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timeoutId = setTimeout(() => {
|
|
56
|
+
controller.abort();
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
|
|
59
|
+
// Unref so it doesn't keep the process alive
|
|
60
|
+
if (typeof timeoutId.unref === 'function') {
|
|
61
|
+
timeoutId.unref();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (signal) {
|
|
65
|
+
// Combine signals manually for older versions
|
|
66
|
+
const combinedController = new AbortController();
|
|
67
|
+
const abortHandler = () => {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
combinedController.abort();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
73
|
+
|
|
74
|
+
return combinedController.signal;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return controller.signal;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wraps a promise with a timeout and abort signal support
|
|
82
|
+
*/
|
|
83
|
+
export function withTimeout<T>(
|
|
84
|
+
promise: Promise<T>,
|
|
85
|
+
timeoutMs: number,
|
|
86
|
+
label: string,
|
|
87
|
+
signal?: AbortSignal
|
|
88
|
+
): Promise<T> {
|
|
89
|
+
const timeoutController = new AbortController();
|
|
90
|
+
const timeoutSignal = timeoutController.signal;
|
|
91
|
+
const combinedSignal = signal
|
|
92
|
+
? createTimeoutSignal(timeoutMs, signal)
|
|
93
|
+
: timeoutSignal;
|
|
94
|
+
|
|
95
|
+
logger.log(`[withTimeout] Starting ${label} with timeout ${timeoutMs}ms. External signal aborted: ${signal?.aborted ?? 'no signal'}`);
|
|
96
|
+
|
|
97
|
+
let raceWon = false;
|
|
98
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
99
|
+
|
|
100
|
+
const wrappedPromise = promise.then((val) => {
|
|
101
|
+
raceWon = true;
|
|
102
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
103
|
+
return val;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const rejectPromise = new Promise<T>((_, reject) => {
|
|
107
|
+
// Timeout handler
|
|
108
|
+
timeoutId = setTimeout(() => {
|
|
109
|
+
logger.error(`[withTimeout] ${label} TIMEOUT after ${timeoutMs}ms`);
|
|
110
|
+
timeoutController.abort();
|
|
111
|
+
reject(new Error(`${label} timeout after ${timeoutMs}ms`));
|
|
112
|
+
}, timeoutMs);
|
|
113
|
+
|
|
114
|
+
// Unref so it doesn't keep the process alive
|
|
115
|
+
(timeoutId as TimeoutHandle).unref?.();
|
|
116
|
+
|
|
117
|
+
if (combinedSignal.aborted) {
|
|
118
|
+
logger.error(`[withTimeout] ${label} ALREADY ABORTED at start`);
|
|
119
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
120
|
+
reject(new Error(`${label} cancelled`));
|
|
121
|
+
} else {
|
|
122
|
+
combinedSignal.addEventListener('abort', () => {
|
|
123
|
+
if (!raceWon) {
|
|
124
|
+
logger.error(`[withTimeout] ${label} ABORT SIGNAL FIRED`);
|
|
125
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
126
|
+
reject(new Error(`${label} cancelled`));
|
|
127
|
+
}
|
|
128
|
+
}, { once: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return Promise.race([wrappedPromise, rejectPromise]).finally(() => {
|
|
133
|
+
// Clean up on completion or error
|
|
134
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
135
|
+
timeoutController.abort();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Retry with Exponential Backoff
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Default transient error detector
|
|
145
|
+
*/
|
|
146
|
+
export function isTransientError(error: unknown): boolean {
|
|
147
|
+
if (!(error instanceof Error)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const message = error.message.toLowerCase();
|
|
152
|
+
|
|
153
|
+
// Network errors
|
|
154
|
+
if (message.includes('econnrefused') || message.includes('enotfound') || message.includes('timeout') || message.includes('etimedout')) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Rate limiting
|
|
159
|
+
if (message.includes('429') || message.includes('rate') || message.includes('quota')) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Temporary service unavailability or server errors
|
|
164
|
+
if (
|
|
165
|
+
message.includes('503') ||
|
|
166
|
+
message.includes('500') ||
|
|
167
|
+
message.includes('502') ||
|
|
168
|
+
message.includes('504') ||
|
|
169
|
+
message.includes('temporarily') ||
|
|
170
|
+
message.includes('unavailable') ||
|
|
171
|
+
message.includes('http 5')
|
|
172
|
+
) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Retry helper with exponential backoff and jitter for transient errors.
|
|
181
|
+
*
|
|
182
|
+
* @param fn - The async function to retry
|
|
183
|
+
* @param options - Retry configuration
|
|
184
|
+
* @returns The result of the successful execution
|
|
185
|
+
* @throws The last error if all retries are exhausted
|
|
186
|
+
*/
|
|
187
|
+
export async function retryWithBackoff<T>(
|
|
188
|
+
fn: () => Promise<T>,
|
|
189
|
+
options: Partial<RetryOptions> = {},
|
|
190
|
+
): Promise<T> {
|
|
191
|
+
const opts: Required<RetryOptions> = {
|
|
192
|
+
maxRetries: 3,
|
|
193
|
+
initialDelay: 1000,
|
|
194
|
+
maxDelay: 10000,
|
|
195
|
+
label: 'operation',
|
|
196
|
+
isTransientError: options.isTransientError ?? isTransientError,
|
|
197
|
+
...options,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
let lastError: Error | null = null;
|
|
201
|
+
let attempt = 0;
|
|
202
|
+
|
|
203
|
+
while (attempt <= opts.maxRetries) {
|
|
204
|
+
try {
|
|
205
|
+
return await fn();
|
|
206
|
+
} catch (error: unknown) {
|
|
207
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
208
|
+
|
|
209
|
+
if (!opts.isTransientError(lastError) || attempt === opts.maxRetries) {
|
|
210
|
+
throw lastError;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Calculate delay with exponential backoff and jitter
|
|
214
|
+
const baseDelay = opts.initialDelay * Math.pow(2, attempt);
|
|
215
|
+
const jitter = Math.random() * 0.5 * baseDelay; // ±50% jitter
|
|
216
|
+
const delay = Math.min(baseDelay + jitter, opts.maxDelay);
|
|
217
|
+
|
|
218
|
+
logger.warn(`[Retry] ${opts.label} failed (attempt ${attempt + 1}/${opts.maxRetries + 1}), retrying in ${Math.round(delay)}ms: ${lastError.message}`);
|
|
219
|
+
|
|
220
|
+
// Wait before retrying
|
|
221
|
+
await new Promise<void>(resolve => {
|
|
222
|
+
const timeoutId = setTimeout(resolve, delay);
|
|
223
|
+
(timeoutId as TimeoutHandle).unref?.();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
attempt++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Should never reach here due to throw in loop, but for type safety
|
|
231
|
+
throw lastError ?? new Error('All retries exhausted');
|
|
232
|
+
}
|