@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/config/searxng/default-settings.yml +189 -0
  4. package/config/searxng/limiter.toml +34 -0
  5. package/config/tooling/eslint.config.js +73 -0
  6. package/config/tooling/vitest.config.integration.ts +17 -0
  7. package/config/tooling/vitest.config.ts +40 -0
  8. package/config/tooling/vitest.config.unit.ts +12 -0
  9. package/docs/ARCHITECTURE.md +109 -0
  10. package/docs/assets/README-banner.jpg +0 -0
  11. package/package.json +92 -0
  12. package/scripts/setup.js +174 -0
  13. package/src/config.ts +206 -0
  14. package/src/constants.ts +148 -0
  15. package/src/healthcheck/index.ts +309 -0
  16. package/src/index.ts +71 -0
  17. package/src/infrastructure/searxng-lifecycle.ts +577 -0
  18. package/src/infrastructure/searxng-manager.ts +1173 -0
  19. package/src/infrastructure/state-manager.ts +918 -0
  20. package/src/logger.ts +246 -0
  21. package/src/orchestration/deep-research-orchestrator.ts +848 -0
  22. package/src/orchestration/deep-research-reducer.ts +129 -0
  23. package/src/orchestration/deep-research-types.ts +54 -0
  24. package/src/orchestration/id-utils.ts +63 -0
  25. package/src/orchestration/researcher.ts +104 -0
  26. package/src/orchestration/session-context.ts +43 -0
  27. package/src/prompts/research-tool-usage.md +45 -0
  28. package/src/prompts/researcher.md +166 -0
  29. package/src/prompts/system-coordinator.md +24 -0
  30. package/src/prompts/system-lead-evaluator.md +225 -0
  31. package/src/security/cisa-kev.ts +242 -0
  32. package/src/security/github-advisories.ts +607 -0
  33. package/src/security/index.ts +406 -0
  34. package/src/security/nvd.ts +532 -0
  35. package/src/security/osv.ts +487 -0
  36. package/src/security/types.ts +138 -0
  37. package/src/stackexchange/cache.ts +111 -0
  38. package/src/stackexchange/index.ts +352 -0
  39. package/src/stackexchange/output/compact.ts +64 -0
  40. package/src/stackexchange/output/index.ts +7 -0
  41. package/src/stackexchange/output/json.ts +21 -0
  42. package/src/stackexchange/output/table.ts +123 -0
  43. package/src/stackexchange/queries.ts +168 -0
  44. package/src/stackexchange/rest-client.ts +127 -0
  45. package/src/stackexchange/types.ts +129 -0
  46. package/src/tool.ts +430 -0
  47. package/src/tools/grep.ts +335 -0
  48. package/src/tools/index.ts +77 -0
  49. package/src/tools/scrape.ts +451 -0
  50. package/src/tools/search.ts +188 -0
  51. package/src/tools/security.ts +292 -0
  52. package/src/tools/stackexchange.ts +101 -0
  53. package/src/tui/research-panel.ts +576 -0
  54. package/src/tui/searxng-status.ts +80 -0
  55. package/src/types/extension-context.ts +60 -0
  56. package/src/types/llm.ts +111 -0
  57. package/src/utils/inject-date.ts +29 -0
  58. package/src/utils/input-validation.ts +103 -0
  59. package/src/utils/json-utils.ts +262 -0
  60. package/src/utils/make-resource-loader.ts +43 -0
  61. package/src/utils/research-error.ts +205 -0
  62. package/src/utils/research-export.ts +199 -0
  63. package/src/utils/searxng-config.ts +94 -0
  64. package/src/utils/session-state.ts +295 -0
  65. package/src/utils/shared-links.ts +123 -0
  66. package/src/utils/shutdown-manager.ts +54 -0
  67. package/src/utils/text-utils.ts +83 -0
  68. package/src/utils/tool-usage-tracker.ts +135 -0
  69. package/src/web-research/dependencies.ts +20 -0
  70. package/src/web-research/retry-utils.ts +232 -0
  71. package/src/web-research/scrapers.ts +935 -0
  72. package/src/web-research/search.ts +235 -0
  73. package/src/web-research/types.ts +47 -0
  74. package/src/web-research/utils.ts +158 -0
  75. 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
+ }