ubersearch 0.0.0-development

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/package.json +76 -0
  4. package/src/app/index.ts +30 -0
  5. package/src/bootstrap/container.ts +157 -0
  6. package/src/cli.ts +380 -0
  7. package/src/config/defineConfig.ts +176 -0
  8. package/src/config/load.ts +368 -0
  9. package/src/config/types.ts +86 -0
  10. package/src/config/validation.ts +148 -0
  11. package/src/core/cache.ts +74 -0
  12. package/src/core/container.ts +268 -0
  13. package/src/core/credits/CreditManager.ts +158 -0
  14. package/src/core/credits/CreditStateProvider.ts +151 -0
  15. package/src/core/credits/FileCreditStateProvider.ts +137 -0
  16. package/src/core/credits/index.ts +3 -0
  17. package/src/core/docker/dockerComposeHelper.ts +177 -0
  18. package/src/core/docker/dockerLifecycleManager.ts +361 -0
  19. package/src/core/docker/index.ts +8 -0
  20. package/src/core/logger.ts +146 -0
  21. package/src/core/orchestrator.ts +103 -0
  22. package/src/core/paths.ts +157 -0
  23. package/src/core/provider/ILifecycleProvider.ts +120 -0
  24. package/src/core/provider/ProviderFactory.ts +120 -0
  25. package/src/core/provider.ts +61 -0
  26. package/src/core/serviceKeys.ts +45 -0
  27. package/src/core/strategy/AllProvidersStrategy.ts +245 -0
  28. package/src/core/strategy/FirstSuccessStrategy.ts +98 -0
  29. package/src/core/strategy/ISearchStrategy.ts +94 -0
  30. package/src/core/strategy/StrategyFactory.ts +204 -0
  31. package/src/core/strategy/index.ts +9 -0
  32. package/src/core/strategy/types.ts +56 -0
  33. package/src/core/types.ts +58 -0
  34. package/src/index.ts +1 -0
  35. package/src/plugin/PluginRegistry.ts +336 -0
  36. package/src/plugin/builtin.ts +130 -0
  37. package/src/plugin/index.ts +33 -0
  38. package/src/plugin/types.ts +212 -0
  39. package/src/providers/BaseProvider.ts +49 -0
  40. package/src/providers/brave.ts +66 -0
  41. package/src/providers/constants.ts +13 -0
  42. package/src/providers/helpers/index.ts +24 -0
  43. package/src/providers/helpers/lifecycleHelpers.ts +110 -0
  44. package/src/providers/helpers/resultMappers.ts +168 -0
  45. package/src/providers/index.ts +6 -0
  46. package/src/providers/linkup.ts +114 -0
  47. package/src/providers/retry.ts +95 -0
  48. package/src/providers/searchxng.ts +163 -0
  49. package/src/providers/tavily.ts +73 -0
  50. package/src/providers/types/index.ts +185 -0
  51. package/src/providers/utils.ts +182 -0
  52. package/src/tool/allSearchTool.ts +110 -0
  53. package/src/tool/interface.ts +71 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Brian Sunter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,374 @@
1
+ # Multi-Search
2
+
3
+ Unified, Bun-first search interface across multiple providers with credit tracking, pluggable strategies, and optional Docker-managed SearXNG.
4
+
5
+ ## Highlights
6
+
7
+ - 🔍 Providers: Tavily, Brave, Linkup, SearXNG (local, Docker auto-start)
8
+ - 🔌 Extensible: Add custom providers via TypeScript plugin system
9
+ - 🤝 Single interface: shared types + CLI + programmatic API
10
+ - 💳 Credits: per-engine quotas with snapshots and low-credit warnings
11
+ - 🧠 Strategies: `all` (merge) or `first-success` (fastest win)
12
+ - ⚙️ Config: JSON or TypeScript (`defineConfig`), XDG-aware resolution
13
+ - 🐳 Auto-start: optional Docker lifecycle for local SearXNG
14
+
15
+ ## Install & Run (Bun)
16
+
17
+ ```bash
18
+ cd /path/to/allsearch
19
+ bun install
20
+
21
+ # CLI (direct)
22
+ bun run src/cli.ts "best TypeScript ORM 2025"
23
+
24
+ # Or use bun link (works from any directory)
25
+ bun link
26
+ allsearch "llm observability" --json
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Basic Search
32
+
33
+ ```bash
34
+ allsearch "your search query"
35
+ ```
36
+
37
+ ### Options
38
+
39
+ ```bash
40
+ allsearch "query" [options]
41
+
42
+ Options:
43
+ --json Output results as JSON
44
+ --engines engine1,engine2 Use specific engines
45
+ --strategy all|first-success Search strategy (default: all)
46
+ --limit number Max results per engine
47
+ --include-raw Include raw provider responses
48
+ --help, -h Show help
49
+ health Run provider health checks (starts Docker-backed ones if needed)
50
+ ```
51
+
52
+ ### Examples
53
+
54
+ ```bash
55
+ # Search with specific engines
56
+ allsearch "hawaii dev meetups" --engines tavily,brave --json
57
+
58
+ # Use first-success strategy (stop after first working provider)
59
+ allsearch "emerging web frameworks" --strategy first-success
60
+
61
+ # Limit results per provider
62
+ allsearch "rust async patterns" --limit 3
63
+
64
+ # Check credit status
65
+ allsearch credits
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ Resolution order (first wins):
71
+ 1. Explicit path passed to CLI/API (`--config /path/to/config.json`)
72
+ 2. `./allsearch.config.(ts|json)` (current directory)
73
+ 3. `$XDG_CONFIG_HOME/allsearch/config.(ts|json)` (default: `~/.config/allsearch/`)
74
+
75
+ ### XDG Directory Structure
76
+
77
+ ```
78
+ ~/.config/allsearch/
79
+ ├── config.json # Main configuration
80
+ └── searxng/
81
+ └── config/
82
+ └── settings.yml # SearXNG settings (auto-copied on first run)
83
+
84
+ ~/.local/share/allsearch/
85
+ └── searxng/
86
+ └── data/ # SearXNG cache (auto-created)
87
+ ```
88
+
89
+ - Example config: see `docs/config/allsearch.config.json`
90
+ - Schema: `docs/config/config.schema.json` (generated from Zod)
91
+ - TS helper: `defineConfig`, `defineTavily`, `defineBrave`, `defineLinkup`, `defineSearchxng`
92
+
93
+ ### SearXNG Configuration
94
+
95
+ SearXNG uses Docker with volumes mounted to XDG directories. On first run, the default `settings.yml` is copied to `~/.config/allsearch/searxng/config/`. You can customize this file to:
96
+ - Enable/disable search engines
97
+ - Adjust rate limiting
98
+ - Configure output formats
99
+
100
+ ## Custom Providers
101
+
102
+ Add your own search providers using the plugin system.
103
+
104
+ ### TypeScript Config with Custom Provider
105
+
106
+ ```typescript
107
+ // ~/.config/allsearch/config.ts
108
+ import { defineConfig, definePlugin } from "allsearch/config";
109
+
110
+ // 1. Define your provider class
111
+ class PerplexityProvider {
112
+ constructor(private config: any) {}
113
+
114
+ get id() { return this.config.id; }
115
+
116
+ async search(query: { query: string; limit?: number }) {
117
+ const response = await fetch("https://api.perplexity.ai/search", {
118
+ method: "POST",
119
+ headers: {
120
+ "Authorization": `Bearer ${process.env[this.config.apiKeyEnv]}`,
121
+ "Content-Type": "application/json",
122
+ },
123
+ body: JSON.stringify({ query: query.query }),
124
+ });
125
+ const data = await response.json();
126
+
127
+ return {
128
+ engineId: this.id,
129
+ items: data.results.map((r: any) => ({
130
+ title: r.title,
131
+ url: r.url,
132
+ snippet: r.snippet,
133
+ sourceEngine: this.id,
134
+ })),
135
+ };
136
+ }
137
+ }
138
+
139
+ // 2. Create the plugin
140
+ const perplexityPlugin = definePlugin({
141
+ type: "perplexity",
142
+ displayName: "Perplexity AI",
143
+ hasLifecycle: false,
144
+ factory: (config) => new PerplexityProvider(config),
145
+ });
146
+
147
+ // 3. Export config with plugin
148
+ export default defineConfig({
149
+ plugins: [perplexityPlugin],
150
+ defaultEngineOrder: ["perplexity", "searxng"],
151
+ engines: [
152
+ {
153
+ id: "perplexity",
154
+ type: "perplexity", // matches plugin type
155
+ enabled: true,
156
+ apiKeyEnv: "PERPLEXITY_API_KEY",
157
+ endpoint: "https://api.perplexity.ai/search",
158
+ monthlyQuota: 1000,
159
+ creditCostPerSearch: 1,
160
+ lowCreditThresholdPercent: 10,
161
+ },
162
+ // ... other engines
163
+ ],
164
+ });
165
+ ```
166
+
167
+ ### Provider Interface
168
+
169
+ Your provider must implement:
170
+
171
+ ```typescript
172
+ interface ISearchProvider {
173
+ id: string;
174
+ search(query: SearchQuery): Promise<SearchResponse>;
175
+ }
176
+
177
+ interface SearchQuery {
178
+ query: string;
179
+ limit?: number;
180
+ includeRaw?: boolean;
181
+ }
182
+
183
+ interface SearchResponse {
184
+ engineId: string;
185
+ items: SearchResultItem[];
186
+ raw?: unknown;
187
+ tookMs?: number;
188
+ }
189
+
190
+ interface SearchResultItem {
191
+ title: string;
192
+ url: string;
193
+ snippet: string;
194
+ score?: number;
195
+ sourceEngine: string;
196
+ }
197
+ ```
198
+
199
+ ### Plugin Helpers
200
+
201
+ - `definePlugin({ type, displayName, hasLifecycle, factory })` - Create a plugin
202
+ - `defineConfig({ plugins, engines, ... })` - Config with plugins
203
+ - `defineEngine<T>(config)` - Type-safe custom engine config
204
+
205
+ ## Architecture (short)
206
+
207
+ - Config resolved and validated (`src/config`), plugins registered
208
+ - DI container bootstraps orchestrator, credit manager, provider registry (`src/bootstrap/container.ts`)
209
+ - Providers registered via plugins (`src/plugin`, `src/providers/*`)
210
+ - Orchestrator runs strategies (`src/core/strategy/*`) and aggregates results
211
+ - Docker-backed providers (SearXNG) use lifecycle manager with auto-start/health checks (`src/core/docker/*`)
212
+
213
+ ## Output Formats
214
+
215
+ ### Human-Readable (Default)
216
+
217
+ ```
218
+ Query: "rust async patterns"
219
+ Found 15 results
220
+
221
+ ============================================================
222
+ tavily (10 results)
223
+ ============================================================
224
+
225
+ 1. Async programming in Rust - Tokio
226
+ https://tokio.rs/
227
+ Score: 0.95
228
+ Tokio is a runtime for writing reliable asynchronous applications with Rust.
229
+
230
+ 2. Asynchronous Programming in Rust
231
+ https://rust-lang.github.io/async-book/
232
+ Score: 0.92
233
+ A book explaining async/await in Rust...
234
+ ```
235
+
236
+ ### JSON (`--json`)
237
+
238
+ ```json
239
+ {
240
+ "query": "rust async patterns",
241
+ "items": [
242
+ {
243
+ "title": "Async programming in Rust - Tokio",
244
+ "url": "https://tokio.rs/",
245
+ "snippet": "Tokio is a runtime...",
246
+ "score": 0.95,
247
+ "sourceEngine": "tavily"
248
+ }
249
+ ],
250
+ "enginesTried": [
251
+ {
252
+ "engineId": "tavily",
253
+ "success": true
254
+ }
255
+ ],
256
+ "credits": [...]
257
+ }
258
+ ```
259
+
260
+ ## Search Strategies
261
+
262
+ ### All (Default)
263
+
264
+ Queries all configured/enabled providers and combines results.
265
+
266
+ ```bash
267
+ allsearch "topic" --strategy all
268
+ ```
269
+
270
+ - Pro: Gets maximum coverage, see different perspectives
271
+ - Con: Uses more credits, slower
272
+ - Best for: Research, comparison, getting API formats
273
+
274
+ ### First Success
275
+
276
+ Stops after the first provider returns results.
277
+
278
+ ```bash
279
+ allsearch "topic" --strategy first-success
280
+ ```
281
+
282
+ - Pro: Saves credits, faster
283
+ - Con: Misses results from other providers
284
+ - Best for: Quick lookups, production use
285
+
286
+ ## Development
287
+
288
+ ### Source layout
289
+
290
+ ```
291
+ src/
292
+ ├── app/ # Public surface (bootstrap + API exports)
293
+ ├── bootstrap/ # DI container wiring
294
+ ├── config/ # Config types, schema, loaders
295
+ ├── core/ # Orchestrator, strategy, credits, docker helpers
296
+ │ ├── docker/ # Docker compose helper, lifecycle manager
297
+ │ ├── paths.ts # XDG path utilities
298
+ │ └── ...
299
+ ├── plugin/ # Plugin registry and built-ins
300
+ ├── providers/ # Provider implementations + shared helpers
301
+ ├── tool/ # CLI-facing tool + interfaces
302
+ └── cli.ts # CLI entry
303
+
304
+ providers/
305
+ └── searxng/
306
+ ├── docker-compose.yml # SearXNG Docker config (uses env var volumes)
307
+ └── config/
308
+ └── settings.yml # Default SearXNG settings (copied to XDG on first run)
309
+ ```
310
+
311
+ ### Building
312
+
313
+ ```bash
314
+ # Bundle to dist/
315
+ bun run build
316
+
317
+ # Creates:
318
+ # dist/cli.js - Bundled CLI
319
+ # dist/providers/searxng/ - Docker compose + default settings
320
+ ```
321
+
322
+ ### Testing (Bun)
323
+
324
+ - All: `SKIP_DOCKER_TESTS=true bun test --preload ./test/setup.ts test/`
325
+ - Unit only: `bun run test:unit`
326
+ - Integration (Docker optional): `SKIP_DOCKER_TESTS=false bun run test:integration`
327
+ - Coverage: `SKIP_DOCKER_TESTS=true bun run test:coverage`
328
+
329
+ See `docs/testing/README.md` for suite layout.
330
+
331
+ ## Troubleshooting
332
+
333
+ - **Missing config**: Copy `docs/config/allsearch.config.json` to `~/.config/allsearch/config.json`
334
+ - **Missing API key**: Set `TAVILY_API_KEY`, `BRAVE_API_KEY`, `LINKUP_API_KEY` environment variables
335
+ - **SearXNG not healthy**: Ensure Docker is running. Check `~/.config/allsearch/searxng/config/settings.yml` exists
336
+ - **SearXNG settings missing**: Run `allsearch health` once to bootstrap default config to XDG directory
337
+ - **Path issues after bun link**: The CLI resolves paths relative to XDG directories, not the working directory
338
+
339
+ ## Providers
340
+
341
+ | Provider | Type | API Key Required | Free Tier | Notes |
342
+ |----------|------|------------------|-----------|-------|
343
+ | **SearXNG** | `searchxng` | No | Unlimited (local) | Self-hosted, Docker auto-start |
344
+ | **Tavily** | `tavily` | Yes | 1000/month | Best for AI/research queries |
345
+ | **Brave** | `brave` | Yes | 2000/month | General web search |
346
+ | **Linkup** | `linkup` | Yes | 1000/month | AI-powered search |
347
+
348
+ ### Getting API Keys
349
+
350
+ - **Tavily**: https://tavily.com/ → Sign up → Dashboard → API Keys
351
+ - **Brave**: https://brave.com/search/api/ → Get Started → Create App
352
+ - **Linkup**: https://linkup.so/ → Sign up → API Keys
353
+ - **SearXNG**: No key needed (runs locally via Docker)
354
+
355
+ ## Environment Variables
356
+
357
+ ### API Keys (required per enabled engine)
358
+
359
+ ```bash
360
+ # Add to ~/.bashrc, ~/.zshrc, or use a secrets manager
361
+ export TAVILY_API_KEY="tvly-..." # From tavily.com dashboard
362
+ export BRAVE_API_KEY="BSA..." # From brave.com/search/api
363
+ export LINKUP_API_KEY="xxxxxxxx-..." # UUID from linkup.so
364
+ # SEARXNG_API_KEY not needed (local Docker)
365
+ ```
366
+
367
+ ### XDG Directories (optional)
368
+ - `XDG_CONFIG_HOME` - Config directory (default: `~/.config`)
369
+ - `XDG_DATA_HOME` - Data directory (default: `~/.local/share`)
370
+ - `XDG_STATE_HOME` - State directory (default: `~/.local/state`)
371
+
372
+ ## License
373
+
374
+ MIT
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "ubersearch",
3
+ "version": "0.0.0-development",
4
+ "module": "src/index.ts",
5
+ "type": "module",
6
+ "bin": {
7
+ "ubersearch": "src/cli.ts"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/index.ts"
12
+ },
13
+ "./cli": "./src/cli.ts",
14
+ "./config": "./src/config/defineConfig.ts",
15
+ "./types": "./src/app/index.ts"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "devDependencies": {
26
+ "@biomejs/biome": "^2.3.8",
27
+ "@semantic-release/commit-analyzer": "^13.0.0",
28
+ "@semantic-release/github": "^11.0.1",
29
+ "@semantic-release/npm": "^12.0.1",
30
+ "@semantic-release/release-notes-generator": "^14.0.1",
31
+ "@types/bun": "latest",
32
+ "semantic-release": "^24.2.0"
33
+ },
34
+ "peerDependencies": {
35
+ "typescript": "^5"
36
+ },
37
+ "dependencies": {
38
+ "zod": "^4.1.12"
39
+ },
40
+ "scripts": {
41
+ "lint": "bunx biome check .",
42
+ "lint:fix": "bunx biome check --write .",
43
+ "format": "bunx biome format --write .",
44
+ "test": "SKIP_DOCKER_TESTS=true bun test --preload ./test/setup.ts test/",
45
+ "test:unit": "bun test --preload ./test/setup.ts test/unit/",
46
+ "test:integration": "bun test --preload ./test/setup.ts test/integration/",
47
+ "test:e2e": "bun test --preload ./test/setup.ts test/e2e/",
48
+ "test:coverage": "SKIP_DOCKER_TESTS=true bun test --preload ./test/setup.ts --coverage test/",
49
+ "test:watch": "SKIP_DOCKER_TESTS=true bun test --preload ./test/setup.ts --watch test/",
50
+ "test:verbose": "DEBUG_TESTS=1 SKIP_DOCKER_TESTS=true bun test --preload ./test/setup.ts test/",
51
+ "test:docker": "SKIP_DOCKER_TESTS=false bun test --preload ./test/setup.ts test/integration/",
52
+ "mcp": "bun run mcp-server.ts",
53
+ "mcp:test": "bun run scripts/test-mcp.ts",
54
+ "build": "bun run scripts/build.ts",
55
+ "build:binary": "bun build --compile --minify src/cli.ts --outfile dist/allsearch"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://github.com/briansunter/allsearch.git"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/briansunter/allsearch/issues"
63
+ },
64
+ "homepage": "https://github.com/briansunter/allsearch#readme",
65
+ "keywords": [
66
+ "search",
67
+ "ai",
68
+ "search-api",
69
+ "search-engine",
70
+ "tavily",
71
+ "brave",
72
+ "searxng"
73
+ ],
74
+ "author": "Brian Sunter",
75
+ "license": "MIT"
76
+ }
@@ -0,0 +1,30 @@
1
+ // Public API surface for consumers importing the library (non-CLI).
2
+
3
+ export { bootstrapContainer } from "../bootstrap/container";
4
+ // Config helpers
5
+ export {
6
+ createConfig,
7
+ defineBrave,
8
+ defineConfig,
9
+ defineEngine,
10
+ defineLinkup,
11
+ definePlugin,
12
+ defineSearchxng,
13
+ defineTavily,
14
+ } from "../config/defineConfig";
15
+ export type {
16
+ BraveConfig,
17
+ EngineConfig,
18
+ LinkupConfig,
19
+ AllSearchConfig,
20
+ SearchxngConfig,
21
+ TavilyConfig,
22
+ } from "../config/types";
23
+ export type { EngineId, SearchQuery, SearchResponse, SearchResultItem } from "../core/types";
24
+ // Types
25
+ export type { AllSearchInput, AllSearchOutput, AllSearchOutputItem } from "../tool/interface";
26
+ export type {
27
+ GetCreditStatusOptions,
28
+ AllSearchOptions as ToolAllSearchOptions,
29
+ } from "../tool/multiSearchTool";
30
+ export { getCreditStatus, multiSearch } from "../tool/multiSearchTool";
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Bootstrap the Dependency Injection Container
3
+ *
4
+ * Sets up all services and their dependencies
5
+ */
6
+
7
+ import { loadConfig } from "../config/load";
8
+ import type { EngineConfig, AllSearchConfig } from "../config/types";
9
+ import { type Container, container } from "../core/container";
10
+ import { CreditManager } from "../core/credits";
11
+ import { FileCreditStateProvider } from "../core/credits/FileCreditStateProvider";
12
+ import { createLogger } from "../core/logger";
13
+ import { AllSearchOrchestrator } from "../core/orchestrator";
14
+ import type { ILifecycleProvider, SearchProvider } from "../core/provider";
15
+ import { ProviderRegistry } from "../core/provider";
16
+ import { ProviderFactory } from "../core/provider/ProviderFactory";
17
+ import { ServiceKeys } from "../core/serviceKeys";
18
+ import { StrategyFactory } from "../core/strategy/StrategyFactory";
19
+ import { PluginRegistry as PluginReg, registerBuiltInPlugins } from "../plugin";
20
+
21
+ const log = createLogger("Bootstrap");
22
+
23
+ /**
24
+ * Bootstrap options
25
+ */
26
+ export interface BootstrapOptions {
27
+ /** Custom credit state path (overrides config) */
28
+ creditStatePath?: string;
29
+ /** Skip plugin registration (for testing) */
30
+ skipPluginRegistration?: boolean;
31
+ }
32
+
33
+ /**
34
+ * Bootstrap the DI container with all services
35
+ *
36
+ * @param configOrPath - Either a config file path (string) or a config object directly
37
+ * @param options - Bootstrap options (or legacy creditStatePath string)
38
+ */
39
+ export async function bootstrapContainer(
40
+ configOrPath?: string | AllSearchConfig,
41
+ options?: BootstrapOptions | string,
42
+ ): Promise<Container> {
43
+ // Clear existing registrations (useful for testing)
44
+ container.reset();
45
+
46
+ // Handle legacy second argument (creditStatePath)
47
+ const opts: BootstrapOptions =
48
+ typeof options === "string" ? { creditStatePath: options } : (options ?? {});
49
+
50
+ // Load or use provided configuration
51
+ let config: AllSearchConfig;
52
+ if (typeof configOrPath === "object" && configOrPath !== null) {
53
+ // Config object provided directly (useful for testing)
54
+ config = configOrPath;
55
+
56
+ // Ensure plugins are registered when config is provided directly
57
+ if (!opts.skipPluginRegistration) {
58
+ await registerBuiltInPlugins(PluginReg.getInstance());
59
+ }
60
+ } else {
61
+ // Load from file (plugins registered by loadConfig)
62
+ config = await loadConfig(configOrPath);
63
+ }
64
+
65
+ // Register configuration as singleton
66
+ container.singleton(ServiceKeys.CONFIG, () => config);
67
+
68
+ // Register credit state provider
69
+ container.singleton(ServiceKeys.CREDIT_STATE_PROVIDER, () => {
70
+ const creditStatePath = opts.creditStatePath ?? config.storage?.creditStatePath;
71
+ return new FileCreditStateProvider(creditStatePath);
72
+ });
73
+
74
+ // Register credit manager
75
+ container.singleton(ServiceKeys.CREDIT_MANAGER, () => {
76
+ const enabledEngines = config.engines.filter((e) => e.enabled);
77
+ const stateProvider = container.get<FileCreditStateProvider>(ServiceKeys.CREDIT_STATE_PROVIDER);
78
+ return new CreditManager(enabledEngines, stateProvider);
79
+ });
80
+
81
+ // Register provider registry
82
+ container.singleton(ServiceKeys.PROVIDER_REGISTRY, () => {
83
+ const registry = new ProviderRegistry();
84
+ const failedProviders: string[] = [];
85
+
86
+ // Register all enabled providers
87
+ for (const engineConfig of config.engines) {
88
+ if (!engineConfig.enabled) {
89
+ continue;
90
+ }
91
+
92
+ try {
93
+ const provider = createProvider(engineConfig);
94
+ registry.register(provider);
95
+ log.info(`Registered provider: ${engineConfig.id}`);
96
+ } catch (error) {
97
+ const errorMsg = error instanceof Error ? error.message : String(error);
98
+ log.warn(`Failed to register provider ${engineConfig.id}: ${errorMsg}`);
99
+ failedProviders.push(engineConfig.id);
100
+ }
101
+ }
102
+
103
+ const availableProviders = registry.list();
104
+ if (availableProviders.length === 0) {
105
+ throw new Error(
106
+ `No providers could be registered. Failed providers: ${failedProviders.join(", ")}. ` +
107
+ "Check your configuration and environment variables.",
108
+ );
109
+ }
110
+
111
+ if (failedProviders.length > 0) {
112
+ log.warn(`Some providers failed to initialize: ${failedProviders.join(", ")}`);
113
+ }
114
+
115
+ return registry;
116
+ });
117
+
118
+ // Register strategy factory
119
+ container.singleton(ServiceKeys.STRATEGY_FACTORY, () => StrategyFactory);
120
+
121
+ // Register orchestrator
122
+ container.singleton(ServiceKeys.ORCHESTRATOR, () => {
123
+ const creditManager = container.get<CreditManager>(ServiceKeys.CREDIT_MANAGER);
124
+ const providerRegistry = container.get<ProviderRegistry>(ServiceKeys.PROVIDER_REGISTRY);
125
+ return new AllSearchOrchestrator(config, creditManager, providerRegistry);
126
+ });
127
+
128
+ // Initialize services that need async setup
129
+ const creditManager = container.get<CreditManager>(ServiceKeys.CREDIT_MANAGER);
130
+ await creditManager.initialize();
131
+
132
+ // Resolve provider registry to trigger validation (throws if no providers registered)
133
+ container.get<ProviderRegistry>(ServiceKeys.PROVIDER_REGISTRY);
134
+
135
+ return container;
136
+ }
137
+
138
+ /**
139
+ * Create a provider instance based on engine configuration
140
+ */
141
+ function createProvider(engineConfig: EngineConfig): SearchProvider {
142
+ return ProviderFactory.createProvider(engineConfig, container);
143
+ }
144
+
145
+ /**
146
+ * Helper function to check if a provider implements ILifecycleProvider
147
+ */
148
+ export function isLifecycleProvider(provider: unknown): provider is ILifecycleProvider {
149
+ return (
150
+ provider != null &&
151
+ typeof provider === "object" &&
152
+ "init" in provider &&
153
+ typeof provider.init === "function" &&
154
+ "healthcheck" in provider &&
155
+ typeof provider.healthcheck === "function"
156
+ );
157
+ }