@lincoln504/pi-research 0.1.13 → 1.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.
Files changed (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -132
  3. package/dist/cli.mjs +23211 -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 +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -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 +1044 -0
  30. package/src/config.ts +1032 -155
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +258 -11
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -1,644 +0,0 @@
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, SEARXNG_URL } 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
- /** External SearXNG base URL. When set, Docker container management is skipped entirely. */
102
- searxngUrl?: string;
103
- }
104
-
105
- type ResolvedConfig = {
106
- extensionDir: string;
107
- logger: ILogger;
108
- manager: DockerSearxngManager | null;
109
- proxyUrl: string | undefined;
110
- braveApiKey: string | undefined;
111
- searxngUrl: string | undefined;
112
- };
113
-
114
- /**
115
- * Manager interface for dependency injection
116
- */
117
- export interface ISearxngLifecycleManager {
118
- init(ctx: ExtensionContext): Promise<void>;
119
- ensureRunning(): Promise<string>;
120
- getManager(): DockerSearxngManager | null;
121
- getStatus(): SearxngLifecycleStatus;
122
- setFunctional(ok: boolean): void;
123
- isFunctional(): boolean;
124
- onStatusChange(callback: StatusCallback): () => void;
125
- shutdown(): Promise<void>;
126
- isInitialized(): boolean;
127
- }
128
-
129
- /**
130
- * Default configuration
131
- */
132
- const DEFAULT_CONFIG: ResolvedConfig = {
133
- extensionDir: EXTENSION_DIR,
134
- logger,
135
- manager: null,
136
- proxyUrl: PROXY_URL,
137
- braveApiKey: BRAVE_SEARCH_API_KEY,
138
- searxngUrl: SEARXNG_URL,
139
- };
140
-
141
- /**
142
- * Searxng Lifecycle Manager Class
143
- *
144
- * Manages the lifecycle of the SearXNG container.
145
- * Uses dependency injection for testability.
146
- */
147
- export class SearxngLifecycleManager implements ISearxngLifecycleManager {
148
- private manager: DockerSearxngManager | null = null;
149
- private sessionId: string | null = null;
150
- private initialized = false;
151
- private currentStatus: SearxngLifecycleStatus = {
152
- state: 'inactive',
153
- url: '',
154
- isFunctional: false,
155
- };
156
- private statusCallbacks: StatusCallback[] = [];
157
- private readonly config: ResolvedConfig;
158
- private proxySettingsPath: string | null = null;
159
- /** Coalesces concurrent init() calls onto a single in-flight promise. */
160
- private initializationPromise: Promise<void> | null = null;
161
- /** Set when SEARXNG_URL is configured; signals that Docker management is bypassed. */
162
- private externalSearxngUrl: string | null = null;
163
-
164
- constructor(config: Partial<SearxngLifecycleConfig> = {}) {
165
- this.config = { ...DEFAULT_CONFIG, ...config } as ResolvedConfig;
166
- // Set manager immediately if provided (for testing)
167
- if (this.config.manager) {
168
- this.manager = this.config.manager;
169
- }
170
- }
171
-
172
- /**
173
- * Get manager instance (for passing to web-research module).
174
- * In external URL mode, returns a minimal shim that satisfies the getSearxngUrl()
175
- * interface used by web-research tools and the health check.
176
- */
177
- getManager(): DockerSearxngManager | null {
178
- if (this.externalSearxngUrl) {
179
- const url = this.externalSearxngUrl;
180
- return { getSearxngUrl: () => url } as unknown as DockerSearxngManager;
181
- }
182
- return this.manager;
183
- }
184
-
185
- setFunctional(ok: boolean): void {
186
- this.currentStatus.isFunctional = ok;
187
- this.notifyStatusChange();
188
- }
189
-
190
- isFunctional(): boolean {
191
- return this.currentStatus.isFunctional;
192
- }
193
-
194
- /**
195
- * Subscribe to status changes
196
- */
197
- onStatusChange(callback: StatusCallback): () => void {
198
- this.statusCallbacks.push(callback);
199
- // Immediately call with current status (handle errors gracefully)
200
- try {
201
- callback(this.currentStatus);
202
- } catch (error) {
203
- // Log errors but don't fail registration
204
- this.config.logger.error('[searxng-lifecycle] Error in status callback:', error);
205
- }
206
- // Return unsubscribe function
207
- return () => {
208
- const index = this.statusCallbacks.indexOf(callback);
209
- if (index !== -1) {
210
- this.statusCallbacks.splice(index, 1);
211
- }
212
- };
213
- }
214
-
215
- private notifyStatusChange(): void {
216
- for (const callback of this.statusCallbacks) {
217
- try {
218
- callback(this.currentStatus);
219
- } catch (error) {
220
- // Log errors but don't fail the entire notification
221
- this.config.logger.error('[searxng-lifecycle] Error in status callback:', error);
222
- }
223
- }
224
- }
225
-
226
- private extractSessionId(ctx: ExtensionContext): string {
227
- if ('sessionId' in ctx && typeof ctx.sessionId === 'string') {
228
- return ctx.sessionId;
229
- }
230
- // Fallback: generate unique ID
231
- return `pi-research-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
232
- }
233
-
234
- /**
235
- * Generate a runtime settings file with optional proxy and/or Brave Search API key injected.
236
- * Localhost proxy addresses are rewritten to Docker's host alias automatically.
237
- */
238
- private async generateRuntimeSettings(proxyUrl?: string, braveApiKey?: string): Promise<string> {
239
- const defaultSettingsPath = path.join(
240
- this.config.extensionDir,
241
- 'config',
242
- 'default-settings.yml'
243
- );
244
- const uniqueId = this.sessionId?.substring(0, 8) || Math.random().toString(36).substring(2, 8);
245
- const runtimeSettingsPath = path.join(
246
- this.config.extensionDir,
247
- 'config',
248
- `runtime-settings-${uniqueId}.yml`
249
- );
250
-
251
- try {
252
- const defaultSettings = await fs.promises.readFile(defaultSettingsPath, 'utf-8');
253
- const yaml = await import('js-yaml');
254
- const settings = yaml.load(defaultSettings) as any;
255
-
256
- if (proxyUrl) {
257
- const containerProxyUrl = rewriteLocalhostProxyForContainer(proxyUrl);
258
- if (containerProxyUrl !== proxyUrl) {
259
- logger.log(`[pi-research] Converting localhost proxy to Docker host alias: ${proxyUrl} -> ${containerProxyUrl}`);
260
- }
261
- settings.outgoing = settings.outgoing || {};
262
- settings.outgoing.proxies = { 'all://': [containerProxyUrl] };
263
- settings.outgoing.extra_proxy_timeout = 10;
264
- logger.log(`[pi-research] Runtime settings: proxy configured (${containerProxyUrl})`);
265
- }
266
-
267
- if (braveApiKey) {
268
- settings.engines = settings.engines || [];
269
- settings.engines.push({
270
- name: 'braveapi',
271
- engine: 'braveapi',
272
- api_key: braveApiKey,
273
- shortcut: 'bapi',
274
- weight: 1.2,
275
- inactive: false,
276
- });
277
- logger.log('[pi-research] Runtime settings: braveapi engine enabled');
278
- }
279
-
280
- await fs.promises.writeFile(runtimeSettingsPath, yaml.dump(settings), 'utf-8');
281
- this.proxySettingsPath = runtimeSettingsPath;
282
-
283
- return runtimeSettingsPath;
284
- } catch (error) {
285
- logger.error('[pi-research] Failed to generate runtime settings:', error);
286
- throw error;
287
- }
288
- }
289
-
290
- async init(ctx: ExtensionContext): Promise<void> {
291
- if (this.initialized) {
292
- logger.log('[pi-research] SearXNG already initialized, skipping');
293
- return;
294
- }
295
-
296
- // Coalesce concurrent init() calls: only the first proceeds; latecomers
297
- // await the in-flight promise and return once it resolves.
298
- // This assignment and check are both synchronous (no await between them),
299
- // so JavaScript's single-threaded event loop guarantees only one caller
300
- // can set initializationPromise before any other caller checks it.
301
- if (this.initializationPromise) {
302
- logger.log('[pi-research] SearXNG init already in progress, awaiting');
303
- await this.initializationPromise;
304
- return;
305
- }
306
-
307
- let resolveInit!: () => void;
308
- let rejectInit!: (err: unknown) => void;
309
- this.initializationPromise = new Promise<void>((resolve, reject) => {
310
- resolveInit = resolve;
311
- rejectInit = reject;
312
- });
313
-
314
- try {
315
- await this._performInit(ctx);
316
- resolveInit();
317
- } catch (err) {
318
- rejectInit(err);
319
- throw err;
320
- } finally {
321
- this.initializationPromise = null;
322
- }
323
- }
324
-
325
- private async _performInit(ctx: ExtensionContext): Promise<void> {
326
- // External SearXNG URL: skip all Docker management and use the URL directly.
327
- if (this.config.searxngUrl) {
328
- this.externalSearxngUrl = this.config.searxngUrl;
329
- this.currentStatus = { state: 'active', url: this.config.searxngUrl, isFunctional: false };
330
- this.initialized = true;
331
- logger.log('[pi-research] External SEARXNG_URL configured — Docker management skipped:', this.config.searxngUrl);
332
- this.notifyStatusChange();
333
- return;
334
- }
335
-
336
- // Skip real Docker verification when a test/dummy manager is injected.
337
- if (!this.config.manager) {
338
- const dockerCheck = await verifyDockerInstalled();
339
- if (!dockerCheck.running) {
340
- this.currentStatus = {
341
- state: 'error',
342
- url: '',
343
- isFunctional: false,
344
- };
345
- this.notifyStatusChange();
346
- const errorMsg = `Docker health check failed: ${dockerCheck.error}`;
347
- logger.error(`[pi-research] ${errorMsg}`);
348
- throw new Error(errorMsg);
349
- }
350
- }
351
-
352
- // Set state to starting_up
353
- this.currentStatus = {
354
- state: 'starting_up',
355
- url: '',
356
- isFunctional: false,
357
- };
358
- this.notifyStatusChange();
359
-
360
- // Use injected manager or create new one
361
- if (this.config.manager) {
362
- this.manager = this.config.manager;
363
- } else {
364
- // Clean up old manager if exists
365
- if (this.manager !== null) {
366
- try {
367
- this.manager.stopHeartbeat();
368
- if (this.sessionId !== null) {
369
- await this.manager.release(this.sessionId).catch(() => {});
370
- }
371
- } catch (error) {
372
- this.config.logger.warn('[pi-research] Error cleaning up old manager:', error);
373
- }
374
- }
375
-
376
- // Generate runtime settings if proxy or Brave API key is configured
377
- let settingsPath: string;
378
- if (this.config.proxyUrl || this.config.braveApiKey) {
379
- try {
380
- if (this.config.proxyUrl) logger.log('[pi-research] Proxy configured, generating runtime settings...');
381
- if (this.config.braveApiKey) logger.log('[pi-research] BRAVE_SEARCH_API_KEY set, enabling braveapi engine...');
382
- settingsPath = await this.generateRuntimeSettings(this.config.proxyUrl, this.config.braveApiKey);
383
- } catch (error) {
384
- const errorMsg = error instanceof Error ? error.message : String(error);
385
- logger.error('[pi-research] Failed to generate runtime settings:', errorMsg);
386
- const configError = new Error(
387
- `Runtime settings generation failed: ${errorMsg}\n\n` +
388
- `To fix:\n` +
389
- `1. Check PROXY_URL format: socks5://host:port or http://host:port\n` +
390
- `2. Check BRAVE_SEARCH_API_KEY is a valid key\n` +
391
- `3. Unset these env vars to use default settings`
392
- );
393
- configError.cause = error;
394
- throw configError;
395
- }
396
- } else {
397
- settingsPath = path.join(
398
- this.config.extensionDir,
399
- 'config',
400
- 'searxng',
401
- 'default-settings.yml'
402
- );
403
- }
404
-
405
- // Create new manager instance
406
- const managerConfig: SearxngManagerConfig = { settingsPath };
407
- this.manager = new DockerSearxngManager(this.config.extensionDir, managerConfig);
408
- }
409
-
410
- this.manager.setContext(ctx);
411
-
412
- const ctxSessionId = this.extractSessionId(ctx);
413
- this.sessionId = ctxSessionId;
414
-
415
- try {
416
- await this.manager.acquire(ctxSessionId);
417
- this.manager.startHeartbeat();
418
-
419
- // Get current status
420
- const searxngStatus = await this.manager.getStatus();
421
-
422
- // Update internal status
423
- this.currentStatus = {
424
- state: 'active',
425
- url: searxngStatus.url || '',
426
- isFunctional: false, // Reset on every re-init, must re-verify
427
- };
428
-
429
- this.initialized = true;
430
-
431
- logger.log('[pi-research] SearXNG lifecycle initialized');
432
- logger.log('[pi-research] SearXNG container:', searxngStatus.url);
433
-
434
- this.notifyStatusChange();
435
-
436
- } catch (error) {
437
- logger.error('[pi-research] Failed to initialize SearXNG:', error);
438
-
439
- this.currentStatus = {
440
- state: 'error',
441
- url: '',
442
- isFunctional: false,
443
- };
444
-
445
- this.initialized = true;
446
- this.notifyStatusChange();
447
-
448
- throw error;
449
- }
450
- }
451
-
452
- async ensureRunning(): Promise<string> {
453
- if (this.externalSearxngUrl) {
454
- return this.externalSearxngUrl;
455
- }
456
-
457
- if (!this.manager) {
458
- throw new Error('SearXNG not initialized. Call initLifecycle() first.');
459
- }
460
-
461
- // ensureReady waits for container to be healthy
462
- await this.manager.ensureReady();
463
-
464
- return this.manager.getSearxngUrl();
465
- }
466
-
467
- getStatus(): SearxngLifecycleStatus {
468
- return { ...this.currentStatus };
469
- }
470
-
471
- async shutdown(): Promise<void> {
472
- if (this.externalSearxngUrl) {
473
- // External URL mode — no container to tear down
474
- this.externalSearxngUrl = null;
475
- this.currentStatus = { state: 'inactive', url: '', isFunctional: false };
476
- this.initialized = false;
477
- this.notifyStatusChange();
478
- return;
479
- }
480
-
481
- if (!this.manager || !this.initialized) {
482
- logger.log('[pi-research] SearXNG not active, skipping shutdown');
483
- return;
484
- }
485
-
486
- try {
487
- this.manager.stopHeartbeat();
488
-
489
- if (this.sessionId !== null) {
490
- await this.manager.release(this.sessionId);
491
- }
492
-
493
- // Cleanup temporary proxy settings file if it exists
494
- if (this.proxySettingsPath && fs.existsSync(this.proxySettingsPath)) {
495
- try {
496
- await fs.promises.unlink(this.proxySettingsPath);
497
- logger.log(`[pi-research] Deleted temporary proxy settings: ${this.proxySettingsPath}`);
498
- } catch (error) {
499
- logger.warn('[pi-research] Failed to delete temporary proxy settings:', error);
500
- }
501
- this.proxySettingsPath = null;
502
- }
503
-
504
- this.currentStatus = {
505
- state: 'inactive',
506
- url: '',
507
- isFunctional: false,
508
- };
509
-
510
- this.initialized = false;
511
-
512
- logger.log('[pi-research] SearXNG lifecycle shut down');
513
- this.notifyStatusChange();
514
-
515
- } catch (error) {
516
- logger.error('[pi-research] Error during shutdown:', error);
517
- } finally {
518
- this.manager = null;
519
- this.sessionId = null;
520
- }
521
- }
522
-
523
- isInitialized(): boolean {
524
- return this.initialized && (this.manager !== null || this.externalSearxngUrl !== null);
525
- }
526
- }
527
-
528
- // ============================================================================
529
- // GLOBAL STATE FOR BACKWARD COMPATIBILITY
530
- // ============================================================================
531
-
532
- let globalManager: SearxngLifecycleManager | null = null;
533
-
534
- /**
535
- * Create a new lifecycle manager instance
536
- */
537
- export function createSearxngLifecycleManager(
538
- config: Partial<SearxngLifecycleConfig> = {}
539
- ): SearxngLifecycleManager {
540
- return new SearxngLifecycleManager(config);
541
- }
542
-
543
- /**
544
- * Get the global lifecycle manager instance
545
- */
546
- export function getLifecycleManager(): SearxngLifecycleManager {
547
- if (!globalManager) {
548
- globalManager = new SearxngLifecycleManager();
549
- }
550
- return globalManager;
551
- }
552
-
553
- /**
554
- * Set the global lifecycle manager instance (for testing)
555
- */
556
- export function setLifecycleManager(manager: SearxngLifecycleManager | null): void {
557
- globalManager = manager;
558
- }
559
-
560
- /**
561
- * Reset the global lifecycle manager instance (for testing)
562
- */
563
- export function resetLifecycleManager(): void {
564
- globalManager = null;
565
- }
566
-
567
- // ============================================================================
568
- // BACKWARD COMPATIBILITY EXPORTS
569
- // ============================================================================
570
-
571
- /**
572
- * Backward compatible: Get manager instance
573
- */
574
- export function getManager(): DockerSearxngManager | null {
575
- return getLifecycleManager().getManager();
576
- }
577
-
578
- /**
579
- * Backward compatible: Initialize lifecycle
580
- */
581
- export async function initLifecycle(ctx: ExtensionContext): Promise<void> {
582
- await getLifecycleManager().init(ctx);
583
- // Register extension-owned cleanup once; repeated initLifecycle() calls are safe.
584
- shutdownManager.register(shutdownLifecycle);
585
- }
586
-
587
- /**
588
- * Backward compatible: Ensure container is running
589
- */
590
- export async function ensureRunning(): Promise<string> {
591
- return getLifecycleManager().ensureRunning();
592
- }
593
-
594
- /**
595
- * Backward compatible: Get status (renamed type to avoid conflict)
596
- */
597
- export function getStatus(): SearxngLifecycleStatus {
598
- return getLifecycleManager().getStatus();
599
- }
600
-
601
- /**
602
- * Backward compatible: Set functional state
603
- */
604
- export function setFunctional(ok: boolean): void {
605
- getLifecycleManager().setFunctional(ok);
606
- }
607
-
608
- /**
609
- * Backward compatible: Get functional state
610
- */
611
- export function isFunctional(): boolean {
612
- return getLifecycleManager().isFunctional();
613
- }
614
-
615
- /**
616
- * Backward compatible: Subscribe to status changes
617
- */
618
- export function onStatusChange(callback: StatusCallback): () => void {
619
- return getLifecycleManager().onStatusChange(callback);
620
- }
621
-
622
- /**
623
- * Backward compatible: Shutdown lifecycle
624
- */
625
- export async function shutdownLifecycle(): Promise<void> {
626
- await getLifecycleManager().shutdown();
627
- }
628
-
629
- /**
630
- * Backward compatible: Check if initialized
631
- */
632
- export function isInitialized(): boolean {
633
- return getLifecycleManager().isInitialized();
634
- }
635
-
636
- /**
637
- * Check if Docker is installed and running
638
- */
639
- export async function checkDockerAvailability(): Promise<{ installed: boolean; running: boolean; error?: string }> {
640
- return verifyDockerInstalled();
641
- }
642
-
643
- // Export original type for compatibility
644
- export type SearxngStatus = SearxngLifecycleStatus;