@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.
- package/LICENSE +1 -1
- package/README.md +55 -138
- package/dist/cli.mjs +23375 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1195 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
package/src/security/osv.ts
DELETED
|
@@ -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,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
|
-
}
|