@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
@@ -0,0 +1,179 @@
1
+ /**
2
+ * NVD (National Vulnerability Database) API Types
3
+ */
4
+
5
+ export interface CVSSData {
6
+ readonly version?: string;
7
+ readonly vectorString?: string;
8
+ readonly baseScore?: number;
9
+ readonly baseSeverity?: string;
10
+ }
11
+
12
+ export interface CVSSMetricV31 {
13
+ readonly cvssData: CVSSData;
14
+ }
15
+
16
+ export interface CVSSMetricV30 {
17
+ readonly cvssData: CVSSData;
18
+ }
19
+
20
+ export interface WeaknessDescription {
21
+ readonly value?: string;
22
+ }
23
+
24
+ export interface Weakness {
25
+ readonly description?: WeaknessDescription[];
26
+ }
27
+
28
+ export interface Reference {
29
+ readonly url?: string;
30
+ }
31
+
32
+ export interface CPEMatch {
33
+ readonly criteria?: string;
34
+ }
35
+
36
+ export interface Node {
37
+ readonly cpeMatch?: CPEMatch[];
38
+ }
39
+
40
+ export interface Configuration {
41
+ readonly nodes?: Node[];
42
+ }
43
+
44
+ export interface Description {
45
+ readonly value?: string;
46
+ }
47
+
48
+ // CVSS v4.0 mirrors v3 (score + qualitative severity live in cvssData). NVD is actively adding it.
49
+ export interface CVSSMetricV40 {
50
+ readonly cvssData: CVSSData;
51
+ }
52
+
53
+ // CVSS v2 differs: cvssData carries the numeric baseScore/vectorString but NO qualitative rating —
54
+ // NVD exposes the v2 baseSeverity (LOW/MEDIUM/HIGH) at the METRIC level instead.
55
+ export interface CVSSMetricV2 {
56
+ readonly cvssData: CVSSData;
57
+ readonly baseSeverity?: string;
58
+ }
59
+
60
+ export interface Metrics {
61
+ readonly cvssMetricV40?: CVSSMetricV40[];
62
+ readonly cvssMetricV31?: CVSSMetricV31[];
63
+ readonly cvssMetricV30?: CVSSMetricV30[];
64
+ readonly cvssMetricV2?: CVSSMetricV2[];
65
+ }
66
+
67
+ export interface CVE {
68
+ readonly id?: string;
69
+ readonly descriptions?: Description[];
70
+ readonly published?: string;
71
+ readonly lastModified?: string;
72
+ readonly metrics?: Metrics;
73
+ readonly weaknesses?: Weakness[];
74
+ readonly references?: Reference[];
75
+ readonly configurations?: Configuration[];
76
+ }
77
+
78
+ export interface NVDEntry {
79
+ readonly cve: CVE;
80
+ }
81
+
82
+ export interface NVDApiResponse {
83
+ readonly vulnerabilities?: NVDEntry[];
84
+ }
85
+
86
+ export type Severity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
87
+
88
+ export interface SearchOptions {
89
+ readonly severity?: Severity;
90
+ readonly maxResults?: number;
91
+ readonly includeExploited?: boolean;
92
+ readonly cweId?: string;
93
+ readonly startDate?: string;
94
+ readonly endDate?: string;
95
+ readonly maxPages?: number;
96
+ // Caller cancellation; composed with the per-request timeout and used to
97
+ // short-circuit the rate-limit spacing, pagination, and retry backoff.
98
+ readonly signal?: AbortSignal;
99
+ }
100
+
101
+ export interface RetryOptions {
102
+ readonly maxRetries: number;
103
+ readonly initialDelay: number;
104
+ readonly maxDelay: number;
105
+ readonly signal?: AbortSignal;
106
+ }
107
+
108
+ // Type guards
109
+ export function isWeaknessDescription(value: unknown): value is WeaknessDescription {
110
+ return (
111
+ typeof value === 'object' &&
112
+ value !== null &&
113
+ 'value' in value
114
+ );
115
+ }
116
+
117
+ export function isWeakness(value: unknown): value is Weakness {
118
+ return (
119
+ typeof value === 'object' &&
120
+ value !== null &&
121
+ 'description' in value
122
+ );
123
+ }
124
+
125
+ export function isReference(value: unknown): value is Reference {
126
+ return (
127
+ typeof value === 'object' &&
128
+ value !== null &&
129
+ 'url' in value
130
+ );
131
+ }
132
+
133
+ export function isCPEMatch(value: unknown): value is CPEMatch {
134
+ return (
135
+ typeof value === 'object' &&
136
+ value !== null &&
137
+ 'criteria' in value
138
+ );
139
+ }
140
+
141
+ export function isNode(value: unknown): value is Node {
142
+ return (
143
+ typeof value === 'object' &&
144
+ value !== null &&
145
+ 'cpeMatch' in value
146
+ );
147
+ }
148
+
149
+ export function isConfiguration(value: unknown): value is Configuration {
150
+ return (
151
+ typeof value === 'object' &&
152
+ value !== null &&
153
+ 'nodes' in value
154
+ );
155
+ }
156
+
157
+ export function isDescription(value: unknown): value is Description {
158
+ return (
159
+ typeof value === 'object' &&
160
+ value !== null &&
161
+ 'value' in value
162
+ );
163
+ }
164
+
165
+ export function isNVDEntry(value: unknown): value is NVDEntry {
166
+ return (
167
+ typeof value === 'object' &&
168
+ value !== null &&
169
+ 'cve' in value
170
+ );
171
+ }
172
+
173
+ export function isNVDApiResponse(value: unknown): value is NVDApiResponse {
174
+ return (
175
+ typeof value === 'object' &&
176
+ value !== null &&
177
+ 'vulnerabilities' in value
178
+ );
179
+ }
@@ -0,0 +1,246 @@
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 type { OsvVulnerability, OsvQueryRequest } from './osv-types.ts';
11
+ import { normalizeEcosystem } from './ecosystem.ts';
12
+ import { createTimeoutSignal, retryWithBackoff, isTransientError } from '../web-research/retry-utils.ts';
13
+ import { logger } from '../logger.ts';
14
+ import { OSV_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_INITIAL_DELAY_MS, DEFAULT_MAX_DELAY_MS } from '../constants.ts';
15
+ import { CircuitBreaker } from '../utils/circuit-breaker.ts';
16
+ import { metrics } from '../utils/metrics.ts';
17
+ import {
18
+ isOsvVulnerability,
19
+ isOsvQueryResponse,
20
+ mapOsvItemToVulnerability,
21
+ } from './osv-types.ts';
22
+
23
+ const osvCircuitBreaker = new CircuitBreaker({
24
+ failureThreshold: 5,
25
+ resetTimeoutMs: 10000,
26
+ name: 'OSV API',
27
+ isTransientError: isTransientError
28
+ });
29
+
30
+ const OSV_BASE_URL = 'https://api.osv.dev/v1';
31
+ const DEFAULT_MAX_RESULTS = 20;
32
+
33
+ /**
34
+ * Fetch with retry for resilience
35
+ */
36
+ async function fetchWithRetry(
37
+ url: string,
38
+ options: RequestInit,
39
+ signal?: AbortSignal,
40
+ ): Promise<Response> {
41
+ const endpoint = new URL(url).pathname;
42
+ return osvCircuitBreaker.execute(async () => {
43
+ return retryWithBackoff(
44
+ async () => {
45
+ const response = await fetch(url, options);
46
+ if (!response.ok) {
47
+ const error = new Error(`HTTP ${response.status}: ${response.statusText}`) as Error & { status?: number };
48
+ error.status = response.status;
49
+ metrics.increment('osv_errors_total', 1, { endpoint, status_code: response.status.toString() });
50
+ if (response.status === 429) {
51
+ metrics.increment('osv_ratelimit_hits_total', 1, { endpoint });
52
+ }
53
+ throw error;
54
+ }
55
+ metrics.increment('osv_requests_total', 1, { endpoint, status: 'success' });
56
+ return response;
57
+ },
58
+ {
59
+ maxRetries: DEFAULT_MAX_RETRIES,
60
+ initialDelay: DEFAULT_INITIAL_DELAY_MS,
61
+ maxDelay: DEFAULT_MAX_DELAY_MS,
62
+ label: `OSV API: ${url}`,
63
+ signal,
64
+ isTransientError: (error) => {
65
+ const status = (error as Error & { status?: number })?.status;
66
+ if (typeof status === 'number') {
67
+ return status === 429 || status === 403 || status >= 500;
68
+ }
69
+ return isTransientError(error);
70
+ },
71
+ },
72
+ );
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Search OSV for vulnerabilities
78
+ *
79
+ * @param terms - Package names, CVE IDs, or keywords
80
+ * @param options - Optional filters
81
+ * @returns OSV search result with vulnerabilities and optional error
82
+ */
83
+ export async function searchOSV(
84
+ terms: string[],
85
+ options?: {
86
+ ecosystem?: string;
87
+ severity?: string;
88
+ maxResults?: number;
89
+ includeAffected?: boolean;
90
+ signal?: AbortSignal;
91
+ },
92
+ ): Promise<OSVResult> {
93
+ const startTime = Date.now();
94
+ const maxResults: number = options?.maxResults ?? DEFAULT_MAX_RESULTS;
95
+ const vulnerabilities: Vulnerability[] = [];
96
+ let error: string | undefined = undefined;
97
+
98
+ try {
99
+ // Each term is fetched and settled independently so one failed term (a
100
+ // transient 5xx, a malformed body, an exhausted retry) never discards the
101
+ // vulnerabilities already gathered for the others. `skipped` flags a package
102
+ // term that needs the ecosystem parameter.
103
+ const fetchTerm = async (term: string): Promise<{ vulns: Vulnerability[]; skipped: boolean }> => {
104
+ const termUpper: string = term.toUpperCase();
105
+ const isIdLookup =
106
+ termUpper.startsWith('CVE-') || termUpper.startsWith('GHSA-') || termUpper.startsWith('OSV-');
107
+ let response: Response;
108
+
109
+ if (isIdLookup) {
110
+ const normalizedId: string = termUpper.startsWith('GHSA-')
111
+ ? `GHSA-${term.slice(term.indexOf('-') + 1).toLowerCase()}`
112
+ : termUpper;
113
+ const url: string = `${OSV_BASE_URL}/vulns/${encodeURIComponent(normalizedId)}`;
114
+ response = await fetchWithRetry(url, {
115
+ headers: { 'User-Agent': 'pi-research/2.0', 'Accept': 'application/json' },
116
+ signal: createTimeoutSignal(OSV_TIMEOUT_MS, options?.signal),
117
+ }, options?.signal);
118
+ } else {
119
+ if (options?.ecosystem === undefined || options.ecosystem === '') {
120
+ return { vulns: [], skipped: true };
121
+ }
122
+ const body: OsvQueryRequest = { package: { name: term, ecosystem: normalizeEcosystem(options.ecosystem, 'osv') } };
123
+ response = await fetchWithRetry(`${OSV_BASE_URL}/query`, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'User-Agent': 'pi-research/2.0',
127
+ 'Accept': 'application/json',
128
+ 'Content-Type': 'application/json',
129
+ },
130
+ body: JSON.stringify(body),
131
+ signal: createTimeoutSignal(OSV_TIMEOUT_MS, options?.signal),
132
+ }, options?.signal);
133
+ }
134
+
135
+ let data: unknown;
136
+ try {
137
+ data = await response.json();
138
+ } catch {
139
+ metrics.increment('osv_search_errors_total', 1, { error_type: 'malformed_json' });
140
+ throw new Error(`OSV returned a malformed JSON response for "${term}".`);
141
+ }
142
+
143
+ let items: OsvVulnerability[];
144
+ if (isOsvVulnerability(data)) {
145
+ items = [data];
146
+ } else if (isOsvQueryResponse(data)) {
147
+ items = data.vulns ?? [];
148
+ } else {
149
+ logger.warn(`OSV returned unexpected format for "${term}"`);
150
+ return { vulns: [], skipped: false };
151
+ }
152
+
153
+ const endpoint = isIdLookup ? 'vulns_by_id' : 'query';
154
+ metrics.increment(items.length > 0 ? 'osv_cache_hits_total' : 'osv_cache_misses_total', 1, { term, endpoint });
155
+
156
+ const vulns: Vulnerability[] = [];
157
+ for (const item of items) {
158
+ // Withdrawn/retracted advisories are no longer valid vulnerabilities — skip them so they
159
+ // are not reported as active findings nor counted toward the researcher grounding gate.
160
+ if (item.withdrawn) continue;
161
+ const vuln: Vulnerability = mapOsvItemToVulnerability(item);
162
+ if (options?.severity !== undefined && options.severity !== '') {
163
+ // OSV/GHSA severity vocabulary is LOW/MODERATE/HIGH/CRITICAL. Accept the
164
+ // common "MEDIUM" synonym for MODERATE (GitHub maps it the same way at
165
+ // github-advisories.ts) — otherwise a severity:"medium" filter silently
166
+ // drops every OSV result.
167
+ // mapOsvItemToVulnerability canonicalizes severity to LOW/MEDIUM/HIGH/CRITICAL (both the
168
+ // OSV "MODERATE" and "MEDIUM" spellings map to 'MEDIUM'). Normalize the caller's filter
169
+ // into the SAME vocabulary — MODERATE→MEDIUM — before comparing. The previous direction
170
+ // (MEDIUM→MODERATE) compared against a value vuln.severity never holds, silently dropping
171
+ // every medium-severity OSV result (the most common filter value).
172
+ const want = options.severity.toUpperCase();
173
+ const normalized = want === 'MODERATE' ? 'MEDIUM' : want;
174
+ if (vuln.severity === 'UNKNOWN') {
175
+ // We could not rank this record. If it nonetheless carries a CVSS vector we
176
+ // don't score — mapOsvItemToVulnerability only computes CVSS v3, so a v4-only
177
+ // (increasingly common) or v2-only record stays UNKNOWN — it IS a scored
178
+ // vulnerability whose number we simply can't compute. Silently dropping it
179
+ // under a severity filter would hide a possible HIGH/CRITICAL, so surface it
180
+ // instead (the safe bias for security triage: a false-include beats a hidden
181
+ // finding). A record with NO severity signal at all stays filtered out so
182
+ // results are not flooded with genuinely unranked noise.
183
+ const hasUnscoredVector =
184
+ (Array.isArray(item.severity) && item.severity.length > 0) ||
185
+ (Array.isArray(item.affected) &&
186
+ item.affected.some((a) => Array.isArray(a.severity) && a.severity.length > 0));
187
+ if (!hasUnscoredVector) continue;
188
+ metrics.increment('osv_unscored_included_total', 1, { filter: normalized });
189
+ } else if (vuln.severity !== normalized) {
190
+ continue;
191
+ }
192
+ }
193
+ vulns.push(vuln);
194
+ }
195
+ return { vulns, skipped: false };
196
+ };
197
+
198
+ const settled = await Promise.allSettled(terms.map(fetchTerm));
199
+ const allVulns: Vulnerability[] = [];
200
+ let skippedNoEcosystem = 0;
201
+ const failures: string[] = [];
202
+ settled.forEach((result, i) => {
203
+ if (result.status === 'fulfilled') {
204
+ allVulns.push(...result.value.vulns);
205
+ if (result.value.skipped) skippedNoEcosystem++;
206
+ } else {
207
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
208
+ // A 404 on an ID lookup means "no such vuln", not a real failure.
209
+ if (!reason.includes('HTTP 404')) failures.push(`${terms[i]} (${reason})`);
210
+ }
211
+ });
212
+
213
+ const uniqueVulns = new Map<string, Vulnerability>();
214
+ for (const vuln of allVulns) {
215
+ if (!uniqueVulns.has(vuln.id)) {
216
+ uniqueVulns.set(vuln.id, vuln);
217
+ }
218
+ }
219
+
220
+ vulnerabilities.push(...Array.from(uniqueVulns.values()));
221
+
222
+ if (failures.length > 0 && failures.length === terms.length) {
223
+ throw new Error(`All OSV lookups failed: ${failures.join('; ')}`);
224
+ }
225
+ const notes: string[] = [];
226
+ if (failures.length > 0) notes.push(`Some OSV lookups failed: ${failures.join('; ')}`);
227
+ if (skippedNoEcosystem > 0) {
228
+ notes.push(`${skippedNoEcosystem} term(s) require the ecosystem parameter for OSV package search (e.g., ecosystem: "npm"). CVE/GHSA/OSV IDs work without it.`);
229
+ }
230
+ if (notes.length > 0) error = notes.join(' ');
231
+
232
+ } catch (err: unknown) {
233
+ error = err instanceof Error ? err.message : String(err);
234
+ metrics.increment('osv_search_errors_total', 1, { error_type: err instanceof Error ? err.name : 'unknown' });
235
+ } finally {
236
+ const duration = Date.now() - startTime;
237
+ metrics.observe('osv_search_duration_ms', duration, { has_error: error ? 'true' : 'false' });
238
+ }
239
+
240
+ return {
241
+ count: vulnerabilities.length,
242
+ vulnerabilities: vulnerabilities.slice(0, maxResults),
243
+ error,
244
+ };
245
+ }
246
+
@@ -0,0 +1,285 @@
1
+ /**
2
+ * OSV (Open Source Vulnerabilities) API Types
3
+ */
4
+
5
+ import type { Vulnerability } from './types.ts';
6
+ import { scoreCvss3 } from './cvss.ts';
7
+
8
+ /**
9
+ * OSV API vulnerability response (from GET /vulns/{id})
10
+ */
11
+ export interface OsvVulnerability {
12
+ id: string;
13
+ summary?: string;
14
+ details?: string;
15
+ published?: string;
16
+ modified?: string;
17
+ withdrawn?: string;
18
+ aliases?: string[];
19
+ related?: string[];
20
+ affected?: OsvAffected[];
21
+ severity?: OsvSeverity[];
22
+ references?: OsvReference[];
23
+ database_specific?: OsvDatabaseSpecific;
24
+ }
25
+
26
+ /**
27
+ * OSV severity entry
28
+ */
29
+ export interface OsvSeverity {
30
+ type: string;
31
+ score: string;
32
+ }
33
+
34
+ /**
35
+ * OSV database-specific metadata
36
+ */
37
+ export interface OsvDatabaseSpecific {
38
+ severity?: string;
39
+ cwe?: Array<{ id: string } | string>;
40
+ [key: string]: unknown;
41
+ }
42
+
43
+ /**
44
+ * OSV affected package entry
45
+ */
46
+ export interface OsvAffected {
47
+ package?: OsvPackage;
48
+ ranges?: OsvRange[];
49
+ versions?: string[];
50
+ database_specific?: Record<string, unknown>;
51
+ severity?: OsvSeverity[];
52
+ }
53
+
54
+ /**
55
+ * OSV package information
56
+ */
57
+ export interface OsvPackage {
58
+ name?: string;
59
+ ecosystem?: string;
60
+ purl?: string;
61
+ ecosystem_specific?: {
62
+ name?: string;
63
+ [key: string]: unknown;
64
+ };
65
+ [key: string]: unknown;
66
+ }
67
+
68
+ /**
69
+ * OSV version range information
70
+ */
71
+ export interface OsvRange {
72
+ type: string;
73
+ repo?: string;
74
+ events: OsvRangeEvent[];
75
+ database_specific?: Record<string, unknown>;
76
+ }
77
+
78
+ /**
79
+ * OSV range event (introduced, fixed, last_affected, limits)
80
+ */
81
+ export interface OsvRangeEvent {
82
+ introduced?: string;
83
+ fixed?: string;
84
+ last_affected?: string;
85
+ limit?: string;
86
+ }
87
+
88
+ /**
89
+ * OSV reference entry
90
+ */
91
+ export interface OsvReference {
92
+ type?: string;
93
+ url?: string;
94
+ }
95
+
96
+ /**
97
+ * OSV query request body
98
+ */
99
+ export interface OsvQueryRequest {
100
+ package: {
101
+ name: string;
102
+ ecosystem: string;
103
+ purl?: string;
104
+ };
105
+ version?: string;
106
+ commit?: string;
107
+ }
108
+
109
+ /**
110
+ * OSV query response (from POST /query)
111
+ */
112
+ export interface OsvQueryResponse {
113
+ vulns?: OsvVulnerability[];
114
+ [key: string]: unknown;
115
+ }
116
+
117
+ // Type guards
118
+ export function isOsvVulnerability(value: unknown): value is OsvVulnerability {
119
+ if (value === null || typeof value !== 'object') {
120
+ return false;
121
+ }
122
+ const obj = value as Record<string, unknown>;
123
+ return typeof obj['id'] === 'string';
124
+ }
125
+
126
+ export function isOsvQueryResponse(value: unknown): value is OsvQueryResponse {
127
+ if (value === null || typeof value !== 'object') {
128
+ return false;
129
+ }
130
+ const obj = value as Record<string, unknown>;
131
+ return Array.isArray(obj['vulns']);
132
+ }
133
+
134
+ // Helper Functions
135
+ export function mapOsvItemToVulnerability(item: OsvVulnerability): Vulnerability {
136
+ const osvId: string = item.id;
137
+ const summary: string = item.summary ?? item.details ?? '';
138
+ const details: string = item.details ?? '';
139
+ const published: string | undefined = item.published;
140
+ const modified: string = item.modified ?? '';
141
+
142
+ let severityStr: string | undefined;
143
+ if (item.database_specific !== undefined &&
144
+ typeof item.database_specific.severity === 'string') {
145
+ severityStr = item.database_specific.severity;
146
+ }
147
+ let severity: string = mapOsvSeverity(severityStr);
148
+
149
+ // When OSV has no database_specific.severity (non-GHSA records: CVE/PyPA/Go/RustSec), the only
150
+ // severity signal is a CVSS vector in item.severity[]. Compute the v3 base score/severity from it
151
+ // rather than reporting UNKNOWN with no score.
152
+ let cvssScore: number | undefined;
153
+ let cvssVector: string | undefined;
154
+ if (severity === 'UNKNOWN' && Array.isArray(item.severity)) {
155
+ for (const sev of item.severity) {
156
+ if (sev && typeof sev.score === 'string' && /^CVSS_V3/i.test(sev.type ?? '')) {
157
+ const scored = scoreCvss3(sev.score);
158
+ if (scored) {
159
+ severity = scored.severity === 'NONE' ? 'UNKNOWN' : scored.severity;
160
+ cvssScore = scored.score;
161
+ cvssVector = sev.score;
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ const aliases: string[] = item.aliases ?? [];
169
+ const references: string[] = [...aliases];
170
+
171
+ if (item.references !== undefined) {
172
+ for (const ref of item.references) {
173
+ if (ref.url !== undefined && ref.url !== '') {
174
+ references.push(ref.url);
175
+ }
176
+ }
177
+ }
178
+
179
+ const affectedProducts: string[] = [];
180
+ const fixes: string[] = [];
181
+
182
+ if (item.affected !== undefined) {
183
+ for (const affectedEntry of item.affected) {
184
+ const pkg: OsvPackage | undefined = affectedEntry.package;
185
+
186
+ if (pkg !== undefined) {
187
+ const pkgName: string = pkg.name ??
188
+ (pkg.ecosystem_specific !== undefined && typeof pkg.ecosystem_specific.name === 'string'
189
+ ? pkg.ecosystem_specific.name
190
+ : '');
191
+
192
+ if (pkgName !== '') {
193
+ affectedProducts.push(pkgName);
194
+ }
195
+
196
+ if (affectedEntry.ranges !== undefined) {
197
+ for (const range of affectedEntry.ranges) {
198
+ // Guard against a malformed range missing `events` (upstream validation only
199
+ // checks the vuln `id`): an unguarded events.find() would throw a TypeError,
200
+ // fail the whole term in Promise.allSettled, and discard all its vulns.
201
+ const events: OsvRangeEvent[] = Array.isArray(range.events) ? range.events : [];
202
+
203
+ const introducedEvent: OsvRangeEvent | undefined = events.find(
204
+ (e: OsvRangeEvent) => e.introduced !== undefined,
205
+ );
206
+ const fixedEvent: OsvRangeEvent | undefined = events.find(
207
+ (e: OsvRangeEvent) => e.fixed !== undefined,
208
+ );
209
+ const lastAffectedEvent: OsvRangeEvent | undefined = events.find(
210
+ (e: OsvRangeEvent) => e.last_affected !== undefined,
211
+ );
212
+
213
+ const versionInfo: string[] = [];
214
+
215
+ if (introducedEvent?.introduced !== undefined) {
216
+ versionInfo.push(`introduced: ${introducedEvent.introduced}`);
217
+ }
218
+ if (fixedEvent?.fixed !== undefined) {
219
+ versionInfo.push(`fixed: ${fixedEvent.fixed}`);
220
+ }
221
+ if (lastAffectedEvent?.last_affected !== undefined) {
222
+ versionInfo.push(`last affected: ${lastAffectedEvent.last_affected}`);
223
+ }
224
+
225
+ if (versionInfo.length > 0 && pkgName !== '') {
226
+ fixes.push(`${pkgName}: ${versionInfo.join(', ')}`);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ const cwes: string[] = [];
235
+ if (item.database_specific !== undefined) {
236
+ if (item.database_specific.cwe !== undefined && Array.isArray(item.database_specific.cwe)) {
237
+ for (const cwe of item.database_specific.cwe) {
238
+ if (typeof cwe === 'string') {
239
+ cwes.push(cwe);
240
+ } else if (typeof cwe === 'object' && typeof (cwe as { id?: string }).id === 'string') {
241
+ cwes.push((cwe as { id: string }).id);
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ return {
248
+ id: osvId,
249
+ source: 'osv',
250
+ severity,
251
+ description: summary !== '' ? summary : details,
252
+ published,
253
+ modified,
254
+ cvssScore,
255
+ cvssVector,
256
+ cwes,
257
+ references,
258
+ affectedProducts,
259
+ fixes,
260
+ knownExploited: false,
261
+ };
262
+ }
263
+
264
+ function mapOsvSeverity(severity?: string): string {
265
+ if (severity === undefined || severity === '') {
266
+ return 'UNKNOWN';
267
+ }
268
+
269
+ const upper: string = severity.toUpperCase();
270
+
271
+ if (upper === 'CRITICAL') {
272
+ return 'CRITICAL';
273
+ }
274
+ if (upper === 'HIGH') {
275
+ return 'HIGH';
276
+ }
277
+ if (upper === 'MEDIUM' || upper === 'MODERATE') {
278
+ return 'MEDIUM';
279
+ }
280
+ if (upper === 'LOW') {
281
+ return 'LOW';
282
+ }
283
+
284
+ return 'UNKNOWN';
285
+ }