@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,282 @@
1
+ /**
2
+ * GitHub Advisory API Types
3
+ */
4
+
5
+ // ============================================================================
6
+ // Type Definitions for Fetch API
7
+ // ============================================================================
8
+
9
+ type FetchType = (
10
+ _input: string | Request,
11
+ _init?: RequestInit,
12
+ ) => Promise<Response>;
13
+
14
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
+ declare const fetch: FetchType;
16
+
17
+ interface Headers {
18
+ append(_name: string, _value: string): void;
19
+ delete(_name: string): void;
20
+ get(_name: string): string | null;
21
+ has(_name: string): boolean;
22
+ set(_name: string, _value: string): void;
23
+ forEach(_callback: (_value: string, _name: string) => void): void;
24
+ }
25
+
26
+ interface Response {
27
+ readonly headers: Headers;
28
+ readonly ok: boolean;
29
+ readonly redirected: boolean;
30
+ readonly status: number;
31
+ readonly statusText: string;
32
+ readonly type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect';
33
+ readonly url: string;
34
+ readonly body: ReadableStream<Uint8Array> | null;
35
+ readonly bodyUsed: boolean;
36
+ clone(): Response;
37
+ arrayBuffer(): Promise<ArrayBuffer>;
38
+ blob(): Promise<Blob>;
39
+ formData(): Promise<FormData>;
40
+ json(): Promise<unknown>;
41
+ text(): Promise<string>;
42
+ }
43
+
44
+ // ============================================================================
45
+ // Type Definitions for GitHub API Responses
46
+ // ============================================================================
47
+
48
+ /**
49
+ * GitHub Advisory Vulnerability Package
50
+ */
51
+ export interface GitHubAdvisoryPackage {
52
+ readonly ecosystem?: string;
53
+ readonly name?: string;
54
+ }
55
+
56
+ /**
57
+ * GitHub Advisory Affected Entry
58
+ */
59
+ export interface GitHubAdvisoryAffected {
60
+ readonly package?: GitHubAdvisoryPackage;
61
+ }
62
+
63
+ /**
64
+ * GitHub Advisory Vulnerability
65
+ */
66
+ export interface GitHubAdvisoryVulnerability {
67
+ readonly package?: GitHubAdvisoryPackage;
68
+ readonly affected?: readonly GitHubAdvisoryAffected[];
69
+ }
70
+
71
+ /**
72
+ * Raw GitHub Advisory from API response
73
+ */
74
+ export interface GitHubAdvisoryRaw {
75
+ readonly ghsa_id?: string;
76
+ readonly id?: string;
77
+ readonly summary?: string;
78
+ readonly description?: string;
79
+ readonly severity?: string;
80
+ readonly published_at?: string;
81
+ readonly updated_at?: string;
82
+ readonly cve_id?: string;
83
+ readonly html_url?: string;
84
+ readonly advisory_url?: string;
85
+ readonly vulnerabilities?: readonly GitHubAdvisoryVulnerability[];
86
+ readonly affected?: readonly GitHubAdvisoryAffected[];
87
+ }
88
+
89
+ /**
90
+ * GitHub API list response wrapper
91
+ */
92
+ export interface GitHubAdvisoryListResponse {
93
+ readonly items?: readonly GitHubAdvisoryRaw[];
94
+ }
95
+
96
+ // ============================================================================
97
+ // Type Guards
98
+ // ============================================================================
99
+
100
+ export function isGitHubAdvisoryRaw(value: unknown): value is GitHubAdvisoryRaw {
101
+ return typeof value === 'object' && value !== null;
102
+ }
103
+
104
+ export function isArray(value: unknown): value is readonly unknown[] {
105
+ return Array.isArray(value);
106
+ }
107
+
108
+ export function isString(value: unknown): value is string {
109
+ return typeof value === 'string';
110
+ }
111
+
112
+ export function isObject(value: unknown): value is Record<string, unknown> {
113
+ return typeof value === 'object' && value !== null && !isArray(value);
114
+ }
115
+
116
+ export function isGitHubAdvisoryPackage(value: unknown): value is GitHubAdvisoryPackage {
117
+ if (!isObject(value)) {
118
+ return false;
119
+ }
120
+ return true;
121
+ }
122
+
123
+ export function isGitHubAdvisoryAffected(value: unknown): value is GitHubAdvisoryAffected {
124
+ if (!isObject(value)) {
125
+ return false;
126
+ }
127
+ return true;
128
+ }
129
+
130
+ export function isGitHubAdvisoryVulnerability(value: unknown): value is GitHubAdvisoryVulnerability {
131
+ if (!isObject(value)) {
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+
137
+ export function isGitHubAdvisoryListResponse(value: unknown): value is GitHubAdvisoryListResponse {
138
+ if (!isObject(value)) {
139
+ return false;
140
+ }
141
+ return 'items' in value;
142
+ }
143
+
144
+ export function isGitHubAdvisoryArray(value: unknown): value is GitHubAdvisoryRaw[] {
145
+ if (!isArray(value)) {
146
+ return false;
147
+ }
148
+ return value.every(isGitHubAdvisoryRaw);
149
+ }
150
+
151
+ export function isSingleGitHubAdvisory(value: unknown): value is GitHubAdvisoryRaw {
152
+ if (!isGitHubAdvisoryRaw(value)) {
153
+ return false;
154
+ }
155
+ // Single advisory typically has ghsa_id
156
+ return 'ghsa_id' in value || 'summary' in value;
157
+ }
158
+
159
+ import { type Advisory } from './types.ts';
160
+
161
+ /**
162
+ * Extract affected packages from an advisory item
163
+ */
164
+ function extractAffectedPackages(item: GitHubAdvisoryRaw): string[] {
165
+ const affectedPackages: string[] = [];
166
+
167
+ // Check for vulnerabilities array
168
+ if (item.vulnerabilities !== undefined && isArray(item.vulnerabilities)) {
169
+ for (const vuln of item.vulnerabilities) {
170
+ if (!isGitHubAdvisoryVulnerability(vuln)) {
171
+ continue;
172
+ }
173
+
174
+ // Check for package in various possible locations
175
+ if (vuln.package !== undefined) {
176
+ if (isGitHubAdvisoryPackage(vuln.package)) {
177
+ const ecosystem = vuln.package.ecosystem ?? '';
178
+ const name = vuln.package.name;
179
+
180
+ if (isString(name) && name !== '') {
181
+ if (ecosystem !== '') {
182
+ affectedPackages.push(`${ecosystem}/${name}`);
183
+ } else {
184
+ affectedPackages.push(name);
185
+ }
186
+ }
187
+ }
188
+ } else if (vuln.affected !== undefined && isArray(vuln.affected)) {
189
+ // Alternative structure with 'affected' array
190
+ for (const aff of vuln.affected) {
191
+ if (isGitHubAdvisoryAffected(aff) && aff.package !== undefined) {
192
+ const pkg = aff.package;
193
+ if (isGitHubAdvisoryPackage(pkg)) {
194
+ const ecosystem = pkg.ecosystem ?? '';
195
+ const name = pkg.name;
196
+
197
+ if (isString(name) && name !== '') {
198
+ if (ecosystem !== '') {
199
+ affectedPackages.push(`${ecosystem}/${name}`);
200
+ } else {
201
+ affectedPackages.push(name);
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ // Check for direct 'affected' array at the top level
212
+ if (affectedPackages.length === 0 && item.affected !== undefined && isArray(item.affected)) {
213
+ for (const aff of item.affected) {
214
+ if (isGitHubAdvisoryAffected(aff) && aff.package !== undefined) {
215
+ const pkg = aff.package;
216
+ if (isGitHubAdvisoryPackage(pkg)) {
217
+ const ecosystem = pkg.ecosystem ?? '';
218
+ const name = pkg.name;
219
+
220
+ if (isString(name) && name !== '') {
221
+ if (ecosystem !== '') {
222
+ affectedPackages.push(`${ecosystem}/${name}`);
223
+ } else {
224
+ affectedPackages.push(name);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ return affectedPackages;
233
+ }
234
+
235
+ /**
236
+ * Extract references from an advisory item
237
+ */
238
+ export function extractReferences(item: GitHubAdvisoryRaw): string[] {
239
+ const references: string[] = [];
240
+
241
+ if (item.html_url !== undefined && item.html_url !== '') {
242
+ references.push(item.html_url);
243
+ }
244
+ if (item.advisory_url !== undefined && item.advisory_url !== '') {
245
+ references.push(item.advisory_url);
246
+ }
247
+
248
+ return references;
249
+ }
250
+
251
+ /**
252
+ * Map GitHub advisory to our Advisory interface
253
+ */
254
+ export function mapGitHubAdvisory(item: GitHubAdvisoryRaw): Advisory {
255
+ const ghsaId = item.ghsa_id ?? item.id ?? '';
256
+ const summary = item.summary ?? '';
257
+ const description = item.description ?? '';
258
+ // Guard the type: isGitHubAdvisoryRaw validates only that the value is an object, so a
259
+ // non-string severity (schema drift / numeric code) would throw on .toUpperCase() inside
260
+ // the unguarded items.map(), failing the whole term in Promise.allSettled and discarding
261
+ // every advisory for it.
262
+ const severity = typeof item.severity === 'string' ? item.severity.toUpperCase() : 'UNKNOWN';
263
+ const published = item.published_at ?? '';
264
+ const modified = item.updated_at ?? '';
265
+ const cveId = item.cve_id ?? '';
266
+
267
+ const affectedPackages = extractAffectedPackages(item);
268
+ const references = extractReferences(item);
269
+
270
+ return {
271
+ id: ghsaId,
272
+ source: 'github',
273
+ severity,
274
+ summary,
275
+ description,
276
+ published,
277
+ modified,
278
+ cveId,
279
+ references,
280
+ affectedPackages,
281
+ };
282
+ }
@@ -22,11 +22,12 @@ import type {
22
22
  GitHubResult,
23
23
  OSVResult,
24
24
  } from './types.ts';
25
- import { REQUEST_DELAY_MS } from '../constants.ts';
26
- import { searchNVD } from './nvd.ts';
25
+ import { REQUEST_DELAY_MS_NVD, REQUEST_DELAY_MS_OTHER } from '../constants.ts';
26
+ import { searchNVD } from './nvd-client.ts';
27
27
  import { searchCisaKev } from './cisa-kev.ts';
28
28
  import { searchGitHubAdvisories } from './github-advisories.ts';
29
- import { searchOSV } from './osv.ts';
29
+ import { searchOSV } from './osv-client.ts';
30
+ import { safeUnref } from '../utils/safe-unref.ts';
30
31
 
31
32
  // ============================================================================
32
33
  // Type Definitions
@@ -50,9 +51,6 @@ interface DatabaseInfoEntry {
50
51
  // Helper Functions
51
52
  // ============================================================================
52
53
 
53
- /**
54
- * Type guard to check if a value is a valid Severity
55
- */
56
54
  function isValidSeverity(value: unknown): value is Severity {
57
55
  return (
58
56
  typeof value === 'string' &&
@@ -60,64 +58,44 @@ function isValidSeverity(value: unknown): value is Severity {
60
58
  );
61
59
  }
62
60
 
63
- /**
64
- * Safely extract severity from params, returning undefined if invalid
65
- */
66
61
  function getSeverityParam(params: SecuritySearchParams): Severity | undefined {
67
- if (params.severity === undefined) {
68
- return undefined;
69
- }
62
+ if (params.severity === undefined) return undefined;
70
63
  return isValidSeverity(params.severity) ? params.severity : undefined;
71
64
  }
72
65
 
73
- /**
74
- * Safely extract error message from unknown error
75
- */
76
66
  function getErrorMessage(error: unknown): string {
77
- if (error instanceof Error) {
78
- return error.message;
79
- }
67
+ if (error instanceof Error) return error.message;
80
68
  return String(error);
81
69
  }
82
70
 
83
71
  // ============================================================================
84
- // Security Searcher Class (with dependency injection)
72
+ // Security Searcher Class
85
73
  // ============================================================================
86
74
 
87
- /**
88
- * Configuration for SecuritySearcher
89
- */
90
75
  export interface SecuritySearcherConfig {
91
76
  readonly nvdClient?: INVDClient;
92
77
  readonly cisaKevClient?: ICisaKevClient;
93
78
  readonly githubAdvisoriesClient?: IGitHubAdvisoriesClient;
94
79
  readonly osvClient?: IOSVClient;
95
- readonly requestDelay?: number; // milliseconds between requests
80
+ readonly requestDelay?: number; // Override for testing
96
81
  }
97
82
 
98
- const DEFAULT_CONFIG: Required<SecuritySearcherConfig> = {
99
- nvdClient: null as unknown as INVDClient,
100
- cisaKevClient: null as unknown as ICisaKevClient,
101
- githubAdvisoriesClient: null as unknown as IGitHubAdvisoriesClient,
102
- osvClient: null as unknown as IOSVClient,
103
- requestDelay: REQUEST_DELAY_MS,
83
+ const DEFAULT_CONFIG: SecuritySearcherConfig = {
84
+ nvdClient: undefined,
85
+ cisaKevClient: undefined,
86
+ githubAdvisoriesClient: undefined,
87
+ osvClient: undefined,
88
+ requestDelay: -1, // Use database-specific logic by default
104
89
  };
105
90
 
106
- /**
107
- * SecuritySearcher - orchestrates searches across multiple security databases
108
- * Uses dependency injection for testability
109
- */
110
91
  export class SecuritySearcher {
111
- private readonly config: Required<SecuritySearcherConfig>;
92
+ private readonly config: SecuritySearcherConfig;
112
93
 
113
94
  constructor(config: Partial<SecuritySearcherConfig> = {}) {
114
95
  this.config = { ...DEFAULT_CONFIG, ...config };
115
96
  }
116
97
 
117
- /**
118
- * Main search function that queries multiple security databases
119
- */
120
- async search(params: SecuritySearchParams): Promise<SecuritySearchResult> {
98
+ async search(params: SecuritySearchParams, signal?: AbortSignal): Promise<SecuritySearchResult> {
121
99
  const startTime = Date.now();
122
100
  const results: { nvd?: NVDResult; cisa_kev?: CisaKevResult; github?: GitHubResult; osv?: OSVResult } = {};
123
101
  const errors: string[] = [];
@@ -125,24 +103,17 @@ export class SecuritySearcher {
125
103
 
126
104
  const searchPromises: Promise<void>[] = [];
127
105
 
128
- // Search each requested database in parallel
129
106
  if (params.databases.includes('nvd')) {
130
107
  searchPromises.push((async () => {
131
108
  try {
132
- const severity = getSeverityParam(params);
133
- const nvdResult = await this.searchNVD(
134
- params.terms,
135
- {
136
- severity: severity,
109
+ const nvdResult = await this.searchNVD(params.terms, {
110
+ severity: getSeverityParam(params),
137
111
  maxResults: params.maxResults,
138
112
  includeExploited: params.includeExploited,
139
- },
140
- );
113
+ signal,
114
+ });
141
115
  results.nvd = nvdResult;
142
116
  totalVulnerabilities += nvdResult.count;
143
- if (nvdResult.error !== undefined) {
144
- errors.push(`NVD: ${nvdResult.error}`);
145
- }
146
117
  } catch (err: unknown) {
147
118
  errors.push(`NVD: ${getErrorMessage(err)}`);
148
119
  }
@@ -152,14 +123,9 @@ export class SecuritySearcher {
152
123
  if (params.databases.includes('cisa_kev')) {
153
124
  searchPromises.push((async () => {
154
125
  try {
155
- const cisaResult = await this.searchCisaKev(params.terms, {
156
- maxResults: params.maxResults,
157
- });
126
+ const cisaResult = await this.searchCisaKev(params.terms, { maxResults: params.maxResults, signal });
158
127
  results.cisa_kev = cisaResult;
159
128
  totalVulnerabilities += cisaResult.count;
160
- if (cisaResult.error !== undefined) {
161
- errors.push(`CISA KEV: ${cisaResult.error}`);
162
- }
163
129
  } catch (err: unknown) {
164
130
  errors.push(`CISA KEV: ${getErrorMessage(err)}`);
165
131
  }
@@ -174,12 +140,10 @@ export class SecuritySearcher {
174
140
  severity: params.severity,
175
141
  maxResults: params.maxResults,
176
142
  repo: params.githubRepo,
143
+ signal,
177
144
  });
178
145
  results.github = githubResult;
179
146
  totalVulnerabilities += githubResult.count;
180
- if (githubResult.error !== undefined) {
181
- errors.push(`GitHub: ${githubResult.error}`);
182
- }
183
147
  } catch (err: unknown) {
184
148
  errors.push(`GitHub: ${getErrorMessage(err)}`);
185
149
  }
@@ -193,36 +157,45 @@ export class SecuritySearcher {
193
157
  ecosystem: params.ecosystem,
194
158
  severity: params.severity,
195
159
  maxResults: params.maxResults,
160
+ signal,
196
161
  });
197
162
  results.osv = osvResult;
198
163
  totalVulnerabilities += osvResult.count;
199
- if (osvResult.error !== undefined) {
200
- errors.push(`OSV: ${osvResult.error}`);
201
- }
202
164
  } catch (err: unknown) {
203
165
  errors.push(`OSV: ${getErrorMessage(err)}`);
204
166
  }
205
167
  })());
206
168
  }
207
169
 
208
- // Wait for all searches to complete
209
170
  await Promise.all(searchPromises);
210
171
 
211
- // Apply delay to avoid rate limiting for subsequent calls
212
- if (this.config.requestDelay > 0) {
172
+ // Apply delay
173
+ let delay = this.config.requestDelay ?? -1;
174
+ if (delay === -1) {
175
+ delay = params.databases.includes('nvd') ? REQUEST_DELAY_MS_NVD : REQUEST_DELAY_MS_OTHER;
176
+ }
177
+
178
+ if (delay > 0) {
179
+ // Abortable: a caller cancel must not sit through the post-sweep delay.
213
180
  await new Promise<void>((resolve) => {
214
- const timeoutId = setTimeout(resolve, this.config.requestDelay);
215
- timeoutId.unref();
181
+ const timeoutId = setTimeout(() => {
182
+ signal?.removeEventListener('abort', onAbort);
183
+ resolve();
184
+ }, delay);
185
+ safeUnref(timeoutId);
186
+ const onAbort = () => { clearTimeout(timeoutId); resolve(); };
187
+ if (signal) signal.addEventListener('abort', onAbort, { once: true });
216
188
  });
217
189
  }
218
190
 
219
- const duration = Date.now() - startTime;
220
-
221
191
  return {
222
192
  results,
223
193
  totalDatabases: Object.keys(results).length,
224
194
  totalVulnerabilities,
225
- duration,
195
+ duration: Date.now() - startTime,
196
+ // Surface databases that threw (vs returned an error field) so a dropped source is visible
197
+ // rather than silently omitted and mistaken for a clean "no results" from that database.
198
+ ...(errors.length > 0 ? { errors } : {}),
226
199
  };
227
200
  }
228
201
 
@@ -248,18 +221,13 @@ export class SecuritySearcher {
248
221
  }
249
222
 
250
223
  // ============================================================================
251
- // Default Client Implementations (wrappers for existing functions)
224
+ // Default Client Implementations
252
225
  // ============================================================================
253
226
 
254
227
  class DefaultNVDClient implements INVDClient {
255
- async search(terms: readonly string[], options?: NVDSearchOptions): Promise<import('./types.ts').NVDResult> {
228
+ async search(terms: readonly string[], options?: NVDSearchOptions): Promise<NVDResult> {
256
229
  return searchNVD(terms as string[], options);
257
230
  }
258
-
259
- async getById(cveId: string): Promise<import('./types.ts').Vulnerability | null> {
260
- const { getCVEById } = await import('./nvd.ts');
261
- return getCVEById(cveId);
262
- }
263
231
  }
264
232
 
265
233
  class DefaultCisaKevClient implements ICisaKevClient {
@@ -269,138 +237,56 @@ class DefaultCisaKevClient implements ICisaKevClient {
269
237
  }
270
238
 
271
239
  class DefaultGitHubClient implements IGitHubAdvisoriesClient {
272
- async search(terms: readonly string[], options?: GitHubSearchOptions): Promise<import('./types.ts').GitHubResult> {
240
+ async search(terms: readonly string[], options?: GitHubSearchOptions): Promise<GitHubResult> {
273
241
  return searchGitHubAdvisories(terms as string[], options);
274
242
  }
275
-
276
- async getById(id: string): Promise<import('./types.ts').Advisory | null> {
277
- const { getAdvisoryById } = await import('./github-advisories.ts');
278
- return getAdvisoryById(id);
279
- }
280
243
  }
281
244
 
282
245
  class DefaultOSVClient implements IOSVClient {
283
- async search(terms: readonly string[], options?: OSVSearchOptions): Promise<import('./types.ts').OSVResult> {
246
+ async search(terms: readonly string[], options?: OSVSearchOptions): Promise<OSVResult> {
284
247
  return searchOSV(terms as string[], options);
285
248
  }
286
-
287
- async getById(osvId: string): Promise<import('./types.ts').Vulnerability | null> {
288
- const { getOSVById } = await import('./osv.ts');
289
- return getOSVById(osvId);
290
- }
291
- }
292
-
293
- // Default client factories
294
- function createDefaultNVDClient(): INVDClient {
295
- return new DefaultNVDClient();
296
- }
297
-
298
- function createDefaultCisaKevClient(): ICisaKevClient {
299
- return new DefaultCisaKevClient();
300
249
  }
301
250
 
302
- function createDefaultGitHubClient(): IGitHubAdvisoriesClient {
303
- return new DefaultGitHubClient();
304
- }
305
-
306
- function createDefaultOSVClient(): IOSVClient {
307
- return new DefaultOSVClient();
308
- }
251
+ function createDefaultNVDClient(): INVDClient { return new DefaultNVDClient(); }
252
+ function createDefaultCisaKevClient(): ICisaKevClient { return new DefaultCisaKevClient(); }
253
+ function createDefaultGitHubClient(): IGitHubAdvisoriesClient { return new DefaultGitHubClient(); }
254
+ function createDefaultOSVClient(): IOSVClient { return new DefaultOSVClient(); }
309
255
 
310
256
  // ============================================================================
311
- // Global State for Backward Compatibility
257
+ // Global State
312
258
  // ============================================================================
313
259
 
314
260
  let globalSearcher: SecuritySearcher | null = null;
315
261
 
316
- /**
317
- * Create a new security searcher instance
318
- */
319
- export function createSecuritySearcher(
320
- config: Partial<SecuritySearcherConfig> = {}
321
- ): SecuritySearcher {
262
+ export function createSecuritySearcher(config: Partial<SecuritySearcherConfig> = {}): SecuritySearcher {
322
263
  return new SecuritySearcher(config);
323
264
  }
324
265
 
325
- /**
326
- * Get the global security searcher instance
327
- */
328
266
  export function getSecuritySearcher(): SecuritySearcher {
329
- if (!globalSearcher) {
330
- globalSearcher = new SecuritySearcher();
331
- }
267
+ if (!globalSearcher) globalSearcher = new SecuritySearcher();
332
268
  return globalSearcher;
333
269
  }
334
270
 
335
- /**
336
- * Set the global security searcher instance (for testing)
337
- */
338
- export function setSecuritySearcher(searcher: SecuritySearcher | null): void {
339
- globalSearcher = searcher;
340
- }
341
-
342
- /**
343
- * Reset the global security searcher instance (for testing)
344
- */
345
- export function resetSecuritySearcher(): void {
346
- globalSearcher = null;
347
- }
348
-
349
- // ============================================================================
350
- // Backward Compatibility Functions
351
- // ============================================================================
271
+ export function setSecuritySearcher(searcher: SecuritySearcher | null): void { globalSearcher = searcher; }
272
+ export function resetSecuritySearcher(): void { globalSearcher = null; }
352
273
 
353
- /**
354
- * Main search function that queries multiple security databases
355
- * @deprecated Use SecuritySearcher class directly for better testability
356
- */
357
- export async function searchSecurityDatabases(
358
- params: SecuritySearchParams,
359
- ): Promise<SecuritySearchResult> {
360
- const searcher = getSecuritySearcher();
361
- return searcher.search(params);
274
+ export async function searchSecurityDatabases(params: SecuritySearchParams, signal?: AbortSignal): Promise<SecuritySearchResult> {
275
+ return getSecuritySearcher().search(params, signal);
362
276
  }
363
277
 
364
- /**
365
- * Get database info for UI
366
- */
367
278
  export function getDatabaseInfo(): DatabaseInfo {
368
279
  return {
369
280
  databases: [
370
- {
371
- id: 'nvd',
372
- name: 'NIST NVD',
373
- description: 'National Vulnerability Database - 340,000+ CVE records',
374
- url: 'https://nvd.nist.gov',
375
- free: true,
376
- },
377
- {
378
- id: 'cisa_kev',
379
- name: 'CISA KEV Catalog',
380
- description: 'Known Exploited Vulnerabilities - actively exploited vulnerabilities',
381
- url: 'https://www.cisa.gov/known-exploited-vulnerabilities-catalog',
382
- free: true,
383
- },
384
- {
385
- id: 'github',
386
- name: 'GitHub Advisory Database',
387
- description: 'GitHub Security Advisories - open source security advisories',
388
- url: 'https://github.com/advisories',
389
- free: true,
390
- },
391
- {
392
- id: 'osv',
393
- name: 'Open Source Vulnerabilities',
394
- description: 'OSV - open source package vulnerabilities',
395
- url: 'https://osv.dev',
396
- free: true,
397
- },
281
+ { id: 'nvd', name: 'NIST NVD', description: 'National Vulnerability Database', url: 'https://nvd.nist.gov', free: true },
282
+ { id: 'cisa_kev', name: 'CISA KEV Catalog', description: 'Known Exploited Vulnerabilities', url: 'https://www.cisa.gov', free: true },
283
+ { id: 'github', name: 'GitHub Advisory Database', description: 'GitHub Security Advisories', url: 'https://github.com/advisories', free: true },
284
+ { id: 'osv', name: 'Open Source Vulnerabilities', description: 'OSV - open source package vulnerabilities', url: 'https://osv.dev', free: true },
398
285
  ] as const,
399
286
  };
400
287
  }
401
288
 
402
- // Export individual database search functions for direct use
403
- export { searchNVD } from './nvd.ts';
289
+ export { searchNVD } from './nvd-client.ts';
404
290
  export { searchCisaKev } from './cisa-kev.ts';
405
291
  export { searchGitHubAdvisories } from './github-advisories.ts';
406
- export { searchOSV } from './osv.ts';
292
+ export { searchOSV } from './osv-client.ts';