@lincoln504/pi-research 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/config/searxng/default-settings.yml +189 -0
- package/config/searxng/limiter.toml +34 -0
- package/config/tooling/eslint.config.js +73 -0
- package/config/tooling/vitest.config.integration.ts +17 -0
- package/config/tooling/vitest.config.ts +40 -0
- package/config/tooling/vitest.config.unit.ts +12 -0
- package/docs/ARCHITECTURE.md +109 -0
- package/docs/assets/README-banner.jpg +0 -0
- package/package.json +92 -0
- package/scripts/setup.js +174 -0
- package/src/config.ts +206 -0
- package/src/constants.ts +148 -0
- package/src/healthcheck/index.ts +309 -0
- package/src/index.ts +71 -0
- package/src/infrastructure/searxng-lifecycle.ts +577 -0
- package/src/infrastructure/searxng-manager.ts +1173 -0
- package/src/infrastructure/state-manager.ts +918 -0
- package/src/logger.ts +246 -0
- package/src/orchestration/deep-research-orchestrator.ts +848 -0
- package/src/orchestration/deep-research-reducer.ts +129 -0
- package/src/orchestration/deep-research-types.ts +54 -0
- package/src/orchestration/id-utils.ts +63 -0
- package/src/orchestration/researcher.ts +104 -0
- package/src/orchestration/session-context.ts +43 -0
- package/src/prompts/research-tool-usage.md +45 -0
- package/src/prompts/researcher.md +166 -0
- package/src/prompts/system-coordinator.md +24 -0
- package/src/prompts/system-lead-evaluator.md +225 -0
- package/src/security/cisa-kev.ts +242 -0
- package/src/security/github-advisories.ts +607 -0
- package/src/security/index.ts +406 -0
- package/src/security/nvd.ts +532 -0
- package/src/security/osv.ts +487 -0
- package/src/security/types.ts +138 -0
- package/src/stackexchange/cache.ts +111 -0
- package/src/stackexchange/index.ts +352 -0
- package/src/stackexchange/output/compact.ts +64 -0
- package/src/stackexchange/output/index.ts +7 -0
- package/src/stackexchange/output/json.ts +21 -0
- package/src/stackexchange/output/table.ts +123 -0
- package/src/stackexchange/queries.ts +168 -0
- package/src/stackexchange/rest-client.ts +127 -0
- package/src/stackexchange/types.ts +129 -0
- package/src/tool.ts +430 -0
- package/src/tools/grep.ts +335 -0
- package/src/tools/index.ts +77 -0
- package/src/tools/scrape.ts +451 -0
- package/src/tools/search.ts +188 -0
- package/src/tools/security.ts +292 -0
- package/src/tools/stackexchange.ts +101 -0
- package/src/tui/research-panel.ts +576 -0
- package/src/tui/searxng-status.ts +80 -0
- package/src/types/extension-context.ts +60 -0
- package/src/types/llm.ts +111 -0
- package/src/utils/inject-date.ts +29 -0
- package/src/utils/input-validation.ts +103 -0
- package/src/utils/json-utils.ts +262 -0
- package/src/utils/make-resource-loader.ts +43 -0
- package/src/utils/research-error.ts +205 -0
- package/src/utils/research-export.ts +199 -0
- package/src/utils/searxng-config.ts +94 -0
- package/src/utils/session-state.ts +295 -0
- package/src/utils/shared-links.ts +123 -0
- package/src/utils/shutdown-manager.ts +54 -0
- package/src/utils/text-utils.ts +83 -0
- package/src/utils/tool-usage-tracker.ts +135 -0
- package/src/web-research/dependencies.ts +20 -0
- package/src/web-research/retry-utils.ts +232 -0
- package/src/web-research/scrapers.ts +935 -0
- package/src/web-research/search.ts +235 -0
- package/src/web-research/types.ts +47 -0
- package/src/web-research/utils.ts +158 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearXNG Lifecycle Manager
|
|
3
|
+
*
|
|
4
|
+
* Refactored to use dependency injection for testability.
|
|
5
|
+
* Maintains backward compatibility with original API.
|
|
6
|
+
*
|
|
7
|
+
* All agents (coordinator + researchers) share same container.
|
|
8
|
+
* Status states: 'starting_up' | 'active' | 'inactive' | 'error'
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle: Container lives for duration of pi process.
|
|
11
|
+
* Cleanup is triggered via pi lifecycle hooks rather than process signal ownership.
|
|
12
|
+
* Use shutdownLifecycle() for extension-owned cleanup paths.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
16
|
+
import {
|
|
17
|
+
DOCKER_HOST_INTERNAL_HOSTNAME,
|
|
18
|
+
DockerSearxngManager,
|
|
19
|
+
type SearxngManagerConfig,
|
|
20
|
+
verifyDockerInstalled,
|
|
21
|
+
} from './searxng-manager.ts';
|
|
22
|
+
import { logger, type ILogger } from '../logger.ts';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as os from 'node:os';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { PROXY_URL, BRAVE_SEARCH_API_KEY } from '../config.ts';
|
|
28
|
+
import { shutdownManager } from '../utils/shutdown-manager.ts';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extension directory for pi-research
|
|
32
|
+
*/
|
|
33
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
34
|
+
const __dirname = path.dirname(__filename);
|
|
35
|
+
|
|
36
|
+
function hasExtensionAssets(dir: string): boolean {
|
|
37
|
+
return (
|
|
38
|
+
fs.existsSync(path.join(dir, 'config', 'searxng', 'default-settings.yml')) &&
|
|
39
|
+
fs.existsSync(path.join(dir, 'config', 'searxng', 'limiter.toml'))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveExtensionDir(): string {
|
|
44
|
+
const envOverride = process.env['PI_RESEARCH_EXTENSION_DIR']?.trim();
|
|
45
|
+
if (envOverride && hasExtensionAssets(envOverride)) {
|
|
46
|
+
return envOverride;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const installedDir = path.join(
|
|
50
|
+
os.homedir(),
|
|
51
|
+
'.pi',
|
|
52
|
+
'agent',
|
|
53
|
+
'extensions',
|
|
54
|
+
'pi-research',
|
|
55
|
+
);
|
|
56
|
+
if (hasExtensionAssets(installedDir)) {
|
|
57
|
+
return installedDir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const repoDir = path.resolve(__dirname, '..', '..');
|
|
61
|
+
if (hasExtensionAssets(repoDir)) {
|
|
62
|
+
return repoDir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return installedDir;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const EXTENSION_DIR = resolveExtensionDir();
|
|
69
|
+
const LOCALHOST_PROXY_HOST_PATTERN = /^([a-z][a-z0-9+.-]*:\/\/(?:[^@/?#]*@)?)(localhost|127\.0\.0\.1)(?=[:/?#]|$)/i;
|
|
70
|
+
|
|
71
|
+
export function rewriteLocalhostProxyForContainer(proxyUrl: string): string {
|
|
72
|
+
return proxyUrl.replace(
|
|
73
|
+
LOCALHOST_PROXY_HOST_PATTERN,
|
|
74
|
+
`$1${DOCKER_HOST_INTERNAL_HOSTNAME}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Track status for TUI display (minimal: starting_up | active | inactive | error)
|
|
80
|
+
*/
|
|
81
|
+
export type SearxngLifecycleStatus = {
|
|
82
|
+
state: 'starting_up' | 'active' | 'inactive' | 'error';
|
|
83
|
+
url: string;
|
|
84
|
+
isFunctional: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Status callback type
|
|
89
|
+
*/
|
|
90
|
+
type StatusCallback = (_status: SearxngLifecycleStatus) => void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Searxng Lifecycle Manager Configuration
|
|
94
|
+
*/
|
|
95
|
+
export interface SearxngLifecycleConfig {
|
|
96
|
+
extensionDir?: string;
|
|
97
|
+
logger?: ILogger;
|
|
98
|
+
manager?: DockerSearxngManager | null;
|
|
99
|
+
proxyUrl?: string;
|
|
100
|
+
braveApiKey?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type ResolvedConfig = {
|
|
104
|
+
extensionDir: string;
|
|
105
|
+
logger: ILogger;
|
|
106
|
+
manager: DockerSearxngManager | null;
|
|
107
|
+
proxyUrl: string | undefined;
|
|
108
|
+
braveApiKey: string | undefined;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Manager interface for dependency injection
|
|
113
|
+
*/
|
|
114
|
+
export interface ISearxngLifecycleManager {
|
|
115
|
+
init(ctx: ExtensionContext): Promise<void>;
|
|
116
|
+
ensureRunning(): Promise<string>;
|
|
117
|
+
getManager(): DockerSearxngManager | null;
|
|
118
|
+
getStatus(): SearxngLifecycleStatus;
|
|
119
|
+
setFunctional(ok: boolean): void;
|
|
120
|
+
isFunctional(): boolean;
|
|
121
|
+
onStatusChange(callback: StatusCallback): () => void;
|
|
122
|
+
shutdown(): Promise<void>;
|
|
123
|
+
isInitialized(): boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Default configuration
|
|
128
|
+
*/
|
|
129
|
+
const DEFAULT_CONFIG: ResolvedConfig = {
|
|
130
|
+
extensionDir: EXTENSION_DIR,
|
|
131
|
+
logger,
|
|
132
|
+
manager: null,
|
|
133
|
+
proxyUrl: PROXY_URL,
|
|
134
|
+
braveApiKey: BRAVE_SEARCH_API_KEY,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Searxng Lifecycle Manager Class
|
|
139
|
+
*
|
|
140
|
+
* Manages the lifecycle of the SearXNG container.
|
|
141
|
+
* Uses dependency injection for testability.
|
|
142
|
+
*/
|
|
143
|
+
export class SearxngLifecycleManager implements ISearxngLifecycleManager {
|
|
144
|
+
private manager: DockerSearxngManager | null = null;
|
|
145
|
+
private sessionId: string | null = null;
|
|
146
|
+
private initialized = false;
|
|
147
|
+
private currentStatus: SearxngLifecycleStatus = {
|
|
148
|
+
state: 'inactive',
|
|
149
|
+
url: '',
|
|
150
|
+
isFunctional: false,
|
|
151
|
+
};
|
|
152
|
+
private statusCallbacks: StatusCallback[] = [];
|
|
153
|
+
private readonly config: ResolvedConfig;
|
|
154
|
+
private proxySettingsPath: string | null = null;
|
|
155
|
+
|
|
156
|
+
constructor(config: Partial<SearxngLifecycleConfig> = {}) {
|
|
157
|
+
this.config = { ...DEFAULT_CONFIG, ...config } as ResolvedConfig;
|
|
158
|
+
// Set manager immediately if provided (for testing)
|
|
159
|
+
if (this.config.manager) {
|
|
160
|
+
this.manager = this.config.manager;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get manager instance (for passing to web-research module)
|
|
166
|
+
*/
|
|
167
|
+
getManager(): DockerSearxngManager | null {
|
|
168
|
+
return this.manager;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setFunctional(ok: boolean): void {
|
|
172
|
+
this.currentStatus.isFunctional = ok;
|
|
173
|
+
this.notifyStatusChange();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
isFunctional(): boolean {
|
|
177
|
+
return this.currentStatus.isFunctional;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Subscribe to status changes
|
|
182
|
+
*/
|
|
183
|
+
onStatusChange(callback: StatusCallback): () => void {
|
|
184
|
+
this.statusCallbacks.push(callback);
|
|
185
|
+
// Immediately call with current status (handle errors gracefully)
|
|
186
|
+
try {
|
|
187
|
+
callback(this.currentStatus);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// Log errors but don't fail registration
|
|
190
|
+
this.config.logger.error('[searxng-lifecycle] Error in status callback:', error);
|
|
191
|
+
}
|
|
192
|
+
// Return unsubscribe function
|
|
193
|
+
return () => {
|
|
194
|
+
const index = this.statusCallbacks.indexOf(callback);
|
|
195
|
+
if (index !== -1) {
|
|
196
|
+
this.statusCallbacks.splice(index, 1);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private notifyStatusChange(): void {
|
|
202
|
+
for (const callback of this.statusCallbacks) {
|
|
203
|
+
try {
|
|
204
|
+
callback(this.currentStatus);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// Log errors but don't fail the entire notification
|
|
207
|
+
this.config.logger.error('[searxng-lifecycle] Error in status callback:', error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private extractSessionId(ctx: ExtensionContext): string {
|
|
213
|
+
if ('sessionId' in ctx && typeof ctx.sessionId === 'string') {
|
|
214
|
+
return ctx.sessionId;
|
|
215
|
+
}
|
|
216
|
+
// Fallback: generate unique ID
|
|
217
|
+
return `pi-research-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate a runtime settings file with optional proxy and/or Brave Search API key injected.
|
|
222
|
+
* Localhost proxy addresses are rewritten to Docker's host alias automatically.
|
|
223
|
+
*/
|
|
224
|
+
private async generateRuntimeSettings(proxyUrl?: string, braveApiKey?: string): Promise<string> {
|
|
225
|
+
const defaultSettingsPath = path.join(
|
|
226
|
+
this.config.extensionDir,
|
|
227
|
+
'config',
|
|
228
|
+
'default-settings.yml'
|
|
229
|
+
);
|
|
230
|
+
const uniqueId = this.sessionId?.substring(0, 8) || Math.random().toString(36).substring(2, 8);
|
|
231
|
+
const runtimeSettingsPath = path.join(
|
|
232
|
+
this.config.extensionDir,
|
|
233
|
+
'config',
|
|
234
|
+
`runtime-settings-${uniqueId}.yml`
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const defaultSettings = await fs.promises.readFile(defaultSettingsPath, 'utf-8');
|
|
239
|
+
const yaml = await import('js-yaml');
|
|
240
|
+
const settings = yaml.load(defaultSettings) as any;
|
|
241
|
+
|
|
242
|
+
if (proxyUrl) {
|
|
243
|
+
const containerProxyUrl = rewriteLocalhostProxyForContainer(proxyUrl);
|
|
244
|
+
if (containerProxyUrl !== proxyUrl) {
|
|
245
|
+
logger.log(`[pi-research] Converting localhost proxy to Docker host alias: ${proxyUrl} -> ${containerProxyUrl}`);
|
|
246
|
+
}
|
|
247
|
+
settings.outgoing = settings.outgoing || {};
|
|
248
|
+
settings.outgoing.proxies = { 'all://': [containerProxyUrl] };
|
|
249
|
+
settings.outgoing.extra_proxy_timeout = 10;
|
|
250
|
+
logger.log(`[pi-research] Runtime settings: proxy configured (${containerProxyUrl})`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (braveApiKey) {
|
|
254
|
+
settings.engines = settings.engines || [];
|
|
255
|
+
settings.engines.push({
|
|
256
|
+
name: 'braveapi',
|
|
257
|
+
engine: 'braveapi',
|
|
258
|
+
api_key: braveApiKey,
|
|
259
|
+
shortcut: 'bapi',
|
|
260
|
+
weight: 1.2,
|
|
261
|
+
inactive: false,
|
|
262
|
+
});
|
|
263
|
+
logger.log('[pi-research] Runtime settings: braveapi engine enabled');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await fs.promises.writeFile(runtimeSettingsPath, yaml.dump(settings), 'utf-8');
|
|
267
|
+
this.proxySettingsPath = runtimeSettingsPath;
|
|
268
|
+
|
|
269
|
+
return runtimeSettingsPath;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger.error('[pi-research] Failed to generate runtime settings:', error);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async init(ctx: ExtensionContext): Promise<void> {
|
|
277
|
+
if (this.initialized) {
|
|
278
|
+
logger.log('[pi-research] SearXNG already initialized, skipping');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Skip real Docker verification when a test/dummy manager is injected.
|
|
283
|
+
if (!this.config.manager) {
|
|
284
|
+
const dockerCheck = await verifyDockerInstalled();
|
|
285
|
+
if (!dockerCheck.running) {
|
|
286
|
+
this.currentStatus = {
|
|
287
|
+
state: 'error',
|
|
288
|
+
url: '',
|
|
289
|
+
isFunctional: false,
|
|
290
|
+
};
|
|
291
|
+
this.notifyStatusChange();
|
|
292
|
+
const errorMsg = `Docker health check failed: ${dockerCheck.error}`;
|
|
293
|
+
logger.error(`[pi-research] ${errorMsg}`);
|
|
294
|
+
throw new Error(errorMsg);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Set state to starting_up
|
|
299
|
+
this.currentStatus = {
|
|
300
|
+
state: 'starting_up',
|
|
301
|
+
url: '',
|
|
302
|
+
isFunctional: false,
|
|
303
|
+
};
|
|
304
|
+
this.notifyStatusChange();
|
|
305
|
+
|
|
306
|
+
// Use injected manager or create new one
|
|
307
|
+
if (this.config.manager) {
|
|
308
|
+
this.manager = this.config.manager;
|
|
309
|
+
} else {
|
|
310
|
+
// Clean up old manager if exists
|
|
311
|
+
if (this.manager !== null) {
|
|
312
|
+
try {
|
|
313
|
+
this.manager.stopHeartbeat();
|
|
314
|
+
if (this.sessionId !== null) {
|
|
315
|
+
await this.manager.release(this.sessionId).catch(() => {});
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
this.config.logger.warn('[pi-research] Error cleaning up old manager:', error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Generate runtime settings if proxy or Brave API key is configured
|
|
323
|
+
let settingsPath: string;
|
|
324
|
+
if (this.config.proxyUrl || this.config.braveApiKey) {
|
|
325
|
+
try {
|
|
326
|
+
if (this.config.proxyUrl) logger.log('[pi-research] Proxy configured, generating runtime settings...');
|
|
327
|
+
if (this.config.braveApiKey) logger.log('[pi-research] BRAVE_SEARCH_API_KEY set, enabling braveapi engine...');
|
|
328
|
+
settingsPath = await this.generateRuntimeSettings(this.config.proxyUrl, this.config.braveApiKey);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
331
|
+
logger.error('[pi-research] Failed to generate runtime settings:', errorMsg);
|
|
332
|
+
const configError = new Error(
|
|
333
|
+
`Runtime settings generation failed: ${errorMsg}\n\n` +
|
|
334
|
+
`To fix:\n` +
|
|
335
|
+
`1. Check PROXY_URL format: socks5://host:port or http://host:port\n` +
|
|
336
|
+
`2. Check BRAVE_SEARCH_API_KEY is a valid key\n` +
|
|
337
|
+
`3. Unset these env vars to use default settings`
|
|
338
|
+
);
|
|
339
|
+
configError.cause = error;
|
|
340
|
+
throw configError;
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
settingsPath = path.join(
|
|
344
|
+
this.config.extensionDir,
|
|
345
|
+
'config',
|
|
346
|
+
'searxng',
|
|
347
|
+
'default-settings.yml'
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Create new manager instance
|
|
352
|
+
const managerConfig: SearxngManagerConfig = { settingsPath };
|
|
353
|
+
this.manager = new DockerSearxngManager(this.config.extensionDir, managerConfig);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.manager.setContext(ctx);
|
|
357
|
+
|
|
358
|
+
const ctxSessionId = this.extractSessionId(ctx);
|
|
359
|
+
this.sessionId = ctxSessionId;
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
await this.manager.acquire(ctxSessionId);
|
|
363
|
+
this.manager.startHeartbeat();
|
|
364
|
+
|
|
365
|
+
// Get current status
|
|
366
|
+
const searxngStatus = await this.manager.getStatus();
|
|
367
|
+
|
|
368
|
+
// Update internal status
|
|
369
|
+
this.currentStatus = {
|
|
370
|
+
state: 'active',
|
|
371
|
+
url: searxngStatus.url || '',
|
|
372
|
+
isFunctional: false, // Reset on every re-init, must re-verify
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
this.initialized = true;
|
|
376
|
+
|
|
377
|
+
logger.log('[pi-research] SearXNG lifecycle initialized');
|
|
378
|
+
logger.log('[pi-research] SearXNG container:', searxngStatus.url);
|
|
379
|
+
|
|
380
|
+
this.notifyStatusChange();
|
|
381
|
+
|
|
382
|
+
} catch (error) {
|
|
383
|
+
logger.error('[pi-research] Failed to initialize SearXNG:', error);
|
|
384
|
+
|
|
385
|
+
this.currentStatus = {
|
|
386
|
+
state: 'error',
|
|
387
|
+
url: '',
|
|
388
|
+
isFunctional: false,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
this.initialized = true;
|
|
392
|
+
this.notifyStatusChange();
|
|
393
|
+
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async ensureRunning(): Promise<string> {
|
|
399
|
+
if (!this.manager) {
|
|
400
|
+
throw new Error('SearXNG not initialized. Call initLifecycle() first.');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ensureReady waits for container to be healthy
|
|
404
|
+
await this.manager.ensureReady();
|
|
405
|
+
|
|
406
|
+
return this.manager.getSearxngUrl();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
getStatus(): SearxngLifecycleStatus {
|
|
410
|
+
return { ...this.currentStatus };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async shutdown(): Promise<void> {
|
|
414
|
+
if (!this.manager || !this.initialized) {
|
|
415
|
+
logger.log('[pi-research] SearXNG not active, skipping shutdown');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
this.manager.stopHeartbeat();
|
|
421
|
+
|
|
422
|
+
if (this.sessionId !== null) {
|
|
423
|
+
await this.manager.release(this.sessionId);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Cleanup temporary proxy settings file if it exists
|
|
427
|
+
if (this.proxySettingsPath && fs.existsSync(this.proxySettingsPath)) {
|
|
428
|
+
try {
|
|
429
|
+
await fs.promises.unlink(this.proxySettingsPath);
|
|
430
|
+
logger.log(`[pi-research] Deleted temporary proxy settings: ${this.proxySettingsPath}`);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.warn('[pi-research] Failed to delete temporary proxy settings:', error);
|
|
433
|
+
}
|
|
434
|
+
this.proxySettingsPath = null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
this.currentStatus = {
|
|
438
|
+
state: 'inactive',
|
|
439
|
+
url: '',
|
|
440
|
+
isFunctional: false,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
this.initialized = false;
|
|
444
|
+
|
|
445
|
+
logger.log('[pi-research] SearXNG lifecycle shut down');
|
|
446
|
+
this.notifyStatusChange();
|
|
447
|
+
|
|
448
|
+
} catch (error) {
|
|
449
|
+
logger.error('[pi-research] Error during shutdown:', error);
|
|
450
|
+
} finally {
|
|
451
|
+
this.manager = null;
|
|
452
|
+
this.sessionId = null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
isInitialized(): boolean {
|
|
457
|
+
return this.initialized && this.manager !== null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// GLOBAL STATE FOR BACKWARD COMPATIBILITY
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
let globalManager: SearxngLifecycleManager | null = null;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Create a new lifecycle manager instance
|
|
469
|
+
*/
|
|
470
|
+
export function createSearxngLifecycleManager(
|
|
471
|
+
config: Partial<SearxngLifecycleConfig> = {}
|
|
472
|
+
): SearxngLifecycleManager {
|
|
473
|
+
return new SearxngLifecycleManager(config);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get the global lifecycle manager instance
|
|
478
|
+
*/
|
|
479
|
+
export function getLifecycleManager(): SearxngLifecycleManager {
|
|
480
|
+
if (!globalManager) {
|
|
481
|
+
globalManager = new SearxngLifecycleManager();
|
|
482
|
+
}
|
|
483
|
+
return globalManager;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Set the global lifecycle manager instance (for testing)
|
|
488
|
+
*/
|
|
489
|
+
export function setLifecycleManager(manager: SearxngLifecycleManager | null): void {
|
|
490
|
+
globalManager = manager;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Reset the global lifecycle manager instance (for testing)
|
|
495
|
+
*/
|
|
496
|
+
export function resetLifecycleManager(): void {
|
|
497
|
+
globalManager = null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// BACKWARD COMPATIBILITY EXPORTS
|
|
502
|
+
// ============================================================================
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Backward compatible: Get manager instance
|
|
506
|
+
*/
|
|
507
|
+
export function getManager(): DockerSearxngManager | null {
|
|
508
|
+
return getLifecycleManager().getManager();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Backward compatible: Initialize lifecycle
|
|
513
|
+
*/
|
|
514
|
+
export async function initLifecycle(ctx: ExtensionContext): Promise<void> {
|
|
515
|
+
await getLifecycleManager().init(ctx);
|
|
516
|
+
// Register extension-owned cleanup once; repeated initLifecycle() calls are safe.
|
|
517
|
+
shutdownManager.register(shutdownLifecycle);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Backward compatible: Ensure container is running
|
|
522
|
+
*/
|
|
523
|
+
export async function ensureRunning(): Promise<string> {
|
|
524
|
+
return getLifecycleManager().ensureRunning();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Backward compatible: Get status (renamed type to avoid conflict)
|
|
529
|
+
*/
|
|
530
|
+
export function getStatus(): SearxngLifecycleStatus {
|
|
531
|
+
return getLifecycleManager().getStatus();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Backward compatible: Set functional state
|
|
536
|
+
*/
|
|
537
|
+
export function setFunctional(ok: boolean): void {
|
|
538
|
+
getLifecycleManager().setFunctional(ok);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Backward compatible: Get functional state
|
|
543
|
+
*/
|
|
544
|
+
export function isFunctional(): boolean {
|
|
545
|
+
return getLifecycleManager().isFunctional();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Backward compatible: Subscribe to status changes
|
|
550
|
+
*/
|
|
551
|
+
export function onStatusChange(callback: StatusCallback): () => void {
|
|
552
|
+
return getLifecycleManager().onStatusChange(callback);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Backward compatible: Shutdown lifecycle
|
|
557
|
+
*/
|
|
558
|
+
export async function shutdownLifecycle(): Promise<void> {
|
|
559
|
+
await getLifecycleManager().shutdown();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Backward compatible: Check if initialized
|
|
564
|
+
*/
|
|
565
|
+
export function isInitialized(): boolean {
|
|
566
|
+
return getLifecycleManager().isInitialized();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Check if Docker is installed and running
|
|
571
|
+
*/
|
|
572
|
+
export async function checkDockerAvailability(): Promise<{ installed: boolean; running: boolean; error?: string }> {
|
|
573
|
+
return verifyDockerInstalled();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Export original type for compatibility
|
|
577
|
+
export type SearxngStatus = SearxngLifecycleStatus;
|