@lincoln504/pi-research 0.1.13 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -138
  3. package/dist/cli.mjs +23375 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +130 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +211 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +122 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +66 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1195 -0
  30. package/src/config.ts +1032 -155
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +258 -11
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -1,487 +0,0 @@
1
- /**
2
- * OSV (Open Source Vulnerabilities) API Client
3
- *
4
- * Open Source Vulnerabilities
5
- * API: https://api.osv.dev/v1/
6
- * Free public API, no authentication required
7
- */
8
-
9
- import type { Vulnerability, OSVResult } from './types.ts';
10
- import { createTimeoutSignal, retryWithBackoff, isTransientError } from '../web-research/retry-utils.ts';
11
- import { logger } from '../logger.ts';
12
- import { OSV_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_INITIAL_DELAY_MS, DEFAULT_MAX_DELAY_MS } from '../constants.ts';
13
-
14
- const OSV_BASE_URL = 'https://api.osv.dev/v1';
15
- const DEFAULT_MAX_RESULTS = 20;
16
-
17
- // ==================== OSV API Type Definitions ====================
18
-
19
- /**
20
- * OSV API vulnerability response (from GET /vulns/{id})
21
- */
22
- interface OsvVulnerability {
23
- id: string;
24
- summary?: string;
25
- details?: string;
26
- published?: string;
27
- modified?: string;
28
- withdrawn?: string;
29
- aliases?: string[];
30
- related?: string[];
31
- affected?: OsvAffected[];
32
- severity?: OsvSeverity[];
33
- references?: OsvReference[];
34
- database_specific?: OsvDatabaseSpecific;
35
- }
36
-
37
- /**
38
- * OSV severity entry
39
- */
40
- interface OsvSeverity {
41
- type: string;
42
- score: string;
43
- }
44
-
45
- /**
46
- * OSV database-specific metadata
47
- */
48
- interface OsvDatabaseSpecific {
49
- severity?: string;
50
- cwe?: Array<{ id: string } | string>;
51
- [key: string]: unknown;
52
- }
53
-
54
- /**
55
- * OSV affected package entry
56
- */
57
- interface OsvAffected {
58
- package?: OsvPackage;
59
- ranges?: OsvRange[];
60
- versions?: string[];
61
- database_specific?: Record<string, unknown>;
62
- severity?: OsvSeverity[];
63
- }
64
-
65
- /**
66
- * OSV package information
67
- */
68
- interface OsvPackage {
69
- name?: string;
70
- ecosystem?: string;
71
- purl?: string;
72
- ecosystem_specific?: {
73
- name?: string;
74
- [key: string]: unknown;
75
- };
76
- [key: string]: unknown;
77
- }
78
-
79
- /**
80
- * OSV version range information
81
- */
82
- interface OsvRange {
83
- type: string;
84
- repo?: string;
85
- events: OsvRangeEvent[];
86
- database_specific?: Record<string, unknown>;
87
- }
88
-
89
- /**
90
- * OSV range event (introduced, fixed, last_affected, limits)
91
- */
92
- interface OsvRangeEvent {
93
- introduced?: string;
94
- fixed?: string;
95
- last_affected?: string;
96
- limit?: string;
97
- }
98
-
99
- /**
100
- * OSV reference entry
101
- */
102
- interface OsvReference {
103
- type?: string;
104
- url?: string;
105
- }
106
-
107
- /**
108
- * OSV query request body
109
- */
110
- interface OsvQueryRequest {
111
- package: {
112
- name: string;
113
- ecosystem: string;
114
- purl?: string;
115
- };
116
- version?: string;
117
- commit?: string;
118
- }
119
-
120
- /**
121
- * OSV query response (from POST /query)
122
- */
123
- interface OsvQueryResponse {
124
- vulns?: OsvVulnerability[];
125
- [key: string]: unknown;
126
- }
127
-
128
- /**
129
- * Type guard to check if value is an OsvVulnerability
130
- */
131
- function isOsvVulnerability(value: unknown): value is OsvVulnerability {
132
- if (value === null || typeof value !== 'object') {
133
- return false;
134
- }
135
- const obj = value as Record<string, unknown>;
136
- return typeof obj['id'] === 'string';
137
- }
138
-
139
- /**
140
- * Type guard to check if value is an OsvQueryResponse
141
- */
142
- function isOsvQueryResponse(value: unknown): value is OsvQueryResponse {
143
- if (value === null || typeof value !== 'object') {
144
- return false;
145
- }
146
- const obj = value as Record<string, unknown>;
147
- return Array.isArray(obj['vulns']);
148
- }
149
-
150
- // ==================== API Functions ====================
151
-
152
- /**
153
- * Fetch with retry for resilience
154
- */
155
- async function fetchWithRetry(
156
- url: string,
157
- options: RequestInit,
158
- ): Promise<Response> {
159
- return retryWithBackoff(
160
- async () => {
161
- const response = await fetch(url, options);
162
- if (!response.ok) {
163
- const error = new Error(`HTTP ${response.status}: ${response.statusText}`) as Error & { status?: number };
164
- error.status = response.status;
165
- throw error;
166
- }
167
- return response;
168
- },
169
- {
170
- maxRetries: DEFAULT_MAX_RETRIES,
171
- initialDelay: DEFAULT_INITIAL_DELAY_MS,
172
- maxDelay: DEFAULT_MAX_DELAY_MS,
173
- label: `OSV API: ${url}`,
174
- isTransientError: (error) => {
175
- // Check if error has status property
176
- const status = (error as Error & { status?: number })?.status;
177
- if (typeof status === 'number') {
178
- // Retry on 429 (rate limit), 403 (forbidden), 5xx (server errors)
179
- return status === 429 || status === 403 || status >= 500;
180
- }
181
- // For other errors, use default transient error check
182
- return isTransientError(error);
183
- },
184
- },
185
- );
186
- }
187
-
188
- /**
189
- * Search OSV for vulnerabilities
190
- *
191
- * @param terms - Package names, CVE IDs, or keywords
192
- * @param options - Optional filters
193
- * @returns OSV search result with vulnerabilities and optional error
194
- */
195
- export async function searchOSV(
196
- terms: string[],
197
- options?: {
198
- ecosystem?: string; // npm, PyPI, Maven, Go, Rust, etc.
199
- severity?: string; // LOW, MODERATE, HIGH, CRITICAL
200
- maxResults?: number;
201
- includeAffected?: boolean; // Include affected version ranges
202
- },
203
- ): Promise<OSVResult> {
204
- const maxResults: number = options?.maxResults ?? DEFAULT_MAX_RESULTS;
205
- const vulnerabilities: Vulnerability[] = [];
206
- let error: string | undefined = undefined;
207
-
208
- try {
209
- const allVulns: Vulnerability[] = [];
210
- let skippedNoEcosystem = 0;
211
-
212
- for (const term of terms) {
213
- let response: Response;
214
-
215
- // OSV API: GET /vulns/{id} for CVE/OSV/GHSA IDs, POST /query for package names
216
- const termUpper: string = term.toUpperCase();
217
- if (termUpper.startsWith('CVE-') || termUpper.startsWith('GHSA-') || termUpper.startsWith('OSV-')) {
218
- // GHSA IDs: OSV requires uppercase prefix + lowercase hex portions.
219
- // Normalize regardless of how the caller typed it.
220
- const normalizedId: string = termUpper.startsWith('GHSA-')
221
- ? `GHSA-${term.slice(term.indexOf('-') + 1).toLowerCase()}`
222
- : termUpper;
223
- const url: string = `${OSV_BASE_URL}/vulns/${encodeURIComponent(normalizedId)}`;
224
- response = await fetchWithRetry(url, {
225
- headers: { 'User-Agent': 'pi-research/2.0', 'Accept': 'application/json' },
226
- signal: createTimeoutSignal(OSV_TIMEOUT_MS),
227
- });
228
- } else {
229
- // POST /query requires package.name + package.ecosystem.
230
- // Sending name without ecosystem always returns HTTP 400 "Invalid query."
231
- if (options?.ecosystem === undefined || options.ecosystem === '') {
232
- skippedNoEcosystem++;
233
- continue;
234
- }
235
- const body: OsvQueryRequest = { package: { name: term, ecosystem: options.ecosystem } };
236
- response = await fetchWithRetry(`${OSV_BASE_URL}/query`, {
237
- method: 'POST',
238
- headers: {
239
- 'User-Agent': 'pi-research/2.0',
240
- 'Accept': 'application/json',
241
- 'Content-Type': 'application/json',
242
- },
243
- body: JSON.stringify(body),
244
- signal: createTimeoutSignal(OSV_TIMEOUT_MS),
245
- });
246
- }
247
-
248
- if (!response.ok) {
249
- logger.warn(`OSV query failed for "${term}": ${response.status}`);
250
- continue;
251
- }
252
-
253
- const data: unknown = await response.json();
254
-
255
- // Single vuln response (GET /vulns/{id}) vs batch response (POST /query)
256
- let items: OsvVulnerability[];
257
-
258
- if (isOsvVulnerability(data)) {
259
- items = [data];
260
- } else if (isOsvQueryResponse(data)) {
261
- items = data.vulns ?? [];
262
- } else {
263
- logger.warn(`OSV returned unexpected format for "${term}"`);
264
- continue;
265
- }
266
-
267
- for (const item of items) {
268
- const vuln: Vulnerability = mapOsvItemToVulnerability(item);
269
-
270
- // Filter by severity if provided
271
- if (options?.severity !== undefined && options.severity !== '') {
272
- const severity: string = options.severity.toUpperCase();
273
- if (vuln.severity !== severity) {
274
- continue;
275
- }
276
- }
277
-
278
- allVulns.push(vuln);
279
- }
280
- }
281
-
282
- // Deduplicate by OSV ID
283
- const uniqueVulns = new Map<string, Vulnerability>();
284
- for (const vuln of allVulns) {
285
- if (!uniqueVulns.has(vuln.id)) {
286
- uniqueVulns.set(vuln.id, vuln);
287
- }
288
- }
289
-
290
- vulnerabilities.push(...Array.from(uniqueVulns.values()));
291
-
292
- if (skippedNoEcosystem > 0) {
293
- error = `${skippedNoEcosystem} term(s) require the ecosystem parameter for OSV package search (e.g., ecosystem: "npm"). CVE/GHSA/OSV IDs work without it.`;
294
- }
295
-
296
- } catch (err: unknown) {
297
- error = err instanceof Error ? err.message : String(err);
298
- }
299
-
300
- return {
301
- count: vulnerabilities.length,
302
- vulnerabilities: vulnerabilities.slice(0, maxResults),
303
- error,
304
- };
305
- }
306
-
307
- /**
308
- * Get vulnerability by OSV ID
309
- *
310
- * @param osvId - OSV vulnerability ID (e.g., CVE-2023-1234, GHSA-abc1-23de-fg45)
311
- * @returns Vulnerability object if found, null otherwise
312
- */
313
- export async function getOSVById(osvId: string): Promise<Vulnerability | null> {
314
- try {
315
- const url: string = `${OSV_BASE_URL}/vulns/${osvId}`;
316
- const response: Response = await fetchWithRetry(url, {
317
- headers: {
318
- 'User-Agent': 'pi-research/2.0',
319
- 'Accept': 'application/json',
320
- },
321
- signal: createTimeoutSignal(OSV_TIMEOUT_MS),
322
- });
323
-
324
- const data: unknown = await response.json();
325
-
326
- if (!isOsvVulnerability(data)) {
327
- logger.error(`OSV ${osvId} returned unexpected format`);
328
- return null;
329
- }
330
-
331
- return mapOsvItemToVulnerability(data);
332
- } catch (err: unknown) {
333
- logger.error(`Error fetching OSV ${osvId}:`, err);
334
- return null;
335
- }
336
- }
337
-
338
- // ==================== Helper Functions ====================
339
-
340
- /**
341
- * Map OSV item to Vulnerability interface
342
- *
343
- * @param item - OSV vulnerability object
344
- * @returns Standardized Vulnerability object
345
- */
346
- function mapOsvItemToVulnerability(item: OsvVulnerability): Vulnerability {
347
- const osvId: string = item.id;
348
- const summary: string = item.summary ?? item.details ?? '';
349
- const details: string = item.details ?? '';
350
- const published: string | undefined = item.published;
351
- const modified: string = item.modified ?? '';
352
-
353
- // OSV severity mapping
354
- // item.severity is an array of {type, score} objects; string severity is in database_specific
355
- let severityStr: string | undefined;
356
- if (item.database_specific !== undefined &&
357
- typeof item.database_specific.severity === 'string') {
358
- severityStr = item.database_specific.severity;
359
- }
360
- const severity: string = mapOsvSeverity(severityStr);
361
-
362
- // Extract aliases (including CVE IDs)
363
- const aliases: string[] = item.aliases ?? [];
364
- const references: string[] = [...aliases];
365
-
366
- if (item.references !== undefined) {
367
- for (const ref of item.references) {
368
- if (ref.url !== undefined && ref.url !== '') {
369
- references.push(ref.url);
370
- }
371
- }
372
- }
373
-
374
- // Extract affected packages and versions
375
- const affectedProducts: string[] = [];
376
- const fixes: string[] = [];
377
-
378
- if (item.affected !== undefined) {
379
- for (const affectedEntry of item.affected) {
380
- const pkg: OsvPackage | undefined = affectedEntry.package;
381
-
382
- if (pkg !== undefined) {
383
- const pkgName: string = pkg.name ??
384
- (pkg.ecosystem_specific !== undefined && typeof pkg.ecosystem_specific.name === 'string'
385
- ? pkg.ecosystem_specific.name
386
- : '');
387
-
388
- if (pkgName !== '') {
389
- affectedProducts.push(pkgName);
390
- }
391
-
392
- // Version ranges
393
- // OSV range format: { type, events: [{introduced: "x"}, {fixed: "y"}] }
394
- if (affectedEntry.ranges !== undefined) {
395
- for (const range of affectedEntry.ranges) {
396
- const events: OsvRangeEvent[] = range.events;
397
-
398
- const introducedEvent: OsvRangeEvent | undefined = events.find(
399
- (e: OsvRangeEvent) => e.introduced !== undefined,
400
- );
401
- const fixedEvent: OsvRangeEvent | undefined = events.find(
402
- (e: OsvRangeEvent) => e.fixed !== undefined,
403
- );
404
- const lastAffectedEvent: OsvRangeEvent | undefined = events.find(
405
- (e: OsvRangeEvent) => e.last_affected !== undefined,
406
- );
407
-
408
- const versionInfo: string[] = [];
409
-
410
- if (introducedEvent?.introduced !== undefined) {
411
- versionInfo.push(`introduced: ${introducedEvent.introduced}`);
412
- }
413
- if (fixedEvent?.fixed !== undefined) {
414
- versionInfo.push(`fixed: ${fixedEvent.fixed}`);
415
- }
416
- if (lastAffectedEvent?.last_affected !== undefined) {
417
- versionInfo.push(`last affected: ${lastAffectedEvent.last_affected}`);
418
- }
419
-
420
- if (versionInfo.length > 0 && pkgName !== '') {
421
- fixes.push(`${pkgName}: ${versionInfo.join(', ')}`);
422
- }
423
- }
424
- }
425
- }
426
- }
427
- }
428
-
429
- // Extract CWEs if available
430
- const cwes: string[] = [];
431
- if (item.database_specific !== undefined) {
432
- if (item.database_specific.cwe !== undefined && Array.isArray(item.database_specific.cwe)) {
433
- for (const cwe of item.database_specific.cwe) {
434
- if (typeof cwe === 'string') {
435
- cwes.push(cwe);
436
- } else if (typeof cwe === 'object' && typeof (cwe as { id?: string }).id === 'string') {
437
- cwes.push((cwe as { id: string }).id);
438
- }
439
- }
440
- }
441
- }
442
-
443
- return {
444
- id: osvId,
445
- source: 'osv',
446
- severity,
447
- description: summary !== '' ? summary : details,
448
- published,
449
- modified,
450
- cvssScore: undefined, // OSV doesn't provide CVSS score
451
- cvssVector: undefined,
452
- cwes,
453
- references,
454
- affectedProducts,
455
- fixes,
456
- knownExploited: false,
457
- };
458
- }
459
-
460
- /**
461
- * Map OSV severity to standard values
462
- *
463
- * @param severity - OSV severity string (e.g., "CRITICAL", "HIGH", "MODERATE", "LOW")
464
- * @returns Standardized severity value ("CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN")
465
- */
466
- function mapOsvSeverity(severity?: string): string {
467
- if (severity === undefined || severity === '') {
468
- return 'UNKNOWN';
469
- }
470
-
471
- const upper: string = severity.toUpperCase();
472
-
473
- if (upper === 'CRITICAL') {
474
- return 'CRITICAL';
475
- }
476
- if (upper === 'HIGH') {
477
- return 'HIGH';
478
- }
479
- if (upper === 'MEDIUM' || upper === 'MODERATE') {
480
- return 'MEDIUM';
481
- }
482
- if (upper === 'LOW') {
483
- return 'LOW';
484
- }
485
-
486
- return 'UNKNOWN';
487
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * Simple file-based cache for Stack Exchange API responses
3
- */
4
-
5
- import * as fs from 'node:fs';
6
- import { logger } from '../logger.ts';
7
- import * as path from 'node:path';
8
- import * as os from 'node:os';
9
-
10
- const CACHE_DIR = path.join(os.homedir(), '.pi', 'cache', 'stackexchange');
11
-
12
- // Lazy-initialize cache directory on first use (prevents race conditions)
13
- let cacheInitialized = false;
14
-
15
- function ensureCacheDir(): void {
16
- if (cacheInitialized) {
17
- return;
18
- }
19
- try {
20
- fs.mkdirSync(CACHE_DIR, { recursive: true });
21
- cacheInitialized = true;
22
- } catch {
23
- // Directory might already exist
24
- cacheInitialized = true;
25
- }
26
- }
27
-
28
- export interface CacheEntry<T> {
29
- data: T;
30
- timestamp: number;
31
- ttl: number;
32
- }
33
-
34
- export function cacheGet<T>(key: string): T | null {
35
- ensureCacheDir();
36
- try {
37
- const cachePath = path.join(CACHE_DIR, `${key}.json`);
38
- const content = fs.readFileSync(cachePath, 'utf-8');
39
- const entry: CacheEntry<T> = JSON.parse(content) as CacheEntry<T>;
40
-
41
- // Check if cache entry is still valid
42
- const now = Date.now();
43
- const age = (now - entry.timestamp) / 1000; // Age in seconds
44
-
45
- if (age > entry.ttl) {
46
- // Cache expired
47
- try {
48
- fs.unlinkSync(cachePath);
49
- } catch {
50
- // Ignore errors when deleting expired cache
51
- }
52
- return null;
53
- }
54
-
55
- return entry.data;
56
- } catch {
57
- // Cache file doesn't exist or is invalid
58
- return null;
59
- }
60
- }
61
-
62
- export function cacheSet<T>(key: string, data: T, ttl: number): void {
63
- ensureCacheDir();
64
- try {
65
- const cachePath = path.join(CACHE_DIR, `${key}.json`);
66
- const entry: CacheEntry<T> = {
67
- data,
68
- timestamp: Date.now(),
69
- ttl,
70
- };
71
- fs.writeFileSync(cachePath, JSON.stringify(entry));
72
- } catch (error) {
73
- logger.warn('[StackExchange] Failed to write cache:', error);
74
- }
75
- }
76
-
77
- export function cacheClear(): void {
78
- try {
79
- const files = fs.readdirSync(CACHE_DIR);
80
- for (const file of files) {
81
- fs.unlinkSync(path.join(CACHE_DIR, file));
82
- }
83
- } catch (error) {
84
- logger.warn('[StackExchange] Failed to clear cache:', error);
85
- }
86
- }
87
-
88
- export function cacheGetStats(): { count: number; size: number } {
89
- try {
90
- const files = fs.readdirSync(CACHE_DIR);
91
- let totalSize = 0;
92
- for (const file of files) {
93
- const filePath = path.join(CACHE_DIR, file);
94
- const stats = fs.statSync(filePath);
95
- totalSize += stats.size;
96
- }
97
- return {
98
- count: files.length,
99
- size: totalSize,
100
- };
101
- } catch {
102
- return { count: 0, size: 0 };
103
- }
104
- }
105
-
106
- // Sanitize cache keys (replace special characters)
107
- export function sanitizeCacheKey(key: string): string {
108
- return key
109
- .replace(/[^a-zA-Z0-9_-]/g, '_')
110
- .substring(0, 100); // Limit length
111
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Output formatters for Stack Exchange results
3
- */
4
-
5
- export * from './table.ts';
6
- export * from './json.ts';
7
- export * from './compact.ts';
@@ -1,21 +0,0 @@
1
- /**
2
- * JSON output formatter for Stack Exchange results
3
- */
4
-
5
- import type { Question, Answer, User, Site } from '../types';
6
-
7
- export function formatQuestionsJSON(questions: Question[]): string {
8
- return JSON.stringify(questions, null, 2);
9
- }
10
-
11
- export function formatAnswersJSON(answers: Answer[]): string {
12
- return JSON.stringify(answers, null, 2);
13
- }
14
-
15
- export function formatUsersJSON(users: User[]): string {
16
- return JSON.stringify(users, null, 2);
17
- }
18
-
19
- export function formatSitesJSON(sites: Site[]): string {
20
- return JSON.stringify(sites, null, 2);
21
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * SearXNG Status Component (Left Box)
3
- *
4
- * Super minimal display: [starting] n:0 or [active] n:1 or [inactive] n:0 or [error] n:0
5
- * This is shown in the left square of the TUI.
6
- */
7
-
8
- import type { Component } from '@mariozechner/pi-tui';
9
- import type { Theme } from '@mariozechner/pi-coding-agent';
10
- import type { SearxngStatus } from '../infrastructure/searxng-lifecycle.ts';
11
-
12
- let capturedTui: { requestRender?(): void } | null = null;
13
-
14
- export function getCapturedTui(): { requestRender?(): void } | null {
15
- return capturedTui;
16
- }
17
-
18
- export function createSearxngStatusComponent(initialStatus: SearxngStatus): (tui: unknown, theme: Theme) => Component & { dispose?(): void } {
19
- let status = initialStatus;
20
-
21
- const updateStatus = (newStatus: SearxngStatus) => {
22
- status = newStatus;
23
- capturedTui?.requestRender?.();
24
- };
25
-
26
- return (tui: unknown, theme: Theme) => {
27
- // Capture TUI reference for external re-render triggers
28
- capturedTui = tui as { requestRender?(): void };
29
-
30
- const component: Component = {
31
- render(width: number): string[] {
32
- const boxWidth = Math.min(width, 14);
33
-
34
- // Format status: [starting] n:0 or [active] n:1 or [inactive] n:0 or [error] n:0
35
- let statusText: string;
36
- let statusColor: (text: string) => string;
37
-
38
- switch (status.state) {
39
- case 'starting_up':
40
- statusText = '[starting]';
41
- statusColor = (text: string) => theme.fg('muted', text);
42
- break;
43
- case 'active':
44
- statusText = '[active]';
45
- statusColor = (text: string) => theme.fg('success', text);
46
- break;
47
- case 'inactive':
48
- statusText = '[inactive]';
49
- statusColor = (text: string) => theme.fg('muted', text);
50
- break;
51
- case 'error':
52
- statusText = '[error]';
53
- statusColor = (text: string) => theme.fg('error', text);
54
- break;
55
- default:
56
- statusText = '[?]';
57
- statusColor = (text: string) => theme.fg('muted', text);
58
- }
59
-
60
- const content = `${statusColor(statusText)}`;
61
-
62
- return [
63
- `┌${'─'.repeat(boxWidth - 2)}┐`,
64
- `│${content.padEnd(boxWidth - 2)}│`,
65
- `│${' '.repeat(boxWidth - 2)}│`,
66
- `└${'─'.repeat(boxWidth - 2)}┘`,
67
- ];
68
- },
69
-
70
- invalidate(): void {
71
- // No cached state to invalidate
72
- },
73
- };
74
-
75
- // Expose update method
76
- (component as any).updateStatus = updateStatus;
77
-
78
- return component;
79
- };
80
- }